diff --git a/data/meeting-repos-seed.json b/data/meeting-repos-seed.json new file mode 100644 index 0000000..cc26f7c --- /dev/null +++ b/data/meeting-repos-seed.json @@ -0,0 +1,36 @@ +{ + "meta": { + "provenance": "Curated seed from AF-Member-Intelligence (14 Zoom chat logs, Feb 19 - Apr 30 2026) + Gemini meeting notes (Apr 22 - Jul 1). For member-intel entries the introducer is association-inferred (the member the repo is associated with) pending verification against raw chat lines; source is 'member-intel'. Owners left null where unverified.", + "not_yet_parsed": [ + "2026-05-06 OSS Google Meet transcript", + "2026-05-13 OSS Google Meet transcript", + "2026-05-20 OSS Google Meet transcript", + "2026-05-27 OSS Google Meet transcript", + "2026-06-03 OSS Google Meet transcript", + "2026-06-10 OSS Google Meet transcript", + "2026-06-24 OSS Google Meet transcript", + "2026-07-01 OSS Google Meet transcript", + "2026-07-08 OSS Google Meet transcript (Gemini notes failed)" + ] + }, + "entries": [ + {"canonical": "agenticsorg/OIA-Model", "name": "OIA-Model", "owner": "agenticsorg", "url": "https://github.com/agenticsorg/OIA-Model", "introducer": "nicholas-ruest", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "globalbusinessadvisors/Synapse-Graph", "name": "Synapse-Graph", "owner": "globalbusinessadvisors", "url": "https://github.com/globalbusinessadvisors/Synapse-Graph", "introducer": "nicholas-ruest", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "ruvnet/RuVector", "name": "RuVector", "owner": "ruvnet", "url": "https://github.com/ruvnet/RuVector", "introducer": "ruvnet", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "ruvnet/ruflo", "name": "ruflo", "owner": "ruvnet", "url": "https://github.com/ruvnet/ruflo", "introducer": "ruvnet", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "ruview", "name": "RuView", "owner": null, "url": null, "introducer": "ruvnet", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "connectome-os", "name": "Connectome-OS", "owner": null, "url": null, "introducer": "ruvnet", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "open-claude-code", "name": "open-claude-code", "owner": null, "url": null, "introducer": "ruvnet", "first_seen": "2026-02-19", "source": "member-intel"}, + {"canonical": "pi.ruv.io", "name": "pi.ruv.io", "owner": null, "url": "https://pi.ruv.io", "introducer": "Stuart Kerr", "first_seen": "2026-03-13", "source": "member-intel"}, + {"canonical": "county-title-search-agent", "name": "County title-search agent", "owner": null, "url": null, "introducer": "rcraw", "first_seen": "2026-02-27", "source": "member-intel"}, + {"canonical": "CraftsMan-Labs/SimpleAgents", "name": "SimpleAgents", "owner": "CraftsMan-Labs", "url": "https://github.com/CraftsMan-Labs/SimpleAgents", "introducer": "CraftsMan-Labs", "first_seen": "2026-04-09", "source": "member-intel"}, + {"canonical": "simpleagenttools", "name": "simpleAgentTools (PyPI)", "owner": null, "url": null, "introducer": "CraftsMan-Labs", "first_seen": "2026-04-09", "source": "member-intel"}, + {"canonical": "simpleflow-sdk", "name": "SimpleFlow SDK", "owner": null, "url": null, "introducer": "CraftsMan-Labs", "first_seen": "2026-04-09", "source": "member-intel"}, + {"canonical": "weftos", "name": "WeftOS", "owner": null, "url": null, "introducer": "Stuart Kerr", "first_seen": "2026-04-10", "source": "member-intel"}, + {"canonical": "vectorvroom", "name": "VectorVroom", "owner": null, "url": "https://vectorvroom.shaal.dev", "introducer": "shaal", "first_seen": "2026-04-24", "source": "member-intel"}, + {"canonical": "proffesor-for-testing/agentic-qe", "name": "agentic-qe", "owner": "proffesor-for-testing", "url": "https://github.com/proffesor-for-testing/agentic-qe", "introducer": "Dragan Spiridonov", "first_seen": "2026-04-30", "source": "member-intel"}, + {"canonical": "summarybot", "name": "SummaryBot", "owner": null, "url": null, "introducer": "mrjcleaver", "first_seen": "2026-05-20", "source": "gemini-notes-2026-05-20 (Issue #33)"}, + {"canonical": "video-sync", "name": "Video Sync tool", "owner": null, "url": null, "introducer": "mrjcleaver", "first_seen": "2026-06-10", "source": "gemini-notes-2026-06-10"}, + {"canonical": "manage.agentics.org", "name": "manage.agentics.org", "owner": null, "url": "https://manage.agentics.org", "introducer": "committee", "first_seen": "2026-06-24", "source": "gemini-notes-2026-06-24 (OIA pilot target)"} + ] +} diff --git a/data/meeting-repos.json b/data/meeting-repos.json new file mode 100644 index 0000000..3556ae5 --- /dev/null +++ b/data/meeting-repos.json @@ -0,0 +1,279 @@ +{ + "generated_at": "2026-07-13T00:00:00Z", + "provenance": "Curated seed from AF-Member-Intelligence (14 Zoom chat logs, Feb 19 - Apr 30 2026) + Gemini meeting notes (Apr 22 - Jul 1). For member-intel entries the introducer is association-inferred (the member the repo is associated with) pending verification against raw chat lines; source is 'member-intel'. Owners left null where unverified.", + "report": { + "parsed_sources": 0, + "not_yet_parsed": [ + "2026-05-06 OSS Google Meet transcript", + "2026-05-13 OSS Google Meet transcript", + "2026-05-20 OSS Google Meet transcript", + "2026-05-27 OSS Google Meet transcript", + "2026-06-03 OSS Google Meet transcript", + "2026-06-10 OSS Google Meet transcript", + "2026-06-24 OSS Google Meet transcript", + "2026-07-01 OSS Google Meet transcript", + "2026-07-08 OSS Google Meet transcript (Gemini notes failed)" + ], + "total_repos": 18 + }, + "entries": [ + { + "canonical": "agenticsorg/OIA-Model", + "name": "OIA-Model", + "owner": "agenticsorg", + "url": "https://github.com/agenticsorg/OIA-Model", + "introducer": "nicholas-ruest", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [] + }, + { + "canonical": "connectome-os", + "name": "Connectome-OS", + "owner": null, + "url": null, + "introducer": "ruvnet", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "globalbusinessadvisors/Synapse-Graph", + "name": "Synapse-Graph", + "owner": "globalbusinessadvisors", + "url": "https://github.com/globalbusinessadvisors/Synapse-Graph", + "introducer": "nicholas-ruest", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [] + }, + { + "canonical": "open-claude-code", + "name": "open-claude-code", + "owner": null, + "url": null, + "introducer": "ruvnet", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "ruview", + "name": "RuView", + "owner": null, + "url": null, + "introducer": "ruvnet", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "ruvnet/RuVector", + "name": "RuVector", + "owner": "ruvnet", + "url": "https://github.com/ruvnet/RuVector", + "introducer": "ruvnet", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [] + }, + { + "canonical": "ruvnet/ruflo", + "name": "ruflo", + "owner": "ruvnet", + "url": "https://github.com/ruvnet/ruflo", + "introducer": "ruvnet", + "first_seen": "2026-02-19", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [] + }, + { + "canonical": "county-title-search-agent", + "name": "County title-search agent", + "owner": null, + "url": null, + "introducer": "rcraw", + "first_seen": "2026-02-27", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "pi.ruv.io", + "name": "pi.ruv.io", + "owner": null, + "url": "https://pi.ruv.io", + "introducer": "Stuart Kerr", + "first_seen": "2026-03-13", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "CraftsMan-Labs/SimpleAgents", + "name": "SimpleAgents", + "owner": "CraftsMan-Labs", + "url": "https://github.com/CraftsMan-Labs/SimpleAgents", + "introducer": "CraftsMan-Labs", + "first_seen": "2026-04-09", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [] + }, + { + "canonical": "simpleagenttools", + "name": "simpleAgentTools (PyPI)", + "owner": null, + "url": null, + "introducer": "CraftsMan-Labs", + "first_seen": "2026-04-09", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "simpleflow-sdk", + "name": "SimpleFlow SDK", + "owner": null, + "url": null, + "introducer": "CraftsMan-Labs", + "first_seen": "2026-04-09", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "weftos", + "name": "WeftOS", + "owner": null, + "url": null, + "introducer": "Stuart Kerr", + "first_seen": "2026-04-10", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "vectorvroom", + "name": "VectorVroom", + "owner": null, + "url": "https://vectorvroom.shaal.dev", + "introducer": "shaal", + "first_seen": "2026-04-24", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "proffesor-for-testing/agentic-qe", + "name": "agentic-qe", + "owner": "proffesor-for-testing", + "url": "https://github.com/proffesor-for-testing/agentic-qe", + "introducer": "Dragan Spiridonov", + "first_seen": "2026-04-30", + "mention_count": 1, + "sources": [ + "member-intel" + ], + "flags": [] + }, + { + "canonical": "summarybot", + "name": "SummaryBot", + "owner": null, + "url": null, + "introducer": "mrjcleaver", + "first_seen": "2026-05-20", + "mention_count": 1, + "sources": [ + "gemini-notes-2026-05-20 (Issue #33)" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "video-sync", + "name": "Video Sync tool", + "owner": null, + "url": null, + "introducer": "mrjcleaver", + "first_seen": "2026-06-10", + "mention_count": 1, + "sources": [ + "gemini-notes-2026-06-10" + ], + "flags": [ + "owner-unverified" + ] + }, + { + "canonical": "manage.agentics.org", + "name": "manage.agentics.org", + "owner": null, + "url": "https://manage.agentics.org", + "introducer": "committee", + "first_seen": "2026-06-24", + "mention_count": 1, + "sources": [ + "gemini-notes-2026-06-24 (OIA pilot target)" + ], + "flags": [ + "owner-unverified" + ] + } + ] +} diff --git a/features/repo-registry.feature b/features/repo-registry.feature new file mode 100644 index 0000000..9cc8502 --- /dev/null +++ b/features/repo-registry.feature @@ -0,0 +1,117 @@ +# Agentics Foundation OSS Committee -- Meeting Repo Registry +# Source: Issue #11 (candidate pool for first consideration); +# AF meeting chats/transcripts + AF-Member-Intelligence (Feb-Jul 2026); +# OIA-as-attractor plan (2026-07-13) -- the registry is the warm-lead +# target list of repos to assess against OIA. +# Status: RED (no implementation) +# +# Purpose: turn "repos people mentioned in AF meetings" into a maintained, +# public-safe registry, each entry attributed to who introduced it and when, +# so the committee has a candidate pool it can grade against OIA. + +@issue-11 @registry +Feature: Meeting repo registry + As the OSS committee chair + I need every repo introduced in an AF meeting captured with its introducer and first-seen date + So that the committee has an auditable candidate pool to assess against OIA + + # ────────────────────────────────────────────────────────── + # Definitions (the judgment calls, made explicit) + # - A "mention" is one occurrence of a repo in one source. + # - The "introducer" is the speaker of the EARLIEST mention + # (first to bring it into a meeting), NOT the repo's owner. + # Owner is recorded separately when known. + # - "first_seen" is the date of the earliest mention. + # - A repo is identified by a canonical key: "owner/name" when a + # GitHub URL is known, else a slugged project name. + # ────────────────────────────────────────────────────────── + + # ---- Extraction ---- + + @extraction @url + Scenario: A GitHub URL in a chat line is captured as a mention + Given a source dated "2026-04-09" with the chat line "Rishub: check out https://github.com/CraftsMan-Labs/SimpleAgents" + When the registry is built + Then the registry contains a repo with canonical key "CraftsMan-Labs/SimpleAgents" + And its introducer is "Rishub" + And its first_seen date is "2026-04-09" + And its owner is "CraftsMan-Labs" + + @extraction @lexicon + Scenario: A known project named without a URL is captured via the lexicon + Given a source dated "2026-04-10" with the chat line "Stuart: WeftOS is the one to watch" + And the lexicon maps "WeftOS" to canonical key "weftos" + When the registry is built + Then the registry contains a repo with canonical key "weftos" + And its introducer is "Stuart" + + @extraction @noise + Scenario: The generic word "repo" alone does not create an entry + Given a source dated "2026-06-18" with the line "So you got a repo harness, point it at some random repo" + When the registry is built + Then the registry contains no repo for that line + + # ---- Attribution ---- + + @attribution @earliest-wins + Scenario: The introducer is the earliest speaker, later mentions do not override + Given a source dated "2026-03-13" with the chat line "Stuart: pi.ruv.io overview attached" + And a source dated "2026-04-24" with the chat line "Reuven: pi.ruv.io is my go-to" + When the registry is built + Then the repo "pi.ruv.io" has introducer "Stuart" + And its first_seen date is "2026-03-13" + And its mention_count is 2 + + @attribution @unknown-speaker + Scenario: A mention with no resolvable speaker is kept but flagged + Given a source dated "2026-05-20" with the line "https://github.com/ruvnet/ruflo was merged" + When the registry is built + Then the repo "ruvnet/ruflo" has introducer "unknown" + And the repo is flagged "introducer-unverified" + + # ---- Seed merge (Feb-Apr member-intelligence already extracted) ---- + + @seed @merge + Scenario: A curated seed entry merges with a later automated mention + Given a seed entry for "agenticsorg/OIA-Model" introduced by "nicholas-ruest" on "2026-02-19" + And a source dated "2026-06-24" mentioning "https://github.com/agenticsorg/OIA-Model" + When the registry is built + Then the registry contains exactly one repo "agenticsorg/OIA-Model" + And its introducer is "nicholas-ruest" + And its first_seen date is "2026-02-19" + And its mention_count is at least 2 + + # ---- Deduplication ---- + + @dedupe + Scenario: URL and lexicon references to the same repo collapse to one entry + Given a source dated "2026-04-09" with the chat line "Rishub: https://github.com/CraftsMan-Labs/SimpleAgents" + And the lexicon maps "SimpleAgents" to canonical key "CraftsMan-Labs/SimpleAgents" + And a source dated "2026-04-09" with the chat line "Martin: loving SimpleAgents" + When the registry is built + Then the registry contains exactly one repo "CraftsMan-Labs/SimpleAgents" + And its mention_count is 2 + + # ---- Output contract (public-safe projection) ---- + + @output @public-safe + Scenario: The published registry carries no personal contact information + Given any built registry + When it is written to the public output file + Then each entry has only the fields: canonical, name, owner, url, introducer, first_seen, mention_count, sources, flags + And no entry contains an email address or phone number + + @output @deterministic + Scenario: Rebuilding the same inputs produces the same registry + Given a fixed set of sources and a fixed seed + When the registry is built twice + Then the two registries are byte-identical except for a generated_at timestamp + + # ---- Coverage honesty ---- + + @coverage @honesty + Scenario: Sources that were not parsed are reported, never silently dropped + Given the May-July Google Meet transcripts are not available as parseable text + When the registry is built + Then the build report lists those meetings under "not_yet_parsed" + And the report states the count of sources actually parsed diff --git a/lib/repo-registry.js b/lib/repo-registry.js new file mode 100644 index 0000000..a81c729 --- /dev/null +++ b/lib/repo-registry.js @@ -0,0 +1,146 @@ +// Meeting repo registry parser. +// Implements features/repo-registry.feature. +// +// Core rule: the INTRODUCER is the earliest speaker to bring a repo into a +// meeting (not the repo's owner). Pure functions only (no Date/random) so the +// registry is deterministic; the CLI wrapper stamps generated_at. + +// ── Lexicon of known projects (alias -> canonical). Owners left null when +// unverified; the parser records null rather than guessing. Extend as needed. +export const LEXICON = [ + { canonical: 'agenticsorg/OIA-Model', name: 'OIA-Model', aliases: ['OIA-Model'], owner: 'agenticsorg', url: 'https://github.com/agenticsorg/OIA-Model' }, + { canonical: 'CraftsMan-Labs/SimpleAgents', name: 'SimpleAgents', aliases: ['SimpleAgents'], owner: 'CraftsMan-Labs', url: 'https://github.com/CraftsMan-Labs/SimpleAgents' }, + { canonical: 'ruvnet/RuVector', name: 'RuVector', aliases: ['RuVector'], owner: 'ruvnet', url: 'https://github.com/ruvnet/RuVector' }, + { canonical: 'ruvnet/ruflo', name: 'ruflo', aliases: ['ruflo'], owner: 'ruvnet', url: 'https://github.com/ruvnet/ruflo' }, + { canonical: 'proffesor-for-testing/agentic-qe', name: 'agentic-qe', aliases: ['agentic-qe'], owner: 'proffesor-for-testing', url: 'https://github.com/proffesor-for-testing/agentic-qe' }, + { canonical: 'globalbusinessadvisors/Synapse-Graph', name: 'Synapse-Graph', aliases: ['Synapse-Graph'], owner: 'globalbusinessadvisors', url: 'https://github.com/globalbusinessadvisors/Synapse-Graph' }, + { canonical: 'weftos', name: 'WeftOS', aliases: ['WeftOS'], owner: null, url: null }, + { canonical: 'connectome-os', name: 'Connectome-OS', aliases: ['Connectome-OS'], owner: null, url: null }, + { canonical: 'vectorvroom', name: 'VectorVroom', aliases: ['VectorVroom'], owner: null, url: 'https://vectorvroom.shaal.dev' }, +]; + +const PUBLIC_FIELDS = ['canonical', 'name', 'owner', 'url', 'introducer', 'first_seen', 'mention_count', 'sources', 'flags']; + +const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +// "Speaker: message" -> {speaker, text}. Only treats a short leading token +// followed by ": " as a speaker (avoids matching URLs like "https:"). +function parseSpeaker(line) { + const m = line.match(/^\s*([A-Za-z][\w .'-]{0,40}?):\s+(.*)$/); + if (m && !/^https?$/i.test(m[1])) return { speaker: m[1].trim(), text: m[2] }; + return { speaker: null, text: line }; +} + +function normalizeRepoName(name) { + return name.replace(/\.git$/i, '').replace(/[).,;:]+$/, ''); +} + +// Extract every repo mention from one source. Within a single line, the same +// canonical repo counts once (URL + lexicon of the same repo do not double). +function extractMentions(text, { date, source }, lexicon) { + const mentions = []; + const urlRe = /github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)/gi; + for (const rawLine of String(text).split(/\r?\n/)) { + const { speaker, text: body } = parseSpeaker(rawLine); + const seenThisLine = new Set(); + const push = (mention) => { + if (seenThisLine.has(mention.canonical)) return; + seenThisLine.add(mention.canonical); + mentions.push(mention); + }; + + // 1. GitHub URLs + let m; + urlRe.lastIndex = 0; + while ((m = urlRe.exec(rawLine)) !== null) { + const owner = m[1]; + const name = normalizeRepoName(m[2]); + push({ canonical: `${owner}/${name}`, name, owner, url: `https://github.com/${owner}/${name}`, speaker, date, source }); + } + + // 2. Lexicon aliases (word-ish boundary; excludes matches inside a URL path) + for (const entry of lexicon) { + for (const alias of entry.aliases) { + const re = new RegExp(`(? extractMentions(src.text, { date: src.date, source: src.source }, lexicon)), + ]; + + const groups = new Map(); + for (const men of mentions) { + if (!groups.has(men.canonical)) groups.set(men.canonical, []); + groups.get(men.canonical).push(men); + } + + const entries = []; + for (const [canonical, list] of groups) { + // earliest date; among ties, prefer a known speaker (seed or named). + const sorted = [...list].sort((a, b) => { + if (a.date !== b.date) return a.date < b.date ? -1 : 1; + const aKnown = a.speaker ? 0 : 1; + const bKnown = b.speaker ? 0 : 1; + return aKnown - bKnown; + }); + const earliest = sorted[0]; + const introducer = earliest.speaker || 'unknown'; + + const pick = (field) => list.map((x) => x[field]).find((v) => v != null) ?? null; + const flags = []; + if (introducer === 'unknown') flags.push('introducer-unverified'); + if (pick('owner') == null) flags.push('owner-unverified'); + + entries.push({ + canonical, + name: pick('name') ?? canonical.split('/').pop(), + owner: pick('owner'), + url: pick('url'), + introducer, + first_seen: earliest.date, + mention_count: list.length, + sources: [...new Set(list.map((x) => x.source))].sort(), + flags, + }); + } + + entries.sort((a, b) => (a.first_seen !== b.first_seen ? (a.first_seen < b.first_seen ? -1 : 1) : a.canonical < b.canonical ? -1 : 1)); + + return { entries, report: { parsed_sources: sources.length, not_yet_parsed: [...notYetParsed] } }; +} + +/** Project entries to the public-safe shape (whitelisted fields, PII-checked). */ +export function toPublicRegistry(entries) { + const clean = entries.map((e) => { + const out = {}; + for (const f of PUBLIC_FIELDS) out[f] = e[f]; + return out; + }); + const blob = JSON.stringify(clean); + if (/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/.test(blob)) throw new Error('public registry contains an email address'); + // Phone = a separator-joined run with >= 10 digits (ISO dates have only 8, so they do not trip this). + for (const run of blob.match(/\+?\d[\d\s().-]{7,}\d/g) || []) { + if (run.replace(/\D/g, '').length >= 10) throw new Error('public registry contains a phone number'); + } + return { entries: clean }; +} diff --git a/scripts/build-repo-registry.mjs b/scripts/build-repo-registry.mjs new file mode 100644 index 0000000..643938b --- /dev/null +++ b/scripts/build-repo-registry.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// Build the meeting repo registry -> data/meeting-repos.json (public-safe). +// Implements features/repo-registry.feature. +// +// Inputs: +// - data/meeting-repos-seed.json : curated Feb-Apr seed + not_yet_parsed list +// - optional --sources