{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,cAAc,GAAG,MAAM,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC3B,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,WAAW,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAqB5D;AA0BD;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAwBrD;AAqBD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,WAAW,CAmCzE;AAED,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAqB7D","sourcesContent":["/**\n * Tier 2 — offline drift check (§2.2).\n *\n * The adapters had drifted in fifteen places across two vendors and nothing\n * noticed. The defect was never a missing doc-fetcher; it was a missing\n * *signal*. This module is that signal: diff what the vendors document against\n * what {@link declaredVocabulary} says we understand, and report the gap.\n *\n * Two properties are non-negotiable, both inherited from Tier 0:\n *\n *  - **Nothing here runs at runtime.** The fetch happens in a CI job (see\n *    `scripts/plugin-drift-check.ts`). The network informs a human's decision to\n *    edit an adapter; it never informs a parse. That is the same rule\n *    `AGENTS.md` sets for `models.generated.ts`.\n *  - **A failed fetch is not a clean report.** Offline must be distinguishable\n *    from \"no drift\", or the check quietly stops checking the day the docs move\n *    behind a redirect.\n *\n * The extraction is deliberately conservative. Manifest keys come from JSON code\n * fences — actual manifest examples, not prose — because a regex over English\n * finds every word that happens to be backticked. Paths come from backticked\n * path-shaped tokens, which is noisier, so path findings are leads for a human\n * to confirm rather than assertions. A drift report nobody trusts gets muted,\n * and a muted report is worse than none.\n */\n\nimport { declaredVocabulary } from \"./formats/index.js\";\n\nexport interface DriftSource {\n\tlabel: string;\n\turl: string;\n\t/** Page text, or undefined when it could not be fetched. */\n\ttext?: string;\n\terror?: string;\n}\n\nexport interface DriftFinding {\n\tkind: \"manifest-key\" | \"path\";\n\tvalue: string;\n\t/** Which reference page it was seen in. */\n\tsource: string;\n}\n\nexport interface DriftReport {\n\t/** True only when every source was fetched *and* nothing new was found. */\n\tclean: boolean;\n\t/** Sources that could not be fetched. Non-empty means the report is incomplete. */\n\tunreachable: Array<{ label: string; error: string }>;\n\tfindings: DriftFinding[];\n\tcheckedSources: string[];\n}\n\n/** Top-level keys of every JSON object in a fenced ```json block. */\nexport function manifestKeysIn(markdown: string): Set<string> {\n\tconst keys = new Set<string>();\n\tfor (const match of markdown.matchAll(/```(?:jsonc?|json5)\\n([\\s\\S]*?)```/g)) {\n\t\tconst body = match[1];\n\t\t// Strip comments and trailing commas so documentation JSON (which is often\n\t\t// neither) still parses.\n\t\tconst cleaned = body\n\t\t\t.replace(/^\\s*\\/\\/.*$/gm, \"\")\n\t\t\t.replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\")\n\t\t\t.replace(/,(\\s*[}\\]])/g, \"$1\");\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(cleaned) as unknown;\n\t\t\tif (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n\t\t\t\tfor (const key of Object.keys(parsed)) keys.add(key);\n\t\t\t}\n\t\t} catch {\n\t\t\t// A fence that does not parse tells us nothing; guessing at its keys with\n\t\t\t// a regex is how a drift report fills up with noise.\n\t\t}\n\t}\n\treturn keys;\n}\n\n/**\n * Plugin component directories worth noticing on their own, without a file\n * extension to identify them.\n */\n/**\n * Files every repository has. They appear in vendor references as scaffolding\n * examples, never as a surface an adapter should parse.\n */\nconst GENERIC_FILES = new Set([\"README.md\", \"CHANGELOG.md\", \"LICENSE.md\", \"CLAUDE.md\", \"AGENTS.md\", \"package.json\"]);\n\nconst COMPONENT_DIR_NAMES = new Set([\n\t\"skills\",\n\t\"commands\",\n\t\"agents\",\n\t\"hooks\",\n\t\"themes\",\n\t\"workflows\",\n\t\"output-styles\",\n\t\"monitors\",\n\t\"bin\",\n\t\"prompts\",\n\t\"chatmodes\",\n]);\n\n/**\n * Backticked tokens that look like a **plugin-relative** file or directory path.\n *\n * Tight on purpose. The first cut of this accepted anything path-shaped and\n * produced twenty findings against the live Claude reference, of which none were\n * real drift: MCP method names (`roots/list`), repo slugs\n * (`anthropics/claude-plugins-community`), workspace paths (`.claude/settings.json`),\n * bare extensions (`.zip`), and example scripts. A drift report with that\n * signal-to-noise ratio gets muted, and a muted report is worse than none — so\n * the filter errs toward missing a real surface rather than crying wolf, and the\n * manifest-key half (which reads parsed JSON, not prose) carries the precision.\n */\nexport function pathsIn(markdown: string): Set<string> {\n\tconst paths = new Set<string>();\n\tfor (const match of markdown.matchAll(/`([^`\\s]+)`/g)) {\n\t\tconst token = match[1].replace(/^\\.\\//, \"\").replace(/\\/$/, \"\");\n\t\tif (!/^[\\w.@-]+(?:\\/[\\w.@-]+)*$/.test(token)) continue;\n\t\t// Workspace surfaces, not plugin surfaces: `.claude/settings.json` and\n\t\t// friends are §1.3's territory and are not what an adapter parses.\n\t\tif (token.startsWith(\".claude/\") || token.startsWith(\".github/workflows\")) continue;\n\t\tif (token.startsWith(\"..\")) continue;\n\n\t\tconst tail = token.slice(token.lastIndexOf(\"/\") + 1);\n\t\tif (GENERIC_FILES.has(tail)) continue;\n\t\t// A real surface is a config file or a component directory. Anything else\n\t\t// path-shaped in prose is an example, a slug, or a protocol method.\n\t\tconst isConfigFile = /\\.(json|jsonc|md|toml|ya?ml)$/.test(tail);\n\t\tconst isComponentDir = COMPONENT_DIR_NAMES.has(tail) || COMPONENT_DIR_NAMES.has(token);\n\t\tif (!isConfigFile && !isComponentDir) continue;\n\t\t// A single token with no separator has to stand on its own: a named config\n\t\t// file (`.mcp.json`, `plugin.json`) or a component directory. A bare\n\t\t// extension like `.zip` is neither.\n\t\tif (!token.includes(\"/\") && !isComponentDir && !/^\\.?[\\w-]+\\.(json|jsonc|md|toml|ya?ml)$/.test(token)) continue;\n\t\tpaths.add(token);\n\t}\n\treturn paths;\n}\n\n/**\n * Whether a documented path is one we already declare.\n *\n * Suffix match, because references write paths from an example plugin root\n * (`my-plugin/hooks/hooks.json`) while adapters declare them plugin-relative\n * (`hooks/hooks.json`). Comparing the two literally reports every documented\n * example as new.\n */\nfunction isDeclaredPath(documented: string, declared: ReadonlySet<string>): boolean {\n\tif (declared.has(documented)) return true;\n\tfor (const known of declared) {\n\t\tif (documented === known || documented.endsWith(`/${known}`) || known.endsWith(`/${documented}`)) return true;\n\t\t// A documented directory whose contents we already declare: the reference\n\t\t// writes `monitors`, the adapter declares `monitors/monitors.json`.\n\t\tif (known.startsWith(`${documented}/`) || documented.startsWith(`${known}/`)) return true;\n\t}\n\treturn false;\n}\n\n/**\n * Diff the fetched references against the declared vocabulary.\n *\n * Everything already declared — modelled, read, or knowingly unsupported — is\n * filtered out, so what remains answers one question: *is the vendor documenting\n * something we have never heard of?*\n */\nexport function analyzeDrift(sources: readonly DriftSource[]): DriftReport {\n\tconst declared = declaredVocabulary();\n\tconst knownKeys = new Set([...declared.manifestKeys, ...declared.marketplaceKeys]);\n\tconst knownPaths = new Set(\n\t\t[...declared.readPaths, ...declared.marketplaceFiles, ...declared.unsupportedSurfaces].map((p) =>\n\t\t\tp.replace(/\\\\/g, \"/\").replace(/\\/$/, \"\"),\n\t\t),\n\t);\n\n\tconst unreachable: DriftReport[\"unreachable\"] = [];\n\tconst findings: DriftFinding[] = [];\n\tconst checkedSources: string[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const source of sources) {\n\t\tif (!source.text) {\n\t\t\tunreachable.push({ label: source.label, error: source.error ?? \"not fetched\" });\n\t\t\tcontinue;\n\t\t}\n\t\tcheckedSources.push(source.label);\n\t\tfor (const key of manifestKeysIn(source.text)) {\n\t\t\tif (knownKeys.has(key) || seen.has(`k:${key}`)) continue;\n\t\t\tseen.add(`k:${key}`);\n\t\t\tfindings.push({ kind: \"manifest-key\", value: key, source: source.label });\n\t\t}\n\t\tfor (const p of pathsIn(source.text)) {\n\t\t\tif (isDeclaredPath(p, knownPaths) || seen.has(`p:${p}`)) continue;\n\t\t\tseen.add(`p:${p}`);\n\t\t\tfindings.push({ kind: \"path\", value: p, source: source.label });\n\t\t}\n\t}\n\n\t// Incomplete is never clean. A partial sweep that reports \"no drift\" is the\n\t// failure this tier exists to prevent, one level up.\n\treturn { clean: unreachable.length === 0 && findings.length === 0, unreachable, findings, checkedSources };\n}\n\n/** Render a report for a CI log or a human-opened issue. */\nexport function formatDriftReport(report: DriftReport): string {\n\tconst lines: string[] = [];\n\tif (report.checkedSources.length > 0) lines.push(`Checked: ${report.checkedSources.join(\", \")}`);\n\tfor (const u of report.unreachable) {\n\t\tlines.push(`UNREACHABLE ${u.label}: ${u.error} — this report is incomplete.`);\n\t}\n\tif (report.findings.length === 0) {\n\t\tlines.push(report.unreachable.length > 0 ? \"No drift in what could be read.\" : \"No drift.\");\n\t\treturn lines.join(\"\\n\");\n\t}\n\tlines.push(\"\", `${report.findings.length} candidate(s) documented upstream but not declared here:`);\n\tfor (const f of report.findings) {\n\t\tlines.push(`  [${f.kind}] ${f.value}  (${f.source})`);\n\t}\n\tlines.push(\n\t\t\"\",\n\t\t\"These are leads, not verdicts — path extraction reads prose and will surface\",\n\t\t\"examples alongside conventions. Confirm against the reference, then either\",\n\t\t\"model the surface in the adapter or add it to the knowingly-unsupported list.\",\n\t);\n\treturn lines.join(\"\\n\");\n}\n"]}