/// /** * Folksonomy Aggregator - Community Tag Telemetry Charm * * This charm collects tag usage events from folksonomy-tags instances and * computes community suggestions ranked by usage count (preferential attachment). * * SETUP: After deploying, FAVORITE this charm with tag "folksonomy-aggregator" * so that folksonomy-tags instances can discover it via wish("#folksonomy-aggregator"). * * HOW IT WORKS: * 1. folksonomy-tags instances post events via the postEvent stream * 2. This aggregator stores events and computes suggestions by scope * 3. Suggestions are sorted by count (most used first) for preferential attachment * * CFC: When Contextual Flow Control is implemented, this aggregator will only * include tags in community suggestions if 5+ independent users have posted * the same scope+tag combination. This protects individual tagging choices * while enabling wisdom of crowds. */ import { computed, type Default, handler, NAME, pattern, Stream, UI, Writable, } from "commontools"; /** * Tag event posted by folksonomy-tags instances. */ interface TagEvent { scope: string; tag: string; action: "add" | "use" | "remove"; timestamp: number; // CFC: In the future, Contextual Flow Control will enforce that // the user's identity is only included in the aggregate if // 5+ independent users have posted the same scope+tag combo. // For now, we track events but don't expose user identity. } /** * Community tag suggestion with usage count. */ interface CommunityTagSuggestion { tag: string; count: number; // CFC: Future uniqueUsers field for threshold enforcement } interface Input { events: Default; } /** * A #folksonomy-aggregator that collects tag usage events and serves community suggestions. * * The #folksonomy-aggregator tag is how folksonomy-tags instances discover this charm via wish(). */ interface Output { events: TagEvent[]; suggestions: Record; postEvent: Stream; } // Handler for receiving new tag events const handlePostEvent = handler }>( (event, { events }) => { // Validate the event has required fields if (!event?.scope || !event?.tag || !event?.action) { console.warn("[folksonomy-aggregator] Invalid event received:", event); return; } const currentEvents = events.get() || []; const newEvent: TagEvent = { scope: event.scope, tag: event.tag, action: event.action, timestamp: event.timestamp || Date.now(), }; // Add the new event to the list events.set([...currentEvents, newEvent]); }, ); export default pattern(({ events }) => { // Compute suggestions by scope - aggregates all events into usage counts const suggestions = computed(() => { const eventList = (events || []) as TagEvent[]; const byScope: Record> = {}; for (const event of eventList) { if (!event?.scope || !event?.tag) continue; if (!byScope[event.scope]) { byScope[event.scope] = new Map(); } const scopeMap = byScope[event.scope]; const currentCount = scopeMap.get(event.tag) || 0; // Add, use, and remove affect counts // CFC: When implemented, only count if 5+ unique users switch (event.action) { case "add": case "use": scopeMap.set(event.tag, currentCount + 1); break; case "remove": // Don't go below 0 scopeMap.set(event.tag, Math.max(0, currentCount - 1)); break; } } // Convert to CommunityTagSuggestion arrays, sorted by count (preferential attachment) const result: Record = {}; for (const [scope, tagCounts] of Object.entries(byScope)) { result[scope] = Array.from(tagCounts.entries()) .filter(([_, count]) => count > 0) .map(([tag, count]) => ({ tag, count })) .sort((a, b) => b.count - a.count); // Most used first } return result; }); // Compute stats for display const totalEvents = computed(() => ((events || []) as TagEvent[]).length); const uniqueScopes = computed(() => { const scopes = new Set(); for (const event of (events || []) as TagEvent[]) { if (event?.scope) scopes.add(event.scope); } return scopes.size; }); // Recent events for display (last 20) const recentEvents = computed(() => { const eventList = (events || []) as TagEvent[]; return eventList.slice(-20).reverse(); }); // Top tags across all scopes const topTags = computed(() => { const suggs = suggestions as Record; const allTags: Map = new Map(); for (const scopeSuggestions of Object.values(suggs)) { for (const { tag, count } of scopeSuggestions) { allTags.set(tag, (allTags.get(tag) || 0) + count); } } return Array.from(allTags.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([tag, count]) => ({ tag, count })); }); return { [NAME]: computed(() => `🏷️ Folksonomy Aggregator (${totalEvents} events)`), [UI]: (

🏷️ Folksonomy Aggregator

Community tag telemetry collector. Favorite this charm with tag "folksonomy-aggregator" for discovery.

{/* Stats */} {totalEvents} Total Events {uniqueScopes} Unique Scopes {/* Top Tags */} Top Tags (All Scopes) {topTags.map( (item: { tag: string; count: number }, idx: number) => ( {item.tag} ({item.count}) ), )} {/* Recent Events */} Recent Events {recentEvents.length === 0 ? ( No events yet ) : ( recentEvents.map((event: TagEvent, idx: number) => ( {event.action} {event.tag} in {event.scope} )) )} {/* CFC Notice */}
Privacy Note (CFC Planned):{" "} When Contextual Flow Control is implemented, tags will only appear in community suggestions if 5+ independent users have added them, protecting individual choices while enabling collective wisdom.
), events, suggestions, postEvent: handlePostEvent({ events }), }; });