@skill-map/cli 0.16.3 → 0.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/tutorial/sm-tutorial.md +24 -10
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.js +5 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/{chunk-3NHGIRPN.js → chunk-NKC42FI7.js} +3 -3
- package/dist/ui/index.html +1 -1
- package/dist/ui/{main-ZNVY5M44.js → main-XSGTD7FQ.js} +1 -1
- package/package.json +5 -1
package/dist/kernel/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../kernel/i18n/registry.texts.ts","../../kernel/util/tx.ts","../../kernel/registry.ts","../../kernel/orchestrator.ts","../../package.json","../../kernel/adapters/in-memory-progress.ts","../../kernel/adapters/silent-logger.ts","../../kernel/util/logger.ts","../../kernel/adapters/plugin-loader.ts","../../kernel/util/ajv-interop.ts","../../kernel/i18n/plugin-store.texts.ts","../../kernel/adapters/plugin-store.ts","../../kernel/extensions/hook.ts","../../kernel/adapters/schema-validators.ts","../../kernel/i18n/orchestrator.texts.ts","../../kernel/scan/watcher.ts","../../kernel/scan/delta.ts","../../kernel/i18n/storage.texts.ts","../../kernel/scan/query.ts","../../kernel/ports/logger.ts","../../kernel/index.ts"],"sourcesContent":["/**\n * Strings emitted by `kernel/registry.ts`. Same `tx(template, vars)`\n * convention as every other `kernel/i18n/*.texts.ts` peer.\n *\n * These messages are thrown as `Error.message`; some surface to the user\n * via CLI verbs that catch them (e.g. `sm scan` registering manifests).\n */\n\nexport const REGISTRY_TEXTS = {\n duplicateExtension:\n 'Extension already registered: {{kind}}:{{qualifiedId}}',\n\n unknownKind:\n 'Unknown extension kind: {{kind}}',\n\n missingPluginId:\n 'Extension {{kind}}:{{id}} is missing pluginId; built-ins declare it directly, user plugins have it injected by PluginLoader.',\n} as const;\n","/**\n * `tx(template, vars)` — string interpolation for the project's text\n * tables (`*.texts.ts` files under `kernel/i18n/` and `cli/i18n/`).\n *\n * Templates use the `{{name}}` placeholder shape (Mustache / Handlebars\n * / Transloco compatible) so the same string tables drop into a real\n * i18n library on the day this project migrates.\n *\n * Contract:\n * - Every `{{name}}` token in `template` MUST have a matching key in\n * `vars`. A missing key throws — silent fallback would hide a\n * forgotten arg in a production build, which is the worst kind of\n * bug to chase down.\n * - Values can be `string | number`. `null` / `undefined` keys are\n * rejected; the caller is expected to coerce upstream (e.g. format\n * a missing path as `'(unknown)'` before passing).\n * - Whitespace inside the braces is tolerated (`{{ name }}`); the\n * parser strips it. This keeps long templates readable when wrapped\n * across multiple TS lines via `+`.\n * - Literal `{{` is not currently supported — no real text needs it.\n * Add escaping the day a template needs to render Handlebars-style\n * content.\n *\n * Plural / conditional logic does NOT live in the template. The caller\n * picks the correct template (e.g. `entries_singular` vs\n * `entries_plural`) or composes the variable value upstream and passes\n * the finished string. Keeping templates flat is the price for staying\n * Transloco-ready.\n */\n\nconst TOKEN_RE = /\\{\\{\\s*([A-Za-z][A-Za-z0-9_]*)\\s*\\}\\}/g;\n\nexport function tx(\n template: string,\n vars: Record<string, string | number> = {},\n): string {\n return template.replace(TOKEN_RE, (_match, name: string) => {\n if (!Object.prototype.hasOwnProperty.call(vars, name)) {\n throw new Error(\n `tx: missing variable \"${name}\" for template \"${template.slice(0, 80)}${template.length > 80 ? '…' : ''}\"`,\n );\n }\n const value = vars[name];\n if (value === null || value === undefined) {\n throw new Error(\n `tx: variable \"${name}\" is null/undefined for template \"${template.slice(0, 80)}${template.length > 80 ? '…' : ''}\"`,\n );\n }\n return String(value);\n });\n}\n","/**\n * Extension registry — six kinds, first-class, loaded through a single API.\n *\n * The `Extension` shape is aligned with `spec/schemas/extensions/base.schema.json`.\n * Kind-specific manifests (provider / extractor / rule / action / formatter /\n * hook) extend this base structurally; the registry stores the base view\n * and each kind's code carries its own fuller type where needed.\n *\n * **Spec § A.6 — qualified ids.** Every extension is keyed in the registry\n * by `<pluginId>/<id>` (e.g. `core/frontmatter`, `claude/slash`,\n * `hello-world/greet`). `Extension.id` carries the **short** id as authored;\n * `Extension.pluginId` carries the namespace; the registry composes the\n * qualifier internally and exposes lookup APIs that operate on either form\n * (qualified for direct lookup, kind-scoped listing for enumeration).\n *\n * Boot invariant: `new Registry()` is empty. `registry.totalCount() === 0`\n * when the kernel boots with zero extensions. This is the data side of the\n * `kernel-empty-boot` conformance contract.\n */\n\nimport { REGISTRY_TEXTS } from './i18n/registry.texts.js';\nimport type { Stability } from './types.js';\nimport { tx } from './util/tx.js';\n\nexport type ExtensionKind =\n | 'provider'\n | 'extractor'\n | 'rule'\n | 'action'\n | 'formatter'\n | 'hook';\n\nexport const EXTENSION_KINDS: readonly ExtensionKind[] = Object.freeze([\n 'provider',\n 'extractor',\n 'rule',\n 'action',\n 'formatter',\n 'hook',\n] as const);\n\nexport interface Extension {\n /** Short (unqualified) extension id as declared in the manifest. */\n id: string;\n /** Owning plugin namespace. Composed with `id` to form the qualified key. */\n pluginId: string;\n kind: ExtensionKind;\n version: string;\n description?: string;\n stability?: Stability;\n preconditions?: string[];\n entry?: string;\n}\n\n/**\n * Compose the qualified registry key for an extension. Single source of\n * truth so callers don't reinvent the format and a future change (e.g. a\n * different separator) lands in one place.\n */\nexport function qualifiedExtensionId(pluginId: string, id: string): string {\n return `${pluginId}/${id}`;\n}\n\nexport class DuplicateExtensionError extends Error {\n constructor(kind: ExtensionKind, qualifiedId: string) {\n super(tx(REGISTRY_TEXTS.duplicateExtension, { kind, qualifiedId }));\n this.name = 'DuplicateExtensionError';\n }\n}\n\nexport class Registry {\n /** kind → qualifiedId → Extension. */\n readonly #byKind: Map<ExtensionKind, Map<string, Extension>>;\n\n constructor() {\n this.#byKind = new Map(\n EXTENSION_KINDS.map((k) => [k, new Map<string, Extension>()]),\n );\n }\n\n register(ext: Extension): void {\n const bucket = this.#byKind.get(ext.kind);\n if (!bucket) {\n throw new Error(tx(REGISTRY_TEXTS.unknownKind, { kind: ext.kind }));\n }\n if (typeof ext.pluginId !== 'string' || ext.pluginId.length === 0) {\n throw new Error(tx(REGISTRY_TEXTS.missingPluginId, { kind: ext.kind, id: ext.id }));\n }\n const key = qualifiedExtensionId(ext.pluginId, ext.id);\n if (bucket.has(key)) {\n throw new DuplicateExtensionError(ext.kind, key);\n }\n bucket.set(key, ext);\n }\n\n /**\n * Lookup by qualified id (`<pluginId>/<id>`). Returns `undefined` when\n * no extension of that kind is registered under the qualifier.\n */\n get(kind: ExtensionKind, qualifiedId: string): Extension | undefined {\n return this.#byKind.get(kind)?.get(qualifiedId);\n }\n\n /**\n * Convenience wrapper that composes the qualified id for the caller.\n * Equivalent to `get(kind, qualifiedExtensionId(pluginId, id))`.\n */\n find(kind: ExtensionKind, pluginId: string, id: string): Extension | undefined {\n return this.get(kind, qualifiedExtensionId(pluginId, id));\n }\n\n all(kind: ExtensionKind): Extension[] {\n const bucket = this.#byKind.get(kind);\n return bucket ? [...bucket.values()] : [];\n }\n\n count(kind: ExtensionKind): number {\n return this.#byKind.get(kind)?.size ?? 0;\n }\n\n totalCount(): number {\n let n = 0;\n for (const bucket of this.#byKind.values()) n += bucket.size;\n return n;\n }\n}\n","/**\n * Scan orchestrator — runs the Provider → extractor → rule pipeline across\n * every registered extension and emits `ProgressEmitterPort` events in\n * canonical order. The callable extension set is injected via\n * `RunScanOptions.extensions` — the Registry holds manifest metadata, the\n * callable set holds the runtime instances the orchestrator actually\n * invokes. Separating the two lets `sm plugins` and `sm help` introspect\n * the graph without loading code.\n *\n * With zero registered extensions (or a callable set that carries none)\n * the pipeline still produces a valid zero-filled `ScanResult` — the\n * kernel-empty-boot invariant.\n *\n * Roots are validated up front: each entry of `RunScanOptions.roots`\n * must exist on disk as a directory. The first failure throws a clear\n * `Error` naming the offending path. This guards every caller (CLI,\n * server, skill-agent) against silently producing a zero-filled\n * `ScanResult` when a Provider walks a non-existent path — the bug\n * that wiped a populated DB via `sm scan -- --dry-run` (clipanion's\n * `--` made `--dry-run` a positional root that did not exist).\n *\n * Incremental scans: when `priorSnapshot` is supplied, the\n * orchestrator walks the filesystem, hashes each file, and reuses the\n * prior node + its prior-extracted internal links whenever both\n * `bodyHash` and `frontmatterHash` match. New / modified files run\n * through the full extractor pipeline (including the external-url-counter\n * which produces ephemeral pseudo-links). Rules ALWAYS run over the\n * fully merged graph — issue state can change even for an unchanged node\n * (e.g. a previously broken `references` link now resolves because a new\n * node was added). For unchanged nodes the prior `externalRefsCount` is\n * preserved as-is (the external pseudo-links were never persisted, so\n * they cannot be reconstructed; the count survived in the node row).\n *\n * Extractor output model (B.1, post-rename from Detector): extractors\n * return `void` and emit through three callbacks injected on the context:\n * - `ctx.emitLink(link)` → orchestrator validates against\n * `emitsLinkKinds` then partitions into internal / external buckets.\n * - `ctx.enrichNode(partial)` → orchestrator records ONE enrichment\n * entry per `(node, extractor)` so attribution survives into the DB.\n * Persisted into `node_enrichments` (A.8). The author-supplied\n * frontmatter on `node.frontmatter` stays immutable from any Extractor\n * — the enrichment layer is the only writable surface, and rules /\n * formatters consume it via `mergeNodeWithEnrichments`.\n * - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).\n * Wired by the driving adapter via `RunScanOptions.pluginStores`,\n * which the orchestrator looks up per-extractor by `pluginId` and\n * attaches to the context. The orchestrator never inspects what\n * plugins write through it; the wrapper handles AJV validation\n * when the manifest declared an output schema.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, statSync } from 'node:fs';\n\n// js-tiktoken ships CJS subpaths without explicit `.cjs` in the import\n// specifier — the lint rule's hard-coded extension matrix doesn't model\n// dual-package CJS subpath exports.\n// eslint-disable-next-line import-x/extensions\nimport { Tiktoken } from 'js-tiktoken/lite';\n// eslint-disable-next-line import-x/extensions\nimport cl100k_base from 'js-tiktoken/ranks/cl100k_base';\nimport yaml from 'js-yaml';\n\nimport pkg from '../package.json' with { type: 'json' };\n\nimport type { IIgnoreFilter } from './scan/ignore.js';\nimport type { Kernel } from './index.js';\nimport type {\n Confidence,\n Issue,\n Link,\n LinkKind,\n Node,\n ScanResult,\n ScanScannedBy,\n Severity,\n TripleSplit,\n} from './types.js';\nimport type {\n ProgressEmitterPort,\n ProgressEvent,\n} from './ports/progress-emitter.js';\nimport { InMemoryProgressEmitter } from './adapters/in-memory-progress.js';\nimport { log } from './util/logger.js';\nimport { installedSpecVersion } from './adapters/plugin-loader.js';\nimport type { IPluginStore } from './adapters/plugin-store.js';\nimport {\n buildProviderFrontmatterValidator,\n type IProviderFrontmatterValidator,\n} from './adapters/schema-validators.js';\nimport { ORCHESTRATOR_TEXTS } from './i18n/orchestrator.texts.js';\nimport { qualifiedExtensionId } from './registry.js';\nimport { tx } from './util/tx.js';\nimport type {\n IProvider,\n IRawNode,\n IExtractorContext,\n IExtractor,\n IHook,\n IHookContext,\n IRule,\n THookTrigger,\n} from './extensions/index.js';\n\n// Resolved once at module init so every scan reuses the same metadata.\n// `installedSpecVersion()` reads `@skill-map/spec/package.json` off disk;\n// failure is non-fatal — fall back to `'unknown'` and keep the field\n// shape spec-conformant (string).\nconst SCANNED_BY: ScanScannedBy = {\n name: 'skill-map',\n version: pkg.version,\n specVersion: resolveSpecVersionSafe(),\n};\n\nfunction resolveSpecVersionSafe(): string {\n try {\n return installedSpecVersion();\n } catch {\n return 'unknown';\n }\n}\n\nexport interface IScanExtensions {\n providers: IProvider[];\n extractors: IExtractor[];\n rules: IRule[];\n /**\n * Optional hooks (spec § A.11). When supplied, the orchestrator's\n * lifecycle dispatcher invokes deterministic hooks subscribed to one\n * of the eight hookable triggers in canonical order with the matching\n * event payload. Absent → no hooks fire (the scan still emits its\n * lifecycle events to `ProgressEmitterPort` for observability).\n * Probabilistic hooks are loaded but skipped here with a stderr\n * advisory until the job subsystem ships once the job subsystem ships.\n */\n hooks?: IHook[];\n}\n\n/**\n * Confidence-tagged plan to repoint `state_*` references from one node\n * path to another. Emitted by the rename heuristic during `runScan` and\n * consumed by `persistScanResult` so the FK migration runs inside the\n * same transaction as the scan zone replace-all.\n */\nexport interface RenameOp {\n from: string;\n to: string;\n confidence: 'high' | 'medium';\n}\n\nexport interface RunScanOptions {\n /**\n * Filesystem roots to walk. Spec requires `minItems: 1`; passing an\n * empty array makes `runScan` throw before any work happens.\n */\n roots: string[];\n emitter?: ProgressEmitterPort;\n /** Runtime extension instances. Absent → empty pipeline. */\n extensions?: IScanExtensions;\n /**\n * Scan scope. Defaults to `'project'`. The CLI flag wiring lands in\n * the config layer wiring; `runScan` already accepts the override\n * so plugins / tests can opt into `'global'` today.\n */\n scope?: 'project' | 'global';\n /**\n * Compute per-node token counts (frontmatter / body / total) using the\n * cl100k_base BPE (the modern OpenAI tokenizer used by GPT-4 / GPT-3.5).\n * Defaults to true. Set false to skip tokenization; `node.tokens` is\n * left undefined (spec-valid: the field is optional).\n */\n tokenize?: boolean;\n /**\n * Prior snapshot for two purposes (decoupled by design):\n *\n * 1. **Rename heuristic** (`spec/db-schema.md` §Rename detection):\n * always evaluated when `priorSnapshot` is supplied. The\n * heuristic compares prior vs current node paths and emits\n * high / medium / ambiguous / orphan classifications. This\n * runs on EVERY `sm scan` (with or without `--changed`) so\n * reorganising files always preserves history, never silently.\n *\n * 2. **Cache reuse** (`sm scan --changed`): only kicks in when\n * `enableCache: true` is also passed. With the flag set, nodes\n * whose `path` exists in the prior with both `bodyHash` and\n * `frontmatterHash` matching the freshly-computed hashes are\n * reused as-is (their internal links and `externalRefsCount`\n * survive); only new / modified nodes run through extractors.\n * Rules always re-run over the merged graph.\n *\n * Pass `null` (or omit) for a fresh scan with no rename detection.\n */\n priorSnapshot?: ScanResult | null;\n /**\n * Reuse unchanged nodes from `priorSnapshot` instead of re-running\n * extractors over them. Defaults to `false` so a plain `sm scan`\n * always re-walks deterministically. `sm scan --changed` flips this\n * to `true` for the perf win on unchanged files.\n *\n * Has no effect without `priorSnapshot`; setting it to `true` with\n * a null prior is a no-op (every file is \"new\").\n */\n enableCache?: boolean;\n /**\n * Filter that decides which paths the Providers skip. Composed by the\n * caller (typically the CLI) from bundled defaults + `config.ignore`\n * + `.skillmapignore`. Providers that omit this option fall back to\n * their own defensive defaults (just enough to keep `.git` /\n * `node_modules` out).\n */\n ignoreFilter?: IIgnoreFilter;\n /**\n * Promote frontmatter-validation findings from `warn` to `error`.\n * Defaults to false. The CLI surfaces this via `--strict` on `sm scan`\n * and the `scan.strict` config key. When false, the orchestrator\n * still emits a `frontmatter-invalid` issue per malformed file but\n * leaves the severity at `warn` so a clean scan exits 0; when true,\n * the same finding becomes `error` and the scan exits 1.\n */\n strict?: boolean;\n /**\n * Spec § A.9 — fine-grained Extractor cache breadcrumbs from the\n * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, bodyHashAtRun>>`.\n * Loaded from the `scan_extractor_runs` table by the CLI before\n * invoking `runScan`; absent / empty for a fresh DB or an out-of-band\n * caller that does not maintain a cache. Decoupled from `priorSnapshot`\n * because the runs live in a sibling table and are useful only when\n * `enableCache` is also set.\n *\n * Cache decision per `(node, extractor)`:\n * - body+frontmatter hashes match the prior node AND every currently-\n * registered extractor that applies to this kind has a matching\n * row → full skip, all prior outbound links reused.\n * - some applicable extractor lacks a matching row (newly registered,\n * or its prior run targeted a different body hash) → run only the\n * missing extractors, drop prior links whose `sources` map to any\n * missing extractor or to an extractor that is no longer registered.\n */\n priorExtractorRuns?: Map<string, Map<string, string>>;\n /**\n * Spec § A.12 — per-plugin storage wrappers exposed to extractors via\n * `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves\n * `ctx.store` undefined for that extractor (the existing contract).\n *\n * The kernel does not construct these — the driving adapter (CLI,\n * future server) builds them with `makePluginStore` from\n * `kernel/adapters/plugin-store.js` and threads them through. This\n * keeps the orchestrator persistence-agnostic (the wrapper supplies\n * its own persist callback) and lets tests inject a captured-call\n * mock without spinning up a DB.\n */\n pluginStores?: ReadonlyMap<string, IPluginStore>;\n}\n\n/**\n * Spec § A.9 — runs to persist into `scan_extractor_runs`. One entry\n * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided\n * \"this extractor is current for this body\". Includes both freshly-run\n * pairs (extractor invoked this scan) and reused pairs (cached node, the\n * extractor's prior run still applies to the same body hash). Excludes\n * obsolete pairs — extractors that ran in the prior but are no longer\n * registered — so a replace-all persist drops them automatically.\n */\nexport interface IExtractorRunRecord {\n nodePath: string;\n extractorId: string;\n bodyHashAtRun: string;\n ranAt: number;\n}\n\n/**\n * Spec § A.8 — universal enrichment layer.\n *\n * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor\n * produced via `ctx.enrichNode(...)` during the walk. Attribution is\n * preserved per-Extractor (rather than merged client-side as B.1 did)\n * so the persistence layer can:\n *\n * - upsert a single row per pair (stable PRIMARY KEY conflict on\n * re-extract);\n * - flag probabilistic rows `stale = 1` when the body changes between\n * scans (preserving the prior LLM cost);\n * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials\n * for last-write-wins per field at read time.\n *\n * `value` is the cumulative merge across every `enrichNode` call that\n * Extractor made for this node within this scan — multiple\n * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation\n * fold into a single row, but two different Extractors hitting the\n * same node yield two distinct rows.\n *\n * `isProbabilistic` is denormalised so the persistence layer's stale\n * flag query stays a single-table read; recomputing from the live\n * registry would force every read-path to thread the runtime extension\n * set through.\n */\nexport interface IEnrichmentRecord {\n nodePath: string;\n extractorId: string;\n bodyHashAtEnrichment: string;\n value: Partial<Node>;\n enrichedAt: number;\n isProbabilistic: boolean;\n}\n\n/**\n * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`\n * — the high- and medium-confidence renames the persistence layer must\n * apply to `state_*` rows inside the same tx as the scan zone replace-\n * all (per `spec/db-schema.md` §Rename detection). Most callers want\n * `runScan` (which returns just `ScanResult`); the CLI's `sm scan`\n * uses this variant so it can hand the ops off to `persistScanResult`.\n *\n * Also returns `extractorRuns` — the Spec § A.9 fine-grained cache\n * breadcrumbs the CLI persists into `scan_extractor_runs` so the next\n * incremental scan can decide per-(node, extractor) whether re-running\n * is required.\n */\nexport async function runScanWithRenames(\n _kernel: Kernel,\n options: RunScanOptions,\n): Promise<{\n result: ScanResult;\n renameOps: RenameOp[];\n extractorRuns: IExtractorRunRecord[];\n enrichments: IEnrichmentRecord[];\n}> {\n return runScanInternal(_kernel, options);\n}\n\nexport async function runScan(\n _kernel: Kernel,\n options: RunScanOptions,\n): Promise<ScanResult> {\n const { result } = await runScanInternal(_kernel, options);\n return result;\n}\n\n// eslint-disable-next-line complexity\nasync function runScanInternal(\n _kernel: Kernel,\n options: RunScanOptions,\n): Promise<{\n result: ScanResult;\n renameOps: RenameOp[];\n extractorRuns: IExtractorRunRecord[];\n enrichments: IEnrichmentRecord[];\n}> {\n validateRoots(options.roots);\n\n const start = Date.now();\n const scannedAt = start;\n const emitter = options.emitter ?? new InMemoryProgressEmitter();\n const exts = options.extensions ?? { providers: [], extractors: [], rules: [] };\n const hookDispatcher = makeHookDispatcher(exts.hooks ?? [], emitter);\n const tokenize = options.tokenize !== false;\n const scope: 'project' | 'global' = options.scope ?? 'project';\n const strict = options.strict === true;\n // Encoder is heavyweight to construct (loads the cl100k_base BPE table\n // once); reuse a single instance across the whole scan.\n const encoder = tokenize ? new Tiktoken(cl100k_base) : null;\n const prior = options.priorSnapshot ?? null;\n const enableCache = options.enableCache === true;\n // Spec § A.9 — `priorExtractorRuns === undefined` means the caller\n // doesn't track the fine-grained Extractor cache (legacy behaviour: out-\n // of-band tests, alternate driving adapters that have no DB). In that\n // case we fall back to the pre-A.9 model where the node-level body /\n // frontmatter hash check is sufficient and every applicable extractor\n // is assumed to have run against the prior body. Passing an explicit\n // (possibly empty) Map opts the caller into the fine-grained path.\n const priorExtractorRuns = options.priorExtractorRuns;\n\n const priorIndex = indexPriorSnapshot(prior);\n\n // Spec 0.8.0: each Provider owns its per-kind frontmatter\n // schemas. Compose a single AJV-backed validator over the live set of\n // Providers so the orchestrator can ask it directly during the walk.\n const providerFrontmatter = buildProviderFrontmatterValidator(exts.providers);\n\n const scanStartedEvent = makeEvent('scan.started', { roots: options.roots });\n emitter.emit(scanStartedEvent);\n await hookDispatcher.dispatch('scan.started', scanStartedEvent);\n\n const walked = await walkAndExtract({\n providers: exts.providers,\n extractors: exts.extractors,\n roots: options.roots,\n ...(options.ignoreFilter ? { ignoreFilter: options.ignoreFilter } : {}),\n emitter,\n encoder,\n strict,\n enableCache,\n prior,\n priorIndex,\n priorExtractorRuns,\n providerFrontmatter,\n pluginStores: options.pluginStores,\n });\n\n // External pseudo-links (target is http(s)://) drive `externalRefsCount`\n // and are then dropped: never persisted, never seen by rules, never in\n // result.links. The string-prefix check is the contract — see\n // external-url-counter/index.ts.\n recomputeLinkCounts(walked.nodes, walked.internalLinks);\n recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);\n\n // Spec § A.11 — Hook dispatch for `extractor.completed`. Aggregated:\n // one event per registered extractor, after the full walk completes.\n // The payload carries the qualified extractor id so a hook with a\n // `filter: { extractorId: '...' }` can target a single extractor.\n // No per-node fan-out — that lives in `scan.progress` which is\n // deliberately NOT hookable (too verbose).\n for (const extractor of exts.extractors) {\n const extractorId = qualifiedExtensionId(extractor.pluginId, extractor.id);\n const evt = makeEvent('extractor.completed', { extractorId });\n emitter.emit(evt);\n await hookDispatcher.dispatch('extractor.completed', evt);\n }\n\n // Rules ALWAYS re-run over the merged graph (no shortcut for\n // incremental scans): the issue set for an \"unchanged\" node can flip\n // when a sibling node changes.\n const issues = await runRules(exts.rules, walked.nodes, walked.internalLinks, emitter, hookDispatcher);\n // Frontmatter-invalid issues from the walk land here so the rename\n // heuristic (next pass) sees them and the final stats.issuesCount\n // reflects them.\n for (const issue of walked.frontmatterIssues) issues.push(issue);\n\n // Rename heuristic runs after rules so the merged graph is final. The\n // returned `RenameOp[]` flows through to `persistScanResult` so FK\n // migration lands inside the same tx as the scan zone replace-all.\n const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];\n\n const stats = {\n // `filesSkipped` is \"files walked but not classified by any Provider\".\n // Today every walked file IS classified by its Provider (the `claude`\n // Provider's `classify()` always returns a kind, falling back to\n // `'note'`), so this is always 0. Wired now so the field shape is\n // spec-conformant; meaningful once multiple Providers compete.\n filesWalked: walked.filesWalked,\n filesSkipped: 0,\n nodesCount: walked.nodes.length,\n linksCount: walked.internalLinks.length,\n issuesCount: issues.length,\n durationMs: Date.now() - start,\n };\n\n const scanCompletedEvent = makeEvent('scan.completed', { stats });\n emitter.emit(scanCompletedEvent);\n await hookDispatcher.dispatch('scan.completed', scanCompletedEvent);\n\n return {\n result: {\n schemaVersion: 1,\n scannedAt,\n scope,\n roots: options.roots,\n providers: exts.providers.map((a) => a.id),\n scannedBy: SCANNED_BY,\n nodes: walked.nodes,\n links: walked.internalLinks,\n issues,\n stats,\n },\n renameOps,\n extractorRuns: walked.extractorRuns,\n enrichments: walked.enrichments,\n };\n}\n\n/**\n * Validate every root exists as a directory BEFORE any IO, BEFORE the\n * tokenizer is constructed, BEFORE `scan.started` fires. Throws on the\n * first failure — single-error feedback is enough; the user fixes it\n * and re-runs. Without this guard the claude Provider's `walk()` swallows\n * ENOENT inside `readdir` and returns silently, which lets a non-existent\n * root produce a valid-looking zero-filled `ScanResult` — directly\n * enabling the `sm scan -- --dry-run` typo-trap that wipes a populated\n * DB.\n *\n * Spec contract (`scan-result.schema.json#/properties/roots/minItems: 1`):\n * a ScanResult must report at least one walked root. The CLI defaults\n * `roots` to `['.']` when no positional args are supplied, so the\n * empty-array branch is a programming error from the CLI surface.\n */\nfunction validateRoots(roots: string[]): void {\n if (roots.length === 0) {\n throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);\n }\n for (const root of roots) {\n if (!existsSync(root) || !statSync(root).isDirectory()) {\n throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));\n }\n }\n}\n\ninterface IPriorIndex {\n /** Prior nodes keyed by path so per-file lookup is O(1). */\n priorNodesByPath: Map<string, Node>;\n /** Set of every prior node path — used to disambiguate inverted\n * `supersedes` links (see `originatingNodeOf`). */\n priorNodePaths: Set<string>;\n /**\n * Prior internal links bucketed by **originating node** — the node\n * whose body / frontmatter the extractor was processing when it emitted\n * the link. For most kinds that equals `link.source`, but the\n * frontmatter extractor emits inverted `supersedes` links where the\n * originating node is `link.target`.\n */\n priorLinksByOriginating: Map<string, Link[]>;\n /**\n * Per-node frontmatter-invalid / -malformed issues from the prior — we\n * reuse them when the cache is hit, otherwise the incremental scan\n * would silently drop the warning that landed on the prior pass.\n */\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n}\n\n// eslint-disable-next-line complexity\nfunction indexPriorSnapshot(prior: ScanResult | null): IPriorIndex {\n const priorNodesByPath = new Map<string, Node>();\n const priorNodePaths = new Set<string>();\n const priorLinksByOriginating = new Map<string, Link[]>();\n const priorFrontmatterIssuesByNode = new Map<string, Issue[]>();\n if (!prior) {\n return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };\n }\n for (const node of prior.nodes) {\n priorNodesByPath.set(node.path, node);\n priorNodePaths.add(node.path);\n }\n for (const link of prior.links) {\n const key = originatingNodeOf(link, priorNodePaths);\n const list = priorLinksByOriginating.get(key);\n if (list) list.push(link);\n else priorLinksByOriginating.set(key, [link]);\n }\n for (const issue of prior.issues) {\n if (issue.ruleId !== 'frontmatter-invalid' && issue.ruleId !== 'frontmatter-malformed') continue;\n if (issue.nodeIds.length !== 1) continue;\n const path = issue.nodeIds[0]!;\n const list = priorFrontmatterIssuesByNode.get(path);\n if (list) list.push(issue);\n else priorFrontmatterIssuesByNode.set(path, [issue]);\n }\n return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };\n}\n\ninterface IWalkAndExtractOptions {\n providers: IProvider[];\n extractors: IExtractor[];\n roots: string[];\n ignoreFilter?: IIgnoreFilter;\n emitter: ProgressEmitterPort;\n encoder: Tiktoken | null;\n strict: boolean;\n enableCache: boolean;\n prior: ScanResult | null;\n priorIndex: IPriorIndex;\n /**\n * Spec § A.9 — fine-grained Extractor cache breadcrumbs from the\n * prior scan, keyed `nodePath → qualifiedExtractorId → bodyHashAtRun`.\n * `undefined` opts out of the fine-grained path (legacy callers that\n * don't track the cache); the orchestrator falls back to the pre-A.9\n * node-level cache check.\n */\n priorExtractorRuns: Map<string, Map<string, string>> | undefined;\n providerFrontmatter: IProviderFrontmatterValidator;\n /**\n * Spec § A.12 — per-plugin `ctx.store` wrappers, keyed by `pluginId`.\n * Threaded through to `runExtractorsForNode → buildExtractorContext`\n * unchanged. `undefined` keeps `ctx.store` undefined for every\n * extractor (the legacy contract).\n */\n pluginStores: ReadonlyMap<string, IPluginStore> | undefined;\n}\n\ninterface IWalkAndExtractResult {\n nodes: Node[];\n internalLinks: Link[];\n externalLinks: Link[];\n /** Node paths reused verbatim from the prior snapshot. Their\n * `externalRefsCount` must NOT be zeroed before recomputation. */\n cachedPaths: Set<string>;\n /** Frontmatter-validation findings collected during the walk; the\n * composer appends these to the rule-emitted issue list so the\n * final ordering stays \"rules first, then derived issues\". */\n frontmatterIssues: Issue[];\n /**\n * Spec § A.8 — per-extractor enrichment records collected from\n * `ctx.enrichNode(...)` calls during the walk. One entry per\n * `(nodePath, extractorId)` pair an Extractor enriched. The\n * persistence layer upserts these into `node_enrichments`; the\n * read-side `mergeNodeWithEnrichments` helper combines them with\n * the author frontmatter for rule consumption.\n *\n * Attribution is preserved per-Extractor: two Extractors enriching\n * the same node produce two records, not one merged value. If a\n * single Extractor calls `ctx.enrichNode(...)` multiple times within\n * one `extract()` invocation, the partials fold into one record's\n * `value` (last-write-wins per field).\n */\n enrichments: IEnrichmentRecord[];\n /** Every `IRawNode` a Provider yielded across the whole scan\n * (including cached reuse). With one Provider it equals\n * `nodesCount`; with future multi-Provider scans walking overlapping\n * roots it can diverge. */\n filesWalked: number;\n /**\n * Spec § A.9 — the rows the persistence layer writes into\n * `scan_extractor_runs`. Includes both freshly-run pairs (extractor\n * invoked this scan) and reused pairs (cached node, the extractor's\n * prior run still applies to the same body hash). Excludes obsolete\n * pairs (extractor was uninstalled since the prior scan).\n */\n extractorRuns: IExtractorRunRecord[];\n}\n\n/**\n * Run a set of extractors against a single node, collecting their link\n * emissions and node-enrichment partials. Each extractor is invoked\n * exactly once with a fresh `IExtractorContext`. Caller decides what\n * to do with the returned arrays (push into per-scan buffers, write to\n * a focused refresh result, etc.).\n *\n * Exported so `cli/commands/refresh.ts` can reuse the same wiring it\n * needs for re-running a single extractor against a single node — the\n * pre-extraction code in `refresh.ts` was hand-duplicating this loop\n * (audit item V4).\n *\n * Within this call, multiple `enrichNode(partial)` calls from the same\n * extractor against the same node fold into one record (last-write-wins\n * per field) — same contract as the in-scan path.\n */\nexport async function runExtractorsForNode(opts: {\n extractors: IExtractor[];\n node: Node;\n body: string;\n frontmatter: Record<string, unknown>;\n bodyHash: string;\n emitter: ProgressEmitterPort;\n /**\n * Spec § A.12 — per-plugin `ctx.store` wrappers keyed by `pluginId`.\n * The map's lookup is per-extractor inside the loop, so callers that\n * don't track plugin storage can omit it; the resulting `ctx.store`\n * stays `undefined` (the existing contract).\n */\n pluginStores?: ReadonlyMap<string, IPluginStore>;\n}): Promise<{\n internalLinks: Link[];\n externalLinks: Link[];\n enrichments: IEnrichmentRecord[];\n}> {\n const internalLinks: Link[] = [];\n const externalLinks: Link[] = [];\n const enrichmentBuffer = new Map<string, IEnrichmentRecord>();\n\n for (const extractor of opts.extractors) {\n const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);\n const isProb = extractor.mode === 'probabilistic';\n const emitLink = (link: Link): void => {\n const validated = validateLink(extractor, link, opts.emitter);\n if (!validated) return;\n if (isExternalUrlLink(validated)) externalLinks.push(validated);\n else internalLinks.push(validated);\n };\n const enrichNode = (partial: Partial<Node>): void => {\n const key = `${opts.node.path}\\x00${qualifiedId}`;\n const existing = enrichmentBuffer.get(key);\n if (existing) {\n existing.value = { ...existing.value, ...partial };\n existing.enrichedAt = Date.now();\n } else {\n enrichmentBuffer.set(key, {\n nodePath: opts.node.path,\n extractorId: qualifiedId,\n bodyHashAtEnrichment: opts.bodyHash,\n value: { ...partial },\n enrichedAt: Date.now(),\n isProbabilistic: isProb,\n });\n }\n };\n const store = opts.pluginStores?.get(extractor.pluginId);\n const ctx = buildExtractorContext(\n extractor,\n opts.node,\n opts.body,\n opts.frontmatter,\n emitLink,\n enrichNode,\n store,\n );\n await extractor.extract(ctx);\n }\n\n return {\n internalLinks,\n externalLinks,\n enrichments: Array.from(enrichmentBuffer.values()),\n };\n}\n\n/**\n * Compute the per-(node, extractor) cache decision for a single node.\n * Returns:\n * - `applicableExtractors` — extractors whose `applicableKinds`\n * accepts this node's kind (or unrestricted).\n * - `applicableQualifiedIds` — set of qualified ids of the above.\n * - `cachedQualifiedIds` — applicable extractors whose prior run for\n * this node's body hash is still valid.\n * - `missingExtractors` — applicable extractors that need to run.\n * - `fullCacheHit` — true iff the node-level hash matched AND every\n * applicable extractor is cached (nothing to re-extract).\n *\n * Legacy fallback: when `priorExtractorRuns === undefined` the caller\n * did not load fine-grained breadcrumbs (out-of-band tests, alternate\n * driving adapters); we treat every applicable extractor as cached\n * when the node-level hashes match — preserves the pre-A.9 contract.\n */\n// eslint-disable-next-line complexity\nfunction computeCacheDecision(opts: {\n extractors: IExtractor[];\n kind: string;\n nodePath: string;\n bodyHash: string;\n nodeHashCacheEligible: boolean;\n priorExtractorRuns: Map<string, Map<string, string>> | undefined;\n}): {\n applicableExtractors: IExtractor[];\n applicableQualifiedIds: Set<string>;\n cachedQualifiedIds: Set<string>;\n missingExtractors: IExtractor[];\n fullCacheHit: boolean;\n} {\n const applicableExtractors = opts.extractors.filter(\n (ex) => ex.applicableKinds === undefined || ex.applicableKinds.includes(opts.kind),\n );\n const applicableQualifiedIds = new Set(\n applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id)),\n );\n const cachedQualifiedIds = new Set<string>();\n const missingExtractors: IExtractor[] = [];\n\n if (opts.priorExtractorRuns === undefined) {\n if (opts.nodeHashCacheEligible) {\n for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);\n } else {\n for (const ex of applicableExtractors) missingExtractors.push(ex);\n }\n } else {\n const priorRunsForNode = opts.priorExtractorRuns.get(opts.nodePath) ?? new Map<string, string>();\n for (const ex of applicableExtractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n const priorBody = priorRunsForNode.get(qualified);\n if (opts.nodeHashCacheEligible && priorBody === opts.bodyHash) {\n cachedQualifiedIds.add(qualified);\n } else {\n missingExtractors.push(ex);\n }\n }\n }\n\n return {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds,\n missingExtractors,\n fullCacheHit: opts.nodeHashCacheEligible && missingExtractors.length === 0,\n };\n}\n\n/**\n * Shallow-clone a prior node, reshape its outbound internal links per\n * A.9 source rules, and re-emit its prior frontmatter issues. Shared\n * by the full-cache-hit and partial-cache-hit branches of\n * `walkAndExtract`.\n *\n * Reshape rules (A.9 sources):\n * - missing source (extractor will re-emit) → drop link\n * - all-obsolete sources → drop link\n * - cached + obsolete → trim obsolete from `sources`\n * - cached only → keep verbatim\n */\nfunction cloneNodeAndReshapeLinks(opts: {\n priorNode: Node;\n strict: boolean;\n cachedQualifiedIds: Set<string>;\n applicableQualifiedIds: Set<string>;\n shortIdToQualified: Map<string, string[]>;\n priorLinksByOriginating: Map<string, Link[]>;\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n}): { node: Node; internalLinks: Link[]; frontmatterIssues: Issue[] } {\n // Shallow-clone to avoid mutating the caller's prior snapshot when\n // `recomputeLinkCounts` resets per-node counts later.\n const node: Node = { ...opts.priorNode, bytes: { ...opts.priorNode.bytes } };\n if (opts.priorNode.tokens) node.tokens = { ...opts.priorNode.tokens };\n\n const internalLinks: Link[] = [];\n const reusedLinks = opts.priorLinksByOriginating.get(opts.priorNode.path) ?? [];\n for (const link of reusedLinks) {\n const reshaped = reuseCachedLink(\n link,\n opts.shortIdToQualified,\n opts.cachedQualifiedIds,\n opts.applicableQualifiedIds,\n );\n if (reshaped) internalLinks.push(reshaped);\n }\n\n // Re-emit prior frontmatter issues unchanged (frontmatter hash is\n // unchanged in both cache branches). `strict` can promote\n // `warn → error` retroactively.\n const frontmatterIssues: Issue[] = [];\n const reusedFm = opts.priorFrontmatterIssuesByNode.get(opts.priorNode.path) ?? [];\n for (const issue of reusedFm) {\n frontmatterIssues.push({ ...issue, severity: opts.strict ? 'error' : 'warn' });\n }\n\n return { node, internalLinks, frontmatterIssues };\n}\n\n/**\n * Build the reused-node bundle for a node that fully cache-hit (body\n * + frontmatter unchanged AND every applicable extractor still has a\n * matching `scan_extractor_runs` row). Caller pushes the returned\n * arrays into its scan-wide buffers and emits the progress event.\n *\n * Adds `extractorRuns` rows for every still-cached extractor so the\n * cache survives the next replace-all persist.\n */\nfunction reusePriorNode(opts: {\n priorNode: Node;\n bodyHash: string;\n strict: boolean;\n cachedQualifiedIds: Set<string>;\n applicableQualifiedIds: Set<string>;\n shortIdToQualified: Map<string, string[]>;\n priorLinksByOriginating: Map<string, Link[]>;\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n}): {\n node: Node;\n internalLinks: Link[];\n frontmatterIssues: Issue[];\n extractorRuns: IExtractorRunRecord[];\n} {\n const base = cloneNodeAndReshapeLinks(opts);\n\n // Persist one `scan_extractor_runs` row per still-cached pair so the\n // cache survives the next replace-all persist (without this, cached\n // pairs silently disappear).\n const ranAt = Date.now();\n const extractorRuns: IExtractorRunRecord[] = [];\n for (const qualified of opts.cachedQualifiedIds) {\n extractorRuns.push({\n nodePath: opts.priorNode.path,\n extractorId: qualified,\n bodyHashAtRun: opts.bodyHash,\n ranAt,\n });\n }\n\n return { ...base, extractorRuns };\n}\n\n/**\n * Build a brand-new `Node` row from raw provider output and validate\n * its frontmatter. Used by the \"no cache hit\" branch of\n * `walkAndExtract`. Two frontmatter issue paths:\n * - With a frontmatter fence: AJV-validate against the Provider's\n * per-kind schema.\n * - Without a fence but a body that opens with malformed `---`:\n * emit `frontmatter-malformed`.\n *\n * Severity defaults to `warn`; `strict` promotes everything to `error`.\n */\nfunction buildFreshNodeAndValidateFrontmatter(opts: {\n raw: IRawNode;\n kind: string;\n provider: IProvider;\n bodyHash: string;\n frontmatterHash: string;\n encoder: Tiktoken | null;\n providerFrontmatter: IProviderFrontmatterValidator;\n strict: boolean;\n}): { node: Node; frontmatterIssues: Issue[] } {\n const node = buildNode({\n path: opts.raw.path,\n kind: opts.kind,\n providerId: opts.provider.id,\n frontmatterRaw: opts.raw.frontmatterRaw,\n body: opts.raw.body,\n frontmatter: opts.raw.frontmatter,\n bodyHash: opts.bodyHash,\n frontmatterHash: opts.frontmatterHash,\n encoder: opts.encoder,\n });\n\n const frontmatterIssues: Issue[] = [];\n if (opts.raw.frontmatterRaw.length > 0) {\n const fmIssue = validateFrontmatter(\n opts.providerFrontmatter,\n opts.provider,\n opts.kind,\n opts.raw.frontmatter,\n opts.raw.path,\n opts.strict,\n );\n if (fmIssue) frontmatterIssues.push(fmIssue);\n } else {\n const malformed = detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict);\n if (malformed) frontmatterIssues.push(malformed);\n }\n\n return { node, frontmatterIssues };\n}\n\n// Main scan loop — for each provider/raw node: hash, classify, decide\n// cache (full / partial / none), reuse or build, run extractors,\n// record runs. Helpers extracted (`computeCacheDecision`,\n// `cloneNodeAndReshapeLinks`, `reusePriorNode`,\n// `buildFreshNodeAndValidateFrontmatter`, `runExtractorsForNode`)\n// already encapsulate the heavy-lift; remaining branching is the\n// dispatch glue that ties them together per-iteration.\n// eslint-disable-next-line complexity\nasync function walkAndExtract(opts: IWalkAndExtractOptions): Promise<IWalkAndExtractResult> {\n const {\n providers,\n extractors,\n roots,\n ignoreFilter,\n emitter,\n encoder,\n strict,\n enableCache,\n prior,\n priorIndex,\n priorExtractorRuns,\n providerFrontmatter,\n pluginStores,\n } = opts;\n const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = priorIndex;\n\n const nodes: Node[] = [];\n const internalLinks: Link[] = [];\n const externalLinks: Link[] = [];\n const cachedPaths = new Set<string>();\n const frontmatterIssues: Issue[] = [];\n // A.8 enrichment buffer. `ctx.enrichNode(partial)` calls fold into a\n // per-Extractor entry keyed by `(nodePath, qualifiedExtractorId)` so the\n // persistence layer can upsert exactly one row per pair into\n // `node_enrichments`. Attribution survives across scans, which lets:\n // - the stale flag query single-table on (extractor_id, body_hash);\n // - `sm refresh` re-run only the Extractor whose row is stale;\n // - the read-time merge sort by `enriched_at` for last-write-wins.\n // Within a single `extract()` invocation, multiple enrichNode calls fold\n // into the same record's `value` (last-write-wins per field).\n const enrichmentBuffer = new Map<string, IEnrichmentRecord>();\n // Spec § A.9 — accumulator for `scan_extractor_runs`. One row per\n // (nodePath, qualifiedExtractorId) pair the orchestrator decided \"this\n // extractor is current for this body\". Includes both freshly-run pairs\n // and pairs whose prior run was reused intact via the cache.\n const extractorRuns: IExtractorRunRecord[] = [];\n let filesWalked = 0;\n let index = 0;\n const walkOptions = ignoreFilter ? { ignoreFilter } : {};\n\n // Build the short→qualified id map once for the whole scan. Used to\n // bridge between author-supplied `link.sources` (short id, e.g.\n // `'slash'`) and the qualified ids (`'claude/slash'`) that drive cache\n // bookkeeping. Multiple plugins can in theory expose extractors with\n // the same short id; we keep all qualifieds per short id so the\n // partial-cache filter recognises any of them as \"still cached\".\n const shortIdToQualified = new Map<string, string[]>();\n for (const ex of extractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n const list = shortIdToQualified.get(ex.id);\n if (list) list.push(qualified);\n else shortIdToQualified.set(ex.id, [qualified]);\n }\n\n for (const provider of providers) {\n for await (const raw of provider.walk(roots, walkOptions)) {\n filesWalked += 1;\n const bodyHash = sha256(raw.body);\n // Canonical-form rationale — hash a CANONICAL form of the frontmatter so a YAML\n // formatter pass (re-indent, sort keys, normalise trailing\n // newline, swap single↔double quotes) doesn't break the\n // medium-confidence rename heuristic. Fallback to raw text when\n // canonicalisation produces empty (parse failed but raw is\n // non-empty) so a malformed-YAML file still hashes\n // deterministically against itself.\n const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));\n const priorNode = priorNodesByPath.get(raw.path);\n // Cache reuse is gated on the explicit `enableCache` option (Step\n // 5.8). The presence of a `prior` alone is no longer enough — a\n // plain `sm scan` always re-walks deterministically; only\n // `sm scan --changed` flips `enableCache` on. The rename heuristic\n // uses `prior` independently of `enableCache`.\n //\n // Spec § A.9 layered the per-(node, extractor) check on top of\n // the existing per-node body+frontmatter check. The node-level\n // hashes still gate cache eligibility (a body change forces a full\n // re-extract regardless of which extractors were registered);\n // within an eligible node we then ask \"did every currently-applicable\n // extractor run against this body hash already?\". A new extractor\n // registered between scans yields a partial hit: we run only the\n // newcomer.\n const nodeHashCacheEligible =\n enableCache &&\n prior !== null &&\n priorNode !== undefined &&\n priorNode.bodyHash === bodyHash &&\n priorNode.frontmatterHash === frontmatterHash;\n\n const kind = provider.classify(raw.path, raw.frontmatter);\n index += 1;\n\n // Per-node, per-extractor cache decision (only meaningful when the\n // node-level hashes already matched). For each extractor that\n // applies to this kind, ask whether the prior runs map already\n // records an entry against the current body hash. Missing entries\n // run; satisfied entries are skipped.\n //\n // Legacy fallback: when `priorExtractorRuns === undefined` the\n // caller did not load the fine-grained breadcrumbs (out-of-band\n // tests, alternate driving adapters), so we treat every applicable\n // extractor as cached when the node-level hashes match. This\n // preserves the pre-A.9 contract for callers that did not opt in.\n const cacheDecision = computeCacheDecision({\n extractors,\n kind,\n nodePath: raw.path,\n bodyHash,\n nodeHashCacheEligible,\n priorExtractorRuns,\n });\n const {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds,\n missingExtractors,\n fullCacheHit,\n } = cacheDecision;\n\n if (fullCacheHit && priorNode) {\n const reused = reusePriorNode({\n priorNode,\n bodyHash,\n strict,\n cachedQualifiedIds,\n applicableQualifiedIds,\n shortIdToQualified,\n priorLinksByOriginating,\n priorFrontmatterIssuesByNode,\n });\n nodes.push(reused.node);\n cachedPaths.add(reused.node.path);\n for (const link of reused.internalLinks) internalLinks.push(link);\n for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);\n for (const run of reused.extractorRuns) extractorRuns.push(run);\n emitter.emit(makeEvent('scan.progress', { index, path: raw.path, kind, cached: true }));\n continue;\n }\n\n // --- partial or full re-extract path -------------------------------\n // Either a brand-new node, a node whose body / frontmatter changed,\n // or a node whose hashes match but at least one applicable\n // extractor lacks a matching `scan_extractor_runs` row (newly\n // registered, or its prior run was against a different body hash).\n\n let node: Node;\n const partialCacheHit =\n nodeHashCacheEligible && cachedQualifiedIds.size > 0 && priorNode !== undefined;\n if (partialCacheHit && priorNode) {\n // Body / frontmatter unchanged AND at least one extractor is\n // still cached; reuse the prior node row + reshape its links\n // and frontmatter issues. NOT marking the path as `cachedPaths`\n // because some extraction is happening — the `externalRefsCount`\n // recompute wants the node re-derived from a fresh extractor\n // pass (the missing extractor may emit URLs).\n const partial = cloneNodeAndReshapeLinks({\n priorNode, strict, cachedQualifiedIds, applicableQualifiedIds,\n shortIdToQualified, priorLinksByOriginating, priorFrontmatterIssuesByNode,\n });\n node = partial.node;\n for (const link of partial.internalLinks) internalLinks.push(link);\n for (const issue of partial.frontmatterIssues) frontmatterIssues.push(issue);\n nodes.push(node);\n } else {\n const fresh = buildFreshNodeAndValidateFrontmatter({\n raw, kind, provider, bodyHash, frontmatterHash, encoder,\n providerFrontmatter, strict,\n });\n node = fresh.node;\n nodes.push(node);\n for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);\n }\n emitter.emit(makeEvent('scan.progress', {\n index,\n path: raw.path,\n kind,\n cached: false,\n ...(partialCacheHit ? { partialCache: true } : {}),\n }));\n\n // Decide which extractors actually run. Full re-extract → all\n // applicable. Partial cache → only the missing ones. Either way,\n // the orchestrator records a fresh `scan_extractor_runs` row for\n // each invocation AND for each cached extractor whose contribution\n // survived intact (so the cache persists across scans).\n const extractorsToRun = partialCacheHit ? missingExtractors : applicableExtractors;\n const extractResult = await runExtractorsForNode({\n extractors: extractorsToRun,\n node,\n body: raw.body,\n frontmatter: raw.frontmatter,\n bodyHash,\n emitter,\n ...(pluginStores ? { pluginStores } : {}),\n });\n for (const link of extractResult.internalLinks) internalLinks.push(link);\n for (const link of extractResult.externalLinks) externalLinks.push(link);\n // Merge per-node enrichment records into the scan-wide buffer.\n // Keys are `${nodePath}\\x00${extractorId}` and unique per node\n // (paths are unique across the scan), so `set()` is collision-free\n // — but we keep the keyed shape in case future code wants to fold\n // across providers walking the same node.\n for (const enr of extractResult.enrichments) {\n enrichmentBuffer.set(`${enr.nodePath}\\x00${enr.extractorId}`, enr);\n }\n\n // Persist a `scan_extractor_runs` row for every applicable\n // extractor (both freshly-run AND cached ones whose contribution\n // we reused). Skipping cached entries here would let the\n // replace-all persist forget them — defeating the whole point of\n // the partial-cache path.\n const ranAt = Date.now();\n for (const ex of applicableExtractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n extractorRuns.push({\n nodePath: node.path,\n extractorId: qualified,\n bodyHashAtRun: bodyHash,\n ranAt,\n });\n }\n }\n }\n\n return {\n nodes,\n internalLinks,\n externalLinks,\n cachedPaths,\n frontmatterIssues,\n filesWalked,\n enrichments: [...enrichmentBuffer.values()],\n extractorRuns,\n };\n}\n\n/**\n * Spec § A.9 — decide whether a prior link can be reused on a cached\n * node, and how its `sources` array should be reshaped.\n *\n * Three buckets per source short id:\n * - **Cached**: short id maps to a currently-registered qualified id\n * that has a matching `scan_extractor_runs` row for this body hash.\n * The contribution is fresh and survives.\n * - **Missing**: short id maps to a currently-registered qualified id\n * that does NOT have a matching row for this body hash (newly\n * registered, or its prior run targeted a different body). The\n * missing extractor is about to run and will re-emit its own link\n * row, so we drop the prior link entirely to avoid duplicates.\n * - **Obsolete**: short id maps to no currently-registered qualified\n * id at all (the extractor was uninstalled). The contribution is\n * stranded but harmless — we strip the obsolete short id from\n * `sources` and keep the link if at least one cached source remains.\n *\n * Decision rules:\n * - Any missing source → return `null` (drop the link).\n * - All cached, no obsolete → return the link as-is.\n * - Cached + obsolete (no missing) → return a clone with obsolete\n * sources filtered out.\n * - All obsolete (no cached, no missing) → return `null` (no live\n * extractor still claims this link).\n *\n * Source-id mapping caveat: `link.sources` carries the short id the\n * extractor author wrote (e.g. `'slash'`); the cache table keys on the\n * qualified id (`'claude/slash'`). Multiple plugins COULD declare an\n * extractor with the same short id; the map keeps every qualified id per\n * short id so this filter recognises any of them as \"still cached\".\n */\n// eslint-disable-next-line complexity\nfunction reuseCachedLink(\n link: Link,\n shortIdToQualified: Map<string, string[]>,\n cachedQualifiedIds: Set<string>,\n applicableQualifiedIds: Set<string>,\n): Link | null {\n if (!Array.isArray(link.sources) || link.sources.length === 0) return null;\n const cachedSources: string[] = [];\n const obsoleteSources: string[] = [];\n let hasMissing = false;\n for (const source of link.sources) {\n const candidates = shortIdToQualified.get(source);\n if (!candidates || candidates.length === 0) {\n // No registered extractor at all carries this short id → obsolete.\n obsoleteSources.push(source);\n continue;\n }\n if (candidates.some((q) => cachedQualifiedIds.has(q))) {\n cachedSources.push(source);\n continue;\n }\n if (candidates.some((q) => applicableQualifiedIds.has(q))) {\n // Registered for this kind but not cached for this body → the\n // missing extractor will re-emit; dropping the prior link avoids\n // duplicates.\n hasMissing = true;\n continue;\n }\n // Registered but not applicable to this kind → treat as obsolete\n // for this node (cannot be re-emitted here).\n obsoleteSources.push(source);\n }\n if (hasMissing) return null;\n if (cachedSources.length === 0) return null;\n if (obsoleteSources.length === 0) return link;\n // Trim the obsolete short ids from `sources` so the persisted row no\n // longer claims attribution from an extractor the user removed.\n return { ...link, sources: cachedSources };\n}\n\n/**\n * Run every registered rule over the merged graph. Rules see internal\n * links only — broken-ref / trigger-collision / superseded all reason\n * about graph relations, not URLs.\n */\nasync function runRules(\n rules: IRule[],\n nodes: Node[],\n internalLinks: Link[],\n emitter: ProgressEmitterPort,\n hookDispatcher: IHookDispatcher,\n): Promise<Issue[]> {\n const issues: Issue[] = [];\n for (const rule of rules) {\n const emitted = await rule.evaluate({ nodes, links: internalLinks });\n for (const issue of emitted) {\n const validated = validateIssue(rule, issue, emitter);\n if (validated) issues.push(validated);\n }\n // Spec § A.11 — `rule.completed`. Aggregated per Rule, after every\n // issue has been validated. Fan-out scope: one event per Rule per\n // scan. The payload carries the qualified rule id so a hook with\n // `filter: { ruleId: '...' }` can scope to a single rule.\n const ruleId = qualifiedExtensionId(rule.pluginId, rule.id);\n const evt = makeEvent('rule.completed', { ruleId });\n emitter.emit(evt);\n await hookDispatcher.dispatch('rule.completed', evt);\n }\n return issues;\n}\n\n/**\n * The \"originating node\" of a link — the node whose body / frontmatter\n * the extractor was processing when it emitted the link. For most kinds\n * this equals `link.source`, but the frontmatter extractor emits inverted\n * `supersedes` links (from a node's `metadata.supersededBy`) where\n * `target` is the originating node and `source` is the (forward-pointing)\n * supersedor. The forward case (`metadata.supersedes`) keeps\n * `originating === source` like every other extractor.\n *\n * Discriminator: the supersedor path in an inverted edge is rarely a\n * real node (it points \"forward\" to a file that may or may not exist on\n * disk under that exact path); the originating node always exists in\n * the prior snapshot (it's the node whose extraction produced the link).\n * So for `kind === 'supersedes'`: prefer `source` when source is a known\n * prior node, otherwise fall back to `target`. This handles BOTH the\n * forward case (originating === source, which IS a known node) and the\n * inverted case (source not a node → fall through to target, the\n * originating older node).\n *\n * Frontmatter is the only extractor that emits cross-source links today;\n * if a future extractor adds another inversion case, escalate to a\n * persisted `Link.extractedFromPath` field with a schema bump rather\n * than extending this heuristic.\n */\nfunction originatingNodeOf(link: Link, priorNodePaths: Set<string>): string {\n if (link.kind === 'supersedes' && !priorNodePaths.has(link.source)) {\n return link.target;\n }\n return link.source;\n}\n\n/**\n * Step 1 of `detectRenamesAndOrphans` — pair every `deletedPath` with a\n * `newPath` whose body hash matches. Greedy by sorted order; on first\n * hit the deletion is claimed and we move on. Mutates the supplied\n * `claimedDeleted` / `claimedNew` sets in place.\n */\nfunction findHighConfidenceRenames(opts: {\n deletedPaths: string[];\n newPaths: string[];\n priorByPath: Map<string, Node>;\n currentByPath: Map<string, Node>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n}): RenameOp[] {\n const ops: RenameOp[] = [];\n for (const fromPath of opts.deletedPaths) {\n if (opts.claimedDeleted.has(fromPath)) continue;\n const fromNode = opts.priorByPath.get(fromPath)!;\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const toNode = opts.currentByPath.get(toPath)!;\n if (toNode.bodyHash === fromNode.bodyHash) {\n ops.push({ from: fromPath, to: toPath, confidence: 'high' });\n opts.claimedDeleted.add(fromPath);\n opts.claimedNew.add(toPath);\n break;\n }\n }\n }\n return ops;\n}\n\n/**\n * Step 2 of `detectRenamesAndOrphans` — bucket every still-unclaimed\n * `newPath` by the set of still-unclaimed `deletedPath`s that share its\n * `frontmatterHash`. The map drives both the medium-confidence claim\n * pass and the ambiguous-flag pass.\n */\nfunction buildFrontmatterRenameCandidates(opts: {\n deletedPaths: string[];\n newPaths: string[];\n priorByPath: Map<string, Node>;\n currentByPath: Map<string, Node>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n}): Map<string, string[]> {\n const candidatesByNew = new Map<string, string[]>();\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const toNode = opts.currentByPath.get(toPath)!;\n const matches: string[] = [];\n for (const fromPath of opts.deletedPaths) {\n if (opts.claimedDeleted.has(fromPath)) continue;\n const fromNode = opts.priorByPath.get(fromPath)!;\n if (toNode.frontmatterHash === fromNode.frontmatterHash) {\n matches.push(fromPath);\n }\n }\n if (matches.length > 0) candidatesByNew.set(toPath, matches);\n }\n return candidatesByNew;\n}\n\n/**\n * Step 3a of `detectRenamesAndOrphans` — first pass over the candidate\n * map: a `newPath` whose surviving candidate set is a singleton wins\n * the deletion, with `auto-rename-medium`. Greedy by sorted `newPath`\n * order so a deletion claimed by an earlier singleton drops out of\n * later candidate filters. Mutates `claimedDeleted` / `claimedNew` /\n * `issues` in place.\n */\nfunction claimSingletonRenames(opts: {\n newPaths: string[];\n candidatesByNew: Map<string, string[]>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n issues: Issue[];\n}): RenameOp[] {\n const ops: RenameOp[] = [];\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const candidates = opts.candidatesByNew.get(toPath);\n if (!candidates) continue;\n const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));\n if (remaining.length === 1) {\n const fromPath = remaining[0]!;\n ops.push({ from: fromPath, to: toPath, confidence: 'medium' });\n opts.issues.push({\n ruleId: 'auto-rename-medium',\n severity: 'warn',\n nodeIds: [toPath],\n message: `Auto-rename (medium confidence): ${fromPath} → ${toPath}`,\n data: { from: fromPath, to: toPath, confidence: 'medium' },\n });\n opts.claimedDeleted.add(fromPath);\n opts.claimedNew.add(toPath);\n }\n }\n return ops;\n}\n\n/**\n * Step 3b of `detectRenamesAndOrphans` — any `newPath` left with more\n * than one viable candidate after singletons settled is ambiguous.\n * Emits one `auto-rename-ambiguous` per `newPath`. Candidates are NOT\n * claimed; they fall through to the orphan step so the user can\n * reconcile manually with `sm orphans undo-rename`.\n */\nfunction flagAmbiguousRenames(opts: {\n newPaths: string[];\n candidatesByNew: Map<string, string[]>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n issues: Issue[];\n}): void {\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const candidates = opts.candidatesByNew.get(toPath);\n if (!candidates) continue;\n const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));\n if (remaining.length > 1) {\n opts.issues.push({\n ruleId: 'auto-rename-ambiguous',\n severity: 'warn',\n nodeIds: [toPath],\n message:\n `Auto-rename ambiguous: ${toPath} matches ${remaining.length} ` +\n `prior frontmatters — pick one with \\`sm orphans undo-rename ` +\n `${toPath} --from <old.path>\\`.`,\n data: { to: toPath, candidates: remaining },\n });\n }\n }\n}\n\n/**\n * Step 4 of `detectRenamesAndOrphans` — every deletion left unclaimed\n * after steps 1-3 yields one `orphan` issue (info severity).\n */\nfunction flagOrphans(opts: {\n deletedPaths: string[];\n claimedDeleted: Set<string>;\n issues: Issue[];\n}): void {\n for (const fromPath of opts.deletedPaths) {\n if (opts.claimedDeleted.has(fromPath)) continue;\n opts.issues.push({\n ruleId: 'orphan',\n severity: 'info',\n nodeIds: [fromPath],\n message: `Orphan history: ${fromPath} was deleted; no rename match found.`,\n data: { path: fromPath },\n });\n }\n}\n\n/**\n * Pure rename / orphan classification per `spec/db-schema.md` §Rename\n * detection. Mutates `issues` in place — caller passes the in-progress\n * issue list; returns the `RenameOp[]` for the persistence layer to\n * apply inside its tx.\n *\n * Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused\n * by another):\n *\n * 1. **High-confidence**: pair each `deletedPath` with a `newPath`\n * that has the same `bodyHash`. No issue, no prompt.\n * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair\n * each with the *unique* unclaimed `newPath` that shares its\n * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)\n * with `data: { from, to, confidence: 'medium' }`.\n * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one\n * remaining frontmatter-matching candidate, emit ONE\n * `auto-rename-ambiguous` issue per `newPath`, listing all\n * candidates in `data.candidates`. NO migration.\n * 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one\n * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.\n *\n * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc\n * order so the same input always produces the same matches —\n * required for reproducible tests and conformance fixtures (the spec\n * does not prescribe an order, but stability is the obvious contract).\n */\nexport function detectRenamesAndOrphans(\n prior: ScanResult,\n current: Node[],\n issues: Issue[],\n): RenameOp[] {\n const priorByPath = new Map<string, Node>();\n for (const n of prior.nodes) priorByPath.set(n.path, n);\n const currentByPath = new Map<string, Node>();\n for (const n of current) currentByPath.set(n.path, n);\n\n // Sets / sorted lists so iteration is deterministic.\n const deletedPaths = [...priorByPath.keys()]\n .filter((p) => !currentByPath.has(p))\n .sort();\n const newPaths = [...currentByPath.keys()]\n .filter((p) => !priorByPath.has(p))\n .sort();\n\n const claimedDeleted = new Set<string>();\n const claimedNew = new Set<string>();\n const ops: RenameOp[] = [];\n\n // Step 1 — high confidence (body hash match).\n ops.push(...findHighConfidenceRenames({\n deletedPaths, newPaths, priorByPath, currentByPath, claimedDeleted, claimedNew,\n }));\n\n // Step 2 — bucket every `newPath` by the deletions that share its\n // frontmatterHash, used by both medium-confidence and ambiguous passes.\n const candidatesByNew = buildFrontmatterRenameCandidates({\n deletedPaths, newPaths, priorByPath, currentByPath, claimedDeleted, claimedNew,\n });\n\n // Step 3a — singleton candidates → medium-confidence renames.\n ops.push(...claimSingletonRenames({\n newPaths, candidatesByNew, claimedDeleted, claimedNew, issues,\n }));\n\n // Step 3b — multi-candidate `newPath`s left after singletons settled.\n flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });\n\n // Step 4 — every unclaimed deletion is an orphan.\n flagOrphans({ deletedPaths, claimedDeleted, issues });\n\n return ops;\n}\n\n/**\n * Any link whose target carries a URL-shaped scheme is external (counted\n * via `externalRefsCount`, dropped from `result.links`). Internal links\n * are filesystem paths — relative or absolute, no scheme.\n *\n * The regex matches RFC 3986's `scheme = ALPHA *( ALPHA / DIGIT / \"+\" /\n * \"-\" / \".\" )` followed by `:`, with the extra constraint of ≥ 2 chars\n * so a Windows-style absolute path (`C:\\foo`) is not misclassified as a\n * URL on the rare cross-platform path that survives normalization.\n *\n * Before this regex the implementation only matched `http://` and\n * `https://`, which silently let `mailto:`, `data:`, `file:///`, `ftp://`\n * etc. pollute the graph as fake-internal links (their lookup against\n * `byPath` always missed, so counts stayed at 0, but the rows survived\n * in `result.links` and the rule pipeline saw them).\n */\nconst EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\\-.]+:/i;\n\nfunction isExternalUrlLink(link: Link): boolean {\n return EXTERNAL_URL_SCHEME_RE.test(link.target);\n}\n\nfunction makeEvent(type: string, data: unknown): ProgressEvent {\n return { type, timestamp: new Date().toISOString(), data };\n}\n\n/**\n * Spec § A.11 — Hook lifecycle dispatcher. Indexes the supplied hooks by\n * trigger and fans the matching event out to every subscribed\n * deterministic hook in registration order. Probabilistic hooks are\n * skipped here with a stderr advisory; they will dispatch via the job\n * subsystem once the job subsystem ships.\n *\n * Filter handling: when the hook declares a `filter` map, the dispatcher\n * walks `event.data` for each declared key and short-circuits the\n * invocation when any value disagrees. Top-level fields only in v0.x\n * (deep-path matching is deferred until a real use case justifies the\n * complexity).\n *\n * Error policy: a hook that throws is caught here, logged through a\n * synthetic `extension.error` event with kind `hook-error`, and the\n * scan continues. A buggy hook MUST NOT block the main pipeline —\n * that would invert the design intent (hooks REACT to events, they\n * never steer them).\n */\ninterface IHookDispatcher {\n dispatch(trigger: THookTrigger, event: ProgressEvent): Promise<void>;\n}\n\nfunction makeHookDispatcher(hooks: IHook[], emitter: ProgressEmitterPort): IHookDispatcher {\n if (hooks.length === 0) {\n // Cheap no-op fast path: most scans don't carry any hooks today.\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return { dispatch: async () => {} };\n }\n\n // Index by trigger so dispatch is O(matching) rather than O(allHooks).\n // Iteration order within a trigger preserves registration order so\n // observers see deterministic fan-out.\n const byTrigger = new Map<THookTrigger, IHook[]>();\n for (const hook of hooks) {\n if (hook.mode === 'probabilistic') {\n // Probabilistic hooks defer to the job subsystem (future job subsystem). Log\n // once per hook at composition time — not per-event — so a noisy\n // scan doesn't flood the logger. The hook still surfaces in\n // `sm plugins list`; it just doesn't fire today.\n const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);\n log.warn(\n `Probabilistic hook ${qualifiedId} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,\n { hookId: qualifiedId, mode: 'probabilistic' },\n );\n continue;\n }\n for (const trig of hook.triggers) {\n const bucket = byTrigger.get(trig);\n if (bucket) bucket.push(hook);\n else byTrigger.set(trig, [hook]);\n }\n }\n\n return {\n async dispatch(trigger, event) {\n const subs = byTrigger.get(trigger);\n if (!subs || subs.length === 0) return;\n for (const hook of subs) {\n if (!matchesFilter(hook, event)) continue;\n const ctx = buildHookContext(hook, trigger, event);\n try {\n await hook.on(ctx);\n } catch (err) {\n const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);\n const message = err instanceof Error ? err.message : String(err);\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'hook-error',\n extensionId: qualifiedId,\n trigger,\n message,\n }),\n );\n }\n }\n },\n };\n}\n\nfunction matchesFilter(hook: IHook, event: ProgressEvent): boolean {\n if (!hook.filter) return true;\n const data = (event.data ?? {}) as Record<string, unknown>;\n for (const [key, expected] of Object.entries(hook.filter)) {\n if (data[key] !== expected) return false;\n }\n return true;\n}\n\n// eslint-disable-next-line complexity\nfunction buildHookContext(\n _hook: IHook,\n trigger: THookTrigger,\n event: ProgressEvent,\n): IHookContext {\n const data = (event.data ?? {}) as Record<string, unknown>;\n const ctx: IHookContext = {\n event: {\n type: trigger,\n timestamp: event.timestamp,\n ...(event.runId !== undefined ? { runId: event.runId } : {}),\n ...(event.jobId !== undefined ? { jobId: event.jobId } : {}),\n data: event.data,\n },\n };\n if (typeof data['extractorId'] === 'string') ctx.extractorId = data['extractorId'];\n if (typeof data['ruleId'] === 'string') ctx.ruleId = data['ruleId'];\n if (typeof data['actionId'] === 'string') ctx.actionId = data['actionId'];\n if (data['node'] && typeof data['node'] === 'object') {\n ctx.node = data['node'] as Node;\n }\n if (data['jobResult'] !== undefined) ctx.jobResult = data['jobResult'];\n return ctx;\n}\n\ninterface IBuildNodeArgs {\n path: string;\n kind: Node['kind'];\n providerId: string;\n frontmatterRaw: string;\n body: string;\n frontmatter: Record<string, unknown>;\n bodyHash: string;\n frontmatterHash: string;\n encoder: Tiktoken | null;\n}\n\nfunction buildNode(args: IBuildNodeArgs): Node {\n const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, 'utf8');\n const bytesBody = Buffer.byteLength(args.body, 'utf8');\n const metadata = pickMetadata(args.frontmatter);\n const node: Node = {\n path: args.path,\n kind: args.kind,\n provider: args.providerId,\n bodyHash: args.bodyHash,\n frontmatterHash: args.frontmatterHash,\n bytes: {\n frontmatter: bytesFrontmatter,\n body: bytesBody,\n total: bytesFrontmatter + bytesBody,\n },\n linksOutCount: 0,\n linksInCount: 0,\n externalRefsCount: 0,\n frontmatter: args.frontmatter,\n title: pickString(args.frontmatter['name']),\n description: pickString(args.frontmatter['description']),\n stability: pickStability(metadata?.['stability']),\n version: pickString(metadata?.['version']),\n author: pickString(args.frontmatter['author']),\n };\n if (args.encoder) {\n node.tokens = countTokens(args.encoder, args.frontmatterRaw, args.body);\n }\n return node;\n}\n\nfunction countTokens(encoder: Tiktoken, frontmatterRaw: string, body: string): TripleSplit {\n // Tokenize the raw frontmatter bytes (not the parsed object) so the\n // count stays reproducible from on-disk content.\n const frontmatter = frontmatterRaw.length > 0 ? encoder.encode(frontmatterRaw).length : 0;\n const bodyTokens = body.length > 0 ? encoder.encode(body).length : 0;\n return { frontmatter, body: bodyTokens, total: frontmatter + bodyTokens };\n}\n\nfunction sha256(input: string): string {\n return createHash('sha256').update(input, 'utf8').digest('hex');\n}\n\n/**\n * Canonical-form rationale — canonical YAML form for frontmatter hashing.\n *\n * Goal: two `.md` files whose frontmatter parses to the same logical\n * value MUST produce the same `frontmatter_hash`, even if the raw bytes\n * differ in indentation, key order, quote style, or trailing whitespace.\n * Without this canonicalisation, a YAML formatter pass on the user's\n * editor (Prettier YAML, IDE autoformat, manual indent fix) silently\n * breaks the medium-confidence rename heuristic.\n *\n * Strategy:\n * 1. Take the parsed object the Provider already produced.\n * 2. Re-emit via `yaml.dump` with `sortKeys: true`, `lineWidth: -1`\n * (no auto-wrap), `noRefs: true` (no `*alias` shorthand),\n * `noCompatMode: true` (modern YAML 1.2 output).\n * 3. Hash the result.\n *\n * Fallback: when `parsed` is the empty object `{}` BUT `raw` is\n * non-empty, the Provider's parse failed silently. We fall back to\n * hashing the raw text — a malformed-YAML file should still hash\n * deterministically against itself across rescans, even if the\n * canonical form would be empty.\n */\nfunction canonicalFrontmatter(\n parsed: Record<string, unknown>,\n raw: string,\n): string {\n const hasParsedKeys = Object.keys(parsed).length > 0;\n const hasRawText = raw.length > 0;\n if (!hasParsedKeys && hasRawText) {\n // Parse failed but raw text exists. Hash the raw — preserves\n // identity for malformed-YAML files across scans.\n return raw;\n }\n return yaml.dump(parsed, {\n sortKeys: true,\n lineWidth: -1,\n noRefs: true,\n noCompatMode: true,\n });\n}\n\nfunction pickMetadata(fm: Record<string, unknown>): Record<string, unknown> | null {\n const m = fm['metadata'];\n return m && typeof m === 'object' && !Array.isArray(m) ? (m as Record<string, unknown>) : null;\n}\n\nfunction pickString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction pickStability(value: unknown): 'experimental' | 'stable' | 'deprecated' | null {\n if (value === 'experimental' || value === 'stable' || value === 'deprecated') return value;\n return null;\n}\n\nfunction buildExtractorContext(\n extractor: IExtractor,\n node: Node,\n body: string,\n frontmatter: Record<string, unknown>,\n emitLink: (link: Link) => void,\n enrichNode: (partial: Partial<Node>) => void,\n store: IPluginStore | undefined,\n): IExtractorContext {\n const scope = extractor.scope;\n // Spread `store` only when present so the resulting context stays\n // strictly-shaped under `exactOptionalPropertyTypes` — assigning\n // `store: undefined` would publish the property with an `undefined`\n // value, which is observably different from the field being absent\n // (the legacy contract for plugins without declared storage).\n return {\n node,\n body: scope === 'frontmatter' ? '' : body,\n frontmatter: scope === 'body' ? {} : frontmatter,\n emitLink,\n enrichNode,\n ...(store !== undefined ? { store } : {}),\n };\n}\n\nfunction validateLink(extractor: IExtractor, link: Link, emitter: ProgressEmitterPort): Link | null {\n if (!extractor.emitsLinkKinds.includes(link.kind as LinkKind)) {\n // Extractor emitted a kind outside its declared set — drop the link.\n // Surface a `extension.error` diagnostic so plugin authors see WHY a\n // link they expected vanished from the result; silent drops are the\n // worst possible plugin-author UX. The orchestrator is the last line\n // of defence against a misbehaving extractor, but the author needs to\n // know the line fired.\n //\n // `extensionId` carries the qualified form `<pluginId>/<id>` (spec\n // § A.6) so the diagnostic matches what `sm plugins list` and\n // registry lookups use. Older builds emitted just the short id; the\n // qualified form is unambiguous across plugins.\n const qualifiedId = `${extractor.pluginId}/${extractor.id}`;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'link-kind-not-declared',\n extensionId: qualifiedId,\n linkKind: link.kind,\n declaredKinds: extractor.emitsLinkKinds,\n link: { source: link.source, target: link.target, kind: link.kind },\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {\n extractorId: qualifiedId,\n linkKind: link.kind,\n declaredKinds: extractor.emitsLinkKinds.join(', '),\n }),\n }),\n );\n return null;\n }\n const confidence: Confidence = link.confidence ?? extractor.defaultConfidence;\n return { ...link, confidence };\n}\n\n/**\n * Validate a node's frontmatter against the per-kind schema declared by\n * the Provider that classified the node. Only called for files that\n * actually declared a fence (caller checks `frontmatterRaw.length > 0`).\n * Returns a single `frontmatter-invalid` issue with the AJV error\n * string, or `null` when the frontmatter is structurally valid. Severity\n * is `warn` by default; `strict` flips it to `error` so the scan exit\n * code rises to 1.\n *\n * Spec 0.8.0: per-kind schemas live with the Provider, not in\n * spec. The orchestrator passes the live `IProviderFrontmatterValidator`\n * (composed from every loaded Provider's `kinds[<kind>].schemaJson`)\n * plus the active Provider so the lookup is `(provider.id, kind) →\n * schema`. A Provider that does not declare an entry for the kind it\n * classified into still gets a `frontmatter-invalid` issue with errors\n * `'no-schema'` so the kernel never silently skips validation.\n */\nfunction validateFrontmatter(\n providerFrontmatter: IProviderFrontmatterValidator,\n provider: IProvider,\n kind: string,\n frontmatter: Record<string, unknown>,\n path: string,\n strict: boolean,\n): Issue | null {\n const result = providerFrontmatter.validate(provider, kind, frontmatter);\n if (result.ok) return null;\n return {\n ruleId: 'frontmatter-invalid',\n severity: strict ? 'error' : 'warn',\n nodeIds: [path],\n message: tx(ORCHESTRATOR_TEXTS.frontmatterInvalid, { path, kind, errors: result.errors }),\n data: { kind, errors: result.errors },\n };\n}\n\n/**\n * Malformed-frontmatter detection — detect cases where the user clearly meant\n * frontmatter but the Provider's regex couldn't recognise the fence.\n * The Provider regex requires `^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?` —\n * column-0 open fence, column-0 close fence, CRLF or LF line endings.\n * Three real-world variants that fall through silently and silently\n * lose every metadata field:\n *\n * - `paste-with-indent`: terminal heredoc auto-indented every line,\n * so the open fence is `<spaces>---`. The most common variant\n * .\n * - `byte-order-mark`: a UTF-8 BOM () precedes the fence. Some\n * editors (notably old VS Code on Windows) inject this; the YAML\n * parser handles BOM, but the Provider regex doesn't anchor past it.\n * - `missing-close`: the open fence is on column 0 but the closing\n * fence is missing or indented. Whole \"frontmatter\" parses as body.\n *\n * Each variant emits a `frontmatter-malformed` warn with a `data.hint`\n * tag so downstream tooling can disambiguate. `--strict` promotes to\n * `error` consistent with the strict-fence policy.\n *\n * False-positive guards:\n *\n * - Indented `---` with no YAML-looking line after → likely a nested\n * horizontal rule, not malformed frontmatter.\n * - Column-0 `---` followed by prose (not a YAML key) → likely a\n * legitimate horizontal rule with prose underneath. Tested.\n *\n * The schema-strict validator above only fires when `frontmatterRaw`\n * is non-empty; this fills the previously-silent path where the Provider\n * couldn't even recognise the fence.\n */\nfunction detectMalformedFrontmatter(body: string, path: string, strict: boolean): Issue | null {\n const hint = classifyMalformedFrontmatter(body);\n if (!hint) return null;\n return {\n ruleId: 'frontmatter-malformed',\n severity: strict ? 'error' : 'warn',\n nodeIds: [path],\n message: malformedMessage(hint, path),\n data: { hint },\n };\n}\n\ntype TMalformedHint = 'paste-with-indent' | 'byte-order-mark' | 'missing-close';\n\nfunction classifyMalformedFrontmatter(body: string): TMalformedHint | null {\n // (a) BOM at the very first byte. Check before everything else\n // because a BOM offsets the column-0 anchor of the Provider's regex.\n // Pattern after BOM is the standard column-0 fence + YAML key-value\n // line, so we still require that shape to avoid false positives on\n // any BOM-prefixed prose.\n if (body.startsWith('')) {\n if (/^---\\r?\\n[\\s\\S]*?[A-Za-z0-9_-]+\\s*:/.test(body)) {\n return 'byte-order-mark';\n }\n }\n\n // (b) Indented opening fence followed by a YAML-looking key-value\n // line. The most common variant (terminal heredoc auto-indent).\n if (/^[ \\t]+---\\r?\\n[ \\t]*[A-Za-z0-9_-]+\\s*:/.test(body)) {\n return 'paste-with-indent';\n }\n\n // (c) Column-0 opening fence followed by a YAML-looking key-value\n // line, but no matching closing fence. The Provider regex needs both\n // fences; a missing close means the entire intended frontmatter\n // (plus the body) parses as body.\n //\n // Heuristic: open at column 0, then at least one `key: value` line\n // immediately, then anywhere in the file there is NO column-0 `---`\n // closing the block. If the body had been parsed as frontmatter the\n // Provider would have set `frontmatterRaw` non-empty and we wouldn't\n // be in this branch — so the absence of close means the regex\n // didn't match.\n if (/^---\\r?\\n[ \\t]*[A-Za-z0-9_-]+\\s*:/.test(body)) {\n // Search for any line that is exactly `---` (column 0, no indent).\n // If found, the Provider regex would have matched and this code\n // path is unreachable; absence here means the close is missing\n // or indented.\n const hasCloseFence = /\\r?\\n---(?:\\r?\\n|$)/.test(body);\n if (!hasCloseFence) {\n return 'missing-close';\n }\n }\n\n return null;\n}\n\nfunction malformedMessage(hint: TMalformedHint, path: string): string {\n switch (hint) {\n case 'paste-with-indent':\n return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedPasteWithIndent, { path });\n case 'byte-order-mark':\n return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedByteOrderMark, { path });\n case 'missing-close':\n return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedMissingClose, { path });\n }\n}\n\nfunction validateIssue(rule: IRule, issue: Issue, emitter: ProgressEmitterPort): Issue | null {\n const severity: Severity | undefined = issue.severity;\n if (severity !== 'error' && severity !== 'warn' && severity !== 'info') {\n // Rule emitted an out-of-spec severity (or none at all) — drop the\n // issue. Surface a diagnostic so plugin authors see the issue\n // disappear FOR A REASON, instead of silently never showing up.\n // Qualified id (spec § A.6) keeps `extension.error` consumers\n // unambiguous across plugin namespaces.\n const qualifiedId = `${rule.pluginId}/${rule.id}`;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'issue-invalid-severity',\n extensionId: qualifiedId,\n severity,\n issue: { ruleId: issue.ruleId || rule.id, message: issue.message, nodeIds: issue.nodeIds },\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {\n ruleId: qualifiedId,\n severity: JSON.stringify(severity),\n }),\n }),\n );\n return null;\n }\n return { ...issue, ruleId: issue.ruleId || rule.id };\n}\n\nfunction recomputeLinkCounts(nodes: Node[], links: Link[]): void {\n const byPath = new Map<string, Node>();\n for (const node of nodes) {\n // Reset counts so a node reused from prior (which carries its prior\n // counts) gets re-counted from the merged internal-link list.\n node.linksOutCount = 0;\n node.linksInCount = 0;\n byPath.set(node.path, node);\n }\n for (const link of links) {\n const source = byPath.get(link.source);\n if (source) source.linksOutCount += 1;\n const target = byPath.get(link.target);\n if (target) target.linksInCount += 1;\n }\n}\n\nfunction recomputeExternalRefsCount(\n nodes: Node[],\n externalLinks: Link[],\n cachedPaths: Set<string>,\n): void {\n const byPath = new Map<string, Node>();\n for (const node of nodes) {\n // Zero only freshly-built nodes. Cached nodes preserve their prior\n // `externalRefsCount` because external pseudo-links were never\n // persisted, so we cannot re-derive the count from a fresh extractor\n // pass — the count survives untouched in the node row.\n if (!cachedPaths.has(node.path)) node.externalRefsCount = 0;\n byPath.set(node.path, node);\n }\n for (const link of externalLinks) {\n const source = byPath.get(link.source);\n // Cached nodes never appear as the source of a freshly-emitted\n // external pseudo-link (extractors didn't run for them), so this\n // increment only ever lands on a freshly-built node — but the guard\n // is cheap and defensive.\n if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;\n }\n}\n\n/**\n * Spec § A.8 — produce the merged read-time view of a Node.\n *\n * Rules / `sm check` / `sm export` consume `node.frontmatter` directly\n * (deterministic CI-safe baseline — author intent, byte-stable). UI / future\n * rules that opt into enrichment context call this helper to merge the\n * author frontmatter with the live enrichment layer.\n *\n * Algorithm:\n *\n * 1. Filter `enrichments` down to rows targeting this node AND not\n * flagged `stale`. Stale rows (probabilistic enrichments whose\n * body changed since their last run) are excluded by default —\n * stale visibility belongs to the UI layer where the marker is\n * shown next to the value.\n * 2. Sort the survivors by `enrichedAt` ASC so iteration order is\n * \"oldest first\". This makes the spread merge below\n * last-write-wins per field — the freshest Extractor's value\n * pisar the older one for any conflicting key.\n * 3. Spread-merge each row's `value` over `node.frontmatter`. The\n * author's keys are the base; enrichment keys overlay them.\n *\n * The returned object is a fresh shallow copy — mutating it does not\n * touch the caller's node. The original `node.frontmatter` reference\n * remains accessible via `node.frontmatter` for callers that want the\n * pristine author baseline.\n *\n * @param node Node to merge against; `node.frontmatter` is the base.\n * @param enrichments Per-(node, extractor) enrichment records — typically\n * loaded via `loadNodeEnrichments(db, node.path)` or\n * pre-filtered to this node by the caller.\n * @param opts.includeStale When true, include rows flagged stale. Defaults\n * to false (the safe, CI-deterministic default).\n * UIs that want to display \"stale (last value: …)\"\n * pass `true` and consult `enrichment.stale`\n * on the source rows.\n */\nexport function mergeNodeWithEnrichments(\n node: Node,\n enrichments: IPersistedEnrichment[],\n opts: { includeStale?: boolean } = {},\n): Record<string, unknown> {\n const includeStale = opts.includeStale === true;\n const applicable = enrichments\n .filter((e) => e.nodePath === node.path)\n .filter((e) => includeStale || !e.stale)\n .sort((a, b) => a.enrichedAt - b.enrichedAt);\n // `assignSafe` strips `__proto__` / `constructor` / `prototype` from\n // every source before copying, so a hostile enrichment value\n // (plugin-authored, persisted as JSON) cannot replace the merged\n // object's prototype via the `__proto__` setter. Prototype stays\n // normal so consumers can `deepStrictEqual` the result against\n // JSON-parse-shaped baselines.\n const base: Record<string, unknown> = {};\n assignSafe(base, node.frontmatter ?? {});\n for (const row of applicable) {\n assignSafe(base, row.value as Record<string, unknown>);\n }\n return base;\n}\n\nconst FORBIDDEN_MERGE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nfunction assignSafe(target: Record<string, unknown>, source: Record<string, unknown>): void {\n for (const [k, v] of Object.entries(source)) {\n if (FORBIDDEN_MERGE_KEYS.has(k)) continue;\n target[k] = v;\n }\n}\n\n/**\n * A persisted enrichment row, post-load. Mirrors the DB row shape\n * but with `value` already deserialised from JSON and `stale` /\n * `isProbabilistic` already decoded from `0 | 1`. Surfaced via\n * `loadNodeEnrichments` (driven adapter) and consumed by\n * `mergeNodeWithEnrichments` and the `sm refresh` command.\n */\nexport interface IPersistedEnrichment {\n nodePath: string;\n extractorId: string;\n bodyHashAtEnrichment: string;\n value: Partial<Node>;\n stale: boolean;\n enrichedAt: number;\n isProbabilistic: boolean;\n}\n","{\n \"name\": \"@skill-map/cli\",\n \"version\": \"0.16.3\",\n \"description\": \"skill-map reference implementation — kernel + CLI + adapters.\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"homepage\": \"https://skill-map.dev\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/crystian/skill-map.git\",\n \"directory\": \"src\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/crystian/skill-map/issues\"\n },\n \"keywords\": [\n \"skill-map\",\n \"markdown\",\n \"ai-agents\",\n \"claude-code\",\n \"graph\"\n ],\n \"bin\": {\n \"sm\": \"bin/sm.js\",\n \"skill-map\": \"bin/sm.js\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./kernel\": {\n \"types\": \"./dist/kernel/index.d.ts\",\n \"import\": \"./dist/kernel/index.js\"\n },\n \"./conformance\": {\n \"types\": \"./dist/conformance/index.d.ts\",\n \"import\": \"./dist/conformance/index.js\"\n }\n },\n \"files\": [\n \"bin/\",\n \"dist/\",\n \"migrations/\",\n \"README.md\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"dev:serve\": \"node ../scripts/dev-serve.js\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"test\": \"tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"test:ci\": \"tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"test:coverage\": \"tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"test:coverage:html\": \"tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"clean\": \"rm -rf dist coverage\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"2.0.1\",\n \"@skill-map/spec\": \"0.16.0\",\n \"ajv\": \"8.18.0\",\n \"ajv-formats\": \"3.0.1\",\n \"chokidar\": \"5.0.0\",\n \"clipanion\": \"4.0.0-rc.4\",\n \"hono\": \"4.12.16\",\n \"ignore\": \"7.0.5\",\n \"js-tiktoken\": \"1.0.21\",\n \"js-yaml\": \"4.1.1\",\n \"kysely\": \"0.28.16\",\n \"semver\": \"7.7.4\",\n \"typanion\": \"3.14.0\",\n \"ws\": \"8.20.0\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"10.0.1\",\n \"@stylistic/eslint-plugin\": \"5.10.0\",\n \"@types/js-yaml\": \"4.0.9\",\n \"@types/node\": \"24.12.2\",\n \"@types/semver\": \"7.7.1\",\n \"@types/ws\": \"8.18.1\",\n \"c8\": \"11.0.0\",\n \"eslint\": \"10.2.1\",\n \"eslint-plugin-import-x\": \"4.16.2\",\n \"tsup\": \"8.5.1\",\n \"tsx\": \"4.21.0\",\n \"typescript\": \"5.9.3\",\n \"typescript-eslint\": \"8.59.1\"\n },\n \"engines\": {\n \"node\": \">=24.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","/**\n * In-memory `ProgressEmitterPort` adapter. No network, no DB — just a\n * synchronous fan-out to registered listeners. Used by the default scan\n * orchestrator; the WebSocket-backed emitter that streams to\n * the Web UI lands.\n */\n\nimport type {\n ProgressEmitterPort,\n ProgressEvent,\n ProgressListener,\n} from '../ports/progress-emitter.js';\n\nexport class InMemoryProgressEmitter implements ProgressEmitterPort {\n readonly #listeners = new Set<ProgressListener>();\n\n emit(event: ProgressEvent): void {\n for (const listener of this.#listeners) listener(event);\n }\n\n subscribe(listener: ProgressListener): () => void {\n this.#listeners.add(listener);\n return () => {\n this.#listeners.delete(listener);\n };\n }\n}\n","/**\n * No-op `LoggerPort`. Default when the kernel is invoked without a\n * logger (tests, embedded usage). Equivalent in spirit to\n * `InMemoryProgressEmitter`: callers that don't care get a working\n * implementation that does nothing.\n *\n * Every method is intentionally empty — that IS the contract of this\n * class. We disable `no-empty-function` for the whole file because\n * adding `// eslint-disable-next-line` to each method would be noise.\n */\n\n/* eslint-disable @typescript-eslint/no-empty-function */\n\nimport type { LoggerPort } from '../ports/logger.js';\n\nexport class SilentLogger implements LoggerPort {\n trace(): void {}\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n","/**\n * Module-level singleton `LoggerPort`. The kernel emits warnings /\n * info / debug through `log.*`; the active implementation defaults to\n * `SilentLogger` (no output) and is swapped by the driving adapter at\n * boot time via `configureLogger(...)`.\n *\n * Why a singleton (vs. per-call injection):\n * - Logging crosses every layer; threading a `logger` argument\n * through every kernel function costs a lot of plumbing for a\n * side-channel concern.\n * - The active impl is a pointer; the exported `log` is a stable\n * proxy. Imports made before `configureLogger` runs still see the\n * new impl on every call — no \"captured stale logger\" bugs.\n *\n * Tradeoffs accepted:\n * - Tests must call `resetLogger()` (or replace the active impl) in\n * teardown to avoid cross-test bleed.\n * - Concurrent scans share the same logger; per-scan logging requires\n * reintroducing an explicit `logger` argument on the call path.\n */\n\nimport { SilentLogger } from '../adapters/silent-logger.js';\nimport type { LoggerPort } from '../ports/logger.js';\n\nlet active: LoggerPort = new SilentLogger();\n\n/** Stable proxy. Methods always delegate to the current `active` impl. */\nexport const log: LoggerPort = {\n trace: (message, context) => active.trace(message, context),\n debug: (message, context) => active.debug(message, context),\n info: (message, context) => active.info(message, context),\n warn: (message, context) => active.warn(message, context),\n error: (message, context) => active.error(message, context),\n};\n\n/** Install a logger as the active implementation. Idempotent. */\nexport function configureLogger(impl: LoggerPort): void {\n active = impl;\n}\n\n/** Restore the default `SilentLogger`. Call from test teardown. */\nexport function resetLogger(): void {\n active = new SilentLogger();\n}\n\n/** Inspect the active logger. Test-only — production code uses `log`. */\nexport function getActiveLogger(): LoggerPort {\n return active;\n}\n","/**\n * `PluginLoader` — default `PluginLoaderPort` implementation.\n *\n * Responsibilities (per spec §Plugin discovery + spec v0.8.0 § A.5 —\n * id uniqueness):\n *\n * 1. Discover plugin directories under one or more search paths, each\n * containing a `plugin.json` at its root.\n * 2. Parse + AJV-validate the manifest against\n * `plugins-registry.schema.json#/$defs/PluginManifest`.\n * 3. Enforce the structural rule **directory name == manifest id**. A\n * mismatch surfaces as `invalid-manifest` with a directed reason.\n * This rule alone rules out same-root collisions by construction\n * (a filesystem cannot host two siblings with the same name).\n * 4. Semver-check `manifest.specCompat` against the installed\n * `@skill-map/spec` version.\n * 5. Dynamic-import every path listed in `manifest.extensions[]`, expect a\n * default export matching the extension-kind schema, validate it, and\n * collect the loaded extensions.\n * 6. After every plugin has been loaded individually, scan the result set\n * for cross-root id collisions. Two plugins claiming the same id (any\n * combination of project + global + `--plugin-dir`) BOTH receive\n * status `id-collision`; no precedence rule applies. The user resolves\n * by renaming one and rerunning.\n * 7. Surface one of the documented failure modes when anything fails:\n * `invalid-manifest` / `incompatible-spec` / `load-error` /\n * `id-collision`. The kernel keeps booting regardless — a bad plugin\n * cannot take the process down.\n */\n\nimport { createRequire } from 'node:module';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\nimport semver from 'semver';\n\nimport type {\n IDiscoveredPlugin,\n ILoadedExtension,\n IPluginManifest,\n IPluginStorageSchema,\n TPluginLoadStatus,\n} from '../types/plugin.js';\nimport type { PluginLoaderPort } from '../ports/plugin-loader.js';\nimport { PLUGIN_LOADER_TEXTS } from '../i18n/plugin-loader.texts.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\nimport { tx } from '../util/tx.js';\nimport { KV_SCHEMA_KEY } from './plugin-store.js';\nimport type { ExtensionKind } from '../registry.js';\nimport type { ISchemaValidators } from './schema-validators.js';\nimport { HOOK_TRIGGERS } from '../extensions/hook.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\n/**\n * Default per-extension dynamic-import timeout. Generous on purpose —\n * a plugin that legitimately takes >5s to import is misbehaving (it\n * should not have heavy work at module top level), but the extra\n * headroom avoids spurious timeouts on cold disk caches and slow CI\n * runners.\n */\nexport const DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS = 5000;\n\nexport interface IPluginLoaderOptions {\n /** Search paths to scan for plugin directories. Non-existent paths are skipped. */\n searchPaths: string[];\n /** Required — used to validate plugin.json and each extension manifest. */\n validators: ISchemaValidators;\n /** Installed @skill-map/spec version, used for specCompat check. */\n specVersion: string;\n /**\n * When supplied, the loader calls this with every parsed plugin id\n * AFTER manifest + specCompat validation succeed. A return value of\n * `false` short-circuits the load: the plugin is reported with\n * `status: 'disabled'` and its extensions are NOT imported. Defaults\n * to \"always enabled\" when omitted (no DB / config integration —\n * useful for tests that assert raw discovery behaviour).\n */\n resolveEnabled?: (pluginId: string) => boolean;\n /**\n * Per-extension dynamic-import timeout in milliseconds. A plugin whose\n * top-level work (imports, side effects) exceeds this is reported as\n * `load-error` with a message naming the timeout, instead of hanging\n * the host CLI command (`sm scan`, `sm plugins list`, `sm watch`).\n * Defaults to `DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS` (5s). Tests pass a\n * smaller value to exercise the timeout path quickly.\n *\n * Note: there is no AbortSignal on `import()` in Node 24 — when the\n * timer wins, the import is abandoned (the dangling promise resolves\n * later and is GC'd) but its side effects, if any, still run. The\n * timeout protects the orchestrator from hanging, not the host\n * process from a misbehaving plugin's runtime cost.\n */\n loadTimeoutMs?: number;\n}\n\n/**\n * Factory — preferred entry point for production callers (CLI). Returns\n * the port shape so the consumer is pinned to the abstract contract,\n * not the concrete class. Tests that need to access internals continue\n * to use `new PluginLoader(...)` directly.\n */\nexport function createPluginLoader(options: IPluginLoaderOptions): PluginLoaderPort {\n return new PluginLoader(options);\n}\n\nexport class PluginLoader implements PluginLoaderPort {\n readonly #options: IPluginLoaderOptions;\n readonly #loadTimeoutMs: number;\n\n constructor(options: IPluginLoaderOptions) {\n this.#options = options;\n this.#loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS;\n }\n\n /**\n * Discover every plugin directory across the configured search paths.\n * Each direct child directory containing a `plugin.json` is considered a\n * plugin root. Non-plugin directories are silently skipped.\n */\n discoverPaths(): string[] {\n const out: string[] = [];\n for (const root of this.#options.searchPaths) {\n if (!existsSync(root)) continue;\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const candidate = join(root, entry.name);\n if (existsSync(join(candidate, 'plugin.json'))) {\n out.push(resolve(candidate));\n }\n }\n }\n return out;\n }\n\n /**\n * Full pass — discover every plugin, attempt to load each, then apply\n * the cross-root id-collision pass over the results. Two plugins that\n * survived their individual load with the same `manifest.id` both get\n * downgraded to status `id-collision` (no precedence — the spec is\n * explicit that \"no extension is privileged\"). Plugins that already\n * failed their individual load (`invalid-manifest` /\n * `incompatible-spec` / `load-error`) keep their original status:\n * their `id` field is untrusted (it may be a fall-back path hint when\n * the manifest could not be parsed) and they would muddy the\n * collision report.\n */\n async discoverAndLoadAll(): Promise<IDiscoveredPlugin[]> {\n const paths = this.discoverPaths();\n const out: IDiscoveredPlugin[] = [];\n for (const path of paths) {\n out.push(await this.loadOne(path));\n }\n return applyIdCollisions(out);\n }\n\n /**\n * Load a single plugin from its directory. Never throws — a failure is\n * reported via the returned status.\n */\n // eslint-disable-next-line complexity\n async loadOne(pluginPath: string): Promise<IDiscoveredPlugin> {\n const manifestResult = this.#parseAndValidateManifest(pluginPath);\n if (!manifestResult.ok) return manifestResult.failure;\n const manifest = manifestResult.manifest;\n\n // --- enabled resolution ----------------------------------------------\n // Only check after manifest + specCompat pass: a `disabled` status\n // implies \"we know this plugin enough to surface it; we just chose\n // not to run it\". An invalid or incompatible plugin gets its own\n // status and never reaches this branch.\n //\n // Spec § A.7 — granularity. The loader's pre-import resolveEnabled()\n // check uses the plugin id (the bundle-level key). Plugins with\n // granularity='extension' that want to gate individual extensions\n // need a richer policy at the runtime composer (see\n // `cli/util/plugin-runtime.ts`); the loader stage is intentionally\n // coarse — disabling the bundle id always wins, so the import work\n // is skipped wholesale.\n if (this.#options.resolveEnabled && !this.#options.resolveEnabled(manifest.id)) {\n return {\n path: pluginPath,\n id: manifest.id,\n status: 'disabled',\n manifest,\n granularity: manifest.granularity ?? 'bundle',\n reason: PLUGIN_LOADER_TEXTS.disabledByConfig,\n };\n }\n\n // --- extension imports + kind validation ------------------------------\n const loaded: ILoadedExtension[] = [];\n for (const relEntry of manifest.extensions) {\n const result = await this.#loadAndValidateExtensionEntry(pluginPath, manifest, relEntry);\n if (!result.ok) return result.failure;\n loaded.push(result.extension);\n }\n\n // --- storage output schemas (spec § A.12) -----------------------------\n // Opt-in: only plugins that declare `storage.schemas` (Mode B) or\n // `storage.schema` (Mode A) trigger the read+compile pass. A schema\n // file missing on disk OR failing AJV compile blocks the load with\n // `load-error` so the user sees the typo or syntax error at boot\n // instead of at first write. Storage modes without any schema\n // declaration stay permissive (status quo) — `storageSchemas` is\n // simply omitted from the discovered plugin row.\n const storageSchemasResult = loadStorageSchemas(pluginPath, manifest);\n if (!storageSchemasResult.ok) {\n return {\n ...fail(pluginPath, manifest.id, 'load-error', storageSchemasResult.reason),\n manifest,\n };\n }\n\n return {\n path: pluginPath,\n id: manifest.id,\n status: 'enabled',\n manifest,\n granularity: manifest.granularity ?? 'bundle',\n extensions: loaded,\n ...(storageSchemasResult.schemas\n ? { storageSchemas: storageSchemasResult.schemas }\n : {}),\n };\n }\n\n /**\n * Phase 1 of `loadOne` — read `plugin.json`, AJV-validate the manifest,\n * enforce the directory-name == manifest.id structural rule, and check\n * specCompat (range syntax + satisfies the installed spec version).\n * Returns either the validated manifest or an `IDiscoveredPlugin` with\n * the appropriate failure status.\n */\n #parseAndValidateManifest(\n pluginPath: string,\n ): { ok: true; manifest: IPluginManifest } | { ok: false; failure: IDiscoveredPlugin } {\n const manifestPath = join(pluginPath, 'plugin.json');\n\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(manifestPath, 'utf8'));\n } catch (err) {\n return { ok: false, failure: fail(\n pluginPath,\n pathId(pluginPath),\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestJsonParse, {\n manifestPath,\n errDescription: describe(err),\n }),\n )};\n }\n\n const manifestResult = this.#options.validators.validatePluginManifest<IPluginManifest>(raw);\n if (!manifestResult.ok) {\n return { ok: false, failure: fail(\n pluginPath,\n pathId(pluginPath),\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestAjv, {\n manifestPath,\n errors: manifestResult.errors,\n }),\n )};\n }\n const manifest = manifestResult.data;\n\n // Cheap structural rule (spec § A.5 — plugin id global uniqueness).\n // Two siblings on the same filesystem cannot share a name; matching\n // the directory to the id rules out same-root collisions by construction.\n const dirName = pathId(pluginPath);\n if (dirName !== manifest.id) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestDirMismatch, {\n dirName,\n manifestId: manifest.id,\n }),\n ),\n manifest,\n }};\n }\n\n if (!semver.validRange(manifest.specCompat)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidSpecCompat, { specCompat: manifest.specCompat }),\n ),\n manifest,\n }};\n }\n if (!semver.satisfies(this.#options.specVersion, manifest.specCompat, { includePrerelease: true })) {\n return { ok: false, failure: {\n path: pluginPath,\n id: manifest.id,\n status: 'incompatible-spec',\n manifest,\n granularity: manifest.granularity ?? 'bundle',\n reason: tx(PLUGIN_LOADER_TEXTS.incompatibleSpec, {\n installedSpecVersion: this.#options.specVersion,\n specCompat: manifest.specCompat,\n }),\n }};\n }\n\n return { ok: true, manifest };\n }\n\n /**\n * Phase 3 of `loadOne` — load and validate one extension entry. Six\n * sub-checks (file exists, dynamic import, has kind, kind known,\n * pluginId match, kind-specific manifest validation including hook\n * trigger pre-check). On success returns the `ILoadedExtension` with\n * `pluginId` injected; on failure returns the `IDiscoveredPlugin`\n * with the appropriate status (`load-error` or `invalid-manifest`).\n */\n // Six sub-validations per extension entry (file exists, dynamic\n // import, has-kind, kind-known, pluginId match, kind-specific schema\n // including hook trigger pre-check). Each branch is one early-return;\n // splitting per sub-check would multiply the discriminated-union\n // boilerplate without making the validation pipeline clearer.\n // eslint-disable-next-line complexity\n async #loadAndValidateExtensionEntry(\n pluginPath: string,\n manifest: IPluginManifest,\n relEntry: string,\n ): Promise<{ ok: true; extension: ILoadedExtension } | { ok: false; failure: IDiscoveredPlugin }> {\n if (!isInsidePlugin(pluginPath, relEntry)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.loadErrorPathEscapesPlugin, { relEntry, pluginPath }),\n ),\n manifest,\n }};\n }\n const abs = resolve(pluginPath, relEntry);\n if (!existsSync(abs)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorFileNotFound, { relEntry, abs }),\n ),\n manifest,\n }};\n }\n\n let mod: unknown;\n try {\n mod = await importWithTimeout(pathToFileURL(abs).href, this.#loadTimeoutMs);\n } catch (err) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorImportFailed, {\n relEntry,\n errDescription: describe(err),\n }),\n ),\n manifest,\n }};\n }\n\n const exported = extractDefault(mod);\n if (!isRecord(exported) || typeof exported['kind'] !== 'string') {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorMissingKind, {\n relEntry,\n knownKindsList: KNOWN_KINDS_LIST,\n }),\n ),\n manifest,\n }};\n }\n\n const kind = exported['kind'] as ExtensionKind;\n if (!KNOWN_KINDS.has(kind)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorUnknownKind, {\n relEntry,\n kindReceived: String(exported['kind']),\n knownKindsList: KNOWN_KINDS_LIST,\n }),\n ),\n manifest,\n }};\n }\n\n // Spec § A.6 — `pluginId` is loader-injected. A hand-declared\n // mismatch is a hard load error; a matching declaration is tolerated\n // (stripped before AJV).\n const declaredPluginId = exported['pluginId'];\n if (typeof declaredPluginId === 'string' && declaredPluginId !== manifest.id) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.loadErrorPluginIdMismatch, {\n relEntry,\n declared: declaredPluginId,\n manifestId: manifest.id,\n }),\n ),\n manifest,\n }};\n }\n\n // Strip runtime methods + `pluginId` so AJV's strict\n // `unevaluatedProperties: false` doesn't reject the export.\n const manifestView = stripFunctionsAndPluginId(exported);\n\n if (kind === 'hook') {\n const hookFailure = validateHookTriggers(pluginPath, manifest, relEntry, exported, manifestView);\n if (hookFailure) return { ok: false, failure: hookFailure };\n }\n\n const extValidator = this.#options.validators.validatorForExtension(kind);\n if (!extValidator(manifestView)) {\n const errors = (extValidator.errors ?? [])\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorManifestInvalid, { relEntry, kind, errors }),\n ),\n manifest,\n }};\n }\n\n // Shallow-clone the runtime instance + inject `pluginId` so two\n // plugins importing the same ESM-cached file don't stomp each\n // other's `pluginId`.\n const instance = isRecord(exported)\n ? { ...exported, pluginId: manifest.id }\n : exported;\n\n return { ok: true, extension: {\n kind,\n id: exported['id'] as string,\n pluginId: manifest.id,\n version: exported['version'] as string,\n entryPath: abs,\n module: mod,\n instance,\n }};\n }\n}\n\n/**\n * Spec § A.11 — Hook triggers validation. Runs BEFORE AJV so the user\n * gets a directed `invalid-manifest` reason (with offending trigger and\n * full hookable list) rather than a generic AJV enum error string under\n * `load-error`. Returns an `IDiscoveredPlugin` failure or `null` if the\n * triggers are valid.\n */\nfunction validateHookTriggers(\n pluginPath: string,\n manifest: IPluginManifest,\n relEntry: string,\n exported: Record<string, unknown>,\n manifestView: unknown,\n): IDiscoveredPlugin | null {\n const triggers = (manifestView as Record<string, unknown>)['triggers'];\n const hookId = (exported['id'] as string) ?? '?';\n if (!Array.isArray(triggers) || triggers.length === 0) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestHookEmptyTriggers, { hookId }),\n ),\n manifest,\n };\n }\n for (const trig of triggers) {\n if (typeof trig !== 'string' || !(HOOK_TRIGGERS as readonly string[]).includes(trig)) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestHookUnknownTrigger, {\n hookId,\n trigger: String(trig),\n hookableList: HOOKABLE_TRIGGERS_LIST,\n }),\n ),\n manifest,\n };\n }\n }\n return null;\n}\n\n// --- helpers ---------------------------------------------------------------\n\nconst KNOWN_KINDS = new Set<ExtensionKind>(['provider', 'extractor', 'rule', 'action', 'formatter', 'hook']);\nconst KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(' / ');\n\n/**\n * Spec § A.11 — curated hookable trigger set. Single source of truth lives\n * in `kernel/extensions/hook.ts` (`HOOK_TRIGGERS`); the loader imports it\n * directly so the loader and the runtime contract cannot drift apart.\n */\nconst HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(', ');\n\n/**\n * Race the dynamic import against a timer. When the timer wins we throw\n * a clear timeout error — the caller turns it into a `load-error` row\n * naming the offending entry. The dangling import promise lingers in\n * Node's loader and resolves later (the result is GC'd unreferenced);\n * there is no public `import()` cancellation API in Node 24, so this\n * is the best we can do without spawning a worker thread.\n */\nasync function importWithTimeout(href: string, timeoutMs: number): Promise<unknown> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(tx(PLUGIN_LOADER_TEXTS.importExceededTimeout, { timeoutMs })));\n }, timeoutMs);\n });\n try {\n return await Promise.race([import(href), timeout]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nfunction fail(\n path: string,\n id: string,\n status: TPluginLoadStatus,\n reason: string,\n): IDiscoveredPlugin {\n return { path, id, status, reason };\n}\n\n/**\n * Check that a manifest-declared relative path stays inside the plugin\n * tree once resolved. Rejects absolute paths and any value whose\n * resolved form lies above (or beside) the plugin root via `..`\n * components. Returns `null` when safe; otherwise the resolved\n * absolute path is returned for diagnostics.\n *\n * Closes the lane where one plugin directory references another\n * plugin's source (or arbitrary files on disk) by way of\n * `extensions: [\"../foo/index.js\"]` or `storage.schema:\n * \"../bar.schema.json\"`.\n */\nfunction isInsidePlugin(pluginPath: string, relEntry: string): boolean {\n if (isAbsolute(relEntry)) return false;\n const abs = resolve(pluginPath, relEntry);\n const rel = relative(pluginPath, abs);\n if (rel === '') return true;\n if (rel.startsWith('..')) return false;\n if (isAbsolute(rel)) return false;\n return true;\n}\n\nfunction describe(err: unknown): string {\n if (err instanceof Error) return err.message;\n try {\n return String(err);\n } catch {\n return 'unknown error';\n }\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction extractDefault(mod: unknown): unknown {\n if (!isRecord(mod)) return mod;\n return 'default' in mod ? mod['default'] : mod;\n}\n\n/**\n * Drop function-typed properties AND the runtime-only `pluginId` so the\n * resulting object is JSON-Schema-validatable. Used on the runtime export\n * before AJV gets it: an extension's `detect` / `render` / etc. method is\n * part of its TypeScript contract, not its declarative manifest, and JSON\n * Schema's `unevaluatedProperties: false` posture would otherwise reject\n * the whole export. Same posture for `pluginId` — per spec § A.6 it's a\n * runtime concern injected by the loader, not a manifest field.\n *\n * Spec 0.8.0: Provider runtime instances carry an additional\n * runtime-only field per `kinds` entry — `schemaJson`, the loaded JSON\n * Schema for the kind. The manifest declares `schema` (a relative path\n * string); `schemaJson` is loaded by the kernel/loader at boot. Strip\n * it before AJV-validating against the strict provider schema (which\n * has `additionalProperties: false` on each kind entry).\n *\n * Cheap shallow + one-level-deep copy — manifests are flat enough.\n */\nfunction stripFunctionsAndPluginId(input: unknown): unknown {\n if (!isRecord(input)) return input;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(input)) {\n if (typeof v === 'function') continue;\n if (k === 'pluginId') continue;\n if (k === 'kinds' && isRecord(v)) {\n out[k] = stripKindsRuntimeFields(v);\n continue;\n }\n out[k] = v;\n }\n return out;\n}\n\n/**\n * Provider `kinds` map: for each entry, drop runtime-only fields\n * (`schemaJson`) so AJV sees only the manifest-level fields the spec\n * declares (`schema`, `defaultRefreshAction`).\n */\nfunction stripKindsRuntimeFields(kinds: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [kind, entry] of Object.entries(kinds)) {\n if (!isRecord(entry)) {\n out[kind] = entry;\n continue;\n }\n const cleaned: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(entry)) {\n if (k === 'schemaJson') continue;\n if (typeof v === 'function') continue;\n cleaned[k] = v;\n }\n out[kind] = cleaned;\n }\n return out;\n}\n\n/** Fall-back plugin id derived from directory name when the manifest is unreadable. */\nfunction pathId(p: string): string {\n const parts = p.split(/[/\\\\]/);\n return parts[parts.length - 1] ?? p;\n}\n\n/**\n * Cross-root id-collision pass. Group survivors (plugins whose individual\n * load reached a status that exposes a *trusted* `manifest.id`) by id, and\n * for any group of size ≥ 2 rewrite every member's status to\n * `id-collision` with a reason naming the other path(s).\n *\n * \"Trusted id\" means the manifest parsed and validated. The eligible\n * statuses are therefore `enabled`, `disabled`, and `incompatible-spec`\n * (each of those keeps `manifest` populated). The remaining failure\n * modes — `invalid-manifest` and `load-error` — either never reached the\n * id-trust point (`invalid-manifest`) or carry a manifest that's still\n * structurally fine; we treat them inclusively. Pragmatically, the only\n * status whose `id` is a path fall-back is `invalid-manifest` from a\n * manifest that failed to parse — and those are excluded because the\n * fall-back id is the directory name, which by the same-root pigeonhole\n * cannot collide with another fall-back id (and a collision against a\n * real id would be misleading noise: \"rename your plugin to fix your\n * neighbour's broken JSON\" is bad guidance).\n *\n * Concretely we only consider plugins that have a `manifest` populated.\n */\n// eslint-disable-next-line complexity\nfunction applyIdCollisions(plugins: IDiscoveredPlugin[]): IDiscoveredPlugin[] {\n const buckets = new Map<string, IDiscoveredPlugin[]>();\n for (const p of plugins) {\n if (!p.manifest) continue; // skip path-fall-back ids (untrusted)\n const id = p.manifest.id;\n const bucket = buckets.get(id);\n if (bucket) bucket.push(p);\n else buckets.set(id, [p]);\n }\n\n const collidingPaths = new Set<string>();\n const collisionReason = new Map<string, string>();\n for (const [id, bucket] of buckets) {\n if (bucket.length < 2) continue;\n // Stable order so the rendered \"collides with\" list is deterministic\n // across runs — essential for snapshot tests and CI output diffs.\n const sorted = [...bucket].sort((a, b) => a.path.localeCompare(b.path));\n for (const member of sorted) {\n collidingPaths.add(member.path);\n const others = sorted.filter((p) => p.path !== member.path).map((p) => p.path);\n // Reason names the FIRST other path explicitly (matches the spec\n // suggestion) and lists the rest (if any) for the rare 3-way case.\n const pathB = others.length === 1 ? others[0]! : others.join(', ');\n collisionReason.set(\n member.path,\n tx(PLUGIN_LOADER_TEXTS.idCollision, { id, pathA: member.path, pathB }),\n );\n }\n }\n\n if (collidingPaths.size === 0) return plugins;\n\n return plugins.map((p) => {\n if (!collidingPaths.has(p.path)) return p;\n const next: IDiscoveredPlugin = {\n ...p,\n status: 'id-collision',\n reason: collisionReason.get(p.path) ?? p.reason ?? '',\n };\n // A colliding plugin's extensions are inert — strip them so a\n // careless caller cannot register them anyway. Manifest is kept\n // for diagnostics (`sm plugins list/show` shows version, author).\n delete next.extensions;\n return next;\n });\n}\n\n/**\n * Spec § A.12 — read and AJV-compile the storage output schemas a\n * plugin declares in its manifest. Returns either:\n *\n * - `{ ok: true, schemas: undefined }` — the plugin declared no\n * schemas (Mode A without `schema`, Mode B without `schemas`, or\n * no storage at all). Permissive — `storageSchemas` is omitted\n * from the discovered row and the runtime store wrapper skips\n * validation.\n * - `{ ok: true, schemas }` — every declared schema was read and\n * compiled. Mode A's single value-shape lives under the sentinel\n * `KV_SCHEMA_KEY`; Mode B's per-table schemas live under their\n * logical table name (matching the manifest map).\n * - `{ ok: false, reason }` — at least one schema file was missing,\n * unparseable as JSON, or rejected by AJV's compiler. The caller\n * surfaces the reason as `load-error`.\n *\n * One fresh Ajv instance per plugin keeps schema `$id` collisions from\n * leaking across plugins (and from polluting the kernel's spec\n * validators, which live on a separate cached instance — see\n * `schema-validators.ts`).\n */\n// eslint-disable-next-line complexity\nfunction loadStorageSchemas(\n pluginPath: string,\n manifest: IPluginManifest,\n):\n | { ok: true; schemas?: Record<string, IPluginStorageSchema> }\n | { ok: false; reason: string } {\n const storage = manifest.storage;\n if (!storage) return { ok: true };\n\n // Mode A — single optional `schema`.\n if (storage.mode === 'kv') {\n if (!storage.schema) return { ok: true };\n const compiled = compilePluginSchema(pluginPath, storage.schema);\n if (!compiled.ok) {\n const reason = tx(\n compiled.phase === 'read'\n ? PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaRead\n : PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaCompile,\n {\n pluginId: manifest.id,\n schemaPath: storage.schema,\n errDescription: compiled.errDescription,\n },\n );\n return { ok: false, reason };\n }\n return {\n ok: true,\n schemas: {\n [KV_SCHEMA_KEY]: {\n schemaPath: storage.schema,\n validate: compiled.validate,\n },\n },\n };\n }\n\n // Mode B — optional `schemas` map keyed by logical table name.\n if (!storage.schemas || Object.keys(storage.schemas).length === 0) {\n return { ok: true };\n }\n const out: Record<string, IPluginStorageSchema> = {};\n for (const [table, relPath] of Object.entries(storage.schemas)) {\n const compiled = compilePluginSchema(pluginPath, relPath);\n if (!compiled.ok) {\n const reason = tx(\n compiled.phase === 'read'\n ? PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaRead\n : PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaCompile,\n {\n pluginId: manifest.id,\n table,\n schemaPath: relPath,\n errDescription: compiled.errDescription,\n },\n );\n return { ok: false, reason };\n }\n out[table] = { schemaPath: relPath, validate: compiled.validate };\n }\n return { ok: true, schemas: out };\n}\n\n/**\n * Read a single JSON Schema file relative to the plugin directory and\n * compile it with a fresh Ajv2020 instance. Two failure modes:\n * - `phase: 'read'` — file missing, unreadable, or not JSON.\n * - `phase: 'compile'` — JSON parsed but AJV rejected it.\n * Both surface to the caller as `load-error` with a phase-specific\n * template message.\n */\nfunction compilePluginSchema(\n pluginPath: string,\n relPath: string,\n):\n | {\n ok: true;\n validate: ValidateFunction & {\n errors?: { instancePath: string; message?: string; keyword: string }[] | null;\n };\n }\n | { ok: false; phase: 'read' | 'compile'; errDescription: string } {\n if (!isInsidePlugin(pluginPath, relPath)) {\n return {\n ok: false,\n phase: 'read',\n errDescription: tx(PLUGIN_LOADER_TEXTS.loadErrorSchemaPathEscapesPlugin, { relPath, pluginPath }),\n };\n }\n const abs = resolve(pluginPath, relPath);\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(abs, 'utf8'));\n } catch (err) {\n return { ok: false, phase: 'read', errDescription: describe(err) };\n }\n try {\n const ajv: TAjv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n const compiled = ajv.compile(raw as object) as ValidateFunction & {\n errors?: { instancePath: string; message?: string; keyword: string }[] | null;\n };\n return { ok: true, validate: compiled };\n } catch (err) {\n return { ok: false, phase: 'compile', errDescription: describe(err) };\n }\n}\n\n/**\n * Locate the installed `@skill-map/spec` version at runtime. Handy default\n * for `IPluginLoaderOptions.specVersion` when the caller just wants the\n * real installed version without plumbing it through.\n */\nexport function installedSpecVersion(): string {\n const require = createRequire(import.meta.url);\n // Spec exports index.json but not package.json; we use the former to\n // locate the package root and then read package.json off disk directly.\n const indexPath = require.resolve('@skill-map/spec/index.json');\n const pkgPath = resolve(indexPath, '..', 'package.json');\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string };\n return pkg.version;\n}\n","/**\n * ESM/CJS interop helper for `ajv-formats`. The package ships CJS-first;\n * the default export is the callable plugin under ESM interop, but TS\n * sometimes types it as the namespace. This helper normalises the\n * import once so adapters that wire `ajv-formats` onto an Ajv instance\n * don't each carry the same `as unknown as ...` cast.\n *\n * Usage:\n * import { applyAjvFormats } from '<...>/kernel/util/ajv-interop.js';\n * applyAjvFormats(ajv);\n */\n\nimport type { Ajv2020 } from 'ajv/dist/2020.js';\nimport addFormatsModule from 'ajv-formats';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\nconst addFormats = (addFormatsModule as unknown as { default?: typeof addFormatsModule })\n .default ?? addFormatsModule;\n\n/**\n * Wire the standard JSON Schema formats (`uri`, `date`, `date-time`,\n * etc.) onto the given Ajv instance.\n */\nexport function applyAjvFormats(ajv: TAjv): void {\n (addFormats as unknown as (a: TAjv) => void)(ajv);\n}\n","/**\n * Kernel-side strings emitted by `kernel/adapters/plugin-store.ts`.\n *\n * Convention: flat string templates with `{{name}}` placeholders. The\n * `tx` helper at `kernel/util/tx.ts` does the interpolation. See\n * `kernel/i18n/orchestrator.texts.ts` header for rationale.\n *\n * Spec § A.12 — opt-in JSON Schema validation for plugin custom\n * storage. Both messages are thrown synchronously from the wrapper\n * when the plugin author's declared output schema rejects the value\n * the plugin tried to persist. Caller (the future kernel-side store\n * adapter) surfaces the throw to the orchestrator's\n * `extension.error` channel.\n */\n\nexport const PLUGIN_STORE_TEXTS = {\n kvValidationFailed:\n \"plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema \" +\n '({{schemaPath}}) — {{errors}}',\n\n dedicatedValidationFailed:\n \"plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema \" +\n '({{schemaPath}}) — {{errors}}',\n} as const;\n","/**\n * Plugin store wrappers — runtime injection for `ctx.store` per spec\n * § A.12 (opt-in `outputSchema` for plugin custom storage).\n *\n * Two shapes, mirroring the manifest's storage modes documented in\n * `spec/plugin-kv-api.md`:\n *\n * - Mode A — `KvStore.set(key, value)`. AJV-validates `value` against\n * the schema declared by `manifest.storage.schema` (single\n * value-shape) when present. Absent = permissive.\n * - Mode B — `DedicatedStore.write(table, row)`. AJV-validates `row`\n * against the per-table schema declared in `manifest.storage.schemas`\n * when present. Tables absent from the map accept any shape.\n *\n * Both wrappers are storage-engine agnostic — they accept a `persist`\n * callback the caller supplies. The persistence side (SQLite, in-memory,\n * mock) is the caller's concern; this wrapper's only job is the\n * AJV gate. That separation lets the test suite exercise the validator\n * without spinning up a real DB and lets the kernel adapter (future\n * `state_plugin_kvs` writer / dedicated-table writer) plug in\n * unchanged.\n *\n * Universal validation (`emitLink` against `link.schema.json`,\n * `enrichNode` against `node.schema.json`) is unaffected — it lives on\n * the orchestrator side and runs regardless of the plugin's\n * `outputSchema` opt-in.\n */\n\nimport type {\n IDiscoveredPlugin,\n IPluginStorageSchema,\n} from '../types/plugin.js';\nimport { tx } from '../util/tx.js';\nimport { PLUGIN_STORE_TEXTS } from '../i18n/plugin-store.texts.js';\n\n/**\n * Sentinel key under which Mode A stores its single value-shape schema\n * inside `IDiscoveredPlugin.storageSchemas`. The sentinel keeps the\n * shared `Record<string, IPluginStorageSchema>` map a single-typed\n * surface across both modes; consumers look up by sentinel for KV and\n * by table name for dedicated.\n */\nexport const KV_SCHEMA_KEY = '__kv__';\n\nexport interface IKvStorePersist {\n (key: string, value: unknown): void | Promise<void>;\n}\n\nexport interface IDedicatedStorePersist {\n (table: string, row: unknown): void | Promise<void>;\n}\n\n/**\n * Mode A wrapper. `set(key, value)` AJV-validates `value` against the\n * Mode A schema (sentinel key `__kv__`) when declared, then forwards\n * to `persist`. Validation failure throws with a message naming the\n * schema path and AJV errors; persistence is skipped on failure.\n *\n * `pluginId` is captured for diagnostics (the throw message names the\n * plugin). The wrapper does NOT itself scope by plugin id — that is\n * the persistence layer's job (the spec's `state_plugin_kvs` PK includes\n * `pluginId` and the kernel-side adapter prepends it before write).\n */\nexport interface IKvStoreWrapper {\n set(key: string, value: unknown): Promise<void>;\n}\n\n/**\n * Union shape exposed to extractors via `ctx.store`. Spec § A.12 — Mode A\n * (`kv`) returns a `set(key, value)` surface; Mode B (`dedicated`) returns\n * `write(table, row)`. Plugin authors narrow at the call site based on\n * the storage mode declared in their `plugin.json`.\n */\nexport type IPluginStore = IKvStoreWrapper | IDedicatedStoreWrapper;\n\nexport function makeKvStoreWrapper(opts: {\n pluginId: string;\n schema: IPluginStorageSchema | undefined;\n persist: IKvStorePersist;\n}): IKvStoreWrapper {\n const { pluginId, schema, persist } = opts;\n return {\n async set(key, value) {\n if (schema) {\n if (!schema.validate(value)) {\n throw new Error(\n tx(PLUGIN_STORE_TEXTS.kvValidationFailed, {\n pluginId,\n schemaPath: schema.schemaPath,\n key,\n errors: formatAjvErrors(schema.validate.errors ?? null),\n }),\n );\n }\n }\n await persist(key, value);\n },\n };\n}\n\n/**\n * Mode B wrapper. `write(table, row)` AJV-validates `row` against\n * `storageSchemas[table]` when declared, then forwards to `persist`.\n * Tables absent from the map are permissive — the wrapper forwards\n * straight to `persist` without validation.\n *\n * The wrapper accepts the full `storageSchemas` map (rather than a\n * single schema) so a plugin author can declare schemas for some\n * tables and leave others permissive in the same map without the\n * caller having to lookup-then-narrow.\n */\nexport interface IDedicatedStoreWrapper {\n write(table: string, row: unknown): Promise<void>;\n}\n\nexport function makeDedicatedStoreWrapper(opts: {\n pluginId: string;\n schemas: Record<string, IPluginStorageSchema> | undefined;\n persist: IDedicatedStorePersist;\n}): IDedicatedStoreWrapper {\n const { pluginId, schemas, persist } = opts;\n return {\n async write(table, row) {\n const schema = schemas?.[table];\n if (schema) {\n if (!schema.validate(row)) {\n throw new Error(\n tx(PLUGIN_STORE_TEXTS.dedicatedValidationFailed, {\n pluginId,\n table,\n schemaPath: schema.schemaPath,\n errors: formatAjvErrors(schema.validate.errors ?? null),\n }),\n );\n }\n }\n await persist(table, row);\n },\n };\n}\n\n/**\n * Convenience entry point: build whichever wrapper matches the\n * discovered plugin's storage mode. Returns `undefined` when the\n * plugin declared no storage at all (the orchestrator omits\n * `ctx.store` in that case, per the existing contract). Mode A\n * extracts the sentinel-keyed schema; Mode B forwards the full map.\n */\nexport function makePluginStore(opts: {\n plugin: IDiscoveredPlugin;\n persistKv?: IKvStorePersist;\n persistDedicated?: IDedicatedStorePersist;\n}): IPluginStore | undefined {\n const manifest = opts.plugin.manifest;\n if (!manifest?.storage) return undefined;\n const storageSchemas = opts.plugin.storageSchemas;\n\n if (manifest.storage.mode === 'kv') {\n if (!opts.persistKv) return undefined;\n const schema = storageSchemas?.[KV_SCHEMA_KEY];\n return makeKvStoreWrapper({\n pluginId: manifest.id,\n schema,\n persist: opts.persistKv,\n });\n }\n\n if (manifest.storage.mode === 'dedicated') {\n if (!opts.persistDedicated) return undefined;\n return makeDedicatedStoreWrapper({\n pluginId: manifest.id,\n schemas: storageSchemas,\n persist: opts.persistDedicated,\n });\n }\n\n return undefined;\n}\n\n/** Compact AJV error string suitable for the throw message. */\nfunction formatAjvErrors(\n errors: { instancePath: string; message?: string; keyword: string }[] | null,\n): string {\n if (!errors || errors.length === 0) return '(no AJV details)';\n return errors\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n}\n","/**\n * Hook runtime contract. The sixth plugin kind (spec § A.11).\n *\n * Hooks subscribe declaratively to a curated set of kernel lifecycle\n * events and react to them. Reaction-only by design: a hook cannot\n * mutate the pipeline, block emission, or alter outputs. Use cases\n * are notification (Slack on `job.completed`), integration glue (CI\n * webhook on `job.failed`), and bookkeeping (per-extractor metrics).\n *\n * The hookable trigger set is INTENTIONALLY SMALL — eight events. The\n * full `ProgressEmitterPort` catalog (per-node `scan.progress`,\n * `model.delta`, `run.*`, internal job lifecycle) is deliberately not\n * hookable: too verbose for a reactive surface, internal to the runner,\n * or covered elsewhere. Declaring a trigger outside the curated set\n * yields `invalid-manifest` at load time.\n *\n * Dual-mode (declared in manifest):\n *\n * - `deterministic` (default): `on(ctx)` runs in-process during the\n * dispatch of the matching event, synchronously between the\n * event's emission and the next pipeline step. Errors are caught\n * by the dispatcher, logged via `extension.error`, and never\n * block the main flow.\n * - `probabilistic`: the hook is enqueued as a job. Until the job\n * subsystem ships, probabilistic hooks load but skip dispatch\n * with a stderr advisory (Decision #114 in `ROADMAP.md`).\n *\n * Curated trigger set (per spec § A.11):\n *\n * 1. `scan.started` — pre-scan setup (one per scan).\n * 2. `scan.completed` — post-scan reaction (one per scan).\n * 3. `extractor.completed` — aggregated per-Extractor outputs.\n * 4. `rule.completed` — aggregated per-Rule outputs.\n * 5. `action.completed` — Action executed on a node.\n * 6. `job.spawning` — pre-spawn of runner subprocess.\n * 7. `job.completed` — most common trigger.\n * 8. `job.failed` — alerts, retry triggers.\n */\n\nimport type { IExtensionBase } from './base.js';\nimport type { Node, TExecutionMode } from '../types.js';\n\n/**\n * The eight hookable lifecycle events. Mirrors the `triggers[]` enum in\n * `spec/schemas/extensions/hook.schema.json`. Anything outside this set\n * is rejected at load time as `invalid-manifest`.\n */\nexport type THookTrigger =\n | 'scan.started'\n | 'scan.completed'\n | 'extractor.completed'\n | 'rule.completed'\n | 'action.completed'\n | 'job.spawning'\n | 'job.completed'\n | 'job.failed';\n\n/**\n * Frozen list mirror of `THookTrigger` for runtime introspection. The\n * loader validates `manifest.triggers[]` against this set; the\n * orchestrator's dispatcher iterates it in order when fanning an event\n * out to subscribed hooks.\n */\nexport const HOOK_TRIGGERS: readonly THookTrigger[] = Object.freeze([\n 'scan.started',\n 'scan.completed',\n 'extractor.completed',\n 'rule.completed',\n 'action.completed',\n 'job.spawning',\n 'job.completed',\n 'job.failed',\n] as const);\n\n/**\n * Context the dispatcher hands to `Hook.on()`. The shape is intentionally\n * narrow: a hook reacts to an event, it does not steer the pipeline.\n *\n * The `event` carries the raw `ProgressEvent` envelope (type, timestamp,\n * runId/jobId when applicable, data). Optional `node` / `extractorId`\n * / `ruleId` / `actionId` are extracted from the event payload by the\n * dispatcher when present so authors don't have to walk `event.data`.\n *\n * Probabilistic hooks additionally receive `runner` for LLM dispatch.\n * Deterministic hooks SHOULD ignore the field.\n */\nexport interface IHookContext {\n /** The raw event the dispatcher matched. */\n event: {\n type: THookTrigger;\n timestamp: string;\n runId?: string;\n jobId?: string;\n data?: unknown;\n };\n /**\n * Convenience extraction of the node payload when the event is\n * node-scoped (`action.completed`). Undefined for run-scoped or\n * scan-scoped events.\n */\n node?: Node;\n /**\n * Set on `extractor.completed` events. Qualified extension id of the\n * Extractor whose work the event aggregates.\n */\n extractorId?: string;\n /**\n * Set on `rule.completed` events. Qualified extension id of the Rule.\n */\n ruleId?: string;\n /**\n * Set on `action.completed` events. Qualified extension id of the\n * Action that just ran.\n */\n actionId?: string;\n /**\n * Set on `job.*` events once the job subsystem lands. Carries the\n * report payload for `job.completed`, the failure record for\n * `job.failed`, and the spawn metadata for `job.spawning`.\n */\n jobResult?: unknown;\n /**\n * `RunnerPort` injection for `probabilistic` hooks. `undefined` for\n * `deterministic` mode (the default). Probabilistic hooks land with\n * the job subsystem; the field is reserved here so the runtime\n * contract is forward-compatible without a major bump.\n */\n runner?: unknown;\n}\n\n/**\n * Optional declarative filter applied by the dispatcher BEFORE\n * invoking `on(ctx)`. Keys are payload field paths (top-level only in\n * v0.x); values are the literal expected match. The dispatcher walks\n * `event.data` for the field and short-circuits the invocation if the\n * value disagrees.\n *\n * Cross-field validation against declared `triggers` is best-effort\n * at load time: when none of the declared triggers carries a given\n * filter field, the loader surfaces `invalid-manifest`. The current\n * impl performs the basic enum check but defers full payload-shape\n * cross-validation to a follow-up — the dispatcher is permissive at\n * runtime (an unknown field never matches → the hook simply never\n * fires for that event, which is a correct interpretation of \"filter\n * by a field that doesn't exist\").\n */\nexport type THookFilter = Record<string, string | number | boolean>;\n\nexport interface IHook extends IExtensionBase {\n kind: 'hook';\n /**\n * Execution mode. Optional in the manifest with a default of\n * `deterministic` per `spec/schemas/extensions/hook.schema.json`.\n * Probabilistic hooks load but skip dispatch with a stderr advisory\n * until the job subsystem ships (Decision #114).\n */\n mode?: TExecutionMode;\n /**\n * Subset of the curated lifecycle trigger set this hook subscribes\n * to. MUST be non-empty; every entry MUST be a member of\n * `HOOK_TRIGGERS`. The loader validates both invariants and surfaces\n * `invalid-manifest` on violation.\n */\n triggers: THookTrigger[];\n /**\n * Optional declarative filter. Absent → invoke on every dispatched\n * event of every declared trigger.\n */\n filter?: THookFilter;\n /**\n * Hook entry point. Returns nothing; reactions are side effects.\n * Errors are caught by the dispatcher (logged as `extension.error`,\n * surfaced via `hook.failed` meta-event) and NEVER block the main\n * pipeline — a buggy hook degrades gracefully.\n */\n on(ctx: IHookContext): void | Promise<void>;\n}\n","/**\n * AJV validator loader. Compiles every JSON Schema the kernel needs into a\n * map of reusable validators keyed by a stable logical name. Schemas load\n * directly from the `@skill-map/spec` package at startup; any missing file\n * is a fatal boot error (the kernel cannot validate without them).\n *\n * Key design choices:\n *\n * - **Single Ajv instance per loader** so `$ref` resolution can reach sibling\n * schemas (e.g. `extensions/base.schema.json` → extended by every kind).\n * - **`strict: false`** because the spec uses a few keywords AJV considers\n * unknown under strict mode (`const` inside `oneOf`, tuple length hints)\n * that are nevertheless valid Draft 2020-12.\n * - **`ajv-formats`** enabled for `uri`, `date`, `date-time` — all used by\n * frontmatter base and plugin manifest.\n * - **Lazy compilation** is NOT used: every validator compiles eagerly on\n * `load()` so the kernel fails fast on a spec corruption instead of\n * crashing the first time a plugin tries to register.\n *\n * **Spec 0.8.0**. Per-kind frontmatter schemas (`skill`, `agent`,\n * `command`, `hook`, `note`) relocated from spec to the Provider that\n * owns them. Spec-only validators no longer cover those\n * five names. `buildProviderFrontmatterValidator(providers)` produces a\n * dedicated AJV instance pre-loaded with `frontmatter/base` (from spec)\n * plus every Provider's per-kind schemas — the kernel composes it once\n * per scan and the orchestrator validates each node's frontmatter\n * through it.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\n\nimport type { IProvider } from '../extensions/index.js';\nimport type { ExtensionKind } from '../registry.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\nexport type TSchemaName =\n | 'node'\n | 'link'\n | 'issue'\n | 'scan-result'\n | 'execution-record'\n | 'project-config'\n | 'plugins-registry'\n | 'job'\n | 'report-base'\n | 'conformance-case'\n | 'history-stats'\n | 'extension-provider'\n | 'extension-extractor'\n | 'extension-rule'\n | 'extension-action'\n | 'extension-formatter'\n | 'extension-hook'\n | 'frontmatter-base';\n\n/**\n * Re-export of `ExtensionKind` (canonical declaration in `kernel/registry.ts`)\n * for callers that already depend on this module for related schema names.\n * Single source of truth keeps the extension-kind set in lock-step with\n * `EXTENSION_KINDS`.\n */\nexport type { ExtensionKind } from '../registry.js';\n\nconst SCHEMA_FILES: Record<TSchemaName, string> = {\n node: 'schemas/node.schema.json',\n link: 'schemas/link.schema.json',\n issue: 'schemas/issue.schema.json',\n 'scan-result': 'schemas/scan-result.schema.json',\n 'execution-record': 'schemas/execution-record.schema.json',\n 'project-config': 'schemas/project-config.schema.json',\n 'plugins-registry': 'schemas/plugins-registry.schema.json',\n job: 'schemas/job.schema.json',\n 'report-base': 'schemas/report-base.schema.json',\n 'conformance-case': 'schemas/conformance-case.schema.json',\n 'history-stats': 'schemas/history-stats.schema.json',\n 'extension-provider': 'schemas/extensions/provider.schema.json',\n 'extension-extractor': 'schemas/extensions/extractor.schema.json',\n 'extension-rule': 'schemas/extensions/rule.schema.json',\n 'extension-action': 'schemas/extensions/action.schema.json',\n 'extension-formatter': 'schemas/extensions/formatter.schema.json',\n 'extension-hook': 'schemas/extensions/hook.schema.json',\n 'frontmatter-base': 'schemas/frontmatter/base.schema.json',\n};\n\n/** Schemas that other schemas reference via $ref but aren't validated directly. */\nconst SUPPORTING_SCHEMAS: string[] = [\n 'schemas/extensions/base.schema.json',\n 'schemas/frontmatter/base.schema.json',\n 'schemas/summaries/security-scanner.schema.json',\n];\n\nexport interface ISchemaValidators {\n validate<T = unknown>(name: TSchemaName, data: unknown): { ok: true; data: T } | { ok: false; errors: string };\n getValidator(name: TSchemaName): ValidateFunction;\n validatorForExtension(kind: ExtensionKind): ValidateFunction;\n /**\n * Validate raw plugin.json against `$defs/PluginManifest` inside\n * plugins-registry.schema.json. Returns the typed manifest on success.\n */\n validatePluginManifest<T = unknown>(data: unknown): { ok: true; data: T } | { ok: false; errors: string };\n}\n\n// Module-level cache. Cold load compiles ~17 validators\n// (~20 schemas counting supporting refs) which is ~100 ms cold for a CLI\n// startup. Subsequent calls in the same process return the same instance,\n// so future verbs that validate at multiple boundaries pay the cost once.\n// `null` means \"not yet loaded\"; we never expose a way to invalidate\n// because the schemas are static, baked-in, and the underlying spec\n// package version doesn't change at runtime.\nlet cachedValidators: ISchemaValidators | null = null;\n\n/** Test-only escape hatch — drop the cache so a test can re-trigger load. */\nexport function _resetSchemaValidatorsCacheForTests(): void {\n cachedValidators = null;\n}\n\nexport function loadSchemaValidators(): ISchemaValidators {\n if (cachedValidators !== null) return cachedValidators;\n cachedValidators = buildSchemaValidators();\n return cachedValidators;\n}\n\nfunction buildSchemaValidators(): ISchemaValidators {\n const specRoot = resolveSpecRoot();\n const ajv: TAjv = new Ajv2020({\n strict: false,\n allErrors: true,\n allowUnionTypes: true,\n });\n applyAjvFormats(ajv);\n\n // Add supporting schemas first so $ref targets resolve during compile.\n for (const rel of SUPPORTING_SCHEMAS) {\n const file = resolve(specRoot, rel);\n if (!existsSyncSafe(file)) continue;\n const schema = JSON.parse(readFileSync(file, 'utf8'));\n ajv.addSchema(schema);\n }\n\n const validators = new Map<TSchemaName, ValidateFunction>();\n for (const [name, rel] of Object.entries(SCHEMA_FILES) as Array<[TSchemaName, string]>) {\n const file = resolve(specRoot, rel);\n const schema = JSON.parse(readFileSync(file, 'utf8'));\n // Reuse existing compilation if the schema was already added above.\n const byId = typeof schema.$id === 'string' ? ajv.getSchema(schema.$id) : undefined;\n validators.set(name, byId ?? ajv.compile(schema));\n }\n\n const extensionByKind: Record<ExtensionKind, TSchemaName> = {\n provider: 'extension-provider',\n extractor: 'extension-extractor',\n rule: 'extension-rule',\n action: 'extension-action',\n formatter: 'extension-formatter',\n hook: 'extension-hook',\n };\n\n // Dedicated validator that targets PluginManifest inside the oneOf of\n // plugins-registry.schema.json, so callers don't have to hand-filter\n // against the combined schema.\n const pluginManifestValidator = ajv.compile({\n $ref: 'https://skill-map.dev/spec/v0/plugins-registry.schema.json#/$defs/PluginManifest',\n });\n\n return {\n getValidator(name) {\n const v = validators.get(name);\n if (!v) throw new Error(`Unknown schema: ${name}`);\n return v;\n },\n validatorForExtension(kind) {\n return validators.get(extensionByKind[kind])!;\n },\n validate<T = unknown>(name: TSchemaName, data: unknown) {\n const v = validators.get(name);\n if (!v) throw new Error(`Unknown schema: ${name}`);\n if (v(data)) return { ok: true as const, data: data as T };\n const errors = (v.errors ?? []).map(formatError).join('; ');\n return { ok: false as const, errors };\n },\n validatePluginManifest<T = unknown>(data: unknown) {\n if (pluginManifestValidator(data)) return { ok: true as const, data: data as T };\n const errors = (pluginManifestValidator.errors ?? []).map(formatError).join('; ');\n return { ok: false as const, errors };\n },\n };\n}\n\n/**\n * Validator for Provider-owned per-kind frontmatter schemas. Built from\n * the live set of registered Providers — each Provider declares its\n * `kinds[<kind>].schemaJson` and the loader compiles them into a single\n * AJV instance that also carries the spec's `frontmatter/base.schema.json`\n * so cross-package `$ref`-by-`$id` resolves. The orchestrator builds\n * one of these per scan via `buildProviderFrontmatterValidator`.\n */\nexport interface IProviderFrontmatterValidator {\n /**\n * Validate a node's frontmatter against the schema declared by\n * `provider.kinds[kind]`. `kind` is the value `provider.classify`\n * returned for the node, so the entry is guaranteed to exist for any\n * Provider implemented per spec; an absent entry returns\n * `{ ok: false, errors: 'no-schema' }` so the caller can emit a\n * directed `frontmatter-invalid` issue without crashing.\n */\n validate(\n provider: IProvider,\n kind: string,\n data: unknown,\n ): { ok: true } | { ok: false; errors: string };\n}\n\n/**\n * Build a Provider-frontmatter validator. Composes one AJV instance,\n * pre-registers `frontmatter/base.schema.json` from spec so per-kind\n * schemas can `$ref` it by `$id`, then compiles every Provider's\n * `kinds[<kind>].schemaJson` keyed by `(providerId, kind)`. Idempotent\n * across providers that share kinds (same `$id` → AJV's `addSchema`\n * dedupes silently); the keying is by `providerId` first so two\n * Providers exporting different schemas under the same kind name don't\n * collide.\n */\nexport function buildProviderFrontmatterValidator(\n providers: IProvider[],\n): IProviderFrontmatterValidator {\n const specRoot = resolveSpecRoot();\n const ajv: TAjv = new Ajv2020({\n strict: false,\n allErrors: true,\n allowUnionTypes: true,\n });\n applyAjvFormats(ajv);\n\n // Register spec's frontmatter/base.schema.json so per-kind schemas can\n // resolve `$ref: 'https://skill-map.dev/spec/v0/frontmatter/base.schema.json'`.\n const baseFile = resolve(specRoot, 'schemas/frontmatter/base.schema.json');\n const baseSchema = JSON.parse(readFileSync(baseFile, 'utf8'));\n ajv.addSchema(baseSchema);\n\n const compiled = new Map<string, ValidateFunction>();\n for (const provider of providers) {\n for (const [kind, entry] of Object.entries(provider.kinds)) {\n const key = `${provider.id}::${kind}`;\n // Reuse a previously-compiled schema (multiple Providers may legitimately\n // share the same `$id` if they bundle a copy of another's schema).\n const json = entry.schemaJson as { $id?: string };\n const existing = typeof json.$id === 'string' ? ajv.getSchema(json.$id) : undefined;\n compiled.set(key, existing ?? ajv.compile(entry.schemaJson as object));\n }\n }\n\n return {\n validate(provider, kind, data) {\n const key = `${provider.id}::${kind}`;\n const v = compiled.get(key);\n if (!v) return { ok: false as const, errors: 'no-schema' };\n if (v(data)) return { ok: true as const };\n const errors = (v.errors ?? []).map(formatError).join('; ');\n return { ok: false as const, errors };\n },\n };\n}\n\nfunction formatError(err: { instancePath: string; message?: string; keyword: string; params?: unknown }): string {\n const path = err.instancePath || '(root)';\n return `${path} ${err.message ?? err.keyword}`;\n}\n\n/**\n * Locate the installed `@skill-map/spec` package root. Prefer Node's\n * resolver (handles npm workspaces + published installs symmetrically)\n * and fall back to the package's `package.json` directory.\n */\nfunction resolveSpecRoot(): string {\n const require = createRequire(import.meta.url);\n // @skill-map/spec's exports field doesn't expose package.json, but\n // ./index.json is always exported and always lives at the package root.\n try {\n const indexPath = require.resolve('@skill-map/spec/index.json');\n return dirname(indexPath);\n } catch {\n throw new Error(\n '@skill-map/spec not resolvable — ensure the workspace is linked or the package is installed.',\n );\n }\n}\n\nfunction existsSyncSafe(path: string): boolean {\n try {\n readFileSync(path, 'utf8');\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Kernel-side strings emitted by `kernel/orchestrator.ts`.\n *\n * Convention: every entry is a flat string with `{{name}}` placeholders\n * (Mustache / Handlebars / Transloco compatible). The `tx` helper at\n * `kernel/util/tx.ts` does the interpolation. Plural / conditional\n * logic lives in the caller — pick the right template, don't branch\n * inside one.\n */\n\nexport const ORCHESTRATOR_TEXTS = {\n frontmatterInvalid:\n 'Frontmatter for {{path}} ({{kind}}) failed schema validation: {{errors}}',\n\n frontmatterMalformedPasteWithIndent:\n 'Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` ' +\n 'at column 0. The file was scanned as body-only — the metadata block was silently lost. ' +\n 'Move the `---` lines to the start of the line.',\n\n frontmatterMalformedByteOrderMark:\n 'Frontmatter fence in {{path}} is preceded by a UTF-8 byte-order mark (BOM); the file ' +\n 'was scanned as body-only. Re-save the file as UTF-8 without BOM. The metadata block ' +\n 'was silently lost.',\n\n frontmatterMalformedMissingClose:\n 'Frontmatter in {{path}} opens with `---` but never closes — no matching `---` line ' +\n 'at column 0 was found. The file was scanned as body-only and every metadata field was ' +\n 'silently lost. Add a closing `---` line below the metadata block.',\n\n extensionErrorLinkKindNotDeclared:\n 'Extractor \"{{extractorId}}\" emitted a link of kind \"{{linkKind}}\" outside its ' +\n 'declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',\n\n extensionErrorIssueInvalidSeverity:\n 'Rule \"{{ruleId}}\" emitted an issue with invalid severity {{severity}} ' +\n \"(allowed: 'error' | 'warn' | 'info'). Issue dropped.\",\n\n runScanRootEmptyArray:\n 'runScan: roots must contain at least one path (spec requires minItems: 1)',\n\n runScanRootMissing: \"runScan: root path '{{root}}' does not exist or is not a directory\",\n} as const;\n","/**\n * File watcher for `sm watch` / `sm scan --watch`.\n *\n * Wraps `chokidar` behind a small `IFsWatcher` interface so:\n *\n * 1. The CLI command is impl-agnostic — swapping chokidar for a\n * different watcher later (Java? Rust port? a future `WatchPort`?)\n * doesn't ripple into the command.\n * 2. Debouncing, batching, and ignore-filter integration live in one\n * place. The CLI just gets `onBatch(paths)` callbacks and decides\n * whether to re-scan.\n *\n * The watcher does NOT call into the orchestrator itself. That decision\n * is deliberate: the CLI owns the scan-and-persist pipeline (`runScan`,\n * `persistScanResult`, optional rebuild of the ignore filter when\n * `.skillmapignore` itself changes). Pulling that into the watcher\n * would couple the kernel module to `SqliteStorageAdapter`, which the\n * Server wouldn't want. Keep this module side-effect free\n * apart from filesystem subscription.\n *\n * Ignore filter integration: the supplied `IIgnoreFilter` is consulted\n * via chokidar's `ignored` predicate, which receives an absolute path.\n * We re-derive the path RELATIVE to the closest matching root before\n * passing it through `IIgnoreFilter.ignores`. This mirrors what the\n * scan walker does (`extensions/providers/claude/index.ts`) so both code\n * paths agree on what \"ignored\" means.\n */\n\nimport { resolve, relative, sep } from 'node:path';\n\nimport chokidar from 'chokidar';\nimport type { FSWatcher } from 'chokidar';\n\nimport type { IIgnoreFilter } from './ignore.js';\n\n// -----------------------------------------------------------------------------\n// Public types\n// -----------------------------------------------------------------------------\n\nexport type TWatchEventKind = 'add' | 'change' | 'unlink';\n\nexport interface IWatchEvent {\n kind: TWatchEventKind;\n /** Absolute path. */\n absolutePath: string;\n}\n\nexport interface IWatchBatch {\n /** Events that arrived inside the debounce window, in arrival order. */\n events: IWatchEvent[];\n /** Convenience: deduplicated absolute paths across the batch. */\n paths: string[];\n}\n\nexport interface IFsWatcher {\n /** Resolves once chokidar has finished its initial directory scan and is ready to emit. */\n ready: Promise<void>;\n /** Tear down the watcher. Resolves after chokidar releases handles. */\n close: () => Promise<void>;\n}\n\nexport interface ICreateFsWatcherOptions {\n /** Roots to watch. Resolved relative to `cwd` if relative paths are passed. */\n roots: string[];\n /** Working directory used to resolve relative roots and the ignore-filter root. */\n cwd: string;\n /** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */\n debounceMs: number;\n /**\n * Optional ignore filter — same instance the scan walker uses.\n *\n * Two shapes are accepted:\n *\n * - **`IIgnoreFilter`** (the static one) — captured by reference at\n * construction. Use this when the filter never changes for the\n * lifetime of the watcher (the typical CLI `sm watch` flow).\n *\n * - **`() => IIgnoreFilter | undefined`** (a getter) — re-evaluated\n * on EVERY chokidar `ignored` predicate call. Use this when the\n * filter can change at runtime — e.g. the BFF rebuilds it after\n * a `.skillmapignore` or `.skill-map/settings.json` edit and\n * wants chokidar to immediately respect the new patterns without\n * tearing down and rebuilding the watcher. A getter that returns\n * `undefined` disables ignore filtering for that call.\n */\n ignoreFilter?: IIgnoreFilter | (() => IIgnoreFilter | undefined) | undefined;\n /** Called once per debounced batch. Awaited; concurrent batches are serialised. */\n onBatch: (batch: IWatchBatch) => void | Promise<void>;\n /**\n * Called when the underlying watcher surfaces an error. The watcher\n * stays open — callers decide whether to log, keep going, or close.\n */\n onError?: (err: Error) => void;\n}\n\n// -----------------------------------------------------------------------------\n// Public API\n// -----------------------------------------------------------------------------\n\n/**\n * Construct a chokidar-backed watcher. Subscribes immediately; the\n * returned `ready` promise resolves once chokidar's initial directory\n * walk completes, at which point only NEW events fire `onBatch`.\n *\n * The initial directory walk is deliberately silent — we set\n * `ignoreInitial: true`. The CLI runs a one-shot scan before flipping\n * the watcher on, so re-emitting an `add` for every existing file\n * would be redundant churn.\n */\nexport function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatcher {\n const absRoots = opts.roots.map((r) => resolve(opts.cwd, r));\n const ignoreFilterOpt = opts.ignoreFilter;\n\n // Normalise the union: the static filter shape becomes a constant getter.\n // Resolving the getter on every call is what enables the BFF to swap\n // filters at runtime without tearing the watcher down.\n const getFilter: (() => IIgnoreFilter | undefined) | undefined =\n ignoreFilterOpt === undefined\n ? undefined\n : typeof ignoreFilterOpt === 'function'\n ? ignoreFilterOpt\n : (): IIgnoreFilter => ignoreFilterOpt;\n\n const ignored = getFilter\n ? (path: string): boolean => {\n const filter = getFilter();\n if (!filter) return false;\n const rel = relativePathFromRoots(path, absRoots);\n if (rel === null) return false;\n return filter.ignores(rel);\n }\n : undefined;\n\n const watcher: FSWatcher = chokidar.watch(absRoots, {\n ignoreInitial: true,\n persistent: true,\n ...(ignored ? { ignored } : {}),\n });\n\n // Pending state for debouncing.\n let pending: IWatchEvent[] = [];\n let timer: NodeJS.Timeout | null = null;\n let inFlight: Promise<void> | null = null;\n let closed = false;\n\n const fire = async (): Promise<void> => {\n timer = null;\n if (pending.length === 0) return;\n if (inFlight) {\n // A previous batch is still running; let it finish first.\n // The current pending events stay queued and will fire in the\n // next tick once `inFlight` resolves.\n return;\n }\n const events = pending;\n pending = [];\n const seen = new Set<string>();\n const paths: string[] = [];\n for (const ev of events) {\n if (!seen.has(ev.absolutePath)) {\n seen.add(ev.absolutePath);\n paths.push(ev.absolutePath);\n }\n }\n inFlight = Promise.resolve(opts.onBatch({ events, paths }))\n .catch((err: unknown) => {\n if (opts.onError) {\n opts.onError(err instanceof Error ? err : new Error(String(err)));\n }\n })\n .finally(() => {\n inFlight = null;\n // If new events accumulated while we were busy, schedule\n // another fire. We respect the debounce window so a slow\n // `onBatch` doesn't immediately re-trigger.\n if (!closed && pending.length > 0 && timer === null) {\n schedule();\n }\n });\n };\n\n const schedule = (): void => {\n if (closed) return;\n if (opts.debounceMs <= 0) {\n void fire();\n return;\n }\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n void fire();\n }, opts.debounceMs);\n };\n\n const enqueue = (kind: TWatchEventKind, absolutePath: string): void => {\n if (closed) return;\n pending.push({ kind, absolutePath });\n schedule();\n };\n\n watcher.on('add', (p) => enqueue('add', p));\n watcher.on('change', (p) => enqueue('change', p));\n watcher.on('unlink', (p) => enqueue('unlink', p));\n if (opts.onError) {\n watcher.on('error', (err) => {\n opts.onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }\n\n const ready: Promise<void> = new Promise((resolveReady) => {\n watcher.once('ready', () => resolveReady());\n });\n\n const close = async (): Promise<void> => {\n closed = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n pending = [];\n if (inFlight) {\n try {\n await inFlight;\n } catch {\n // already routed through onError above\n }\n }\n await watcher.close();\n };\n\n return { ready, close };\n}\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * Pick the matching root for `absolute` and return the path RELATIVE to\n * it, in POSIX form. Returns `null` when the path is outside every\n * supplied root (chokidar shouldn't emit those, but the contract on\n * `IIgnoreFilter.ignores` requires a relative path so we guard\n * defensively).\n */\nfunction relativePathFromRoots(absolute: string, absRoots: string[]): string | null {\n for (const root of absRoots) {\n const rel = relative(root, absolute);\n if (rel === '' || rel === '.') return '';\n if (!rel.startsWith('..') && !rel.startsWith(`..${sep}`)) {\n return rel.split(sep).join('/');\n }\n }\n return null;\n}\n","/**\n * Scan delta — pure comparison of two `ScanResult` snapshots. Drives\n * `sm scan --compare-with <path>` and is the single place the kernel\n * knows how to identify \"the same\" entity across two scans.\n *\n * **Identity contract** (mirrors decisions made at earlier sub-steps):\n *\n * - **Node**: `node.path`. The path is the only field stable across\n * edits — every other Node field is content-derived (hashes, counts,\n * denormalised frontmatter). Two nodes with the same path are the\n * \"same\" node; differences are reported as a `changed` entry with\n * a reason narrowing what diverged.\n *\n * - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This\n * mirrors the link-conflict rule and `sm show` aggregation —\n * two links with identical endpoints, kind, and (optional) trigger\n * are the same link, even if emitted by different extractors. The\n * `sources[]` union and confidence are NOT part of identity; they\n * are presentation facets that can churn without making the link\n * \"different\" for delta purposes.\n *\n * - **Issue**: `(ruleId, sorted nodeIds, message)`. Mirrors\n * `spec/job-events.md` §issue.* — same key → same issue, even when\n * `data` / `severity` / `linkIndices` shift. A meaningful change in\n * `message` (or a different set of node ids) is a different issue.\n * This is the same key future job events will use; keep it aligned\n * so consumers can reuse logic.\n *\n * No \"changed\" bucket for links / issues — identity already captures\n * everything that matters there. Nodes get a \"changed\" bucket because\n * the path stays stable while the body / frontmatter rewrite, and that\n * change is meaningful (formatters, summarisers, downstream consumers\n * all care about it).\n *\n * Pure: no IO, no DB, no FS. Safe to run in-memory inside `sm scan`\n * without polluting the persisted snapshot.\n */\n\nimport type { Issue, Link, Node, ScanResult } from '../types.js';\n\nexport type TNodeChangeReason = 'body' | 'frontmatter' | 'both';\n\nexport interface INodeChange {\n before: Node;\n after: Node;\n /**\n * Which hash diverged. `'body'` means body rewritten, frontmatter\n * untouched; `'frontmatter'` means metadata rewritten, body\n * untouched; `'both'` means both rewritten in the same edit.\n */\n reason: TNodeChangeReason;\n}\n\nexport interface IScanDelta {\n /** Path the current scan was compared against (echoed for the report header). */\n comparedWith: string;\n nodes: {\n added: Node[];\n removed: Node[];\n changed: INodeChange[];\n };\n links: {\n added: Link[];\n removed: Link[];\n };\n issues: {\n added: Issue[];\n removed: Issue[];\n };\n}\n\nexport function computeScanDelta(\n prior: ScanResult,\n current: ScanResult,\n comparedWith: string,\n): IScanDelta {\n return {\n comparedWith,\n nodes: diffNodes(prior.nodes, current.nodes),\n links: diffLinks(prior.links, current.links),\n issues: diffIssues(prior.issues, current.issues),\n };\n}\n\n/**\n * `true` iff every bucket is empty. Callers use this to decide the\n * exit code (`0` clean, `1` non-empty delta).\n */\nexport function isEmptyDelta(delta: IScanDelta): boolean {\n return (\n delta.nodes.added.length === 0 &&\n delta.nodes.removed.length === 0 &&\n delta.nodes.changed.length === 0 &&\n delta.links.added.length === 0 &&\n delta.links.removed.length === 0 &&\n delta.issues.added.length === 0 &&\n delta.issues.removed.length === 0\n );\n}\n\n// --- node delta ------------------------------------------------------------\n\nfunction diffNodes(\n priorNodes: Node[],\n currentNodes: Node[],\n): IScanDelta['nodes'] {\n const priorByPath = new Map(priorNodes.map((n) => [n.path, n]));\n const currentByPath = new Map(currentNodes.map((n) => [n.path, n]));\n\n const added: Node[] = [];\n const removed: Node[] = [];\n const changed: INodeChange[] = [];\n\n for (const [path, after] of currentByPath) {\n const before = priorByPath.get(path);\n if (!before) {\n added.push(after);\n continue;\n }\n const reason = compareNodeHashes(before, after);\n if (reason !== null) changed.push({ before, after, reason });\n }\n for (const [path, before] of priorByPath) {\n if (!currentByPath.has(path)) removed.push(before);\n }\n\n // Deterministic ordering — by path so two consumers comparing the same\n // pair of scans always see the same delta. Match the existing read-side\n // sort (used by `sm list`, ASCII formatter, etc.).\n added.sort(byPath);\n removed.sort(byPath);\n changed.sort((a, b) => byPath(a.after, b.after));\n\n return { added, removed, changed };\n}\n\nfunction compareNodeHashes(before: Node, after: Node): TNodeChangeReason | null {\n const bodyChanged = before.bodyHash !== after.bodyHash;\n const fmChanged = before.frontmatterHash !== after.frontmatterHash;\n if (bodyChanged && fmChanged) return 'both';\n if (bodyChanged) return 'body';\n if (fmChanged) return 'frontmatter';\n return null;\n}\n\nfunction byPath(a: { path: string }, b: { path: string }): number {\n return a.path.localeCompare(b.path);\n}\n\n// --- link delta ------------------------------------------------------------\n\nfunction diffLinks(\n priorLinks: Link[],\n currentLinks: Link[],\n): IScanDelta['links'] {\n const priorKeys = new Set(priorLinks.map(linkIdentity));\n const currentKeys = new Set(currentLinks.map(linkIdentity));\n\n const added: Link[] = [];\n const removed: Link[] = [];\n\n for (const link of currentLinks) {\n if (!priorKeys.has(linkIdentity(link))) added.push(link);\n }\n for (const link of priorLinks) {\n if (!currentKeys.has(linkIdentity(link))) removed.push(link);\n }\n\n added.sort(byLinkSort);\n removed.sort(byLinkSort);\n\n return { added, removed };\n}\n\nfunction linkIdentity(link: Link): string {\n // NUL separator — collision-free against any path (POSIX paths cannot\n // contain NUL) or trigger string. Same rule used by `sm show`'s\n // aggregation and by the link-conflict rule.\n const trigger = link.trigger?.normalizedTrigger ?? '';\n return `${link.source}\\x00${link.target}\\x00${link.kind}\\x00${trigger}`;\n}\n\nfunction byLinkSort(a: Link, b: Link): number {\n if (a.source !== b.source) return a.source.localeCompare(b.source);\n if (a.target !== b.target) return a.target.localeCompare(b.target);\n return a.kind.localeCompare(b.kind);\n}\n\n// --- issue delta -----------------------------------------------------------\n\nfunction diffIssues(\n priorIssues: Issue[],\n currentIssues: Issue[],\n): IScanDelta['issues'] {\n const priorKeys = new Set(priorIssues.map(issueIdentity));\n const currentKeys = new Set(currentIssues.map(issueIdentity));\n\n const added: Issue[] = [];\n const removed: Issue[] = [];\n\n for (const issue of currentIssues) {\n if (!priorKeys.has(issueIdentity(issue))) added.push(issue);\n }\n for (const issue of priorIssues) {\n if (!currentKeys.has(issueIdentity(issue))) removed.push(issue);\n }\n\n added.sort(byIssueSort);\n removed.sort(byIssueSort);\n\n return { added, removed };\n}\n\nfunction issueIdentity(issue: Issue): string {\n // Matches the spec/job-events.md §issue.* diff key so future job-event\n // consumers can reuse the same identity across the kernel.\n const ids = [...issue.nodeIds].sort().join(',');\n return `${issue.ruleId}\\x00${ids}\\x00${issue.message}`;\n}\n\nfunction byIssueSort(a: Issue, b: Issue): number {\n if (a.ruleId !== b.ruleId) return a.ruleId.localeCompare(b.ruleId);\n return a.message.localeCompare(b.message);\n}\n","/**\n * Kernel-side strings emitted by `kernel/adapters/sqlite/*` and the\n * scan-query parser (`kernel/scan/query.ts`). Same `tx(template, vars)`\n * convention as every other `kernel/i18n/*.texts.ts` peer.\n *\n * These are error messages from the storage adapter and the export-\n * query parser. Some of them surface as user-visible CLI errors via\n * `cli/commands/*` `formatErrorMessage(err)` paths; keeping them in\n * the catalog makes the future translator pipeline trivial.\n */\n\nexport const STORAGE_TEXTS = {\n scanPersistInvalidScannedAt:\n 'persistScanResult: invalid scannedAt {{value}} (expected non-negative integer ms)',\n\n findNodesInvalidSortBy:\n 'findNodes: invalid sortBy \"{{sortBy}}\". Allowed: {{allowed}}.',\n\n findNodesInvalidLimit:\n 'findNodes: invalid limit {{value}}; expected positive integer.',\n} as const;\n\nexport const QUERY_TEXTS = {\n exportQueryInvalidToken:\n 'invalid token \"{{token}}\": expected key=value (e.g. kind=skill, has=issues, path=foo/*).',\n\n exportQueryDuplicateKey:\n 'key \"{{key}}\" appears more than once; combine values with a comma instead (e.g. kind=skill,agent).',\n\n exportQueryEmptyValues: 'key \"{{key}}\" has no values.',\n\n exportQueryUnknownKey:\n 'unknown key \"{{key}}\". Valid keys: kind, has, path.',\n\n exportQueryEmptyKind:\n 'kind=\"\" is not a valid node kind (empty).',\n\n exportQueryUnsupportedHas:\n 'has=\"{{value}}\" is not supported. Valid: {{allowed}}. (findings / summary land at Steps 10 / 11.)',\n} as const;\n","/**\n * Export query — minimal filter language for `sm export <query>` (Step 8.3).\n *\n * Spec contract: `spec/cli-contract.md` line 190 says \"Query syntax is\n * implementation-defined pre-1.0\". This module defines the v0.5.0 syntax.\n *\n * **Grammar** (BNF-ish, intentionally tiny):\n *\n * query := token (WS+ token)*\n * token := key \"=\" value-list\n * key := \"kind\" | \"has\" | \"path\"\n * value-list := value (\",\" value)*\n * value := non-comma, non-whitespace string\n *\n * Tokens AND together; values within one token OR. An empty / whitespace-only\n * query is valid and matches every node (\"export everything\").\n *\n * **Filters**:\n *\n * - `kind=skill` / `kind=skill,agent` — node kind whitelist.\n * - `has=issues` — node must appear in some issue's `nodeIds`. (Future\n * expansion: `has=findings` / `has=summary` once Step 10 / 11 land.\n * Unknown values are a parse error today; we'll ratchet up the\n * accepted set additively.)\n * - `path=foo/*` / `path=.claude/agents/**` — POSIX glob over `node.path`.\n * Supports `*` (any chars except `/`) and `**` (any chars including `/`).\n *\n * **Subset semantics** (`applyExportQuery`):\n *\n * - Nodes pass when every specified filter matches (AND across keys,\n * OR within values).\n * - Links survive only when BOTH endpoints (`source` + `target`) belong\n * to the filtered node set. A subset that includes \"edges out to\n * unfiltered nodes\" would be confusing — the user asked for a focused\n * subgraph, not its boundary. External-URL pseudo-links are already\n * stripped by the orchestrator and never reach this layer.\n * - Issues survive when ANY of the issue's `nodeIds` is in the filtered\n * set. Issues span multiple nodes (e.g. `trigger-collision` over two\n * advertisers); dropping an issue when one of its nodes is outside\n * would hide cross-cutting problems the user is investigating.\n *\n * Pure: no IO, no DB, no FS.\n */\n\nimport type { Issue, Link, Node } from '../types.js';\nimport { QUERY_TEXTS } from '../i18n/storage.texts.js';\nimport { tx } from '../util/tx.js';\n\nconst HAS_VALUES = new Set(['issues']);\n\nexport interface IExportQuery {\n /** Original query string echoed back so consumers can render the header. */\n raw: string;\n /**\n * Whitelist of node kinds (`node.kind` is open string — built-in\n * Claude catalog `skill` / `agent` / `command` / `hook` / `note`,\n * plus whatever external Providers declare). The query parser does\n * not validate values against a closed enum; an unknown kind simply\n * yields zero matches at filter time.\n */\n kinds?: string[];\n hasIssues?: boolean;\n pathGlobs?: string[];\n}\n\nexport interface IExportSubset {\n query: IExportQuery;\n nodes: Node[];\n links: Link[];\n issues: Issue[];\n}\n\nexport class ExportQueryError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ExportQueryError';\n }\n}\n\n// Token-by-token parser with switch over keys + per-key validators.\n// Branching is intrinsic to the multi-key query grammar.\n// eslint-disable-next-line complexity\nexport function parseExportQuery(raw: string): IExportQuery {\n const trimmed = raw.trim();\n const out: IExportQuery = { raw: trimmed };\n if (trimmed.length === 0) return out;\n\n // Tokens are whitespace-separated key=value pairs. Values within one\n // token are comma-separated (multi-value OR). Keys repeated across\n // tokens are an error — the user should comma-separate within one\n // token instead, which is the documented form.\n const seen = new Set<string>();\n for (const token of trimmed.split(/\\s+/)) {\n const eq = token.indexOf('=');\n if (eq <= 0 || eq === token.length - 1) {\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryInvalidToken, { token }),\n );\n }\n const key = token.slice(0, eq).toLowerCase();\n const valuePart = token.slice(eq + 1);\n if (seen.has(key)) {\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryDuplicateKey, { key }),\n );\n }\n seen.add(key);\n\n const values = valuePart.split(',').map((v) => v.trim()).filter((v) => v.length > 0);\n if (values.length === 0) {\n throw new ExportQueryError(tx(QUERY_TEXTS.exportQueryEmptyValues, { key }));\n }\n\n switch (key) {\n case 'kind':\n out.kinds = parseKindValues(values);\n break;\n case 'has':\n if (parseHasValues(values)) out.hasIssues = true;\n break;\n case 'path':\n out.pathGlobs = values;\n break;\n default:\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryUnknownKey, { key }),\n );\n }\n }\n\n return out;\n}\n\n/**\n * Validate every token of a `kind=...` clause. Per\n * `node.schema.json#/properties/kind`, kinds are an open string — any\n * non-empty value is structurally valid. We still reject empty tokens\n * (a typo like `kind=,skill` shouldn't silently match every node).\n * Unknown-but-non-empty kinds simply yield zero matches at filter time.\n */\nfunction parseKindValues(values: string[]): string[] {\n for (const v of values) {\n if (v.length === 0) {\n throw new ExportQueryError(QUERY_TEXTS.exportQueryEmptyKind);\n }\n }\n return values;\n}\n\n/** Validate every token of a `has=...` clause; returns true iff `issues` is present. */\nfunction parseHasValues(values: string[]): boolean {\n for (const v of values) {\n if (!HAS_VALUES.has(v)) {\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryUnsupportedHas, {\n value: v,\n allowed: [...HAS_VALUES].join(', '),\n }),\n );\n }\n }\n return values.includes('issues');\n}\n\nexport function applyExportQuery(\n scan: { nodes: Node[]; links: Link[]; issues: Issue[] },\n query: IExportQuery,\n): IExportSubset {\n const nodesWithIssues = query.hasIssues\n ? collectNodesWithIssues(scan.issues)\n : null;\n const compiledGlobs = query.pathGlobs\n ? query.pathGlobs.map(compileGlob)\n : null;\n\n const filteredNodes = scan.nodes.filter((node) => {\n if (query.kinds && !query.kinds.includes(node.kind)) return false;\n if (nodesWithIssues && !nodesWithIssues.has(node.path)) return false;\n if (compiledGlobs && !compiledGlobs.some((re) => re.test(node.path))) return false;\n return true;\n });\n\n const survivingPaths = new Set(filteredNodes.map((n) => n.path));\n\n // Links: both endpoints must survive. See module-level commentary on\n // why we close the subgraph instead of carrying boundary edges.\n const filteredLinks = scan.links.filter(\n (link) => survivingPaths.has(link.source) && survivingPaths.has(link.target),\n );\n\n // Issues: any node in the issue's nodeIds being in scope keeps the\n // issue. See module-level commentary on why we don't require all.\n const filteredIssues = scan.issues.filter((issue) =>\n issue.nodeIds.some((id) => survivingPaths.has(id)),\n );\n\n return {\n query,\n nodes: filteredNodes,\n links: filteredLinks,\n issues: filteredIssues,\n };\n}\n\nfunction collectNodesWithIssues(issues: Issue[]): Set<string> {\n const out = new Set<string>();\n for (const issue of issues) {\n for (const nodeId of issue.nodeIds) out.add(nodeId);\n }\n return out;\n}\n\n/**\n * Compile a minimal POSIX glob into a RegExp. Supports:\n *\n * - `*` — any sequence of chars except `/` (single segment wildcard).\n * - `**` — any sequence of chars including `/` (cross-segment wildcard).\n * - everything else is literal (regex metacharacters escaped).\n *\n * No `?`, no `[abc]`, no brace expansion. The grammar is explicitly\n * minimal so the spec doesn't bind us to a specific glob library before\n * v1.0; we can grow this when consumers ask for it.\n */\nfunction compileGlob(pattern: string): RegExp {\n // First escape every regex metachar EXCEPT `*` (which we'll process\n // in a second pass). A negated character class is the cleanest way\n // to enumerate \"everything that needs escaping in a path glob\".\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n // `**` first so the `*` pass below doesn't double-process it. Use a\n // sentinel that can't appear in user input post-escape.\n const withDouble = escaped.replace(/\\*\\*/g, '\u0000DOUBLESTAR\u0000');\n const withSingle = withDouble.replace(/\\*/g, '[^/]*');\n // Null-byte sentinel is intentional — guarantees the marker can't\n // collide with anything in user-supplied glob patterns post-escape.\n // eslint-disable-next-line no-control-regex\n const final = withSingle.replace(/\u0000DOUBLESTAR\u0000/g, '.*');\n return new RegExp(`^${final}$`);\n}\n","/**\n * `LoggerPort` — structured logging port for the kernel.\n *\n * The kernel must NOT write to stdout/stderr directly. Anything that\n * would historically have been a `console.log` / `console.error` goes\n * through this port; the adapter (CLI, server, test harness) decides\n * format, level filter, and destination.\n *\n * Levels follow the conventional ordering, lowest = most verbose:\n *\n * trace < debug < info < warn < error < silent\n *\n * `silent` is a sentinel for filtering only — it never appears as a\n * `LogRecord.level`. Setting an adapter to `silent` disables every\n * method.\n */\n\nexport type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';\n\nexport type LogMethodLevel = Exclude<LogLevel, 'silent'>;\n\nexport const LOG_LEVELS: readonly LogLevel[] = [\n 'trace',\n 'debug',\n 'info',\n 'warn',\n 'error',\n 'silent',\n] as const;\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n trace: 0,\n debug: 1,\n info: 2,\n warn: 3,\n error: 4,\n silent: 5,\n};\n\nexport function logLevelRank(level: LogLevel): number {\n return LEVEL_RANK[level];\n}\n\nexport function isLogLevel(value: unknown): value is LogLevel {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LEVEL_RANK, value);\n}\n\n/**\n * Parse a string into a `LogLevel`. Returns `null` for invalid input\n * (incl. `undefined` / `null` / empty). Case-insensitive; trims\n * whitespace.\n */\nexport function parseLogLevel(value: string | undefined | null): LogLevel | null {\n if (value === undefined || value === null) return null;\n const normalized = value.trim().toLowerCase();\n if (normalized === '') return null;\n return isLogLevel(normalized) ? normalized : null;\n}\n\nexport interface LogRecord {\n level: LogMethodLevel;\n /** ISO 8601 timestamp produced at the moment the log call was made. */\n timestamp: string;\n message: string;\n /** Optional structured context. Caller-owned; serialization is up to the formatter. */\n context?: Record<string, unknown>;\n}\n\nexport interface LoggerPort {\n trace(message: string, context?: Record<string, unknown>): void;\n debug(message: string, context?: Record<string, unknown>): void;\n info(message: string, context?: Record<string, unknown>): void;\n warn(message: string, context?: Record<string, unknown>): void;\n error(message: string, context?: Record<string, unknown>): void;\n}\n","/**\n * Kernel entry point. `createKernel()` returns a shell with an empty registry\n * and no bound ports. Driving adapters (CLI, Server, Skill) are expected to\n * wire adapters before invoking use cases.\n */\n\nimport { Registry } from './registry.js';\n\nexport interface Kernel {\n registry: Registry;\n}\n\nexport function createKernel(): Kernel {\n return { registry: new Registry() };\n}\n\n// Pre-1.0 export surface — every name is enumerated explicitly so a\n// rename / addition in any of the underlying modules requires an\n// explicit edit here. The previous `export type *` wildcards from\n// `./types.js` and `./ports/index.js` re-published every internal type\n// implicitly; pre-1.0 that's quiet drift, post-1.0 it would silently\n// turn refactors into major bumps. Group order: registry, domain\n// types, orchestrator, watcher / delta / query, ports, extension\n// kinds.\n\nexport { Registry, EXTENSION_KINDS, DuplicateExtensionError, qualifiedExtensionId } from './registry.js';\nexport type { Extension, ExtensionKind } from './registry.js';\n\n// --- domain types (./types.ts) -----------------------------------------\nexport type {\n // unions\n NodeKind,\n LinkKind,\n Confidence,\n Severity,\n Stability,\n TExecutionMode,\n ExecutionKind,\n ExecutionStatus,\n ExecutionFailureReason,\n ExecutionRunner,\n // value objects\n TripleSplit,\n LinkTrigger,\n LinkLocation,\n // graph\n Node,\n Link,\n IssueFix,\n Issue,\n ScanStats,\n ScanScannedBy,\n ScanResult,\n // history surface\n ExecutionRecord,\n HistoryStatsTotals,\n HistoryStatsTokensPerAction,\n HistoryStatsExecutionsPerPeriod,\n HistoryStatsTopNode,\n HistoryStatsPerActionRate,\n HistoryStatsErrorRates,\n HistoryStats,\n} from './types.js';\n\n// --- orchestrator (./orchestrator.ts) ---------------------------------\nexport {\n runScan,\n runScanWithRenames,\n detectRenamesAndOrphans,\n mergeNodeWithEnrichments,\n runExtractorsForNode,\n} from './orchestrator.js';\nexport type {\n RunScanOptions,\n RenameOp,\n IExtractorRunRecord,\n IEnrichmentRecord,\n IPersistedEnrichment,\n} from './orchestrator.js';\n\n// --- adapters (./adapters/...) -----------------------------------------\nexport { InMemoryProgressEmitter } from './adapters/in-memory-progress.js';\nexport {\n KV_SCHEMA_KEY,\n makeDedicatedStoreWrapper,\n makeKvStoreWrapper,\n makePluginStore,\n} from './adapters/plugin-store.js';\nexport type {\n IDedicatedStorePersist,\n IDedicatedStoreWrapper,\n IKvStorePersist,\n IKvStoreWrapper,\n IPluginStore,\n} from './adapters/plugin-store.js';\n\n// --- scan utilities (./scan/...) ---------------------------------------\nexport { createChokidarWatcher } from './scan/watcher.js';\nexport type {\n IFsWatcher,\n IWatchBatch,\n IWatchEvent,\n ICreateFsWatcherOptions,\n TWatchEventKind,\n} from './scan/watcher.js';\nexport { computeScanDelta, isEmptyDelta } from './scan/delta.js';\nexport type { IScanDelta, INodeChange, TNodeChangeReason } from './scan/delta.js';\nexport { parseExportQuery, applyExportQuery, ExportQueryError } from './scan/query.js';\nexport type { IExportQuery, IExportSubset } from './scan/query.js';\n\n// --- ports (./ports/...) -----------------------------------------------\nexport type { ITransactionalStorage, StoragePort } from './ports/storage.js';\nexport type {\n IIssueRow,\n INodeBundle,\n INodeCounts,\n INodeFilter,\n IPersistOptions,\n} from './types/storage.js';\nexport type { FilesystemPort, IWalkOptions, NodeStat } from './ports/filesystem.js';\nexport type {\n PluginLoaderPort,\n IDiscoveredPlugin,\n ILoadedExtension,\n IPluginManifest,\n IPluginStorageSchema,\n TGranularity,\n TPluginLoadStatus,\n TPluginStorage,\n} from './ports/plugin-loader.js';\nexport type { IRunOptions, IRunResult, RunnerPort } from './ports/runner.js';\nexport type {\n ProgressEmitterPort,\n ProgressEvent,\n ProgressListener,\n} from './ports/progress-emitter.js';\nexport type {\n LoggerPort,\n LogLevel,\n LogMethodLevel,\n LogRecord,\n} from './ports/logger.js';\nexport {\n LOG_LEVELS,\n isLogLevel,\n logLevelRank,\n parseLogLevel,\n} from './ports/logger.js';\nexport { SilentLogger } from './adapters/silent-logger.js';\nexport { log, configureLogger, resetLogger, getActiveLogger } from './util/logger.js';\n\n// --- extension kinds (./extensions/...) --------------------------------\nexport type {\n IProvider,\n IRawNode,\n IExtractor,\n IExtractorContext,\n IExtractorCallbacks,\n IRule,\n IRuleContext,\n IAction,\n IActionPrecondition,\n IFormatter,\n IFormatterContext,\n IHook,\n IHookContext,\n THookTrigger,\n THookFilter,\n IExtensionBase,\n} from './extensions/index.js';\nexport { HOOK_TRIGGERS } from './extensions/index.js';\n"],"mappings":"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","names":["existsSync","require","readFileSync","resolve","createRequire","Ajv2020","Ajv2020","resolve","readFileSync","require","createRequire","existsSync","byPath","resolve","relative"]}
|
|
1
|
+
{"version":3,"sources":["../../kernel/i18n/registry.texts.ts","../../kernel/util/tx.ts","../../kernel/registry.ts","../../kernel/orchestrator.ts","../../package.json","../../kernel/adapters/in-memory-progress.ts","../../kernel/adapters/silent-logger.ts","../../kernel/util/logger.ts","../../kernel/adapters/plugin-loader.ts","../../kernel/util/ajv-interop.ts","../../kernel/i18n/plugin-store.texts.ts","../../kernel/adapters/plugin-store.ts","../../kernel/extensions/hook.ts","../../kernel/adapters/schema-validators.ts","../../kernel/i18n/orchestrator.texts.ts","../../kernel/scan/watcher.ts","../../kernel/scan/delta.ts","../../kernel/i18n/storage.texts.ts","../../kernel/scan/query.ts","../../kernel/ports/logger.ts","../../kernel/index.ts"],"sourcesContent":["/**\n * Strings emitted by `kernel/registry.ts`. Same `tx(template, vars)`\n * convention as every other `kernel/i18n/*.texts.ts` peer.\n *\n * These messages are thrown as `Error.message`; some surface to the user\n * via CLI verbs that catch them (e.g. `sm scan` registering manifests).\n */\n\nexport const REGISTRY_TEXTS = {\n duplicateExtension:\n 'Extension already registered: {{kind}}:{{qualifiedId}}',\n\n unknownKind:\n 'Unknown extension kind: {{kind}}',\n\n missingPluginId:\n 'Extension {{kind}}:{{id}} is missing pluginId; built-ins declare it directly, user plugins have it injected by PluginLoader.',\n} as const;\n","/**\n * `tx(template, vars)` — string interpolation for the project's text\n * tables (`*.texts.ts` files under `kernel/i18n/` and `cli/i18n/`).\n *\n * Templates use the `{{name}}` placeholder shape (Mustache / Handlebars\n * / Transloco compatible) so the same string tables drop into a real\n * i18n library on the day this project migrates.\n *\n * Contract:\n * - Every `{{name}}` token in `template` MUST have a matching key in\n * `vars`. A missing key throws — silent fallback would hide a\n * forgotten arg in a production build, which is the worst kind of\n * bug to chase down.\n * - Values can be `string | number`. `null` / `undefined` keys are\n * rejected; the caller is expected to coerce upstream (e.g. format\n * a missing path as `'(unknown)'` before passing).\n * - Whitespace inside the braces is tolerated (`{{ name }}`); the\n * parser strips it. This keeps long templates readable when wrapped\n * across multiple TS lines via `+`.\n * - Literal `{{` is not currently supported — no real text needs it.\n * Add escaping the day a template needs to render Handlebars-style\n * content.\n *\n * Plural / conditional logic does NOT live in the template. The caller\n * picks the correct template (e.g. `entries_singular` vs\n * `entries_plural`) or composes the variable value upstream and passes\n * the finished string. Keeping templates flat is the price for staying\n * Transloco-ready.\n */\n\nconst TOKEN_RE = /\\{\\{\\s*([A-Za-z][A-Za-z0-9_]*)\\s*\\}\\}/g;\n\nexport function tx(\n template: string,\n vars: Record<string, string | number> = {},\n): string {\n return template.replace(TOKEN_RE, (_match, name: string) => {\n if (!Object.prototype.hasOwnProperty.call(vars, name)) {\n throw new Error(\n `tx: missing variable \"${name}\" for template \"${template.slice(0, 80)}${template.length > 80 ? '…' : ''}\"`,\n );\n }\n const value = vars[name];\n if (value === null || value === undefined) {\n throw new Error(\n `tx: variable \"${name}\" is null/undefined for template \"${template.slice(0, 80)}${template.length > 80 ? '…' : ''}\"`,\n );\n }\n return String(value);\n });\n}\n","/**\n * Extension registry — six kinds, first-class, loaded through a single API.\n *\n * The `Extension` shape is aligned with `spec/schemas/extensions/base.schema.json`.\n * Kind-specific manifests (provider / extractor / rule / action / formatter /\n * hook) extend this base structurally; the registry stores the base view\n * and each kind's code carries its own fuller type where needed.\n *\n * **Spec § A.6 — qualified ids.** Every extension is keyed in the registry\n * by `<pluginId>/<id>` (e.g. `core/frontmatter`, `claude/slash`,\n * `hello-world/greet`). `Extension.id` carries the **short** id as authored;\n * `Extension.pluginId` carries the namespace; the registry composes the\n * qualifier internally and exposes lookup APIs that operate on either form\n * (qualified for direct lookup, kind-scoped listing for enumeration).\n *\n * Boot invariant: `new Registry()` is empty. `registry.totalCount() === 0`\n * when the kernel boots with zero extensions. This is the data side of the\n * `kernel-empty-boot` conformance contract.\n */\n\nimport { REGISTRY_TEXTS } from './i18n/registry.texts.js';\nimport type { Stability } from './types.js';\nimport { tx } from './util/tx.js';\n\nexport type ExtensionKind =\n | 'provider'\n | 'extractor'\n | 'rule'\n | 'action'\n | 'formatter'\n | 'hook';\n\nexport const EXTENSION_KINDS: readonly ExtensionKind[] = Object.freeze([\n 'provider',\n 'extractor',\n 'rule',\n 'action',\n 'formatter',\n 'hook',\n] as const);\n\nexport interface Extension {\n /** Short (unqualified) extension id as declared in the manifest. */\n id: string;\n /** Owning plugin namespace. Composed with `id` to form the qualified key. */\n pluginId: string;\n kind: ExtensionKind;\n version: string;\n description?: string;\n stability?: Stability;\n preconditions?: string[];\n entry?: string;\n}\n\n/**\n * Compose the qualified registry key for an extension. Single source of\n * truth so callers don't reinvent the format and a future change (e.g. a\n * different separator) lands in one place.\n */\nexport function qualifiedExtensionId(pluginId: string, id: string): string {\n return `${pluginId}/${id}`;\n}\n\nexport class DuplicateExtensionError extends Error {\n constructor(kind: ExtensionKind, qualifiedId: string) {\n super(tx(REGISTRY_TEXTS.duplicateExtension, { kind, qualifiedId }));\n this.name = 'DuplicateExtensionError';\n }\n}\n\nexport class Registry {\n /** kind → qualifiedId → Extension. */\n readonly #byKind: Map<ExtensionKind, Map<string, Extension>>;\n\n constructor() {\n this.#byKind = new Map(\n EXTENSION_KINDS.map((k) => [k, new Map<string, Extension>()]),\n );\n }\n\n register(ext: Extension): void {\n const bucket = this.#byKind.get(ext.kind);\n if (!bucket) {\n throw new Error(tx(REGISTRY_TEXTS.unknownKind, { kind: ext.kind }));\n }\n if (typeof ext.pluginId !== 'string' || ext.pluginId.length === 0) {\n throw new Error(tx(REGISTRY_TEXTS.missingPluginId, { kind: ext.kind, id: ext.id }));\n }\n const key = qualifiedExtensionId(ext.pluginId, ext.id);\n if (bucket.has(key)) {\n throw new DuplicateExtensionError(ext.kind, key);\n }\n bucket.set(key, ext);\n }\n\n /**\n * Lookup by qualified id (`<pluginId>/<id>`). Returns `undefined` when\n * no extension of that kind is registered under the qualifier.\n */\n get(kind: ExtensionKind, qualifiedId: string): Extension | undefined {\n return this.#byKind.get(kind)?.get(qualifiedId);\n }\n\n /**\n * Convenience wrapper that composes the qualified id for the caller.\n * Equivalent to `get(kind, qualifiedExtensionId(pluginId, id))`.\n */\n find(kind: ExtensionKind, pluginId: string, id: string): Extension | undefined {\n return this.get(kind, qualifiedExtensionId(pluginId, id));\n }\n\n all(kind: ExtensionKind): Extension[] {\n const bucket = this.#byKind.get(kind);\n return bucket ? [...bucket.values()] : [];\n }\n\n count(kind: ExtensionKind): number {\n return this.#byKind.get(kind)?.size ?? 0;\n }\n\n totalCount(): number {\n let n = 0;\n for (const bucket of this.#byKind.values()) n += bucket.size;\n return n;\n }\n}\n","/**\n * Scan orchestrator — runs the Provider → extractor → rule pipeline across\n * every registered extension and emits `ProgressEmitterPort` events in\n * canonical order. The callable extension set is injected via\n * `RunScanOptions.extensions` — the Registry holds manifest metadata, the\n * callable set holds the runtime instances the orchestrator actually\n * invokes. Separating the two lets `sm plugins` and `sm help` introspect\n * the graph without loading code.\n *\n * With zero registered extensions (or a callable set that carries none)\n * the pipeline still produces a valid zero-filled `ScanResult` — the\n * kernel-empty-boot invariant.\n *\n * Roots are validated up front: each entry of `RunScanOptions.roots`\n * must exist on disk as a directory. The first failure throws a clear\n * `Error` naming the offending path. This guards every caller (CLI,\n * server, skill-agent) against silently producing a zero-filled\n * `ScanResult` when a Provider walks a non-existent path — the bug\n * that wiped a populated DB via `sm scan -- --dry-run` (clipanion's\n * `--` made `--dry-run` a positional root that did not exist).\n *\n * Incremental scans: when `priorSnapshot` is supplied, the\n * orchestrator walks the filesystem, hashes each file, and reuses the\n * prior node + its prior-extracted internal links whenever both\n * `bodyHash` and `frontmatterHash` match. New / modified files run\n * through the full extractor pipeline (including the external-url-counter\n * which produces ephemeral pseudo-links). Rules ALWAYS run over the\n * fully merged graph — issue state can change even for an unchanged node\n * (e.g. a previously broken `references` link now resolves because a new\n * node was added). For unchanged nodes the prior `externalRefsCount` is\n * preserved as-is (the external pseudo-links were never persisted, so\n * they cannot be reconstructed; the count survived in the node row).\n *\n * Extractor output model (B.1, post-rename from Detector): extractors\n * return `void` and emit through three callbacks injected on the context:\n * - `ctx.emitLink(link)` → orchestrator validates against\n * `emitsLinkKinds` then partitions into internal / external buckets.\n * - `ctx.enrichNode(partial)` → orchestrator records ONE enrichment\n * entry per `(node, extractor)` so attribution survives into the DB.\n * Persisted into `node_enrichments` (A.8). The author-supplied\n * frontmatter on `node.frontmatter` stays immutable from any Extractor\n * — the enrichment layer is the only writable surface, and rules /\n * formatters consume it via `mergeNodeWithEnrichments`.\n * - `ctx.store` → plugin's own KV / dedicated tables (spec § A.12).\n * Wired by the driving adapter via `RunScanOptions.pluginStores`,\n * which the orchestrator looks up per-extractor by `pluginId` and\n * attaches to the context. The orchestrator never inspects what\n * plugins write through it; the wrapper handles AJV validation\n * when the manifest declared an output schema.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync, statSync } from 'node:fs';\n\n// js-tiktoken ships CJS subpaths without explicit `.cjs` in the import\n// specifier — the lint rule's hard-coded extension matrix doesn't model\n// dual-package CJS subpath exports.\n// eslint-disable-next-line import-x/extensions\nimport { Tiktoken } from 'js-tiktoken/lite';\n// eslint-disable-next-line import-x/extensions\nimport cl100k_base from 'js-tiktoken/ranks/cl100k_base';\nimport yaml from 'js-yaml';\n\nimport pkg from '../package.json' with { type: 'json' };\n\nimport type { IIgnoreFilter } from './scan/ignore.js';\nimport type { Kernel } from './index.js';\nimport type {\n Confidence,\n Issue,\n Link,\n LinkKind,\n Node,\n ScanResult,\n ScanScannedBy,\n Severity,\n TripleSplit,\n} from './types.js';\nimport type {\n ProgressEmitterPort,\n ProgressEvent,\n} from './ports/progress-emitter.js';\nimport { InMemoryProgressEmitter } from './adapters/in-memory-progress.js';\nimport { log } from './util/logger.js';\nimport { installedSpecVersion } from './adapters/plugin-loader.js';\nimport type { IPluginStore } from './adapters/plugin-store.js';\nimport {\n buildProviderFrontmatterValidator,\n type IProviderFrontmatterValidator,\n} from './adapters/schema-validators.js';\nimport { ORCHESTRATOR_TEXTS } from './i18n/orchestrator.texts.js';\nimport { qualifiedExtensionId } from './registry.js';\nimport { tx } from './util/tx.js';\nimport type {\n IProvider,\n IRawNode,\n IExtractorContext,\n IExtractor,\n IHook,\n IHookContext,\n IRule,\n THookTrigger,\n} from './extensions/index.js';\n\n// Resolved once at module init so every scan reuses the same metadata.\n// `installedSpecVersion()` reads `@skill-map/spec/package.json` off disk;\n// failure is non-fatal — fall back to `'unknown'` and keep the field\n// shape spec-conformant (string).\nconst SCANNED_BY: ScanScannedBy = {\n name: 'skill-map',\n version: pkg.version,\n specVersion: resolveSpecVersionSafe(),\n};\n\nfunction resolveSpecVersionSafe(): string {\n try {\n return installedSpecVersion();\n } catch {\n return 'unknown';\n }\n}\n\nexport interface IScanExtensions {\n providers: IProvider[];\n extractors: IExtractor[];\n rules: IRule[];\n /**\n * Optional hooks (spec § A.11). When supplied, the orchestrator's\n * lifecycle dispatcher invokes deterministic hooks subscribed to one\n * of the eight hookable triggers in canonical order with the matching\n * event payload. Absent → no hooks fire (the scan still emits its\n * lifecycle events to `ProgressEmitterPort` for observability).\n * Probabilistic hooks are loaded but skipped here with a stderr\n * advisory until the job subsystem ships once the job subsystem ships.\n */\n hooks?: IHook[];\n}\n\n/**\n * Confidence-tagged plan to repoint `state_*` references from one node\n * path to another. Emitted by the rename heuristic during `runScan` and\n * consumed by `persistScanResult` so the FK migration runs inside the\n * same transaction as the scan zone replace-all.\n */\nexport interface RenameOp {\n from: string;\n to: string;\n confidence: 'high' | 'medium';\n}\n\nexport interface RunScanOptions {\n /**\n * Filesystem roots to walk. Spec requires `minItems: 1`; passing an\n * empty array makes `runScan` throw before any work happens.\n */\n roots: string[];\n emitter?: ProgressEmitterPort;\n /** Runtime extension instances. Absent → empty pipeline. */\n extensions?: IScanExtensions;\n /**\n * Scan scope. Defaults to `'project'`. The CLI flag wiring lands in\n * the config layer wiring; `runScan` already accepts the override\n * so plugins / tests can opt into `'global'` today.\n */\n scope?: 'project' | 'global';\n /**\n * Compute per-node token counts (frontmatter / body / total) using the\n * cl100k_base BPE (the modern OpenAI tokenizer used by GPT-4 / GPT-3.5).\n * Defaults to true. Set false to skip tokenization; `node.tokens` is\n * left undefined (spec-valid: the field is optional).\n */\n tokenize?: boolean;\n /**\n * Prior snapshot for two purposes (decoupled by design):\n *\n * 1. **Rename heuristic** (`spec/db-schema.md` §Rename detection):\n * always evaluated when `priorSnapshot` is supplied. The\n * heuristic compares prior vs current node paths and emits\n * high / medium / ambiguous / orphan classifications. This\n * runs on EVERY `sm scan` (with or without `--changed`) so\n * reorganising files always preserves history, never silently.\n *\n * 2. **Cache reuse** (`sm scan --changed`): only kicks in when\n * `enableCache: true` is also passed. With the flag set, nodes\n * whose `path` exists in the prior with both `bodyHash` and\n * `frontmatterHash` matching the freshly-computed hashes are\n * reused as-is (their internal links and `externalRefsCount`\n * survive); only new / modified nodes run through extractors.\n * Rules always re-run over the merged graph.\n *\n * Pass `null` (or omit) for a fresh scan with no rename detection.\n */\n priorSnapshot?: ScanResult | null;\n /**\n * Reuse unchanged nodes from `priorSnapshot` instead of re-running\n * extractors over them. Defaults to `false` so a plain `sm scan`\n * always re-walks deterministically. `sm scan --changed` flips this\n * to `true` for the perf win on unchanged files.\n *\n * Has no effect without `priorSnapshot`; setting it to `true` with\n * a null prior is a no-op (every file is \"new\").\n */\n enableCache?: boolean;\n /**\n * Filter that decides which paths the Providers skip. Composed by the\n * caller (typically the CLI) from bundled defaults + `config.ignore`\n * + `.skillmapignore`. Providers that omit this option fall back to\n * their own defensive defaults (just enough to keep `.git` /\n * `node_modules` out).\n */\n ignoreFilter?: IIgnoreFilter;\n /**\n * Promote frontmatter-validation findings from `warn` to `error`.\n * Defaults to false. The CLI surfaces this via `--strict` on `sm scan`\n * and the `scan.strict` config key. When false, the orchestrator\n * still emits a `frontmatter-invalid` issue per malformed file but\n * leaves the severity at `warn` so a clean scan exits 0; when true,\n * the same finding becomes `error` and the scan exits 1.\n */\n strict?: boolean;\n /**\n * Spec § A.9 — fine-grained Extractor cache breadcrumbs from the\n * prior scan. Shape: `Map<nodePath, Map<qualifiedExtractorId, bodyHashAtRun>>`.\n * Loaded from the `scan_extractor_runs` table by the CLI before\n * invoking `runScan`; absent / empty for a fresh DB or an out-of-band\n * caller that does not maintain a cache. Decoupled from `priorSnapshot`\n * because the runs live in a sibling table and are useful only when\n * `enableCache` is also set.\n *\n * Cache decision per `(node, extractor)`:\n * - body+frontmatter hashes match the prior node AND every currently-\n * registered extractor that applies to this kind has a matching\n * row → full skip, all prior outbound links reused.\n * - some applicable extractor lacks a matching row (newly registered,\n * or its prior run targeted a different body hash) → run only the\n * missing extractors, drop prior links whose `sources` map to any\n * missing extractor or to an extractor that is no longer registered.\n */\n priorExtractorRuns?: Map<string, Map<string, string>>;\n /**\n * Spec § A.12 — per-plugin storage wrappers exposed to extractors via\n * `ctx.store`. Keyed by `pluginId`; absent / missing entry leaves\n * `ctx.store` undefined for that extractor (the existing contract).\n *\n * The kernel does not construct these — the driving adapter (CLI,\n * future server) builds them with `makePluginStore` from\n * `kernel/adapters/plugin-store.js` and threads them through. This\n * keeps the orchestrator persistence-agnostic (the wrapper supplies\n * its own persist callback) and lets tests inject a captured-call\n * mock without spinning up a DB.\n */\n pluginStores?: ReadonlyMap<string, IPluginStore>;\n}\n\n/**\n * Spec § A.9 — runs to persist into `scan_extractor_runs`. One entry\n * per `(nodePath, qualifiedExtractorId)` pair the orchestrator decided\n * \"this extractor is current for this body\". Includes both freshly-run\n * pairs (extractor invoked this scan) and reused pairs (cached node, the\n * extractor's prior run still applies to the same body hash). Excludes\n * obsolete pairs — extractors that ran in the prior but are no longer\n * registered — so a replace-all persist drops them automatically.\n */\nexport interface IExtractorRunRecord {\n nodePath: string;\n extractorId: string;\n bodyHashAtRun: string;\n ranAt: number;\n}\n\n/**\n * Spec § A.8 — universal enrichment layer.\n *\n * One entry per `(nodePath, qualifiedExtractorId)` pair an Extractor\n * produced via `ctx.enrichNode(...)` during the walk. Attribution is\n * preserved per-Extractor (rather than merged client-side as B.1 did)\n * so the persistence layer can:\n *\n * - upsert a single row per pair (stable PRIMARY KEY conflict on\n * re-extract);\n * - flag probabilistic rows `stale = 1` when the body changes between\n * scans (preserving the prior LLM cost);\n * - feed `mergeNodeWithEnrichments` with `enrichedAt`-sorted partials\n * for last-write-wins per field at read time.\n *\n * `value` is the cumulative merge across every `enrichNode` call that\n * Extractor made for this node within this scan — multiple\n * `ctx.enrichNode({...})` calls inside one `extract(ctx)` invocation\n * fold into a single row, but two different Extractors hitting the\n * same node yield two distinct rows.\n *\n * `isProbabilistic` is denormalised so the persistence layer's stale\n * flag query stays a single-table read; recomputing from the live\n * registry would force every read-path to thread the runtime extension\n * set through.\n */\nexport interface IEnrichmentRecord {\n nodePath: string;\n extractorId: string;\n bodyHashAtEnrichment: string;\n value: Partial<Node>;\n enrichedAt: number;\n isProbabilistic: boolean;\n}\n\n/**\n * Same as `runScan` but also returns the rename heuristic's `RenameOp[]`\n * — the high- and medium-confidence renames the persistence layer must\n * apply to `state_*` rows inside the same tx as the scan zone replace-\n * all (per `spec/db-schema.md` §Rename detection). Most callers want\n * `runScan` (which returns just `ScanResult`); the CLI's `sm scan`\n * uses this variant so it can hand the ops off to `persistScanResult`.\n *\n * Also returns `extractorRuns` — the Spec § A.9 fine-grained cache\n * breadcrumbs the CLI persists into `scan_extractor_runs` so the next\n * incremental scan can decide per-(node, extractor) whether re-running\n * is required.\n */\nexport async function runScanWithRenames(\n _kernel: Kernel,\n options: RunScanOptions,\n): Promise<{\n result: ScanResult;\n renameOps: RenameOp[];\n extractorRuns: IExtractorRunRecord[];\n enrichments: IEnrichmentRecord[];\n}> {\n return runScanInternal(_kernel, options);\n}\n\nexport async function runScan(\n _kernel: Kernel,\n options: RunScanOptions,\n): Promise<ScanResult> {\n const { result } = await runScanInternal(_kernel, options);\n return result;\n}\n\n// eslint-disable-next-line complexity\nasync function runScanInternal(\n _kernel: Kernel,\n options: RunScanOptions,\n): Promise<{\n result: ScanResult;\n renameOps: RenameOp[];\n extractorRuns: IExtractorRunRecord[];\n enrichments: IEnrichmentRecord[];\n}> {\n validateRoots(options.roots);\n\n const start = Date.now();\n const scannedAt = start;\n const emitter = options.emitter ?? new InMemoryProgressEmitter();\n const exts = options.extensions ?? { providers: [], extractors: [], rules: [] };\n const hookDispatcher = makeHookDispatcher(exts.hooks ?? [], emitter);\n const tokenize = options.tokenize !== false;\n const scope: 'project' | 'global' = options.scope ?? 'project';\n const strict = options.strict === true;\n // Encoder is heavyweight to construct (loads the cl100k_base BPE table\n // once); reuse a single instance across the whole scan.\n const encoder = tokenize ? new Tiktoken(cl100k_base) : null;\n const prior = options.priorSnapshot ?? null;\n const enableCache = options.enableCache === true;\n // Spec § A.9 — `priorExtractorRuns === undefined` means the caller\n // doesn't track the fine-grained Extractor cache (legacy behaviour: out-\n // of-band tests, alternate driving adapters that have no DB). In that\n // case we fall back to the pre-A.9 model where the node-level body /\n // frontmatter hash check is sufficient and every applicable extractor\n // is assumed to have run against the prior body. Passing an explicit\n // (possibly empty) Map opts the caller into the fine-grained path.\n const priorExtractorRuns = options.priorExtractorRuns;\n\n const priorIndex = indexPriorSnapshot(prior);\n\n // Spec 0.8.0: each Provider owns its per-kind frontmatter\n // schemas. Compose a single AJV-backed validator over the live set of\n // Providers so the orchestrator can ask it directly during the walk.\n const providerFrontmatter = buildProviderFrontmatterValidator(exts.providers);\n\n const scanStartedEvent = makeEvent('scan.started', { roots: options.roots });\n emitter.emit(scanStartedEvent);\n await hookDispatcher.dispatch('scan.started', scanStartedEvent);\n\n const walked = await walkAndExtract({\n providers: exts.providers,\n extractors: exts.extractors,\n roots: options.roots,\n ...(options.ignoreFilter ? { ignoreFilter: options.ignoreFilter } : {}),\n emitter,\n encoder,\n strict,\n enableCache,\n prior,\n priorIndex,\n priorExtractorRuns,\n providerFrontmatter,\n pluginStores: options.pluginStores,\n });\n\n // External pseudo-links (target is http(s)://) drive `externalRefsCount`\n // and are then dropped: never persisted, never seen by rules, never in\n // result.links. The string-prefix check is the contract — see\n // external-url-counter/index.ts.\n recomputeLinkCounts(walked.nodes, walked.internalLinks);\n recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);\n\n // Spec § A.11 — Hook dispatch for `extractor.completed`. Aggregated:\n // one event per registered extractor, after the full walk completes.\n // The payload carries the qualified extractor id so a hook with a\n // `filter: { extractorId: '...' }` can target a single extractor.\n // No per-node fan-out — that lives in `scan.progress` which is\n // deliberately NOT hookable (too verbose).\n for (const extractor of exts.extractors) {\n const extractorId = qualifiedExtensionId(extractor.pluginId, extractor.id);\n const evt = makeEvent('extractor.completed', { extractorId });\n emitter.emit(evt);\n await hookDispatcher.dispatch('extractor.completed', evt);\n }\n\n // Rules ALWAYS re-run over the merged graph (no shortcut for\n // incremental scans): the issue set for an \"unchanged\" node can flip\n // when a sibling node changes.\n const issues = await runRules(exts.rules, walked.nodes, walked.internalLinks, emitter, hookDispatcher);\n // Frontmatter-invalid issues from the walk land here so the rename\n // heuristic (next pass) sees them and the final stats.issuesCount\n // reflects them.\n for (const issue of walked.frontmatterIssues) issues.push(issue);\n\n // Rename heuristic runs after rules so the merged graph is final. The\n // returned `RenameOp[]` flows through to `persistScanResult` so FK\n // migration lands inside the same tx as the scan zone replace-all.\n const renameOps = prior ? detectRenamesAndOrphans(prior, walked.nodes, issues) : [];\n\n const stats = {\n // `filesSkipped` is \"files walked but not classified by any Provider\".\n // Today every walked file IS classified by its Provider (the `claude`\n // Provider's `classify()` always returns a kind, falling back to\n // `'note'`), so this is always 0. Wired now so the field shape is\n // spec-conformant; meaningful once multiple Providers compete.\n filesWalked: walked.filesWalked,\n filesSkipped: 0,\n nodesCount: walked.nodes.length,\n linksCount: walked.internalLinks.length,\n issuesCount: issues.length,\n durationMs: Date.now() - start,\n };\n\n const scanCompletedEvent = makeEvent('scan.completed', { stats });\n emitter.emit(scanCompletedEvent);\n await hookDispatcher.dispatch('scan.completed', scanCompletedEvent);\n\n return {\n result: {\n schemaVersion: 1,\n scannedAt,\n scope,\n roots: options.roots,\n providers: exts.providers.map((a) => a.id),\n scannedBy: SCANNED_BY,\n nodes: walked.nodes,\n links: walked.internalLinks,\n issues,\n stats,\n },\n renameOps,\n extractorRuns: walked.extractorRuns,\n enrichments: walked.enrichments,\n };\n}\n\n/**\n * Validate every root exists as a directory BEFORE any IO, BEFORE the\n * tokenizer is constructed, BEFORE `scan.started` fires. Throws on the\n * first failure — single-error feedback is enough; the user fixes it\n * and re-runs. Without this guard the claude Provider's `walk()` swallows\n * ENOENT inside `readdir` and returns silently, which lets a non-existent\n * root produce a valid-looking zero-filled `ScanResult` — directly\n * enabling the `sm scan -- --dry-run` typo-trap that wipes a populated\n * DB.\n *\n * Spec contract (`scan-result.schema.json#/properties/roots/minItems: 1`):\n * a ScanResult must report at least one walked root. The CLI defaults\n * `roots` to `['.']` when no positional args are supplied, so the\n * empty-array branch is a programming error from the CLI surface.\n */\nfunction validateRoots(roots: string[]): void {\n if (roots.length === 0) {\n throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);\n }\n for (const root of roots) {\n if (!existsSync(root) || !statSync(root).isDirectory()) {\n throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));\n }\n }\n}\n\ninterface IPriorIndex {\n /** Prior nodes keyed by path so per-file lookup is O(1). */\n priorNodesByPath: Map<string, Node>;\n /** Set of every prior node path — used to disambiguate inverted\n * `supersedes` links (see `originatingNodeOf`). */\n priorNodePaths: Set<string>;\n /**\n * Prior internal links bucketed by **originating node** — the node\n * whose body / frontmatter the extractor was processing when it emitted\n * the link. For most kinds that equals `link.source`, but the\n * frontmatter extractor emits inverted `supersedes` links where the\n * originating node is `link.target`.\n */\n priorLinksByOriginating: Map<string, Link[]>;\n /**\n * Per-node frontmatter-invalid / -malformed issues from the prior — we\n * reuse them when the cache is hit, otherwise the incremental scan\n * would silently drop the warning that landed on the prior pass.\n */\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n}\n\n// eslint-disable-next-line complexity\nfunction indexPriorSnapshot(prior: ScanResult | null): IPriorIndex {\n const priorNodesByPath = new Map<string, Node>();\n const priorNodePaths = new Set<string>();\n const priorLinksByOriginating = new Map<string, Link[]>();\n const priorFrontmatterIssuesByNode = new Map<string, Issue[]>();\n if (!prior) {\n return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };\n }\n for (const node of prior.nodes) {\n priorNodesByPath.set(node.path, node);\n priorNodePaths.add(node.path);\n }\n for (const link of prior.links) {\n const key = originatingNodeOf(link, priorNodePaths);\n const list = priorLinksByOriginating.get(key);\n if (list) list.push(link);\n else priorLinksByOriginating.set(key, [link]);\n }\n for (const issue of prior.issues) {\n if (issue.ruleId !== 'frontmatter-invalid' && issue.ruleId !== 'frontmatter-malformed') continue;\n if (issue.nodeIds.length !== 1) continue;\n const path = issue.nodeIds[0]!;\n const list = priorFrontmatterIssuesByNode.get(path);\n if (list) list.push(issue);\n else priorFrontmatterIssuesByNode.set(path, [issue]);\n }\n return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };\n}\n\ninterface IWalkAndExtractOptions {\n providers: IProvider[];\n extractors: IExtractor[];\n roots: string[];\n ignoreFilter?: IIgnoreFilter;\n emitter: ProgressEmitterPort;\n encoder: Tiktoken | null;\n strict: boolean;\n enableCache: boolean;\n prior: ScanResult | null;\n priorIndex: IPriorIndex;\n /**\n * Spec § A.9 — fine-grained Extractor cache breadcrumbs from the\n * prior scan, keyed `nodePath → qualifiedExtractorId → bodyHashAtRun`.\n * `undefined` opts out of the fine-grained path (legacy callers that\n * don't track the cache); the orchestrator falls back to the pre-A.9\n * node-level cache check.\n */\n priorExtractorRuns: Map<string, Map<string, string>> | undefined;\n providerFrontmatter: IProviderFrontmatterValidator;\n /**\n * Spec § A.12 — per-plugin `ctx.store` wrappers, keyed by `pluginId`.\n * Threaded through to `runExtractorsForNode → buildExtractorContext`\n * unchanged. `undefined` keeps `ctx.store` undefined for every\n * extractor (the legacy contract).\n */\n pluginStores: ReadonlyMap<string, IPluginStore> | undefined;\n}\n\ninterface IWalkAndExtractResult {\n nodes: Node[];\n internalLinks: Link[];\n externalLinks: Link[];\n /** Node paths reused verbatim from the prior snapshot. Their\n * `externalRefsCount` must NOT be zeroed before recomputation. */\n cachedPaths: Set<string>;\n /** Frontmatter-validation findings collected during the walk; the\n * composer appends these to the rule-emitted issue list so the\n * final ordering stays \"rules first, then derived issues\". */\n frontmatterIssues: Issue[];\n /**\n * Spec § A.8 — per-extractor enrichment records collected from\n * `ctx.enrichNode(...)` calls during the walk. One entry per\n * `(nodePath, extractorId)` pair an Extractor enriched. The\n * persistence layer upserts these into `node_enrichments`; the\n * read-side `mergeNodeWithEnrichments` helper combines them with\n * the author frontmatter for rule consumption.\n *\n * Attribution is preserved per-Extractor: two Extractors enriching\n * the same node produce two records, not one merged value. If a\n * single Extractor calls `ctx.enrichNode(...)` multiple times within\n * one `extract()` invocation, the partials fold into one record's\n * `value` (last-write-wins per field).\n */\n enrichments: IEnrichmentRecord[];\n /** Every `IRawNode` a Provider yielded across the whole scan\n * (including cached reuse). With one Provider it equals\n * `nodesCount`; with future multi-Provider scans walking overlapping\n * roots it can diverge. */\n filesWalked: number;\n /**\n * Spec § A.9 — the rows the persistence layer writes into\n * `scan_extractor_runs`. Includes both freshly-run pairs (extractor\n * invoked this scan) and reused pairs (cached node, the extractor's\n * prior run still applies to the same body hash). Excludes obsolete\n * pairs (extractor was uninstalled since the prior scan).\n */\n extractorRuns: IExtractorRunRecord[];\n}\n\n/**\n * Run a set of extractors against a single node, collecting their link\n * emissions and node-enrichment partials. Each extractor is invoked\n * exactly once with a fresh `IExtractorContext`. Caller decides what\n * to do with the returned arrays (push into per-scan buffers, write to\n * a focused refresh result, etc.).\n *\n * Exported so `cli/commands/refresh.ts` can reuse the same wiring it\n * needs for re-running a single extractor against a single node — the\n * pre-extraction code in `refresh.ts` was hand-duplicating this loop\n * (audit item V4).\n *\n * Within this call, multiple `enrichNode(partial)` calls from the same\n * extractor against the same node fold into one record (last-write-wins\n * per field) — same contract as the in-scan path.\n */\nexport async function runExtractorsForNode(opts: {\n extractors: IExtractor[];\n node: Node;\n body: string;\n frontmatter: Record<string, unknown>;\n bodyHash: string;\n emitter: ProgressEmitterPort;\n /**\n * Spec § A.12 — per-plugin `ctx.store` wrappers keyed by `pluginId`.\n * The map's lookup is per-extractor inside the loop, so callers that\n * don't track plugin storage can omit it; the resulting `ctx.store`\n * stays `undefined` (the existing contract).\n */\n pluginStores?: ReadonlyMap<string, IPluginStore>;\n}): Promise<{\n internalLinks: Link[];\n externalLinks: Link[];\n enrichments: IEnrichmentRecord[];\n}> {\n const internalLinks: Link[] = [];\n const externalLinks: Link[] = [];\n const enrichmentBuffer = new Map<string, IEnrichmentRecord>();\n\n for (const extractor of opts.extractors) {\n const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);\n const isProb = extractor.mode === 'probabilistic';\n const emitLink = (link: Link): void => {\n const validated = validateLink(extractor, link, opts.emitter);\n if (!validated) return;\n if (isExternalUrlLink(validated)) externalLinks.push(validated);\n else internalLinks.push(validated);\n };\n const enrichNode = (partial: Partial<Node>): void => {\n const key = `${opts.node.path}\\x00${qualifiedId}`;\n const existing = enrichmentBuffer.get(key);\n if (existing) {\n existing.value = { ...existing.value, ...partial };\n existing.enrichedAt = Date.now();\n } else {\n enrichmentBuffer.set(key, {\n nodePath: opts.node.path,\n extractorId: qualifiedId,\n bodyHashAtEnrichment: opts.bodyHash,\n value: { ...partial },\n enrichedAt: Date.now(),\n isProbabilistic: isProb,\n });\n }\n };\n const store = opts.pluginStores?.get(extractor.pluginId);\n const ctx = buildExtractorContext(\n extractor,\n opts.node,\n opts.body,\n opts.frontmatter,\n emitLink,\n enrichNode,\n store,\n );\n await extractor.extract(ctx);\n }\n\n return {\n internalLinks,\n externalLinks,\n enrichments: Array.from(enrichmentBuffer.values()),\n };\n}\n\n/**\n * Compute the per-(node, extractor) cache decision for a single node.\n * Returns:\n * - `applicableExtractors` — extractors whose `applicableKinds`\n * accepts this node's kind (or unrestricted).\n * - `applicableQualifiedIds` — set of qualified ids of the above.\n * - `cachedQualifiedIds` — applicable extractors whose prior run for\n * this node's body hash is still valid.\n * - `missingExtractors` — applicable extractors that need to run.\n * - `fullCacheHit` — true iff the node-level hash matched AND every\n * applicable extractor is cached (nothing to re-extract).\n *\n * Legacy fallback: when `priorExtractorRuns === undefined` the caller\n * did not load fine-grained breadcrumbs (out-of-band tests, alternate\n * driving adapters); we treat every applicable extractor as cached\n * when the node-level hashes match — preserves the pre-A.9 contract.\n */\n// eslint-disable-next-line complexity\nfunction computeCacheDecision(opts: {\n extractors: IExtractor[];\n kind: string;\n nodePath: string;\n bodyHash: string;\n nodeHashCacheEligible: boolean;\n priorExtractorRuns: Map<string, Map<string, string>> | undefined;\n}): {\n applicableExtractors: IExtractor[];\n applicableQualifiedIds: Set<string>;\n cachedQualifiedIds: Set<string>;\n missingExtractors: IExtractor[];\n fullCacheHit: boolean;\n} {\n const applicableExtractors = opts.extractors.filter(\n (ex) => ex.applicableKinds === undefined || ex.applicableKinds.includes(opts.kind),\n );\n const applicableQualifiedIds = new Set(\n applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id)),\n );\n const cachedQualifiedIds = new Set<string>();\n const missingExtractors: IExtractor[] = [];\n\n if (opts.priorExtractorRuns === undefined) {\n if (opts.nodeHashCacheEligible) {\n for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);\n } else {\n for (const ex of applicableExtractors) missingExtractors.push(ex);\n }\n } else {\n const priorRunsForNode = opts.priorExtractorRuns.get(opts.nodePath) ?? new Map<string, string>();\n for (const ex of applicableExtractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n const priorBody = priorRunsForNode.get(qualified);\n if (opts.nodeHashCacheEligible && priorBody === opts.bodyHash) {\n cachedQualifiedIds.add(qualified);\n } else {\n missingExtractors.push(ex);\n }\n }\n }\n\n return {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds,\n missingExtractors,\n fullCacheHit: opts.nodeHashCacheEligible && missingExtractors.length === 0,\n };\n}\n\n/**\n * Shallow-clone a prior node, reshape its outbound internal links per\n * A.9 source rules, and re-emit its prior frontmatter issues. Shared\n * by the full-cache-hit and partial-cache-hit branches of\n * `walkAndExtract`.\n *\n * Reshape rules (A.9 sources):\n * - missing source (extractor will re-emit) → drop link\n * - all-obsolete sources → drop link\n * - cached + obsolete → trim obsolete from `sources`\n * - cached only → keep verbatim\n */\nfunction cloneNodeAndReshapeLinks(opts: {\n priorNode: Node;\n strict: boolean;\n cachedQualifiedIds: Set<string>;\n applicableQualifiedIds: Set<string>;\n shortIdToQualified: Map<string, string[]>;\n priorLinksByOriginating: Map<string, Link[]>;\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n}): { node: Node; internalLinks: Link[]; frontmatterIssues: Issue[] } {\n // Shallow-clone to avoid mutating the caller's prior snapshot when\n // `recomputeLinkCounts` resets per-node counts later.\n const node: Node = { ...opts.priorNode, bytes: { ...opts.priorNode.bytes } };\n if (opts.priorNode.tokens) node.tokens = { ...opts.priorNode.tokens };\n\n const internalLinks: Link[] = [];\n const reusedLinks = opts.priorLinksByOriginating.get(opts.priorNode.path) ?? [];\n for (const link of reusedLinks) {\n const reshaped = reuseCachedLink(\n link,\n opts.shortIdToQualified,\n opts.cachedQualifiedIds,\n opts.applicableQualifiedIds,\n );\n if (reshaped) internalLinks.push(reshaped);\n }\n\n // Re-emit prior frontmatter issues unchanged (frontmatter hash is\n // unchanged in both cache branches). `strict` can promote\n // `warn → error` retroactively.\n const frontmatterIssues: Issue[] = [];\n const reusedFm = opts.priorFrontmatterIssuesByNode.get(opts.priorNode.path) ?? [];\n for (const issue of reusedFm) {\n frontmatterIssues.push({ ...issue, severity: opts.strict ? 'error' : 'warn' });\n }\n\n return { node, internalLinks, frontmatterIssues };\n}\n\n/**\n * Build the reused-node bundle for a node that fully cache-hit (body\n * + frontmatter unchanged AND every applicable extractor still has a\n * matching `scan_extractor_runs` row). Caller pushes the returned\n * arrays into its scan-wide buffers and emits the progress event.\n *\n * Adds `extractorRuns` rows for every still-cached extractor so the\n * cache survives the next replace-all persist.\n */\nfunction reusePriorNode(opts: {\n priorNode: Node;\n bodyHash: string;\n strict: boolean;\n cachedQualifiedIds: Set<string>;\n applicableQualifiedIds: Set<string>;\n shortIdToQualified: Map<string, string[]>;\n priorLinksByOriginating: Map<string, Link[]>;\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n}): {\n node: Node;\n internalLinks: Link[];\n frontmatterIssues: Issue[];\n extractorRuns: IExtractorRunRecord[];\n} {\n const base = cloneNodeAndReshapeLinks(opts);\n\n // Persist one `scan_extractor_runs` row per still-cached pair so the\n // cache survives the next replace-all persist (without this, cached\n // pairs silently disappear).\n const ranAt = Date.now();\n const extractorRuns: IExtractorRunRecord[] = [];\n for (const qualified of opts.cachedQualifiedIds) {\n extractorRuns.push({\n nodePath: opts.priorNode.path,\n extractorId: qualified,\n bodyHashAtRun: opts.bodyHash,\n ranAt,\n });\n }\n\n return { ...base, extractorRuns };\n}\n\n/**\n * Build a brand-new `Node` row from raw provider output and validate\n * its frontmatter. Used by the \"no cache hit\" branch of\n * `walkAndExtract`. Two frontmatter issue paths:\n * - With a frontmatter fence: AJV-validate against the Provider's\n * per-kind schema.\n * - Without a fence but a body that opens with malformed `---`:\n * emit `frontmatter-malformed`.\n *\n * Severity defaults to `warn`; `strict` promotes everything to `error`.\n */\nfunction buildFreshNodeAndValidateFrontmatter(opts: {\n raw: IRawNode;\n kind: string;\n provider: IProvider;\n bodyHash: string;\n frontmatterHash: string;\n encoder: Tiktoken | null;\n providerFrontmatter: IProviderFrontmatterValidator;\n strict: boolean;\n}): { node: Node; frontmatterIssues: Issue[] } {\n const node = buildNode({\n path: opts.raw.path,\n kind: opts.kind,\n providerId: opts.provider.id,\n frontmatterRaw: opts.raw.frontmatterRaw,\n body: opts.raw.body,\n frontmatter: opts.raw.frontmatter,\n bodyHash: opts.bodyHash,\n frontmatterHash: opts.frontmatterHash,\n encoder: opts.encoder,\n });\n\n const frontmatterIssues: Issue[] = [];\n if (opts.raw.frontmatterRaw.length > 0) {\n const fmIssue = validateFrontmatter(\n opts.providerFrontmatter,\n opts.provider,\n opts.kind,\n opts.raw.frontmatter,\n opts.raw.path,\n opts.strict,\n );\n if (fmIssue) frontmatterIssues.push(fmIssue);\n } else {\n const malformed = detectMalformedFrontmatter(opts.raw.body, opts.raw.path, opts.strict);\n if (malformed) frontmatterIssues.push(malformed);\n }\n\n return { node, frontmatterIssues };\n}\n\n// Main scan loop — for each provider/raw node: hash, classify, decide\n// cache (full / partial / none), reuse or build, run extractors,\n// record runs. Helpers extracted (`computeCacheDecision`,\n// `cloneNodeAndReshapeLinks`, `reusePriorNode`,\n// `buildFreshNodeAndValidateFrontmatter`, `runExtractorsForNode`)\n// already encapsulate the heavy-lift; remaining branching is the\n// dispatch glue that ties them together per-iteration.\n// eslint-disable-next-line complexity\nasync function walkAndExtract(opts: IWalkAndExtractOptions): Promise<IWalkAndExtractResult> {\n const {\n providers,\n extractors,\n roots,\n ignoreFilter,\n emitter,\n encoder,\n strict,\n enableCache,\n prior,\n priorIndex,\n priorExtractorRuns,\n providerFrontmatter,\n pluginStores,\n } = opts;\n const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = priorIndex;\n\n const nodes: Node[] = [];\n const internalLinks: Link[] = [];\n const externalLinks: Link[] = [];\n const cachedPaths = new Set<string>();\n const frontmatterIssues: Issue[] = [];\n // A.8 enrichment buffer. `ctx.enrichNode(partial)` calls fold into a\n // per-Extractor entry keyed by `(nodePath, qualifiedExtractorId)` so the\n // persistence layer can upsert exactly one row per pair into\n // `node_enrichments`. Attribution survives across scans, which lets:\n // - the stale flag query single-table on (extractor_id, body_hash);\n // - `sm refresh` re-run only the Extractor whose row is stale;\n // - the read-time merge sort by `enriched_at` for last-write-wins.\n // Within a single `extract()` invocation, multiple enrichNode calls fold\n // into the same record's `value` (last-write-wins per field).\n const enrichmentBuffer = new Map<string, IEnrichmentRecord>();\n // Spec § A.9 — accumulator for `scan_extractor_runs`. One row per\n // (nodePath, qualifiedExtractorId) pair the orchestrator decided \"this\n // extractor is current for this body\". Includes both freshly-run pairs\n // and pairs whose prior run was reused intact via the cache.\n const extractorRuns: IExtractorRunRecord[] = [];\n let filesWalked = 0;\n let index = 0;\n const walkOptions = ignoreFilter ? { ignoreFilter } : {};\n\n // Build the short→qualified id map once for the whole scan. Used to\n // bridge between author-supplied `link.sources` (short id, e.g.\n // `'slash'`) and the qualified ids (`'claude/slash'`) that drive cache\n // bookkeeping. Multiple plugins can in theory expose extractors with\n // the same short id; we keep all qualifieds per short id so the\n // partial-cache filter recognises any of them as \"still cached\".\n const shortIdToQualified = new Map<string, string[]>();\n for (const ex of extractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n const list = shortIdToQualified.get(ex.id);\n if (list) list.push(qualified);\n else shortIdToQualified.set(ex.id, [qualified]);\n }\n\n for (const provider of providers) {\n for await (const raw of provider.walk(roots, walkOptions)) {\n filesWalked += 1;\n const bodyHash = sha256(raw.body);\n // Canonical-form rationale — hash a CANONICAL form of the frontmatter so a YAML\n // formatter pass (re-indent, sort keys, normalise trailing\n // newline, swap single↔double quotes) doesn't break the\n // medium-confidence rename heuristic. Fallback to raw text when\n // canonicalisation produces empty (parse failed but raw is\n // non-empty) so a malformed-YAML file still hashes\n // deterministically against itself.\n const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));\n const priorNode = priorNodesByPath.get(raw.path);\n // Cache reuse is gated on the explicit `enableCache` option (Step\n // 5.8). The presence of a `prior` alone is no longer enough — a\n // plain `sm scan` always re-walks deterministically; only\n // `sm scan --changed` flips `enableCache` on. The rename heuristic\n // uses `prior` independently of `enableCache`.\n //\n // Spec § A.9 layered the per-(node, extractor) check on top of\n // the existing per-node body+frontmatter check. The node-level\n // hashes still gate cache eligibility (a body change forces a full\n // re-extract regardless of which extractors were registered);\n // within an eligible node we then ask \"did every currently-applicable\n // extractor run against this body hash already?\". A new extractor\n // registered between scans yields a partial hit: we run only the\n // newcomer.\n const nodeHashCacheEligible =\n enableCache &&\n prior !== null &&\n priorNode !== undefined &&\n priorNode.bodyHash === bodyHash &&\n priorNode.frontmatterHash === frontmatterHash;\n\n const kind = provider.classify(raw.path, raw.frontmatter);\n index += 1;\n\n // Per-node, per-extractor cache decision (only meaningful when the\n // node-level hashes already matched). For each extractor that\n // applies to this kind, ask whether the prior runs map already\n // records an entry against the current body hash. Missing entries\n // run; satisfied entries are skipped.\n //\n // Legacy fallback: when `priorExtractorRuns === undefined` the\n // caller did not load the fine-grained breadcrumbs (out-of-band\n // tests, alternate driving adapters), so we treat every applicable\n // extractor as cached when the node-level hashes match. This\n // preserves the pre-A.9 contract for callers that did not opt in.\n const cacheDecision = computeCacheDecision({\n extractors,\n kind,\n nodePath: raw.path,\n bodyHash,\n nodeHashCacheEligible,\n priorExtractorRuns,\n });\n const {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds,\n missingExtractors,\n fullCacheHit,\n } = cacheDecision;\n\n if (fullCacheHit && priorNode) {\n const reused = reusePriorNode({\n priorNode,\n bodyHash,\n strict,\n cachedQualifiedIds,\n applicableQualifiedIds,\n shortIdToQualified,\n priorLinksByOriginating,\n priorFrontmatterIssuesByNode,\n });\n nodes.push(reused.node);\n cachedPaths.add(reused.node.path);\n for (const link of reused.internalLinks) internalLinks.push(link);\n for (const issue of reused.frontmatterIssues) frontmatterIssues.push(issue);\n for (const run of reused.extractorRuns) extractorRuns.push(run);\n emitter.emit(makeEvent('scan.progress', { index, path: raw.path, kind, cached: true }));\n continue;\n }\n\n // --- partial or full re-extract path -------------------------------\n // Either a brand-new node, a node whose body / frontmatter changed,\n // or a node whose hashes match but at least one applicable\n // extractor lacks a matching `scan_extractor_runs` row (newly\n // registered, or its prior run was against a different body hash).\n\n let node: Node;\n const partialCacheHit =\n nodeHashCacheEligible && cachedQualifiedIds.size > 0 && priorNode !== undefined;\n if (partialCacheHit && priorNode) {\n // Body / frontmatter unchanged AND at least one extractor is\n // still cached; reuse the prior node row + reshape its links\n // and frontmatter issues. NOT marking the path as `cachedPaths`\n // because some extraction is happening — the `externalRefsCount`\n // recompute wants the node re-derived from a fresh extractor\n // pass (the missing extractor may emit URLs).\n const partial = cloneNodeAndReshapeLinks({\n priorNode, strict, cachedQualifiedIds, applicableQualifiedIds,\n shortIdToQualified, priorLinksByOriginating, priorFrontmatterIssuesByNode,\n });\n node = partial.node;\n for (const link of partial.internalLinks) internalLinks.push(link);\n for (const issue of partial.frontmatterIssues) frontmatterIssues.push(issue);\n nodes.push(node);\n } else {\n const fresh = buildFreshNodeAndValidateFrontmatter({\n raw, kind, provider, bodyHash, frontmatterHash, encoder,\n providerFrontmatter, strict,\n });\n node = fresh.node;\n nodes.push(node);\n for (const issue of fresh.frontmatterIssues) frontmatterIssues.push(issue);\n }\n emitter.emit(makeEvent('scan.progress', {\n index,\n path: raw.path,\n kind,\n cached: false,\n ...(partialCacheHit ? { partialCache: true } : {}),\n }));\n\n // Decide which extractors actually run. Full re-extract → all\n // applicable. Partial cache → only the missing ones. Either way,\n // the orchestrator records a fresh `scan_extractor_runs` row for\n // each invocation AND for each cached extractor whose contribution\n // survived intact (so the cache persists across scans).\n const extractorsToRun = partialCacheHit ? missingExtractors : applicableExtractors;\n const extractResult = await runExtractorsForNode({\n extractors: extractorsToRun,\n node,\n body: raw.body,\n frontmatter: raw.frontmatter,\n bodyHash,\n emitter,\n ...(pluginStores ? { pluginStores } : {}),\n });\n for (const link of extractResult.internalLinks) internalLinks.push(link);\n for (const link of extractResult.externalLinks) externalLinks.push(link);\n // Merge per-node enrichment records into the scan-wide buffer.\n // Keys are `${nodePath}\\x00${extractorId}` and unique per node\n // (paths are unique across the scan), so `set()` is collision-free\n // — but we keep the keyed shape in case future code wants to fold\n // across providers walking the same node.\n for (const enr of extractResult.enrichments) {\n enrichmentBuffer.set(`${enr.nodePath}\\x00${enr.extractorId}`, enr);\n }\n\n // Persist a `scan_extractor_runs` row for every applicable\n // extractor (both freshly-run AND cached ones whose contribution\n // we reused). Skipping cached entries here would let the\n // replace-all persist forget them — defeating the whole point of\n // the partial-cache path.\n const ranAt = Date.now();\n for (const ex of applicableExtractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n extractorRuns.push({\n nodePath: node.path,\n extractorId: qualified,\n bodyHashAtRun: bodyHash,\n ranAt,\n });\n }\n }\n }\n\n return {\n nodes,\n internalLinks,\n externalLinks,\n cachedPaths,\n frontmatterIssues,\n filesWalked,\n enrichments: [...enrichmentBuffer.values()],\n extractorRuns,\n };\n}\n\n/**\n * Spec § A.9 — decide whether a prior link can be reused on a cached\n * node, and how its `sources` array should be reshaped.\n *\n * Three buckets per source short id:\n * - **Cached**: short id maps to a currently-registered qualified id\n * that has a matching `scan_extractor_runs` row for this body hash.\n * The contribution is fresh and survives.\n * - **Missing**: short id maps to a currently-registered qualified id\n * that does NOT have a matching row for this body hash (newly\n * registered, or its prior run targeted a different body). The\n * missing extractor is about to run and will re-emit its own link\n * row, so we drop the prior link entirely to avoid duplicates.\n * - **Obsolete**: short id maps to no currently-registered qualified\n * id at all (the extractor was uninstalled). The contribution is\n * stranded but harmless — we strip the obsolete short id from\n * `sources` and keep the link if at least one cached source remains.\n *\n * Decision rules:\n * - Any missing source → return `null` (drop the link).\n * - All cached, no obsolete → return the link as-is.\n * - Cached + obsolete (no missing) → return a clone with obsolete\n * sources filtered out.\n * - All obsolete (no cached, no missing) → return `null` (no live\n * extractor still claims this link).\n *\n * Source-id mapping caveat: `link.sources` carries the short id the\n * extractor author wrote (e.g. `'slash'`); the cache table keys on the\n * qualified id (`'claude/slash'`). Multiple plugins COULD declare an\n * extractor with the same short id; the map keeps every qualified id per\n * short id so this filter recognises any of them as \"still cached\".\n */\n// eslint-disable-next-line complexity\nfunction reuseCachedLink(\n link: Link,\n shortIdToQualified: Map<string, string[]>,\n cachedQualifiedIds: Set<string>,\n applicableQualifiedIds: Set<string>,\n): Link | null {\n if (!Array.isArray(link.sources) || link.sources.length === 0) return null;\n const cachedSources: string[] = [];\n const obsoleteSources: string[] = [];\n let hasMissing = false;\n for (const source of link.sources) {\n const candidates = shortIdToQualified.get(source);\n if (!candidates || candidates.length === 0) {\n // No registered extractor at all carries this short id → obsolete.\n obsoleteSources.push(source);\n continue;\n }\n if (candidates.some((q) => cachedQualifiedIds.has(q))) {\n cachedSources.push(source);\n continue;\n }\n if (candidates.some((q) => applicableQualifiedIds.has(q))) {\n // Registered for this kind but not cached for this body → the\n // missing extractor will re-emit; dropping the prior link avoids\n // duplicates.\n hasMissing = true;\n continue;\n }\n // Registered but not applicable to this kind → treat as obsolete\n // for this node (cannot be re-emitted here).\n obsoleteSources.push(source);\n }\n if (hasMissing) return null;\n if (cachedSources.length === 0) return null;\n if (obsoleteSources.length === 0) return link;\n // Trim the obsolete short ids from `sources` so the persisted row no\n // longer claims attribution from an extractor the user removed.\n return { ...link, sources: cachedSources };\n}\n\n/**\n * Run every registered rule over the merged graph. Rules see internal\n * links only — broken-ref / trigger-collision / superseded all reason\n * about graph relations, not URLs.\n */\nasync function runRules(\n rules: IRule[],\n nodes: Node[],\n internalLinks: Link[],\n emitter: ProgressEmitterPort,\n hookDispatcher: IHookDispatcher,\n): Promise<Issue[]> {\n const issues: Issue[] = [];\n for (const rule of rules) {\n const emitted = await rule.evaluate({ nodes, links: internalLinks });\n for (const issue of emitted) {\n const validated = validateIssue(rule, issue, emitter);\n if (validated) issues.push(validated);\n }\n // Spec § A.11 — `rule.completed`. Aggregated per Rule, after every\n // issue has been validated. Fan-out scope: one event per Rule per\n // scan. The payload carries the qualified rule id so a hook with\n // `filter: { ruleId: '...' }` can scope to a single rule.\n const ruleId = qualifiedExtensionId(rule.pluginId, rule.id);\n const evt = makeEvent('rule.completed', { ruleId });\n emitter.emit(evt);\n await hookDispatcher.dispatch('rule.completed', evt);\n }\n return issues;\n}\n\n/**\n * The \"originating node\" of a link — the node whose body / frontmatter\n * the extractor was processing when it emitted the link. For most kinds\n * this equals `link.source`, but the frontmatter extractor emits inverted\n * `supersedes` links (from a node's `metadata.supersededBy`) where\n * `target` is the originating node and `source` is the (forward-pointing)\n * supersedor. The forward case (`metadata.supersedes`) keeps\n * `originating === source` like every other extractor.\n *\n * Discriminator: the supersedor path in an inverted edge is rarely a\n * real node (it points \"forward\" to a file that may or may not exist on\n * disk under that exact path); the originating node always exists in\n * the prior snapshot (it's the node whose extraction produced the link).\n * So for `kind === 'supersedes'`: prefer `source` when source is a known\n * prior node, otherwise fall back to `target`. This handles BOTH the\n * forward case (originating === source, which IS a known node) and the\n * inverted case (source not a node → fall through to target, the\n * originating older node).\n *\n * Frontmatter is the only extractor that emits cross-source links today;\n * if a future extractor adds another inversion case, escalate to a\n * persisted `Link.extractedFromPath` field with a schema bump rather\n * than extending this heuristic.\n */\nfunction originatingNodeOf(link: Link, priorNodePaths: Set<string>): string {\n if (link.kind === 'supersedes' && !priorNodePaths.has(link.source)) {\n return link.target;\n }\n return link.source;\n}\n\n/**\n * Step 1 of `detectRenamesAndOrphans` — pair every `deletedPath` with a\n * `newPath` whose body hash matches. Greedy by sorted order; on first\n * hit the deletion is claimed and we move on. Mutates the supplied\n * `claimedDeleted` / `claimedNew` sets in place.\n */\nfunction findHighConfidenceRenames(opts: {\n deletedPaths: string[];\n newPaths: string[];\n priorByPath: Map<string, Node>;\n currentByPath: Map<string, Node>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n}): RenameOp[] {\n const ops: RenameOp[] = [];\n for (const fromPath of opts.deletedPaths) {\n if (opts.claimedDeleted.has(fromPath)) continue;\n const fromNode = opts.priorByPath.get(fromPath)!;\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const toNode = opts.currentByPath.get(toPath)!;\n if (toNode.bodyHash === fromNode.bodyHash) {\n ops.push({ from: fromPath, to: toPath, confidence: 'high' });\n opts.claimedDeleted.add(fromPath);\n opts.claimedNew.add(toPath);\n break;\n }\n }\n }\n return ops;\n}\n\n/**\n * Step 2 of `detectRenamesAndOrphans` — bucket every still-unclaimed\n * `newPath` by the set of still-unclaimed `deletedPath`s that share its\n * `frontmatterHash`. The map drives both the medium-confidence claim\n * pass and the ambiguous-flag pass.\n */\nfunction buildFrontmatterRenameCandidates(opts: {\n deletedPaths: string[];\n newPaths: string[];\n priorByPath: Map<string, Node>;\n currentByPath: Map<string, Node>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n}): Map<string, string[]> {\n const candidatesByNew = new Map<string, string[]>();\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const toNode = opts.currentByPath.get(toPath)!;\n const matches: string[] = [];\n for (const fromPath of opts.deletedPaths) {\n if (opts.claimedDeleted.has(fromPath)) continue;\n const fromNode = opts.priorByPath.get(fromPath)!;\n if (toNode.frontmatterHash === fromNode.frontmatterHash) {\n matches.push(fromPath);\n }\n }\n if (matches.length > 0) candidatesByNew.set(toPath, matches);\n }\n return candidatesByNew;\n}\n\n/**\n * Step 3a of `detectRenamesAndOrphans` — first pass over the candidate\n * map: a `newPath` whose surviving candidate set is a singleton wins\n * the deletion, with `auto-rename-medium`. Greedy by sorted `newPath`\n * order so a deletion claimed by an earlier singleton drops out of\n * later candidate filters. Mutates `claimedDeleted` / `claimedNew` /\n * `issues` in place.\n */\nfunction claimSingletonRenames(opts: {\n newPaths: string[];\n candidatesByNew: Map<string, string[]>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n issues: Issue[];\n}): RenameOp[] {\n const ops: RenameOp[] = [];\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const candidates = opts.candidatesByNew.get(toPath);\n if (!candidates) continue;\n const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));\n if (remaining.length === 1) {\n const fromPath = remaining[0]!;\n ops.push({ from: fromPath, to: toPath, confidence: 'medium' });\n opts.issues.push({\n ruleId: 'auto-rename-medium',\n severity: 'warn',\n nodeIds: [toPath],\n message: `Auto-rename (medium confidence): ${fromPath} → ${toPath}`,\n data: { from: fromPath, to: toPath, confidence: 'medium' },\n });\n opts.claimedDeleted.add(fromPath);\n opts.claimedNew.add(toPath);\n }\n }\n return ops;\n}\n\n/**\n * Step 3b of `detectRenamesAndOrphans` — any `newPath` left with more\n * than one viable candidate after singletons settled is ambiguous.\n * Emits one `auto-rename-ambiguous` per `newPath`. Candidates are NOT\n * claimed; they fall through to the orphan step so the user can\n * reconcile manually with `sm orphans undo-rename`.\n */\nfunction flagAmbiguousRenames(opts: {\n newPaths: string[];\n candidatesByNew: Map<string, string[]>;\n claimedDeleted: Set<string>;\n claimedNew: Set<string>;\n issues: Issue[];\n}): void {\n for (const toPath of opts.newPaths) {\n if (opts.claimedNew.has(toPath)) continue;\n const candidates = opts.candidatesByNew.get(toPath);\n if (!candidates) continue;\n const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));\n if (remaining.length > 1) {\n opts.issues.push({\n ruleId: 'auto-rename-ambiguous',\n severity: 'warn',\n nodeIds: [toPath],\n message:\n `Auto-rename ambiguous: ${toPath} matches ${remaining.length} ` +\n `prior frontmatters — pick one with \\`sm orphans undo-rename ` +\n `${toPath} --from <old.path>\\`.`,\n data: { to: toPath, candidates: remaining },\n });\n }\n }\n}\n\n/**\n * Step 4 of `detectRenamesAndOrphans` — every deletion left unclaimed\n * after steps 1-3 yields one `orphan` issue (info severity).\n */\nfunction flagOrphans(opts: {\n deletedPaths: string[];\n claimedDeleted: Set<string>;\n issues: Issue[];\n}): void {\n for (const fromPath of opts.deletedPaths) {\n if (opts.claimedDeleted.has(fromPath)) continue;\n opts.issues.push({\n ruleId: 'orphan',\n severity: 'info',\n nodeIds: [fromPath],\n message: `Orphan history: ${fromPath} was deleted; no rename match found.`,\n data: { path: fromPath },\n });\n }\n}\n\n/**\n * Pure rename / orphan classification per `spec/db-schema.md` §Rename\n * detection. Mutates `issues` in place — caller passes the in-progress\n * issue list; returns the `RenameOp[]` for the persistence layer to\n * apply inside its tx.\n *\n * Pipeline (1-to-1: a `newPath` claimed by one stage cannot be reused\n * by another):\n *\n * 1. **High-confidence**: pair each `deletedPath` with a `newPath`\n * that has the same `bodyHash`. No issue, no prompt.\n * 2. **Medium-confidence (1:1)**: of the remaining deletions, pair\n * each with the *unique* unclaimed `newPath` that shares its\n * `frontmatterHash`. Emits `auto-rename-medium` (severity warn)\n * with `data: { from, to, confidence: 'medium' }`.\n * 3. **Ambiguous (N:1)**: when a single `newPath` has more than one\n * remaining frontmatter-matching candidate, emit ONE\n * `auto-rename-ambiguous` issue per `newPath`, listing all\n * candidates in `data.candidates`. NO migration.\n * 4. **Orphan**: every `deletedPath` left after steps 1-3 yields one\n * `orphan` issue (severity info) with `data: { path: <deletedPath> }`.\n *\n * Determinism: `deletedPaths` and `newPaths` are iterated in lex-asc\n * order so the same input always produces the same matches —\n * required for reproducible tests and conformance fixtures (the spec\n * does not prescribe an order, but stability is the obvious contract).\n */\nexport function detectRenamesAndOrphans(\n prior: ScanResult,\n current: Node[],\n issues: Issue[],\n): RenameOp[] {\n const priorByPath = new Map<string, Node>();\n for (const n of prior.nodes) priorByPath.set(n.path, n);\n const currentByPath = new Map<string, Node>();\n for (const n of current) currentByPath.set(n.path, n);\n\n // Sets / sorted lists so iteration is deterministic.\n const deletedPaths = [...priorByPath.keys()]\n .filter((p) => !currentByPath.has(p))\n .sort();\n const newPaths = [...currentByPath.keys()]\n .filter((p) => !priorByPath.has(p))\n .sort();\n\n const claimedDeleted = new Set<string>();\n const claimedNew = new Set<string>();\n const ops: RenameOp[] = [];\n\n // Step 1 — high confidence (body hash match).\n ops.push(...findHighConfidenceRenames({\n deletedPaths, newPaths, priorByPath, currentByPath, claimedDeleted, claimedNew,\n }));\n\n // Step 2 — bucket every `newPath` by the deletions that share its\n // frontmatterHash, used by both medium-confidence and ambiguous passes.\n const candidatesByNew = buildFrontmatterRenameCandidates({\n deletedPaths, newPaths, priorByPath, currentByPath, claimedDeleted, claimedNew,\n });\n\n // Step 3a — singleton candidates → medium-confidence renames.\n ops.push(...claimSingletonRenames({\n newPaths, candidatesByNew, claimedDeleted, claimedNew, issues,\n }));\n\n // Step 3b — multi-candidate `newPath`s left after singletons settled.\n flagAmbiguousRenames({ newPaths, candidatesByNew, claimedDeleted, claimedNew, issues });\n\n // Step 4 — every unclaimed deletion is an orphan.\n flagOrphans({ deletedPaths, claimedDeleted, issues });\n\n return ops;\n}\n\n/**\n * Any link whose target carries a URL-shaped scheme is external (counted\n * via `externalRefsCount`, dropped from `result.links`). Internal links\n * are filesystem paths — relative or absolute, no scheme.\n *\n * The regex matches RFC 3986's `scheme = ALPHA *( ALPHA / DIGIT / \"+\" /\n * \"-\" / \".\" )` followed by `:`, with the extra constraint of ≥ 2 chars\n * so a Windows-style absolute path (`C:\\foo`) is not misclassified as a\n * URL on the rare cross-platform path that survives normalization.\n *\n * Before this regex the implementation only matched `http://` and\n * `https://`, which silently let `mailto:`, `data:`, `file:///`, `ftp://`\n * etc. pollute the graph as fake-internal links (their lookup against\n * `byPath` always missed, so counts stayed at 0, but the rows survived\n * in `result.links` and the rule pipeline saw them).\n */\nconst EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\\-.]+:/i;\n\nfunction isExternalUrlLink(link: Link): boolean {\n return EXTERNAL_URL_SCHEME_RE.test(link.target);\n}\n\nfunction makeEvent(type: string, data: unknown): ProgressEvent {\n return { type, timestamp: new Date().toISOString(), data };\n}\n\n/**\n * Spec § A.11 — Hook lifecycle dispatcher. Indexes the supplied hooks by\n * trigger and fans the matching event out to every subscribed\n * deterministic hook in registration order. Probabilistic hooks are\n * skipped here with a stderr advisory; they will dispatch via the job\n * subsystem once the job subsystem ships.\n *\n * Filter handling: when the hook declares a `filter` map, the dispatcher\n * walks `event.data` for each declared key and short-circuits the\n * invocation when any value disagrees. Top-level fields only in v0.x\n * (deep-path matching is deferred until a real use case justifies the\n * complexity).\n *\n * Error policy: a hook that throws is caught here, logged through a\n * synthetic `extension.error` event with kind `hook-error`, and the\n * scan continues. A buggy hook MUST NOT block the main pipeline —\n * that would invert the design intent (hooks REACT to events, they\n * never steer them).\n */\ninterface IHookDispatcher {\n dispatch(trigger: THookTrigger, event: ProgressEvent): Promise<void>;\n}\n\nfunction makeHookDispatcher(hooks: IHook[], emitter: ProgressEmitterPort): IHookDispatcher {\n if (hooks.length === 0) {\n // Cheap no-op fast path: most scans don't carry any hooks today.\n // eslint-disable-next-line @typescript-eslint/no-empty-function\n return { dispatch: async () => {} };\n }\n\n // Index by trigger so dispatch is O(matching) rather than O(allHooks).\n // Iteration order within a trigger preserves registration order so\n // observers see deterministic fan-out.\n const byTrigger = new Map<THookTrigger, IHook[]>();\n for (const hook of hooks) {\n if (hook.mode === 'probabilistic') {\n // Probabilistic hooks defer to the job subsystem (future job subsystem). Log\n // once per hook at composition time — not per-event — so a noisy\n // scan doesn't flood the logger. The hook still surfaces in\n // `sm plugins list`; it just doesn't fire today.\n const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);\n log.warn(\n `Probabilistic hook ${qualifiedId} deferred to job subsystem (future job subsystem). The hook is registered but will not dispatch in-scan.`,\n { hookId: qualifiedId, mode: 'probabilistic' },\n );\n continue;\n }\n for (const trig of hook.triggers) {\n const bucket = byTrigger.get(trig);\n if (bucket) bucket.push(hook);\n else byTrigger.set(trig, [hook]);\n }\n }\n\n return {\n async dispatch(trigger, event) {\n const subs = byTrigger.get(trigger);\n if (!subs || subs.length === 0) return;\n for (const hook of subs) {\n if (!matchesFilter(hook, event)) continue;\n const ctx = buildHookContext(hook, trigger, event);\n try {\n await hook.on(ctx);\n } catch (err) {\n const qualifiedId = qualifiedExtensionId(hook.pluginId, hook.id);\n const message = err instanceof Error ? err.message : String(err);\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'hook-error',\n extensionId: qualifiedId,\n trigger,\n message,\n }),\n );\n }\n }\n },\n };\n}\n\nfunction matchesFilter(hook: IHook, event: ProgressEvent): boolean {\n if (!hook.filter) return true;\n const data = (event.data ?? {}) as Record<string, unknown>;\n for (const [key, expected] of Object.entries(hook.filter)) {\n if (data[key] !== expected) return false;\n }\n return true;\n}\n\n// eslint-disable-next-line complexity\nfunction buildHookContext(\n _hook: IHook,\n trigger: THookTrigger,\n event: ProgressEvent,\n): IHookContext {\n const data = (event.data ?? {}) as Record<string, unknown>;\n const ctx: IHookContext = {\n event: {\n type: trigger,\n timestamp: event.timestamp,\n ...(event.runId !== undefined ? { runId: event.runId } : {}),\n ...(event.jobId !== undefined ? { jobId: event.jobId } : {}),\n data: event.data,\n },\n };\n if (typeof data['extractorId'] === 'string') ctx.extractorId = data['extractorId'];\n if (typeof data['ruleId'] === 'string') ctx.ruleId = data['ruleId'];\n if (typeof data['actionId'] === 'string') ctx.actionId = data['actionId'];\n if (data['node'] && typeof data['node'] === 'object') {\n ctx.node = data['node'] as Node;\n }\n if (data['jobResult'] !== undefined) ctx.jobResult = data['jobResult'];\n return ctx;\n}\n\ninterface IBuildNodeArgs {\n path: string;\n kind: Node['kind'];\n providerId: string;\n frontmatterRaw: string;\n body: string;\n frontmatter: Record<string, unknown>;\n bodyHash: string;\n frontmatterHash: string;\n encoder: Tiktoken | null;\n}\n\nfunction buildNode(args: IBuildNodeArgs): Node {\n const bytesFrontmatter = Buffer.byteLength(args.frontmatterRaw, 'utf8');\n const bytesBody = Buffer.byteLength(args.body, 'utf8');\n const metadata = pickMetadata(args.frontmatter);\n const node: Node = {\n path: args.path,\n kind: args.kind,\n provider: args.providerId,\n bodyHash: args.bodyHash,\n frontmatterHash: args.frontmatterHash,\n bytes: {\n frontmatter: bytesFrontmatter,\n body: bytesBody,\n total: bytesFrontmatter + bytesBody,\n },\n linksOutCount: 0,\n linksInCount: 0,\n externalRefsCount: 0,\n frontmatter: args.frontmatter,\n title: pickString(args.frontmatter['name']),\n description: pickString(args.frontmatter['description']),\n stability: pickStability(metadata?.['stability']),\n version: pickString(metadata?.['version']),\n author: pickString(args.frontmatter['author']),\n };\n if (args.encoder) {\n node.tokens = countTokens(args.encoder, args.frontmatterRaw, args.body);\n }\n return node;\n}\n\nfunction countTokens(encoder: Tiktoken, frontmatterRaw: string, body: string): TripleSplit {\n // Tokenize the raw frontmatter bytes (not the parsed object) so the\n // count stays reproducible from on-disk content.\n const frontmatter = frontmatterRaw.length > 0 ? encoder.encode(frontmatterRaw).length : 0;\n const bodyTokens = body.length > 0 ? encoder.encode(body).length : 0;\n return { frontmatter, body: bodyTokens, total: frontmatter + bodyTokens };\n}\n\nfunction sha256(input: string): string {\n return createHash('sha256').update(input, 'utf8').digest('hex');\n}\n\n/**\n * Canonical-form rationale — canonical YAML form for frontmatter hashing.\n *\n * Goal: two `.md` files whose frontmatter parses to the same logical\n * value MUST produce the same `frontmatter_hash`, even if the raw bytes\n * differ in indentation, key order, quote style, or trailing whitespace.\n * Without this canonicalisation, a YAML formatter pass on the user's\n * editor (Prettier YAML, IDE autoformat, manual indent fix) silently\n * breaks the medium-confidence rename heuristic.\n *\n * Strategy:\n * 1. Take the parsed object the Provider already produced.\n * 2. Re-emit via `yaml.dump` with `sortKeys: true`, `lineWidth: -1`\n * (no auto-wrap), `noRefs: true` (no `*alias` shorthand),\n * `noCompatMode: true` (modern YAML 1.2 output).\n * 3. Hash the result.\n *\n * Fallback: when `parsed` is the empty object `{}` BUT `raw` is\n * non-empty, the Provider's parse failed silently. We fall back to\n * hashing the raw text — a malformed-YAML file should still hash\n * deterministically against itself across rescans, even if the\n * canonical form would be empty.\n */\nfunction canonicalFrontmatter(\n parsed: Record<string, unknown>,\n raw: string,\n): string {\n const hasParsedKeys = Object.keys(parsed).length > 0;\n const hasRawText = raw.length > 0;\n if (!hasParsedKeys && hasRawText) {\n // Parse failed but raw text exists. Hash the raw — preserves\n // identity for malformed-YAML files across scans.\n return raw;\n }\n return yaml.dump(parsed, {\n sortKeys: true,\n lineWidth: -1,\n noRefs: true,\n noCompatMode: true,\n });\n}\n\nfunction pickMetadata(fm: Record<string, unknown>): Record<string, unknown> | null {\n const m = fm['metadata'];\n return m && typeof m === 'object' && !Array.isArray(m) ? (m as Record<string, unknown>) : null;\n}\n\nfunction pickString(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction pickStability(value: unknown): 'experimental' | 'stable' | 'deprecated' | null {\n if (value === 'experimental' || value === 'stable' || value === 'deprecated') return value;\n return null;\n}\n\nfunction buildExtractorContext(\n extractor: IExtractor,\n node: Node,\n body: string,\n frontmatter: Record<string, unknown>,\n emitLink: (link: Link) => void,\n enrichNode: (partial: Partial<Node>) => void,\n store: IPluginStore | undefined,\n): IExtractorContext {\n const scope = extractor.scope;\n // Spread `store` only when present so the resulting context stays\n // strictly-shaped under `exactOptionalPropertyTypes` — assigning\n // `store: undefined` would publish the property with an `undefined`\n // value, which is observably different from the field being absent\n // (the legacy contract for plugins without declared storage).\n return {\n node,\n body: scope === 'frontmatter' ? '' : body,\n frontmatter: scope === 'body' ? {} : frontmatter,\n emitLink,\n enrichNode,\n ...(store !== undefined ? { store } : {}),\n };\n}\n\nfunction validateLink(extractor: IExtractor, link: Link, emitter: ProgressEmitterPort): Link | null {\n if (!extractor.emitsLinkKinds.includes(link.kind as LinkKind)) {\n // Extractor emitted a kind outside its declared set — drop the link.\n // Surface a `extension.error` diagnostic so plugin authors see WHY a\n // link they expected vanished from the result; silent drops are the\n // worst possible plugin-author UX. The orchestrator is the last line\n // of defence against a misbehaving extractor, but the author needs to\n // know the line fired.\n //\n // `extensionId` carries the qualified form `<pluginId>/<id>` (spec\n // § A.6) so the diagnostic matches what `sm plugins list` and\n // registry lookups use. Older builds emitted just the short id; the\n // qualified form is unambiguous across plugins.\n const qualifiedId = `${extractor.pluginId}/${extractor.id}`;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'link-kind-not-declared',\n extensionId: qualifiedId,\n linkKind: link.kind,\n declaredKinds: extractor.emitsLinkKinds,\n link: { source: link.source, target: link.target, kind: link.kind },\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorLinkKindNotDeclared, {\n extractorId: qualifiedId,\n linkKind: link.kind,\n declaredKinds: extractor.emitsLinkKinds.join(', '),\n }),\n }),\n );\n return null;\n }\n const confidence: Confidence = link.confidence ?? extractor.defaultConfidence;\n return { ...link, confidence };\n}\n\n/**\n * Validate a node's frontmatter against the per-kind schema declared by\n * the Provider that classified the node. Only called for files that\n * actually declared a fence (caller checks `frontmatterRaw.length > 0`).\n * Returns a single `frontmatter-invalid` issue with the AJV error\n * string, or `null` when the frontmatter is structurally valid. Severity\n * is `warn` by default; `strict` flips it to `error` so the scan exit\n * code rises to 1.\n *\n * Spec 0.8.0: per-kind schemas live with the Provider, not in\n * spec. The orchestrator passes the live `IProviderFrontmatterValidator`\n * (composed from every loaded Provider's `kinds[<kind>].schemaJson`)\n * plus the active Provider so the lookup is `(provider.id, kind) →\n * schema`. A Provider that does not declare an entry for the kind it\n * classified into still gets a `frontmatter-invalid` issue with errors\n * `'no-schema'` so the kernel never silently skips validation.\n */\nfunction validateFrontmatter(\n providerFrontmatter: IProviderFrontmatterValidator,\n provider: IProvider,\n kind: string,\n frontmatter: Record<string, unknown>,\n path: string,\n strict: boolean,\n): Issue | null {\n const result = providerFrontmatter.validate(provider, kind, frontmatter);\n if (result.ok) return null;\n return {\n ruleId: 'frontmatter-invalid',\n severity: strict ? 'error' : 'warn',\n nodeIds: [path],\n message: tx(ORCHESTRATOR_TEXTS.frontmatterInvalid, { path, kind, errors: result.errors }),\n data: { kind, errors: result.errors },\n };\n}\n\n/**\n * Malformed-frontmatter detection — detect cases where the user clearly meant\n * frontmatter but the Provider's regex couldn't recognise the fence.\n * The Provider regex requires `^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?` —\n * column-0 open fence, column-0 close fence, CRLF or LF line endings.\n * Three real-world variants that fall through silently and silently\n * lose every metadata field:\n *\n * - `paste-with-indent`: terminal heredoc auto-indented every line,\n * so the open fence is `<spaces>---`. The most common variant\n * .\n * - `byte-order-mark`: a UTF-8 BOM () precedes the fence. Some\n * editors (notably old VS Code on Windows) inject this; the YAML\n * parser handles BOM, but the Provider regex doesn't anchor past it.\n * - `missing-close`: the open fence is on column 0 but the closing\n * fence is missing or indented. Whole \"frontmatter\" parses as body.\n *\n * Each variant emits a `frontmatter-malformed` warn with a `data.hint`\n * tag so downstream tooling can disambiguate. `--strict` promotes to\n * `error` consistent with the strict-fence policy.\n *\n * False-positive guards:\n *\n * - Indented `---` with no YAML-looking line after → likely a nested\n * horizontal rule, not malformed frontmatter.\n * - Column-0 `---` followed by prose (not a YAML key) → likely a\n * legitimate horizontal rule with prose underneath. Tested.\n *\n * The schema-strict validator above only fires when `frontmatterRaw`\n * is non-empty; this fills the previously-silent path where the Provider\n * couldn't even recognise the fence.\n */\nfunction detectMalformedFrontmatter(body: string, path: string, strict: boolean): Issue | null {\n const hint = classifyMalformedFrontmatter(body);\n if (!hint) return null;\n return {\n ruleId: 'frontmatter-malformed',\n severity: strict ? 'error' : 'warn',\n nodeIds: [path],\n message: malformedMessage(hint, path),\n data: { hint },\n };\n}\n\ntype TMalformedHint = 'paste-with-indent' | 'byte-order-mark' | 'missing-close';\n\nfunction classifyMalformedFrontmatter(body: string): TMalformedHint | null {\n // (a) BOM at the very first byte. Check before everything else\n // because a BOM offsets the column-0 anchor of the Provider's regex.\n // Pattern after BOM is the standard column-0 fence + YAML key-value\n // line, so we still require that shape to avoid false positives on\n // any BOM-prefixed prose.\n if (body.startsWith('')) {\n if (/^---\\r?\\n[\\s\\S]*?[A-Za-z0-9_-]+\\s*:/.test(body)) {\n return 'byte-order-mark';\n }\n }\n\n // (b) Indented opening fence followed by a YAML-looking key-value\n // line. The most common variant (terminal heredoc auto-indent).\n if (/^[ \\t]+---\\r?\\n[ \\t]*[A-Za-z0-9_-]+\\s*:/.test(body)) {\n return 'paste-with-indent';\n }\n\n // (c) Column-0 opening fence followed by a YAML-looking key-value\n // line, but no matching closing fence. The Provider regex needs both\n // fences; a missing close means the entire intended frontmatter\n // (plus the body) parses as body.\n //\n // Heuristic: open at column 0, then at least one `key: value` line\n // immediately, then anywhere in the file there is NO column-0 `---`\n // closing the block. If the body had been parsed as frontmatter the\n // Provider would have set `frontmatterRaw` non-empty and we wouldn't\n // be in this branch — so the absence of close means the regex\n // didn't match.\n if (/^---\\r?\\n[ \\t]*[A-Za-z0-9_-]+\\s*:/.test(body)) {\n // Search for any line that is exactly `---` (column 0, no indent).\n // If found, the Provider regex would have matched and this code\n // path is unreachable; absence here means the close is missing\n // or indented.\n const hasCloseFence = /\\r?\\n---(?:\\r?\\n|$)/.test(body);\n if (!hasCloseFence) {\n return 'missing-close';\n }\n }\n\n return null;\n}\n\nfunction malformedMessage(hint: TMalformedHint, path: string): string {\n switch (hint) {\n case 'paste-with-indent':\n return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedPasteWithIndent, { path });\n case 'byte-order-mark':\n return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedByteOrderMark, { path });\n case 'missing-close':\n return tx(ORCHESTRATOR_TEXTS.frontmatterMalformedMissingClose, { path });\n }\n}\n\nfunction validateIssue(rule: IRule, issue: Issue, emitter: ProgressEmitterPort): Issue | null {\n const severity: Severity | undefined = issue.severity;\n if (severity !== 'error' && severity !== 'warn' && severity !== 'info') {\n // Rule emitted an out-of-spec severity (or none at all) — drop the\n // issue. Surface a diagnostic so plugin authors see the issue\n // disappear FOR A REASON, instead of silently never showing up.\n // Qualified id (spec § A.6) keeps `extension.error` consumers\n // unambiguous across plugin namespaces.\n const qualifiedId = `${rule.pluginId}/${rule.id}`;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'issue-invalid-severity',\n extensionId: qualifiedId,\n severity,\n issue: { ruleId: issue.ruleId || rule.id, message: issue.message, nodeIds: issue.nodeIds },\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {\n ruleId: qualifiedId,\n severity: JSON.stringify(severity),\n }),\n }),\n );\n return null;\n }\n return { ...issue, ruleId: issue.ruleId || rule.id };\n}\n\nfunction recomputeLinkCounts(nodes: Node[], links: Link[]): void {\n const byPath = new Map<string, Node>();\n for (const node of nodes) {\n // Reset counts so a node reused from prior (which carries its prior\n // counts) gets re-counted from the merged internal-link list.\n node.linksOutCount = 0;\n node.linksInCount = 0;\n byPath.set(node.path, node);\n }\n for (const link of links) {\n const source = byPath.get(link.source);\n if (source) source.linksOutCount += 1;\n const target = byPath.get(link.target);\n if (target) target.linksInCount += 1;\n }\n}\n\nfunction recomputeExternalRefsCount(\n nodes: Node[],\n externalLinks: Link[],\n cachedPaths: Set<string>,\n): void {\n const byPath = new Map<string, Node>();\n for (const node of nodes) {\n // Zero only freshly-built nodes. Cached nodes preserve their prior\n // `externalRefsCount` because external pseudo-links were never\n // persisted, so we cannot re-derive the count from a fresh extractor\n // pass — the count survives untouched in the node row.\n if (!cachedPaths.has(node.path)) node.externalRefsCount = 0;\n byPath.set(node.path, node);\n }\n for (const link of externalLinks) {\n const source = byPath.get(link.source);\n // Cached nodes never appear as the source of a freshly-emitted\n // external pseudo-link (extractors didn't run for them), so this\n // increment only ever lands on a freshly-built node — but the guard\n // is cheap and defensive.\n if (source && !cachedPaths.has(source.path)) source.externalRefsCount += 1;\n }\n}\n\n/**\n * Spec § A.8 — produce the merged read-time view of a Node.\n *\n * Rules / `sm check` / `sm export` consume `node.frontmatter` directly\n * (deterministic CI-safe baseline — author intent, byte-stable). UI / future\n * rules that opt into enrichment context call this helper to merge the\n * author frontmatter with the live enrichment layer.\n *\n * Algorithm:\n *\n * 1. Filter `enrichments` down to rows targeting this node AND not\n * flagged `stale`. Stale rows (probabilistic enrichments whose\n * body changed since their last run) are excluded by default —\n * stale visibility belongs to the UI layer where the marker is\n * shown next to the value.\n * 2. Sort the survivors by `enrichedAt` ASC so iteration order is\n * \"oldest first\". This makes the spread merge below\n * last-write-wins per field — the freshest Extractor's value\n * pisar the older one for any conflicting key.\n * 3. Spread-merge each row's `value` over `node.frontmatter`. The\n * author's keys are the base; enrichment keys overlay them.\n *\n * The returned object is a fresh shallow copy — mutating it does not\n * touch the caller's node. The original `node.frontmatter` reference\n * remains accessible via `node.frontmatter` for callers that want the\n * pristine author baseline.\n *\n * @param node Node to merge against; `node.frontmatter` is the base.\n * @param enrichments Per-(node, extractor) enrichment records — typically\n * loaded via `loadNodeEnrichments(db, node.path)` or\n * pre-filtered to this node by the caller.\n * @param opts.includeStale When true, include rows flagged stale. Defaults\n * to false (the safe, CI-deterministic default).\n * UIs that want to display \"stale (last value: …)\"\n * pass `true` and consult `enrichment.stale`\n * on the source rows.\n */\nexport function mergeNodeWithEnrichments(\n node: Node,\n enrichments: IPersistedEnrichment[],\n opts: { includeStale?: boolean } = {},\n): Record<string, unknown> {\n const includeStale = opts.includeStale === true;\n const applicable = enrichments\n .filter((e) => e.nodePath === node.path)\n .filter((e) => includeStale || !e.stale)\n .sort((a, b) => a.enrichedAt - b.enrichedAt);\n // `assignSafe` strips `__proto__` / `constructor` / `prototype` from\n // every source before copying, so a hostile enrichment value\n // (plugin-authored, persisted as JSON) cannot replace the merged\n // object's prototype via the `__proto__` setter. Prototype stays\n // normal so consumers can `deepStrictEqual` the result against\n // JSON-parse-shaped baselines.\n const base: Record<string, unknown> = {};\n assignSafe(base, node.frontmatter ?? {});\n for (const row of applicable) {\n assignSafe(base, row.value as Record<string, unknown>);\n }\n return base;\n}\n\nconst FORBIDDEN_MERGE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nfunction assignSafe(target: Record<string, unknown>, source: Record<string, unknown>): void {\n for (const [k, v] of Object.entries(source)) {\n if (FORBIDDEN_MERGE_KEYS.has(k)) continue;\n target[k] = v;\n }\n}\n\n/**\n * A persisted enrichment row, post-load. Mirrors the DB row shape\n * but with `value` already deserialised from JSON and `stale` /\n * `isProbabilistic` already decoded from `0 | 1`. Surfaced via\n * `loadNodeEnrichments` (driven adapter) and consumed by\n * `mergeNodeWithEnrichments` and the `sm refresh` command.\n */\nexport interface IPersistedEnrichment {\n nodePath: string;\n extractorId: string;\n bodyHashAtEnrichment: string;\n value: Partial<Node>;\n stale: boolean;\n enrichedAt: number;\n isProbabilistic: boolean;\n}\n","{\n \"name\": \"@skill-map/cli\",\n \"version\": \"0.16.5\",\n \"description\": \"skill-map reference implementation — kernel + CLI + adapters.\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"homepage\": \"https://skill-map.dev\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/crystian/skill-map.git\",\n \"directory\": \"src\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/crystian/skill-map/issues\"\n },\n \"keywords\": [\n \"skill-map\",\n \"markdown\",\n \"ai-agents\",\n \"claude-code\",\n \"graph\"\n ],\n \"bin\": {\n \"sm\": \"bin/sm.js\",\n \"skill-map\": \"bin/sm.js\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./kernel\": {\n \"types\": \"./dist/kernel/index.d.ts\",\n \"import\": \"./dist/kernel/index.js\"\n },\n \"./conformance\": {\n \"types\": \"./dist/conformance/index.d.ts\",\n \"import\": \"./dist/conformance/index.js\"\n }\n },\n \"files\": [\n \"bin/\",\n \"dist/\",\n \"migrations/\",\n \"README.md\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"dev:serve\": \"node ../scripts/dev-serve.js\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"pretest\": \"tsup\",\n \"pretest:ci\": \"tsup\",\n \"pretest:coverage\": \"tsup\",\n \"pretest:coverage:html\": \"tsup\",\n \"test\": \"tsc --noEmit && node --import tsx --test --test-reporter=spec 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"test:ci\": \"tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"test:coverage\": \"tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 node --experimental-default-config-file --import tsx --test --experimental-test-coverage 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"test:coverage:html\": \"tsc --noEmit && SKILL_MAP_SKIP_BENCHMARK=1 c8 node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts'\",\n \"clean\": \"rm -rf dist coverage\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"2.0.1\",\n \"@skill-map/spec\": \"0.16.0\",\n \"ajv\": \"8.18.0\",\n \"ajv-formats\": \"3.0.1\",\n \"chokidar\": \"5.0.0\",\n \"clipanion\": \"4.0.0-rc.4\",\n \"hono\": \"4.12.16\",\n \"ignore\": \"7.0.5\",\n \"js-tiktoken\": \"1.0.21\",\n \"js-yaml\": \"4.1.1\",\n \"kysely\": \"0.28.16\",\n \"semver\": \"7.7.4\",\n \"typanion\": \"3.14.0\",\n \"ws\": \"8.20.0\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"10.0.1\",\n \"@stylistic/eslint-plugin\": \"5.10.0\",\n \"@types/js-yaml\": \"4.0.9\",\n \"@types/node\": \"24.12.2\",\n \"@types/semver\": \"7.7.1\",\n \"@types/ws\": \"8.18.1\",\n \"c8\": \"11.0.0\",\n \"eslint\": \"10.2.1\",\n \"eslint-plugin-import-x\": \"4.16.2\",\n \"tsup\": \"8.5.1\",\n \"tsx\": \"4.21.0\",\n \"typescript\": \"5.9.3\",\n \"typescript-eslint\": \"8.59.1\"\n },\n \"engines\": {\n \"node\": \">=24.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","/**\n * In-memory `ProgressEmitterPort` adapter. No network, no DB — just a\n * synchronous fan-out to registered listeners. Used by the default scan\n * orchestrator; the WebSocket-backed emitter that streams to\n * the Web UI lands.\n */\n\nimport type {\n ProgressEmitterPort,\n ProgressEvent,\n ProgressListener,\n} from '../ports/progress-emitter.js';\n\nexport class InMemoryProgressEmitter implements ProgressEmitterPort {\n readonly #listeners = new Set<ProgressListener>();\n\n emit(event: ProgressEvent): void {\n for (const listener of this.#listeners) listener(event);\n }\n\n subscribe(listener: ProgressListener): () => void {\n this.#listeners.add(listener);\n return () => {\n this.#listeners.delete(listener);\n };\n }\n}\n","/**\n * No-op `LoggerPort`. Default when the kernel is invoked without a\n * logger (tests, embedded usage). Equivalent in spirit to\n * `InMemoryProgressEmitter`: callers that don't care get a working\n * implementation that does nothing.\n *\n * Every method is intentionally empty — that IS the contract of this\n * class. We disable `no-empty-function` for the whole file because\n * adding `// eslint-disable-next-line` to each method would be noise.\n */\n\n/* eslint-disable @typescript-eslint/no-empty-function */\n\nimport type { LoggerPort } from '../ports/logger.js';\n\nexport class SilentLogger implements LoggerPort {\n trace(): void {}\n debug(): void {}\n info(): void {}\n warn(): void {}\n error(): void {}\n}\n","/**\n * Module-level singleton `LoggerPort`. The kernel emits warnings /\n * info / debug through `log.*`; the active implementation defaults to\n * `SilentLogger` (no output) and is swapped by the driving adapter at\n * boot time via `configureLogger(...)`.\n *\n * Why a singleton (vs. per-call injection):\n * - Logging crosses every layer; threading a `logger` argument\n * through every kernel function costs a lot of plumbing for a\n * side-channel concern.\n * - The active impl is a pointer; the exported `log` is a stable\n * proxy. Imports made before `configureLogger` runs still see the\n * new impl on every call — no \"captured stale logger\" bugs.\n *\n * Tradeoffs accepted:\n * - Tests must call `resetLogger()` (or replace the active impl) in\n * teardown to avoid cross-test bleed.\n * - Concurrent scans share the same logger; per-scan logging requires\n * reintroducing an explicit `logger` argument on the call path.\n */\n\nimport { SilentLogger } from '../adapters/silent-logger.js';\nimport type { LoggerPort } from '../ports/logger.js';\n\nlet active: LoggerPort = new SilentLogger();\n\n/** Stable proxy. Methods always delegate to the current `active` impl. */\nexport const log: LoggerPort = {\n trace: (message, context) => active.trace(message, context),\n debug: (message, context) => active.debug(message, context),\n info: (message, context) => active.info(message, context),\n warn: (message, context) => active.warn(message, context),\n error: (message, context) => active.error(message, context),\n};\n\n/** Install a logger as the active implementation. Idempotent. */\nexport function configureLogger(impl: LoggerPort): void {\n active = impl;\n}\n\n/** Restore the default `SilentLogger`. Call from test teardown. */\nexport function resetLogger(): void {\n active = new SilentLogger();\n}\n\n/** Inspect the active logger. Test-only — production code uses `log`. */\nexport function getActiveLogger(): LoggerPort {\n return active;\n}\n","/**\n * `PluginLoader` — default `PluginLoaderPort` implementation.\n *\n * Responsibilities (per spec §Plugin discovery + spec v0.8.0 § A.5 —\n * id uniqueness):\n *\n * 1. Discover plugin directories under one or more search paths, each\n * containing a `plugin.json` at its root.\n * 2. Parse + AJV-validate the manifest against\n * `plugins-registry.schema.json#/$defs/PluginManifest`.\n * 3. Enforce the structural rule **directory name == manifest id**. A\n * mismatch surfaces as `invalid-manifest` with a directed reason.\n * This rule alone rules out same-root collisions by construction\n * (a filesystem cannot host two siblings with the same name).\n * 4. Semver-check `manifest.specCompat` against the installed\n * `@skill-map/spec` version.\n * 5. Dynamic-import every path listed in `manifest.extensions[]`, expect a\n * default export matching the extension-kind schema, validate it, and\n * collect the loaded extensions.\n * 6. After every plugin has been loaded individually, scan the result set\n * for cross-root id collisions. Two plugins claiming the same id (any\n * combination of project + global + `--plugin-dir`) BOTH receive\n * status `id-collision`; no precedence rule applies. The user resolves\n * by renaming one and rerunning.\n * 7. Surface one of the documented failure modes when anything fails:\n * `invalid-manifest` / `incompatible-spec` / `load-error` /\n * `id-collision`. The kernel keeps booting regardless — a bad plugin\n * cannot take the process down.\n */\n\nimport { createRequire } from 'node:module';\nimport { existsSync, readFileSync, readdirSync } from 'node:fs';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\nimport semver from 'semver';\n\nimport type {\n IDiscoveredPlugin,\n ILoadedExtension,\n IPluginManifest,\n IPluginStorageSchema,\n TPluginLoadStatus,\n} from '../types/plugin.js';\nimport type { PluginLoaderPort } from '../ports/plugin-loader.js';\nimport { PLUGIN_LOADER_TEXTS } from '../i18n/plugin-loader.texts.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\nimport { tx } from '../util/tx.js';\nimport { KV_SCHEMA_KEY } from './plugin-store.js';\nimport type { ExtensionKind } from '../registry.js';\nimport type { ISchemaValidators } from './schema-validators.js';\nimport { HOOK_TRIGGERS } from '../extensions/hook.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\n/**\n * Default per-extension dynamic-import timeout. Generous on purpose —\n * a plugin that legitimately takes >5s to import is misbehaving (it\n * should not have heavy work at module top level), but the extra\n * headroom avoids spurious timeouts on cold disk caches and slow CI\n * runners.\n */\nexport const DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS = 5000;\n\nexport interface IPluginLoaderOptions {\n /** Search paths to scan for plugin directories. Non-existent paths are skipped. */\n searchPaths: string[];\n /** Required — used to validate plugin.json and each extension manifest. */\n validators: ISchemaValidators;\n /** Installed @skill-map/spec version, used for specCompat check. */\n specVersion: string;\n /**\n * When supplied, the loader calls this with every parsed plugin id\n * AFTER manifest + specCompat validation succeed. A return value of\n * `false` short-circuits the load: the plugin is reported with\n * `status: 'disabled'` and its extensions are NOT imported. Defaults\n * to \"always enabled\" when omitted (no DB / config integration —\n * useful for tests that assert raw discovery behaviour).\n */\n resolveEnabled?: (pluginId: string) => boolean;\n /**\n * Per-extension dynamic-import timeout in milliseconds. A plugin whose\n * top-level work (imports, side effects) exceeds this is reported as\n * `load-error` with a message naming the timeout, instead of hanging\n * the host CLI command (`sm scan`, `sm plugins list`, `sm watch`).\n * Defaults to `DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS` (5s). Tests pass a\n * smaller value to exercise the timeout path quickly.\n *\n * Note: there is no AbortSignal on `import()` in Node 24 — when the\n * timer wins, the import is abandoned (the dangling promise resolves\n * later and is GC'd) but its side effects, if any, still run. The\n * timeout protects the orchestrator from hanging, not the host\n * process from a misbehaving plugin's runtime cost.\n */\n loadTimeoutMs?: number;\n}\n\n/**\n * Factory — preferred entry point for production callers (CLI). Returns\n * the port shape so the consumer is pinned to the abstract contract,\n * not the concrete class. Tests that need to access internals continue\n * to use `new PluginLoader(...)` directly.\n */\nexport function createPluginLoader(options: IPluginLoaderOptions): PluginLoaderPort {\n return new PluginLoader(options);\n}\n\nexport class PluginLoader implements PluginLoaderPort {\n readonly #options: IPluginLoaderOptions;\n readonly #loadTimeoutMs: number;\n\n constructor(options: IPluginLoaderOptions) {\n this.#options = options;\n this.#loadTimeoutMs = options.loadTimeoutMs ?? DEFAULT_PLUGIN_IMPORT_TIMEOUT_MS;\n }\n\n /**\n * Discover every plugin directory across the configured search paths.\n * Each direct child directory containing a `plugin.json` is considered a\n * plugin root. Non-plugin directories are silently skipped.\n */\n discoverPaths(): string[] {\n const out: string[] = [];\n for (const root of this.#options.searchPaths) {\n if (!existsSync(root)) continue;\n for (const entry of readdirSync(root, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const candidate = join(root, entry.name);\n if (existsSync(join(candidate, 'plugin.json'))) {\n out.push(resolve(candidate));\n }\n }\n }\n return out;\n }\n\n /**\n * Full pass — discover every plugin, attempt to load each, then apply\n * the cross-root id-collision pass over the results. Two plugins that\n * survived their individual load with the same `manifest.id` both get\n * downgraded to status `id-collision` (no precedence — the spec is\n * explicit that \"no extension is privileged\"). Plugins that already\n * failed their individual load (`invalid-manifest` /\n * `incompatible-spec` / `load-error`) keep their original status:\n * their `id` field is untrusted (it may be a fall-back path hint when\n * the manifest could not be parsed) and they would muddy the\n * collision report.\n */\n async discoverAndLoadAll(): Promise<IDiscoveredPlugin[]> {\n const paths = this.discoverPaths();\n const out: IDiscoveredPlugin[] = [];\n for (const path of paths) {\n out.push(await this.loadOne(path));\n }\n return applyIdCollisions(out);\n }\n\n /**\n * Load a single plugin from its directory. Never throws — a failure is\n * reported via the returned status.\n */\n // eslint-disable-next-line complexity\n async loadOne(pluginPath: string): Promise<IDiscoveredPlugin> {\n const manifestResult = this.#parseAndValidateManifest(pluginPath);\n if (!manifestResult.ok) return manifestResult.failure;\n const manifest = manifestResult.manifest;\n\n // --- enabled resolution ----------------------------------------------\n // Only check after manifest + specCompat pass: a `disabled` status\n // implies \"we know this plugin enough to surface it; we just chose\n // not to run it\". An invalid or incompatible plugin gets its own\n // status and never reaches this branch.\n //\n // Spec § A.7 — granularity. The loader's pre-import resolveEnabled()\n // check uses the plugin id (the bundle-level key). Plugins with\n // granularity='extension' that want to gate individual extensions\n // need a richer policy at the runtime composer (see\n // `cli/util/plugin-runtime.ts`); the loader stage is intentionally\n // coarse — disabling the bundle id always wins, so the import work\n // is skipped wholesale.\n if (this.#options.resolveEnabled && !this.#options.resolveEnabled(manifest.id)) {\n return {\n path: pluginPath,\n id: manifest.id,\n status: 'disabled',\n manifest,\n granularity: manifest.granularity ?? 'bundle',\n reason: PLUGIN_LOADER_TEXTS.disabledByConfig,\n };\n }\n\n // --- extension imports + kind validation ------------------------------\n const loaded: ILoadedExtension[] = [];\n for (const relEntry of manifest.extensions) {\n const result = await this.#loadAndValidateExtensionEntry(pluginPath, manifest, relEntry);\n if (!result.ok) return result.failure;\n loaded.push(result.extension);\n }\n\n // --- storage output schemas (spec § A.12) -----------------------------\n // Opt-in: only plugins that declare `storage.schemas` (Mode B) or\n // `storage.schema` (Mode A) trigger the read+compile pass. A schema\n // file missing on disk OR failing AJV compile blocks the load with\n // `load-error` so the user sees the typo or syntax error at boot\n // instead of at first write. Storage modes without any schema\n // declaration stay permissive (status quo) — `storageSchemas` is\n // simply omitted from the discovered plugin row.\n const storageSchemasResult = loadStorageSchemas(pluginPath, manifest);\n if (!storageSchemasResult.ok) {\n return {\n ...fail(pluginPath, manifest.id, 'load-error', storageSchemasResult.reason),\n manifest,\n };\n }\n\n return {\n path: pluginPath,\n id: manifest.id,\n status: 'enabled',\n manifest,\n granularity: manifest.granularity ?? 'bundle',\n extensions: loaded,\n ...(storageSchemasResult.schemas\n ? { storageSchemas: storageSchemasResult.schemas }\n : {}),\n };\n }\n\n /**\n * Phase 1 of `loadOne` — read `plugin.json`, AJV-validate the manifest,\n * enforce the directory-name == manifest.id structural rule, and check\n * specCompat (range syntax + satisfies the installed spec version).\n * Returns either the validated manifest or an `IDiscoveredPlugin` with\n * the appropriate failure status.\n */\n #parseAndValidateManifest(\n pluginPath: string,\n ): { ok: true; manifest: IPluginManifest } | { ok: false; failure: IDiscoveredPlugin } {\n const manifestPath = join(pluginPath, 'plugin.json');\n\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(manifestPath, 'utf8'));\n } catch (err) {\n return { ok: false, failure: fail(\n pluginPath,\n pathId(pluginPath),\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestJsonParse, {\n manifestPath,\n errDescription: describe(err),\n }),\n )};\n }\n\n const manifestResult = this.#options.validators.validatePluginManifest<IPluginManifest>(raw);\n if (!manifestResult.ok) {\n return { ok: false, failure: fail(\n pluginPath,\n pathId(pluginPath),\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestAjv, {\n manifestPath,\n errors: manifestResult.errors,\n }),\n )};\n }\n const manifest = manifestResult.data;\n\n // Cheap structural rule (spec § A.5 — plugin id global uniqueness).\n // Two siblings on the same filesystem cannot share a name; matching\n // the directory to the id rules out same-root collisions by construction.\n const dirName = pathId(pluginPath);\n if (dirName !== manifest.id) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestDirMismatch, {\n dirName,\n manifestId: manifest.id,\n }),\n ),\n manifest,\n }};\n }\n\n if (!semver.validRange(manifest.specCompat)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidSpecCompat, { specCompat: manifest.specCompat }),\n ),\n manifest,\n }};\n }\n if (!semver.satisfies(this.#options.specVersion, manifest.specCompat, { includePrerelease: true })) {\n return { ok: false, failure: {\n path: pluginPath,\n id: manifest.id,\n status: 'incompatible-spec',\n manifest,\n granularity: manifest.granularity ?? 'bundle',\n reason: tx(PLUGIN_LOADER_TEXTS.incompatibleSpec, {\n installedSpecVersion: this.#options.specVersion,\n specCompat: manifest.specCompat,\n }),\n }};\n }\n\n return { ok: true, manifest };\n }\n\n /**\n * Phase 3 of `loadOne` — load and validate one extension entry. Six\n * sub-checks (file exists, dynamic import, has kind, kind known,\n * pluginId match, kind-specific manifest validation including hook\n * trigger pre-check). On success returns the `ILoadedExtension` with\n * `pluginId` injected; on failure returns the `IDiscoveredPlugin`\n * with the appropriate status (`load-error` or `invalid-manifest`).\n */\n // Six sub-validations per extension entry (file exists, dynamic\n // import, has-kind, kind-known, pluginId match, kind-specific schema\n // including hook trigger pre-check). Each branch is one early-return;\n // splitting per sub-check would multiply the discriminated-union\n // boilerplate without making the validation pipeline clearer.\n // eslint-disable-next-line complexity\n async #loadAndValidateExtensionEntry(\n pluginPath: string,\n manifest: IPluginManifest,\n relEntry: string,\n ): Promise<{ ok: true; extension: ILoadedExtension } | { ok: false; failure: IDiscoveredPlugin }> {\n if (!isInsidePlugin(pluginPath, relEntry)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.loadErrorPathEscapesPlugin, { relEntry, pluginPath }),\n ),\n manifest,\n }};\n }\n const abs = resolve(pluginPath, relEntry);\n if (!existsSync(abs)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorFileNotFound, { relEntry, abs }),\n ),\n manifest,\n }};\n }\n\n let mod: unknown;\n try {\n mod = await importWithTimeout(pathToFileURL(abs).href, this.#loadTimeoutMs);\n } catch (err) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorImportFailed, {\n relEntry,\n errDescription: describe(err),\n }),\n ),\n manifest,\n }};\n }\n\n const exported = extractDefault(mod);\n if (!isRecord(exported) || typeof exported['kind'] !== 'string') {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorMissingKind, {\n relEntry,\n knownKindsList: KNOWN_KINDS_LIST,\n }),\n ),\n manifest,\n }};\n }\n\n const kind = exported['kind'] as ExtensionKind;\n if (!KNOWN_KINDS.has(kind)) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorUnknownKind, {\n relEntry,\n kindReceived: String(exported['kind']),\n knownKindsList: KNOWN_KINDS_LIST,\n }),\n ),\n manifest,\n }};\n }\n\n // Spec § A.6 — `pluginId` is loader-injected. A hand-declared\n // mismatch is a hard load error; a matching declaration is tolerated\n // (stripped before AJV).\n const declaredPluginId = exported['pluginId'];\n if (typeof declaredPluginId === 'string' && declaredPluginId !== manifest.id) {\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.loadErrorPluginIdMismatch, {\n relEntry,\n declared: declaredPluginId,\n manifestId: manifest.id,\n }),\n ),\n manifest,\n }};\n }\n\n // Strip runtime methods + `pluginId` so AJV's strict\n // `unevaluatedProperties: false` doesn't reject the export.\n const manifestView = stripFunctionsAndPluginId(exported);\n\n if (kind === 'hook') {\n const hookFailure = validateHookTriggers(pluginPath, manifest, relEntry, exported, manifestView);\n if (hookFailure) return { ok: false, failure: hookFailure };\n }\n\n const extValidator = this.#options.validators.validatorForExtension(kind);\n if (!extValidator(manifestView)) {\n const errors = (extValidator.errors ?? [])\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n return { ok: false, failure: {\n ...fail(\n pluginPath,\n manifest.id,\n 'load-error',\n tx(PLUGIN_LOADER_TEXTS.loadErrorManifestInvalid, { relEntry, kind, errors }),\n ),\n manifest,\n }};\n }\n\n // Shallow-clone the runtime instance + inject `pluginId` so two\n // plugins importing the same ESM-cached file don't stomp each\n // other's `pluginId`.\n const instance = isRecord(exported)\n ? { ...exported, pluginId: manifest.id }\n : exported;\n\n return { ok: true, extension: {\n kind,\n id: exported['id'] as string,\n pluginId: manifest.id,\n version: exported['version'] as string,\n entryPath: abs,\n module: mod,\n instance,\n }};\n }\n}\n\n/**\n * Spec § A.11 — Hook triggers validation. Runs BEFORE AJV so the user\n * gets a directed `invalid-manifest` reason (with offending trigger and\n * full hookable list) rather than a generic AJV enum error string under\n * `load-error`. Returns an `IDiscoveredPlugin` failure or `null` if the\n * triggers are valid.\n */\nfunction validateHookTriggers(\n pluginPath: string,\n manifest: IPluginManifest,\n relEntry: string,\n exported: Record<string, unknown>,\n manifestView: unknown,\n): IDiscoveredPlugin | null {\n const triggers = (manifestView as Record<string, unknown>)['triggers'];\n const hookId = (exported['id'] as string) ?? '?';\n if (!Array.isArray(triggers) || triggers.length === 0) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestHookEmptyTriggers, { hookId }),\n ),\n manifest,\n };\n }\n for (const trig of triggers) {\n if (typeof trig !== 'string' || !(HOOK_TRIGGERS as readonly string[]).includes(trig)) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestHookUnknownTrigger, {\n hookId,\n trigger: String(trig),\n hookableList: HOOKABLE_TRIGGERS_LIST,\n }),\n ),\n manifest,\n };\n }\n }\n return null;\n}\n\n// --- helpers ---------------------------------------------------------------\n\nconst KNOWN_KINDS = new Set<ExtensionKind>(['provider', 'extractor', 'rule', 'action', 'formatter', 'hook']);\nconst KNOWN_KINDS_LIST = [...KNOWN_KINDS].join(' / ');\n\n/**\n * Spec § A.11 — curated hookable trigger set. Single source of truth lives\n * in `kernel/extensions/hook.ts` (`HOOK_TRIGGERS`); the loader imports it\n * directly so the loader and the runtime contract cannot drift apart.\n */\nconst HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(', ');\n\n/**\n * Race the dynamic import against a timer. When the timer wins we throw\n * a clear timeout error — the caller turns it into a `load-error` row\n * naming the offending entry. The dangling import promise lingers in\n * Node's loader and resolves later (the result is GC'd unreferenced);\n * there is no public `import()` cancellation API in Node 24, so this\n * is the best we can do without spawning a worker thread.\n */\nasync function importWithTimeout(href: string, timeoutMs: number): Promise<unknown> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(new Error(tx(PLUGIN_LOADER_TEXTS.importExceededTimeout, { timeoutMs })));\n }, timeoutMs);\n });\n try {\n return await Promise.race([import(href), timeout]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nfunction fail(\n path: string,\n id: string,\n status: TPluginLoadStatus,\n reason: string,\n): IDiscoveredPlugin {\n return { path, id, status, reason };\n}\n\n/**\n * Check that a manifest-declared relative path stays inside the plugin\n * tree once resolved. Rejects absolute paths and any value whose\n * resolved form lies above (or beside) the plugin root via `..`\n * components. Returns `null` when safe; otherwise the resolved\n * absolute path is returned for diagnostics.\n *\n * Closes the lane where one plugin directory references another\n * plugin's source (or arbitrary files on disk) by way of\n * `extensions: [\"../foo/index.js\"]` or `storage.schema:\n * \"../bar.schema.json\"`.\n */\nfunction isInsidePlugin(pluginPath: string, relEntry: string): boolean {\n if (isAbsolute(relEntry)) return false;\n const abs = resolve(pluginPath, relEntry);\n const rel = relative(pluginPath, abs);\n if (rel === '') return true;\n if (rel.startsWith('..')) return false;\n if (isAbsolute(rel)) return false;\n return true;\n}\n\nfunction describe(err: unknown): string {\n if (err instanceof Error) return err.message;\n try {\n return String(err);\n } catch {\n return 'unknown error';\n }\n}\n\nfunction isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\nfunction extractDefault(mod: unknown): unknown {\n if (!isRecord(mod)) return mod;\n return 'default' in mod ? mod['default'] : mod;\n}\n\n/**\n * Drop function-typed properties AND the runtime-only `pluginId` so the\n * resulting object is JSON-Schema-validatable. Used on the runtime export\n * before AJV gets it: an extension's `detect` / `render` / etc. method is\n * part of its TypeScript contract, not its declarative manifest, and JSON\n * Schema's `unevaluatedProperties: false` posture would otherwise reject\n * the whole export. Same posture for `pluginId` — per spec § A.6 it's a\n * runtime concern injected by the loader, not a manifest field.\n *\n * Spec 0.8.0: Provider runtime instances carry an additional\n * runtime-only field per `kinds` entry — `schemaJson`, the loaded JSON\n * Schema for the kind. The manifest declares `schema` (a relative path\n * string); `schemaJson` is loaded by the kernel/loader at boot. Strip\n * it before AJV-validating against the strict provider schema (which\n * has `additionalProperties: false` on each kind entry).\n *\n * Cheap shallow + one-level-deep copy — manifests are flat enough.\n */\nfunction stripFunctionsAndPluginId(input: unknown): unknown {\n if (!isRecord(input)) return input;\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(input)) {\n if (typeof v === 'function') continue;\n if (k === 'pluginId') continue;\n if (k === 'kinds' && isRecord(v)) {\n out[k] = stripKindsRuntimeFields(v);\n continue;\n }\n out[k] = v;\n }\n return out;\n}\n\n/**\n * Provider `kinds` map: for each entry, drop runtime-only fields\n * (`schemaJson`) so AJV sees only the manifest-level fields the spec\n * declares (`schema`, `defaultRefreshAction`).\n */\nfunction stripKindsRuntimeFields(kinds: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [kind, entry] of Object.entries(kinds)) {\n if (!isRecord(entry)) {\n out[kind] = entry;\n continue;\n }\n const cleaned: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(entry)) {\n if (k === 'schemaJson') continue;\n if (typeof v === 'function') continue;\n cleaned[k] = v;\n }\n out[kind] = cleaned;\n }\n return out;\n}\n\n/** Fall-back plugin id derived from directory name when the manifest is unreadable. */\nfunction pathId(p: string): string {\n const parts = p.split(/[/\\\\]/);\n return parts[parts.length - 1] ?? p;\n}\n\n/**\n * Cross-root id-collision pass. Group survivors (plugins whose individual\n * load reached a status that exposes a *trusted* `manifest.id`) by id, and\n * for any group of size ≥ 2 rewrite every member's status to\n * `id-collision` with a reason naming the other path(s).\n *\n * \"Trusted id\" means the manifest parsed and validated. The eligible\n * statuses are therefore `enabled`, `disabled`, and `incompatible-spec`\n * (each of those keeps `manifest` populated). The remaining failure\n * modes — `invalid-manifest` and `load-error` — either never reached the\n * id-trust point (`invalid-manifest`) or carry a manifest that's still\n * structurally fine; we treat them inclusively. Pragmatically, the only\n * status whose `id` is a path fall-back is `invalid-manifest` from a\n * manifest that failed to parse — and those are excluded because the\n * fall-back id is the directory name, which by the same-root pigeonhole\n * cannot collide with another fall-back id (and a collision against a\n * real id would be misleading noise: \"rename your plugin to fix your\n * neighbour's broken JSON\" is bad guidance).\n *\n * Concretely we only consider plugins that have a `manifest` populated.\n */\n// eslint-disable-next-line complexity\nfunction applyIdCollisions(plugins: IDiscoveredPlugin[]): IDiscoveredPlugin[] {\n const buckets = new Map<string, IDiscoveredPlugin[]>();\n for (const p of plugins) {\n if (!p.manifest) continue; // skip path-fall-back ids (untrusted)\n const id = p.manifest.id;\n const bucket = buckets.get(id);\n if (bucket) bucket.push(p);\n else buckets.set(id, [p]);\n }\n\n const collidingPaths = new Set<string>();\n const collisionReason = new Map<string, string>();\n for (const [id, bucket] of buckets) {\n if (bucket.length < 2) continue;\n // Stable order so the rendered \"collides with\" list is deterministic\n // across runs — essential for snapshot tests and CI output diffs.\n const sorted = [...bucket].sort((a, b) => a.path.localeCompare(b.path));\n for (const member of sorted) {\n collidingPaths.add(member.path);\n const others = sorted.filter((p) => p.path !== member.path).map((p) => p.path);\n // Reason names the FIRST other path explicitly (matches the spec\n // suggestion) and lists the rest (if any) for the rare 3-way case.\n const pathB = others.length === 1 ? others[0]! : others.join(', ');\n collisionReason.set(\n member.path,\n tx(PLUGIN_LOADER_TEXTS.idCollision, { id, pathA: member.path, pathB }),\n );\n }\n }\n\n if (collidingPaths.size === 0) return plugins;\n\n return plugins.map((p) => {\n if (!collidingPaths.has(p.path)) return p;\n const next: IDiscoveredPlugin = {\n ...p,\n status: 'id-collision',\n reason: collisionReason.get(p.path) ?? p.reason ?? '',\n };\n // A colliding plugin's extensions are inert — strip them so a\n // careless caller cannot register them anyway. Manifest is kept\n // for diagnostics (`sm plugins list/show` shows version, author).\n delete next.extensions;\n return next;\n });\n}\n\n/**\n * Spec § A.12 — read and AJV-compile the storage output schemas a\n * plugin declares in its manifest. Returns either:\n *\n * - `{ ok: true, schemas: undefined }` — the plugin declared no\n * schemas (Mode A without `schema`, Mode B without `schemas`, or\n * no storage at all). Permissive — `storageSchemas` is omitted\n * from the discovered row and the runtime store wrapper skips\n * validation.\n * - `{ ok: true, schemas }` — every declared schema was read and\n * compiled. Mode A's single value-shape lives under the sentinel\n * `KV_SCHEMA_KEY`; Mode B's per-table schemas live under their\n * logical table name (matching the manifest map).\n * - `{ ok: false, reason }` — at least one schema file was missing,\n * unparseable as JSON, or rejected by AJV's compiler. The caller\n * surfaces the reason as `load-error`.\n *\n * One fresh Ajv instance per plugin keeps schema `$id` collisions from\n * leaking across plugins (and from polluting the kernel's spec\n * validators, which live on a separate cached instance — see\n * `schema-validators.ts`).\n */\n// eslint-disable-next-line complexity\nfunction loadStorageSchemas(\n pluginPath: string,\n manifest: IPluginManifest,\n):\n | { ok: true; schemas?: Record<string, IPluginStorageSchema> }\n | { ok: false; reason: string } {\n const storage = manifest.storage;\n if (!storage) return { ok: true };\n\n // Mode A — single optional `schema`.\n if (storage.mode === 'kv') {\n if (!storage.schema) return { ok: true };\n const compiled = compilePluginSchema(pluginPath, storage.schema);\n if (!compiled.ok) {\n const reason = tx(\n compiled.phase === 'read'\n ? PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaRead\n : PLUGIN_LOADER_TEXTS.loadErrorStorageKvSchemaCompile,\n {\n pluginId: manifest.id,\n schemaPath: storage.schema,\n errDescription: compiled.errDescription,\n },\n );\n return { ok: false, reason };\n }\n return {\n ok: true,\n schemas: {\n [KV_SCHEMA_KEY]: {\n schemaPath: storage.schema,\n validate: compiled.validate,\n },\n },\n };\n }\n\n // Mode B — optional `schemas` map keyed by logical table name.\n if (!storage.schemas || Object.keys(storage.schemas).length === 0) {\n return { ok: true };\n }\n const out: Record<string, IPluginStorageSchema> = {};\n for (const [table, relPath] of Object.entries(storage.schemas)) {\n const compiled = compilePluginSchema(pluginPath, relPath);\n if (!compiled.ok) {\n const reason = tx(\n compiled.phase === 'read'\n ? PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaRead\n : PLUGIN_LOADER_TEXTS.loadErrorStorageSchemaCompile,\n {\n pluginId: manifest.id,\n table,\n schemaPath: relPath,\n errDescription: compiled.errDescription,\n },\n );\n return { ok: false, reason };\n }\n out[table] = { schemaPath: relPath, validate: compiled.validate };\n }\n return { ok: true, schemas: out };\n}\n\n/**\n * Read a single JSON Schema file relative to the plugin directory and\n * compile it with a fresh Ajv2020 instance. Two failure modes:\n * - `phase: 'read'` — file missing, unreadable, or not JSON.\n * - `phase: 'compile'` — JSON parsed but AJV rejected it.\n * Both surface to the caller as `load-error` with a phase-specific\n * template message.\n */\nfunction compilePluginSchema(\n pluginPath: string,\n relPath: string,\n):\n | {\n ok: true;\n validate: ValidateFunction & {\n errors?: { instancePath: string; message?: string; keyword: string }[] | null;\n };\n }\n | { ok: false; phase: 'read' | 'compile'; errDescription: string } {\n if (!isInsidePlugin(pluginPath, relPath)) {\n return {\n ok: false,\n phase: 'read',\n errDescription: tx(PLUGIN_LOADER_TEXTS.loadErrorSchemaPathEscapesPlugin, { relPath, pluginPath }),\n };\n }\n const abs = resolve(pluginPath, relPath);\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(abs, 'utf8'));\n } catch (err) {\n return { ok: false, phase: 'read', errDescription: describe(err) };\n }\n try {\n const ajv: TAjv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n const compiled = ajv.compile(raw as object) as ValidateFunction & {\n errors?: { instancePath: string; message?: string; keyword: string }[] | null;\n };\n return { ok: true, validate: compiled };\n } catch (err) {\n return { ok: false, phase: 'compile', errDescription: describe(err) };\n }\n}\n\n/**\n * Locate the installed `@skill-map/spec` version at runtime. Handy default\n * for `IPluginLoaderOptions.specVersion` when the caller just wants the\n * real installed version without plumbing it through.\n */\nexport function installedSpecVersion(): string {\n const require = createRequire(import.meta.url);\n // Spec exports index.json but not package.json; we use the former to\n // locate the package root and then read package.json off disk directly.\n const indexPath = require.resolve('@skill-map/spec/index.json');\n const pkgPath = resolve(indexPath, '..', 'package.json');\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string };\n return pkg.version;\n}\n","/**\n * ESM/CJS interop helper for `ajv-formats`. The package ships CJS-first;\n * the default export is the callable plugin under ESM interop, but TS\n * sometimes types it as the namespace. This helper normalises the\n * import once so adapters that wire `ajv-formats` onto an Ajv instance\n * don't each carry the same `as unknown as ...` cast.\n *\n * Usage:\n * import { applyAjvFormats } from '<...>/kernel/util/ajv-interop.js';\n * applyAjvFormats(ajv);\n */\n\nimport type { Ajv2020 } from 'ajv/dist/2020.js';\nimport addFormatsModule from 'ajv-formats';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\nconst addFormats = (addFormatsModule as unknown as { default?: typeof addFormatsModule })\n .default ?? addFormatsModule;\n\n/**\n * Wire the standard JSON Schema formats (`uri`, `date`, `date-time`,\n * etc.) onto the given Ajv instance.\n */\nexport function applyAjvFormats(ajv: TAjv): void {\n (addFormats as unknown as (a: TAjv) => void)(ajv);\n}\n","/**\n * Kernel-side strings emitted by `kernel/adapters/plugin-store.ts`.\n *\n * Convention: flat string templates with `{{name}}` placeholders. The\n * `tx` helper at `kernel/util/tx.ts` does the interpolation. See\n * `kernel/i18n/orchestrator.texts.ts` header for rationale.\n *\n * Spec § A.12 — opt-in JSON Schema validation for plugin custom\n * storage. Both messages are thrown synchronously from the wrapper\n * when the plugin author's declared output schema rejects the value\n * the plugin tried to persist. Caller (the future kernel-side store\n * adapter) surfaces the throw to the orchestrator's\n * `extension.error` channel.\n */\n\nexport const PLUGIN_STORE_TEXTS = {\n kvValidationFailed:\n \"plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema \" +\n '({{schemaPath}}) — {{errors}}',\n\n dedicatedValidationFailed:\n \"plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema \" +\n '({{schemaPath}}) — {{errors}}',\n} as const;\n","/**\n * Plugin store wrappers — runtime injection for `ctx.store` per spec\n * § A.12 (opt-in `outputSchema` for plugin custom storage).\n *\n * Two shapes, mirroring the manifest's storage modes documented in\n * `spec/plugin-kv-api.md`:\n *\n * - Mode A — `KvStore.set(key, value)`. AJV-validates `value` against\n * the schema declared by `manifest.storage.schema` (single\n * value-shape) when present. Absent = permissive.\n * - Mode B — `DedicatedStore.write(table, row)`. AJV-validates `row`\n * against the per-table schema declared in `manifest.storage.schemas`\n * when present. Tables absent from the map accept any shape.\n *\n * Both wrappers are storage-engine agnostic — they accept a `persist`\n * callback the caller supplies. The persistence side (SQLite, in-memory,\n * mock) is the caller's concern; this wrapper's only job is the\n * AJV gate. That separation lets the test suite exercise the validator\n * without spinning up a real DB and lets the kernel adapter (future\n * `state_plugin_kvs` writer / dedicated-table writer) plug in\n * unchanged.\n *\n * Universal validation (`emitLink` against `link.schema.json`,\n * `enrichNode` against `node.schema.json`) is unaffected — it lives on\n * the orchestrator side and runs regardless of the plugin's\n * `outputSchema` opt-in.\n */\n\nimport type {\n IDiscoveredPlugin,\n IPluginStorageSchema,\n} from '../types/plugin.js';\nimport { tx } from '../util/tx.js';\nimport { PLUGIN_STORE_TEXTS } from '../i18n/plugin-store.texts.js';\n\n/**\n * Sentinel key under which Mode A stores its single value-shape schema\n * inside `IDiscoveredPlugin.storageSchemas`. The sentinel keeps the\n * shared `Record<string, IPluginStorageSchema>` map a single-typed\n * surface across both modes; consumers look up by sentinel for KV and\n * by table name for dedicated.\n */\nexport const KV_SCHEMA_KEY = '__kv__';\n\nexport interface IKvStorePersist {\n (key: string, value: unknown): void | Promise<void>;\n}\n\nexport interface IDedicatedStorePersist {\n (table: string, row: unknown): void | Promise<void>;\n}\n\n/**\n * Mode A wrapper. `set(key, value)` AJV-validates `value` against the\n * Mode A schema (sentinel key `__kv__`) when declared, then forwards\n * to `persist`. Validation failure throws with a message naming the\n * schema path and AJV errors; persistence is skipped on failure.\n *\n * `pluginId` is captured for diagnostics (the throw message names the\n * plugin). The wrapper does NOT itself scope by plugin id — that is\n * the persistence layer's job (the spec's `state_plugin_kvs` PK includes\n * `pluginId` and the kernel-side adapter prepends it before write).\n */\nexport interface IKvStoreWrapper {\n set(key: string, value: unknown): Promise<void>;\n}\n\n/**\n * Union shape exposed to extractors via `ctx.store`. Spec § A.12 — Mode A\n * (`kv`) returns a `set(key, value)` surface; Mode B (`dedicated`) returns\n * `write(table, row)`. Plugin authors narrow at the call site based on\n * the storage mode declared in their `plugin.json`.\n */\nexport type IPluginStore = IKvStoreWrapper | IDedicatedStoreWrapper;\n\nexport function makeKvStoreWrapper(opts: {\n pluginId: string;\n schema: IPluginStorageSchema | undefined;\n persist: IKvStorePersist;\n}): IKvStoreWrapper {\n const { pluginId, schema, persist } = opts;\n return {\n async set(key, value) {\n if (schema) {\n if (!schema.validate(value)) {\n throw new Error(\n tx(PLUGIN_STORE_TEXTS.kvValidationFailed, {\n pluginId,\n schemaPath: schema.schemaPath,\n key,\n errors: formatAjvErrors(schema.validate.errors ?? null),\n }),\n );\n }\n }\n await persist(key, value);\n },\n };\n}\n\n/**\n * Mode B wrapper. `write(table, row)` AJV-validates `row` against\n * `storageSchemas[table]` when declared, then forwards to `persist`.\n * Tables absent from the map are permissive — the wrapper forwards\n * straight to `persist` without validation.\n *\n * The wrapper accepts the full `storageSchemas` map (rather than a\n * single schema) so a plugin author can declare schemas for some\n * tables and leave others permissive in the same map without the\n * caller having to lookup-then-narrow.\n */\nexport interface IDedicatedStoreWrapper {\n write(table: string, row: unknown): Promise<void>;\n}\n\nexport function makeDedicatedStoreWrapper(opts: {\n pluginId: string;\n schemas: Record<string, IPluginStorageSchema> | undefined;\n persist: IDedicatedStorePersist;\n}): IDedicatedStoreWrapper {\n const { pluginId, schemas, persist } = opts;\n return {\n async write(table, row) {\n const schema = schemas?.[table];\n if (schema) {\n if (!schema.validate(row)) {\n throw new Error(\n tx(PLUGIN_STORE_TEXTS.dedicatedValidationFailed, {\n pluginId,\n table,\n schemaPath: schema.schemaPath,\n errors: formatAjvErrors(schema.validate.errors ?? null),\n }),\n );\n }\n }\n await persist(table, row);\n },\n };\n}\n\n/**\n * Convenience entry point: build whichever wrapper matches the\n * discovered plugin's storage mode. Returns `undefined` when the\n * plugin declared no storage at all (the orchestrator omits\n * `ctx.store` in that case, per the existing contract). Mode A\n * extracts the sentinel-keyed schema; Mode B forwards the full map.\n */\nexport function makePluginStore(opts: {\n plugin: IDiscoveredPlugin;\n persistKv?: IKvStorePersist;\n persistDedicated?: IDedicatedStorePersist;\n}): IPluginStore | undefined {\n const manifest = opts.plugin.manifest;\n if (!manifest?.storage) return undefined;\n const storageSchemas = opts.plugin.storageSchemas;\n\n if (manifest.storage.mode === 'kv') {\n if (!opts.persistKv) return undefined;\n const schema = storageSchemas?.[KV_SCHEMA_KEY];\n return makeKvStoreWrapper({\n pluginId: manifest.id,\n schema,\n persist: opts.persistKv,\n });\n }\n\n if (manifest.storage.mode === 'dedicated') {\n if (!opts.persistDedicated) return undefined;\n return makeDedicatedStoreWrapper({\n pluginId: manifest.id,\n schemas: storageSchemas,\n persist: opts.persistDedicated,\n });\n }\n\n return undefined;\n}\n\n/** Compact AJV error string suitable for the throw message. */\nfunction formatAjvErrors(\n errors: { instancePath: string; message?: string; keyword: string }[] | null,\n): string {\n if (!errors || errors.length === 0) return '(no AJV details)';\n return errors\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n}\n","/**\n * Hook runtime contract. The sixth plugin kind (spec § A.11).\n *\n * Hooks subscribe declaratively to a curated set of kernel lifecycle\n * events and react to them. Reaction-only by design: a hook cannot\n * mutate the pipeline, block emission, or alter outputs. Use cases\n * are notification (Slack on `job.completed`), integration glue (CI\n * webhook on `job.failed`), and bookkeeping (per-extractor metrics).\n *\n * The hookable trigger set is INTENTIONALLY SMALL — eight events. The\n * full `ProgressEmitterPort` catalog (per-node `scan.progress`,\n * `model.delta`, `run.*`, internal job lifecycle) is deliberately not\n * hookable: too verbose for a reactive surface, internal to the runner,\n * or covered elsewhere. Declaring a trigger outside the curated set\n * yields `invalid-manifest` at load time.\n *\n * Dual-mode (declared in manifest):\n *\n * - `deterministic` (default): `on(ctx)` runs in-process during the\n * dispatch of the matching event, synchronously between the\n * event's emission and the next pipeline step. Errors are caught\n * by the dispatcher, logged via `extension.error`, and never\n * block the main flow.\n * - `probabilistic`: the hook is enqueued as a job. Until the job\n * subsystem ships, probabilistic hooks load but skip dispatch\n * with a stderr advisory (Decision #114 in `ROADMAP.md`).\n *\n * Curated trigger set (per spec § A.11):\n *\n * 1. `scan.started` — pre-scan setup (one per scan).\n * 2. `scan.completed` — post-scan reaction (one per scan).\n * 3. `extractor.completed` — aggregated per-Extractor outputs.\n * 4. `rule.completed` — aggregated per-Rule outputs.\n * 5. `action.completed` — Action executed on a node.\n * 6. `job.spawning` — pre-spawn of runner subprocess.\n * 7. `job.completed` — most common trigger.\n * 8. `job.failed` — alerts, retry triggers.\n */\n\nimport type { IExtensionBase } from './base.js';\nimport type { Node, TExecutionMode } from '../types.js';\n\n/**\n * The eight hookable lifecycle events. Mirrors the `triggers[]` enum in\n * `spec/schemas/extensions/hook.schema.json`. Anything outside this set\n * is rejected at load time as `invalid-manifest`.\n */\nexport type THookTrigger =\n | 'scan.started'\n | 'scan.completed'\n | 'extractor.completed'\n | 'rule.completed'\n | 'action.completed'\n | 'job.spawning'\n | 'job.completed'\n | 'job.failed';\n\n/**\n * Frozen list mirror of `THookTrigger` for runtime introspection. The\n * loader validates `manifest.triggers[]` against this set; the\n * orchestrator's dispatcher iterates it in order when fanning an event\n * out to subscribed hooks.\n */\nexport const HOOK_TRIGGERS: readonly THookTrigger[] = Object.freeze([\n 'scan.started',\n 'scan.completed',\n 'extractor.completed',\n 'rule.completed',\n 'action.completed',\n 'job.spawning',\n 'job.completed',\n 'job.failed',\n] as const);\n\n/**\n * Context the dispatcher hands to `Hook.on()`. The shape is intentionally\n * narrow: a hook reacts to an event, it does not steer the pipeline.\n *\n * The `event` carries the raw `ProgressEvent` envelope (type, timestamp,\n * runId/jobId when applicable, data). Optional `node` / `extractorId`\n * / `ruleId` / `actionId` are extracted from the event payload by the\n * dispatcher when present so authors don't have to walk `event.data`.\n *\n * Probabilistic hooks additionally receive `runner` for LLM dispatch.\n * Deterministic hooks SHOULD ignore the field.\n */\nexport interface IHookContext {\n /** The raw event the dispatcher matched. */\n event: {\n type: THookTrigger;\n timestamp: string;\n runId?: string;\n jobId?: string;\n data?: unknown;\n };\n /**\n * Convenience extraction of the node payload when the event is\n * node-scoped (`action.completed`). Undefined for run-scoped or\n * scan-scoped events.\n */\n node?: Node;\n /**\n * Set on `extractor.completed` events. Qualified extension id of the\n * Extractor whose work the event aggregates.\n */\n extractorId?: string;\n /**\n * Set on `rule.completed` events. Qualified extension id of the Rule.\n */\n ruleId?: string;\n /**\n * Set on `action.completed` events. Qualified extension id of the\n * Action that just ran.\n */\n actionId?: string;\n /**\n * Set on `job.*` events once the job subsystem lands. Carries the\n * report payload for `job.completed`, the failure record for\n * `job.failed`, and the spawn metadata for `job.spawning`.\n */\n jobResult?: unknown;\n /**\n * `RunnerPort` injection for `probabilistic` hooks. `undefined` for\n * `deterministic` mode (the default). Probabilistic hooks land with\n * the job subsystem; the field is reserved here so the runtime\n * contract is forward-compatible without a major bump.\n */\n runner?: unknown;\n}\n\n/**\n * Optional declarative filter applied by the dispatcher BEFORE\n * invoking `on(ctx)`. Keys are payload field paths (top-level only in\n * v0.x); values are the literal expected match. The dispatcher walks\n * `event.data` for the field and short-circuits the invocation if the\n * value disagrees.\n *\n * Cross-field validation against declared `triggers` is best-effort\n * at load time: when none of the declared triggers carries a given\n * filter field, the loader surfaces `invalid-manifest`. The current\n * impl performs the basic enum check but defers full payload-shape\n * cross-validation to a follow-up — the dispatcher is permissive at\n * runtime (an unknown field never matches → the hook simply never\n * fires for that event, which is a correct interpretation of \"filter\n * by a field that doesn't exist\").\n */\nexport type THookFilter = Record<string, string | number | boolean>;\n\nexport interface IHook extends IExtensionBase {\n kind: 'hook';\n /**\n * Execution mode. Optional in the manifest with a default of\n * `deterministic` per `spec/schemas/extensions/hook.schema.json`.\n * Probabilistic hooks load but skip dispatch with a stderr advisory\n * until the job subsystem ships (Decision #114).\n */\n mode?: TExecutionMode;\n /**\n * Subset of the curated lifecycle trigger set this hook subscribes\n * to. MUST be non-empty; every entry MUST be a member of\n * `HOOK_TRIGGERS`. The loader validates both invariants and surfaces\n * `invalid-manifest` on violation.\n */\n triggers: THookTrigger[];\n /**\n * Optional declarative filter. Absent → invoke on every dispatched\n * event of every declared trigger.\n */\n filter?: THookFilter;\n /**\n * Hook entry point. Returns nothing; reactions are side effects.\n * Errors are caught by the dispatcher (logged as `extension.error`,\n * surfaced via `hook.failed` meta-event) and NEVER block the main\n * pipeline — a buggy hook degrades gracefully.\n */\n on(ctx: IHookContext): void | Promise<void>;\n}\n","/**\n * AJV validator loader. Compiles every JSON Schema the kernel needs into a\n * map of reusable validators keyed by a stable logical name. Schemas load\n * directly from the `@skill-map/spec` package at startup; any missing file\n * is a fatal boot error (the kernel cannot validate without them).\n *\n * Key design choices:\n *\n * - **Single Ajv instance per loader** so `$ref` resolution can reach sibling\n * schemas (e.g. `extensions/base.schema.json` → extended by every kind).\n * - **`strict: false`** because the spec uses a few keywords AJV considers\n * unknown under strict mode (`const` inside `oneOf`, tuple length hints)\n * that are nevertheless valid Draft 2020-12.\n * - **`ajv-formats`** enabled for `uri`, `date`, `date-time` — all used by\n * frontmatter base and plugin manifest.\n * - **Lazy compilation** is NOT used: every validator compiles eagerly on\n * `load()` so the kernel fails fast on a spec corruption instead of\n * crashing the first time a plugin tries to register.\n *\n * **Spec 0.8.0**. Per-kind frontmatter schemas (`skill`, `agent`,\n * `command`, `hook`, `note`) relocated from spec to the Provider that\n * owns them. Spec-only validators no longer cover those\n * five names. `buildProviderFrontmatterValidator(providers)` produces a\n * dedicated AJV instance pre-loaded with `frontmatter/base` (from spec)\n * plus every Provider's per-kind schemas — the kernel composes it once\n * per scan and the orchestrator validates each node's frontmatter\n * through it.\n */\n\nimport { readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\n\nimport type { IProvider } from '../extensions/index.js';\nimport type { ExtensionKind } from '../registry.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\nexport type TSchemaName =\n | 'node'\n | 'link'\n | 'issue'\n | 'scan-result'\n | 'execution-record'\n | 'project-config'\n | 'plugins-registry'\n | 'job'\n | 'report-base'\n | 'conformance-case'\n | 'history-stats'\n | 'extension-provider'\n | 'extension-extractor'\n | 'extension-rule'\n | 'extension-action'\n | 'extension-formatter'\n | 'extension-hook'\n | 'frontmatter-base';\n\n/**\n * Re-export of `ExtensionKind` (canonical declaration in `kernel/registry.ts`)\n * for callers that already depend on this module for related schema names.\n * Single source of truth keeps the extension-kind set in lock-step with\n * `EXTENSION_KINDS`.\n */\nexport type { ExtensionKind } from '../registry.js';\n\nconst SCHEMA_FILES: Record<TSchemaName, string> = {\n node: 'schemas/node.schema.json',\n link: 'schemas/link.schema.json',\n issue: 'schemas/issue.schema.json',\n 'scan-result': 'schemas/scan-result.schema.json',\n 'execution-record': 'schemas/execution-record.schema.json',\n 'project-config': 'schemas/project-config.schema.json',\n 'plugins-registry': 'schemas/plugins-registry.schema.json',\n job: 'schemas/job.schema.json',\n 'report-base': 'schemas/report-base.schema.json',\n 'conformance-case': 'schemas/conformance-case.schema.json',\n 'history-stats': 'schemas/history-stats.schema.json',\n 'extension-provider': 'schemas/extensions/provider.schema.json',\n 'extension-extractor': 'schemas/extensions/extractor.schema.json',\n 'extension-rule': 'schemas/extensions/rule.schema.json',\n 'extension-action': 'schemas/extensions/action.schema.json',\n 'extension-formatter': 'schemas/extensions/formatter.schema.json',\n 'extension-hook': 'schemas/extensions/hook.schema.json',\n 'frontmatter-base': 'schemas/frontmatter/base.schema.json',\n};\n\n/** Schemas that other schemas reference via $ref but aren't validated directly. */\nconst SUPPORTING_SCHEMAS: string[] = [\n 'schemas/extensions/base.schema.json',\n 'schemas/frontmatter/base.schema.json',\n 'schemas/summaries/security-scanner.schema.json',\n];\n\nexport interface ISchemaValidators {\n validate<T = unknown>(name: TSchemaName, data: unknown): { ok: true; data: T } | { ok: false; errors: string };\n getValidator(name: TSchemaName): ValidateFunction;\n validatorForExtension(kind: ExtensionKind): ValidateFunction;\n /**\n * Validate raw plugin.json against `$defs/PluginManifest` inside\n * plugins-registry.schema.json. Returns the typed manifest on success.\n */\n validatePluginManifest<T = unknown>(data: unknown): { ok: true; data: T } | { ok: false; errors: string };\n}\n\n// Module-level cache. Cold load compiles ~17 validators\n// (~20 schemas counting supporting refs) which is ~100 ms cold for a CLI\n// startup. Subsequent calls in the same process return the same instance,\n// so future verbs that validate at multiple boundaries pay the cost once.\n// `null` means \"not yet loaded\"; we never expose a way to invalidate\n// because the schemas are static, baked-in, and the underlying spec\n// package version doesn't change at runtime.\nlet cachedValidators: ISchemaValidators | null = null;\n\n/** Test-only escape hatch — drop the cache so a test can re-trigger load. */\nexport function _resetSchemaValidatorsCacheForTests(): void {\n cachedValidators = null;\n}\n\nexport function loadSchemaValidators(): ISchemaValidators {\n if (cachedValidators !== null) return cachedValidators;\n cachedValidators = buildSchemaValidators();\n return cachedValidators;\n}\n\nfunction buildSchemaValidators(): ISchemaValidators {\n const specRoot = resolveSpecRoot();\n const ajv: TAjv = new Ajv2020({\n strict: false,\n allErrors: true,\n allowUnionTypes: true,\n });\n applyAjvFormats(ajv);\n\n // Add supporting schemas first so $ref targets resolve during compile.\n for (const rel of SUPPORTING_SCHEMAS) {\n const file = resolve(specRoot, rel);\n if (!existsSyncSafe(file)) continue;\n const schema = JSON.parse(readFileSync(file, 'utf8'));\n ajv.addSchema(schema);\n }\n\n const validators = new Map<TSchemaName, ValidateFunction>();\n for (const [name, rel] of Object.entries(SCHEMA_FILES) as Array<[TSchemaName, string]>) {\n const file = resolve(specRoot, rel);\n const schema = JSON.parse(readFileSync(file, 'utf8'));\n // Reuse existing compilation if the schema was already added above.\n const byId = typeof schema.$id === 'string' ? ajv.getSchema(schema.$id) : undefined;\n validators.set(name, byId ?? ajv.compile(schema));\n }\n\n const extensionByKind: Record<ExtensionKind, TSchemaName> = {\n provider: 'extension-provider',\n extractor: 'extension-extractor',\n rule: 'extension-rule',\n action: 'extension-action',\n formatter: 'extension-formatter',\n hook: 'extension-hook',\n };\n\n // Dedicated validator that targets PluginManifest inside the oneOf of\n // plugins-registry.schema.json, so callers don't have to hand-filter\n // against the combined schema.\n const pluginManifestValidator = ajv.compile({\n $ref: 'https://skill-map.dev/spec/v0/plugins-registry.schema.json#/$defs/PluginManifest',\n });\n\n return {\n getValidator(name) {\n const v = validators.get(name);\n if (!v) throw new Error(`Unknown schema: ${name}`);\n return v;\n },\n validatorForExtension(kind) {\n return validators.get(extensionByKind[kind])!;\n },\n validate<T = unknown>(name: TSchemaName, data: unknown) {\n const v = validators.get(name);\n if (!v) throw new Error(`Unknown schema: ${name}`);\n if (v(data)) return { ok: true as const, data: data as T };\n const errors = (v.errors ?? []).map(formatError).join('; ');\n return { ok: false as const, errors };\n },\n validatePluginManifest<T = unknown>(data: unknown) {\n if (pluginManifestValidator(data)) return { ok: true as const, data: data as T };\n const errors = (pluginManifestValidator.errors ?? []).map(formatError).join('; ');\n return { ok: false as const, errors };\n },\n };\n}\n\n/**\n * Validator for Provider-owned per-kind frontmatter schemas. Built from\n * the live set of registered Providers — each Provider declares its\n * `kinds[<kind>].schemaJson` and the loader compiles them into a single\n * AJV instance that also carries the spec's `frontmatter/base.schema.json`\n * so cross-package `$ref`-by-`$id` resolves. The orchestrator builds\n * one of these per scan via `buildProviderFrontmatterValidator`.\n */\nexport interface IProviderFrontmatterValidator {\n /**\n * Validate a node's frontmatter against the schema declared by\n * `provider.kinds[kind]`. `kind` is the value `provider.classify`\n * returned for the node, so the entry is guaranteed to exist for any\n * Provider implemented per spec; an absent entry returns\n * `{ ok: false, errors: 'no-schema' }` so the caller can emit a\n * directed `frontmatter-invalid` issue without crashing.\n */\n validate(\n provider: IProvider,\n kind: string,\n data: unknown,\n ): { ok: true } | { ok: false; errors: string };\n}\n\n/**\n * Build a Provider-frontmatter validator. Composes one AJV instance,\n * pre-registers `frontmatter/base.schema.json` from spec so per-kind\n * schemas can `$ref` it by `$id`, then compiles every Provider's\n * `kinds[<kind>].schemaJson` keyed by `(providerId, kind)`. Idempotent\n * across providers that share kinds (same `$id` → AJV's `addSchema`\n * dedupes silently); the keying is by `providerId` first so two\n * Providers exporting different schemas under the same kind name don't\n * collide.\n */\nexport function buildProviderFrontmatterValidator(\n providers: IProvider[],\n): IProviderFrontmatterValidator {\n const specRoot = resolveSpecRoot();\n const ajv: TAjv = new Ajv2020({\n strict: false,\n allErrors: true,\n allowUnionTypes: true,\n });\n applyAjvFormats(ajv);\n\n // Register spec's frontmatter/base.schema.json so per-kind schemas can\n // resolve `$ref: 'https://skill-map.dev/spec/v0/frontmatter/base.schema.json'`.\n const baseFile = resolve(specRoot, 'schemas/frontmatter/base.schema.json');\n const baseSchema = JSON.parse(readFileSync(baseFile, 'utf8'));\n ajv.addSchema(baseSchema);\n\n const compiled = new Map<string, ValidateFunction>();\n for (const provider of providers) {\n for (const [kind, entry] of Object.entries(provider.kinds)) {\n const key = `${provider.id}::${kind}`;\n // Reuse a previously-compiled schema (multiple Providers may legitimately\n // share the same `$id` if they bundle a copy of another's schema).\n const json = entry.schemaJson as { $id?: string };\n const existing = typeof json.$id === 'string' ? ajv.getSchema(json.$id) : undefined;\n compiled.set(key, existing ?? ajv.compile(entry.schemaJson as object));\n }\n }\n\n return {\n validate(provider, kind, data) {\n const key = `${provider.id}::${kind}`;\n const v = compiled.get(key);\n if (!v) return { ok: false as const, errors: 'no-schema' };\n if (v(data)) return { ok: true as const };\n const errors = (v.errors ?? []).map(formatError).join('; ');\n return { ok: false as const, errors };\n },\n };\n}\n\nfunction formatError(err: { instancePath: string; message?: string; keyword: string; params?: unknown }): string {\n const path = err.instancePath || '(root)';\n return `${path} ${err.message ?? err.keyword}`;\n}\n\n/**\n * Locate the installed `@skill-map/spec` package root. Prefer Node's\n * resolver (handles npm workspaces + published installs symmetrically)\n * and fall back to the package's `package.json` directory.\n */\nfunction resolveSpecRoot(): string {\n const require = createRequire(import.meta.url);\n // @skill-map/spec's exports field doesn't expose package.json, but\n // ./index.json is always exported and always lives at the package root.\n try {\n const indexPath = require.resolve('@skill-map/spec/index.json');\n return dirname(indexPath);\n } catch {\n throw new Error(\n '@skill-map/spec not resolvable — ensure the workspace is linked or the package is installed.',\n );\n }\n}\n\nfunction existsSyncSafe(path: string): boolean {\n try {\n readFileSync(path, 'utf8');\n return true;\n } catch {\n return false;\n }\n}\n","/**\n * Kernel-side strings emitted by `kernel/orchestrator.ts`.\n *\n * Convention: every entry is a flat string with `{{name}}` placeholders\n * (Mustache / Handlebars / Transloco compatible). The `tx` helper at\n * `kernel/util/tx.ts` does the interpolation. Plural / conditional\n * logic lives in the caller — pick the right template, don't branch\n * inside one.\n */\n\nexport const ORCHESTRATOR_TEXTS = {\n frontmatterInvalid:\n 'Frontmatter for {{path}} ({{kind}}) failed schema validation: {{errors}}',\n\n frontmatterMalformedPasteWithIndent:\n 'Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` ' +\n 'at column 0. The file was scanned as body-only — the metadata block was silently lost. ' +\n 'Move the `---` lines to the start of the line.',\n\n frontmatterMalformedByteOrderMark:\n 'Frontmatter fence in {{path}} is preceded by a UTF-8 byte-order mark (BOM); the file ' +\n 'was scanned as body-only. Re-save the file as UTF-8 without BOM. The metadata block ' +\n 'was silently lost.',\n\n frontmatterMalformedMissingClose:\n 'Frontmatter in {{path}} opens with `---` but never closes — no matching `---` line ' +\n 'at column 0 was found. The file was scanned as body-only and every metadata field was ' +\n 'silently lost. Add a closing `---` line below the metadata block.',\n\n extensionErrorLinkKindNotDeclared:\n 'Extractor \"{{extractorId}}\" emitted a link of kind \"{{linkKind}}\" outside its ' +\n 'declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',\n\n extensionErrorIssueInvalidSeverity:\n 'Rule \"{{ruleId}}\" emitted an issue with invalid severity {{severity}} ' +\n \"(allowed: 'error' | 'warn' | 'info'). Issue dropped.\",\n\n runScanRootEmptyArray:\n 'runScan: roots must contain at least one path (spec requires minItems: 1)',\n\n runScanRootMissing: \"runScan: root path '{{root}}' does not exist or is not a directory\",\n} as const;\n","/**\n * File watcher for `sm watch` / `sm scan --watch`.\n *\n * Wraps `chokidar` behind a small `IFsWatcher` interface so:\n *\n * 1. The CLI command is impl-agnostic — swapping chokidar for a\n * different watcher later (Java? Rust port? a future `WatchPort`?)\n * doesn't ripple into the command.\n * 2. Debouncing, batching, and ignore-filter integration live in one\n * place. The CLI just gets `onBatch(paths)` callbacks and decides\n * whether to re-scan.\n *\n * The watcher does NOT call into the orchestrator itself. That decision\n * is deliberate: the CLI owns the scan-and-persist pipeline (`runScan`,\n * `persistScanResult`, optional rebuild of the ignore filter when\n * `.skillmapignore` itself changes). Pulling that into the watcher\n * would couple the kernel module to `SqliteStorageAdapter`, which the\n * Server wouldn't want. Keep this module side-effect free\n * apart from filesystem subscription.\n *\n * Ignore filter integration: the supplied `IIgnoreFilter` is consulted\n * via chokidar's `ignored` predicate, which receives an absolute path.\n * We re-derive the path RELATIVE to the closest matching root before\n * passing it through `IIgnoreFilter.ignores`. This mirrors what the\n * scan walker does (`extensions/providers/claude/index.ts`) so both code\n * paths agree on what \"ignored\" means.\n */\n\nimport { resolve, relative, sep } from 'node:path';\n\nimport chokidar from 'chokidar';\nimport type { FSWatcher } from 'chokidar';\n\nimport type { IIgnoreFilter } from './ignore.js';\n\n// -----------------------------------------------------------------------------\n// Public types\n// -----------------------------------------------------------------------------\n\nexport type TWatchEventKind = 'add' | 'change' | 'unlink';\n\nexport interface IWatchEvent {\n kind: TWatchEventKind;\n /** Absolute path. */\n absolutePath: string;\n}\n\nexport interface IWatchBatch {\n /** Events that arrived inside the debounce window, in arrival order. */\n events: IWatchEvent[];\n /** Convenience: deduplicated absolute paths across the batch. */\n paths: string[];\n}\n\nexport interface IFsWatcher {\n /** Resolves once chokidar has finished its initial directory scan and is ready to emit. */\n ready: Promise<void>;\n /** Tear down the watcher. Resolves after chokidar releases handles. */\n close: () => Promise<void>;\n}\n\nexport interface ICreateFsWatcherOptions {\n /** Roots to watch. Resolved relative to `cwd` if relative paths are passed. */\n roots: string[];\n /** Working directory used to resolve relative roots and the ignore-filter root. */\n cwd: string;\n /** Debounce window in milliseconds. `0` triggers `onBatch` synchronously per event. */\n debounceMs: number;\n /**\n * Optional ignore filter — same instance the scan walker uses.\n *\n * Two shapes are accepted:\n *\n * - **`IIgnoreFilter`** (the static one) — captured by reference at\n * construction. Use this when the filter never changes for the\n * lifetime of the watcher (the typical CLI `sm watch` flow).\n *\n * - **`() => IIgnoreFilter | undefined`** (a getter) — re-evaluated\n * on EVERY chokidar `ignored` predicate call. Use this when the\n * filter can change at runtime — e.g. the BFF rebuilds it after\n * a `.skillmapignore` or `.skill-map/settings.json` edit and\n * wants chokidar to immediately respect the new patterns without\n * tearing down and rebuilding the watcher. A getter that returns\n * `undefined` disables ignore filtering for that call.\n */\n ignoreFilter?: IIgnoreFilter | (() => IIgnoreFilter | undefined) | undefined;\n /** Called once per debounced batch. Awaited; concurrent batches are serialised. */\n onBatch: (batch: IWatchBatch) => void | Promise<void>;\n /**\n * Called when the underlying watcher surfaces an error. The watcher\n * stays open — callers decide whether to log, keep going, or close.\n */\n onError?: (err: Error) => void;\n}\n\n// -----------------------------------------------------------------------------\n// Public API\n// -----------------------------------------------------------------------------\n\n/**\n * Construct a chokidar-backed watcher. Subscribes immediately; the\n * returned `ready` promise resolves once chokidar's initial directory\n * walk completes, at which point only NEW events fire `onBatch`.\n *\n * The initial directory walk is deliberately silent — we set\n * `ignoreInitial: true`. The CLI runs a one-shot scan before flipping\n * the watcher on, so re-emitting an `add` for every existing file\n * would be redundant churn.\n */\nexport function createChokidarWatcher(opts: ICreateFsWatcherOptions): IFsWatcher {\n const absRoots = opts.roots.map((r) => resolve(opts.cwd, r));\n const ignoreFilterOpt = opts.ignoreFilter;\n\n // Normalise the union: the static filter shape becomes a constant getter.\n // Resolving the getter on every call is what enables the BFF to swap\n // filters at runtime without tearing the watcher down.\n const getFilter: (() => IIgnoreFilter | undefined) | undefined =\n ignoreFilterOpt === undefined\n ? undefined\n : typeof ignoreFilterOpt === 'function'\n ? ignoreFilterOpt\n : (): IIgnoreFilter => ignoreFilterOpt;\n\n const ignored = getFilter\n ? (path: string): boolean => {\n const filter = getFilter();\n if (!filter) return false;\n const rel = relativePathFromRoots(path, absRoots);\n if (rel === null) return false;\n return filter.ignores(rel);\n }\n : undefined;\n\n const watcher: FSWatcher = chokidar.watch(absRoots, {\n ignoreInitial: true,\n persistent: true,\n ...(ignored ? { ignored } : {}),\n });\n\n // Pending state for debouncing.\n let pending: IWatchEvent[] = [];\n let timer: NodeJS.Timeout | null = null;\n let inFlight: Promise<void> | null = null;\n let closed = false;\n\n const fire = async (): Promise<void> => {\n timer = null;\n if (pending.length === 0) return;\n if (inFlight) {\n // A previous batch is still running; let it finish first.\n // The current pending events stay queued and will fire in the\n // next tick once `inFlight` resolves.\n return;\n }\n const events = pending;\n pending = [];\n const seen = new Set<string>();\n const paths: string[] = [];\n for (const ev of events) {\n if (!seen.has(ev.absolutePath)) {\n seen.add(ev.absolutePath);\n paths.push(ev.absolutePath);\n }\n }\n inFlight = Promise.resolve(opts.onBatch({ events, paths }))\n .catch((err: unknown) => {\n if (opts.onError) {\n opts.onError(err instanceof Error ? err : new Error(String(err)));\n }\n })\n .finally(() => {\n inFlight = null;\n // If new events accumulated while we were busy, schedule\n // another fire. We respect the debounce window so a slow\n // `onBatch` doesn't immediately re-trigger.\n if (!closed && pending.length > 0 && timer === null) {\n schedule();\n }\n });\n };\n\n const schedule = (): void => {\n if (closed) return;\n if (opts.debounceMs <= 0) {\n void fire();\n return;\n }\n if (timer !== null) clearTimeout(timer);\n timer = setTimeout(() => {\n void fire();\n }, opts.debounceMs);\n };\n\n const enqueue = (kind: TWatchEventKind, absolutePath: string): void => {\n if (closed) return;\n pending.push({ kind, absolutePath });\n schedule();\n };\n\n watcher.on('add', (p) => enqueue('add', p));\n watcher.on('change', (p) => enqueue('change', p));\n watcher.on('unlink', (p) => enqueue('unlink', p));\n if (opts.onError) {\n watcher.on('error', (err) => {\n opts.onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }\n\n const ready: Promise<void> = new Promise((resolveReady) => {\n watcher.once('ready', () => resolveReady());\n });\n\n const close = async (): Promise<void> => {\n closed = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n pending = [];\n if (inFlight) {\n try {\n await inFlight;\n } catch {\n // already routed through onError above\n }\n }\n await watcher.close();\n };\n\n return { ready, close };\n}\n\n// -----------------------------------------------------------------------------\n// Helpers\n// -----------------------------------------------------------------------------\n\n/**\n * Pick the matching root for `absolute` and return the path RELATIVE to\n * it, in POSIX form. Returns `null` when the path is outside every\n * supplied root (chokidar shouldn't emit those, but the contract on\n * `IIgnoreFilter.ignores` requires a relative path so we guard\n * defensively).\n */\nfunction relativePathFromRoots(absolute: string, absRoots: string[]): string | null {\n for (const root of absRoots) {\n const rel = relative(root, absolute);\n if (rel === '' || rel === '.') return '';\n if (!rel.startsWith('..') && !rel.startsWith(`..${sep}`)) {\n return rel.split(sep).join('/');\n }\n }\n return null;\n}\n","/**\n * Scan delta — pure comparison of two `ScanResult` snapshots. Drives\n * `sm scan --compare-with <path>` and is the single place the kernel\n * knows how to identify \"the same\" entity across two scans.\n *\n * **Identity contract** (mirrors decisions made at earlier sub-steps):\n *\n * - **Node**: `node.path`. The path is the only field stable across\n * edits — every other Node field is content-derived (hashes, counts,\n * denormalised frontmatter). Two nodes with the same path are the\n * \"same\" node; differences are reported as a `changed` entry with\n * a reason narrowing what diverged.\n *\n * - **Link**: `(source, target, kind, normalizedTrigger ?? '')`. This\n * mirrors the link-conflict rule and `sm show` aggregation —\n * two links with identical endpoints, kind, and (optional) trigger\n * are the same link, even if emitted by different extractors. The\n * `sources[]` union and confidence are NOT part of identity; they\n * are presentation facets that can churn without making the link\n * \"different\" for delta purposes.\n *\n * - **Issue**: `(ruleId, sorted nodeIds, message)`. Mirrors\n * `spec/job-events.md` §issue.* — same key → same issue, even when\n * `data` / `severity` / `linkIndices` shift. A meaningful change in\n * `message` (or a different set of node ids) is a different issue.\n * This is the same key future job events will use; keep it aligned\n * so consumers can reuse logic.\n *\n * No \"changed\" bucket for links / issues — identity already captures\n * everything that matters there. Nodes get a \"changed\" bucket because\n * the path stays stable while the body / frontmatter rewrite, and that\n * change is meaningful (formatters, summarisers, downstream consumers\n * all care about it).\n *\n * Pure: no IO, no DB, no FS. Safe to run in-memory inside `sm scan`\n * without polluting the persisted snapshot.\n */\n\nimport type { Issue, Link, Node, ScanResult } from '../types.js';\n\nexport type TNodeChangeReason = 'body' | 'frontmatter' | 'both';\n\nexport interface INodeChange {\n before: Node;\n after: Node;\n /**\n * Which hash diverged. `'body'` means body rewritten, frontmatter\n * untouched; `'frontmatter'` means metadata rewritten, body\n * untouched; `'both'` means both rewritten in the same edit.\n */\n reason: TNodeChangeReason;\n}\n\nexport interface IScanDelta {\n /** Path the current scan was compared against (echoed for the report header). */\n comparedWith: string;\n nodes: {\n added: Node[];\n removed: Node[];\n changed: INodeChange[];\n };\n links: {\n added: Link[];\n removed: Link[];\n };\n issues: {\n added: Issue[];\n removed: Issue[];\n };\n}\n\nexport function computeScanDelta(\n prior: ScanResult,\n current: ScanResult,\n comparedWith: string,\n): IScanDelta {\n return {\n comparedWith,\n nodes: diffNodes(prior.nodes, current.nodes),\n links: diffLinks(prior.links, current.links),\n issues: diffIssues(prior.issues, current.issues),\n };\n}\n\n/**\n * `true` iff every bucket is empty. Callers use this to decide the\n * exit code (`0` clean, `1` non-empty delta).\n */\nexport function isEmptyDelta(delta: IScanDelta): boolean {\n return (\n delta.nodes.added.length === 0 &&\n delta.nodes.removed.length === 0 &&\n delta.nodes.changed.length === 0 &&\n delta.links.added.length === 0 &&\n delta.links.removed.length === 0 &&\n delta.issues.added.length === 0 &&\n delta.issues.removed.length === 0\n );\n}\n\n// --- node delta ------------------------------------------------------------\n\nfunction diffNodes(\n priorNodes: Node[],\n currentNodes: Node[],\n): IScanDelta['nodes'] {\n const priorByPath = new Map(priorNodes.map((n) => [n.path, n]));\n const currentByPath = new Map(currentNodes.map((n) => [n.path, n]));\n\n const added: Node[] = [];\n const removed: Node[] = [];\n const changed: INodeChange[] = [];\n\n for (const [path, after] of currentByPath) {\n const before = priorByPath.get(path);\n if (!before) {\n added.push(after);\n continue;\n }\n const reason = compareNodeHashes(before, after);\n if (reason !== null) changed.push({ before, after, reason });\n }\n for (const [path, before] of priorByPath) {\n if (!currentByPath.has(path)) removed.push(before);\n }\n\n // Deterministic ordering — by path so two consumers comparing the same\n // pair of scans always see the same delta. Match the existing read-side\n // sort (used by `sm list`, ASCII formatter, etc.).\n added.sort(byPath);\n removed.sort(byPath);\n changed.sort((a, b) => byPath(a.after, b.after));\n\n return { added, removed, changed };\n}\n\nfunction compareNodeHashes(before: Node, after: Node): TNodeChangeReason | null {\n const bodyChanged = before.bodyHash !== after.bodyHash;\n const fmChanged = before.frontmatterHash !== after.frontmatterHash;\n if (bodyChanged && fmChanged) return 'both';\n if (bodyChanged) return 'body';\n if (fmChanged) return 'frontmatter';\n return null;\n}\n\nfunction byPath(a: { path: string }, b: { path: string }): number {\n return a.path.localeCompare(b.path);\n}\n\n// --- link delta ------------------------------------------------------------\n\nfunction diffLinks(\n priorLinks: Link[],\n currentLinks: Link[],\n): IScanDelta['links'] {\n const priorKeys = new Set(priorLinks.map(linkIdentity));\n const currentKeys = new Set(currentLinks.map(linkIdentity));\n\n const added: Link[] = [];\n const removed: Link[] = [];\n\n for (const link of currentLinks) {\n if (!priorKeys.has(linkIdentity(link))) added.push(link);\n }\n for (const link of priorLinks) {\n if (!currentKeys.has(linkIdentity(link))) removed.push(link);\n }\n\n added.sort(byLinkSort);\n removed.sort(byLinkSort);\n\n return { added, removed };\n}\n\nfunction linkIdentity(link: Link): string {\n // NUL separator — collision-free against any path (POSIX paths cannot\n // contain NUL) or trigger string. Same rule used by `sm show`'s\n // aggregation and by the link-conflict rule.\n const trigger = link.trigger?.normalizedTrigger ?? '';\n return `${link.source}\\x00${link.target}\\x00${link.kind}\\x00${trigger}`;\n}\n\nfunction byLinkSort(a: Link, b: Link): number {\n if (a.source !== b.source) return a.source.localeCompare(b.source);\n if (a.target !== b.target) return a.target.localeCompare(b.target);\n return a.kind.localeCompare(b.kind);\n}\n\n// --- issue delta -----------------------------------------------------------\n\nfunction diffIssues(\n priorIssues: Issue[],\n currentIssues: Issue[],\n): IScanDelta['issues'] {\n const priorKeys = new Set(priorIssues.map(issueIdentity));\n const currentKeys = new Set(currentIssues.map(issueIdentity));\n\n const added: Issue[] = [];\n const removed: Issue[] = [];\n\n for (const issue of currentIssues) {\n if (!priorKeys.has(issueIdentity(issue))) added.push(issue);\n }\n for (const issue of priorIssues) {\n if (!currentKeys.has(issueIdentity(issue))) removed.push(issue);\n }\n\n added.sort(byIssueSort);\n removed.sort(byIssueSort);\n\n return { added, removed };\n}\n\nfunction issueIdentity(issue: Issue): string {\n // Matches the spec/job-events.md §issue.* diff key so future job-event\n // consumers can reuse the same identity across the kernel.\n const ids = [...issue.nodeIds].sort().join(',');\n return `${issue.ruleId}\\x00${ids}\\x00${issue.message}`;\n}\n\nfunction byIssueSort(a: Issue, b: Issue): number {\n if (a.ruleId !== b.ruleId) return a.ruleId.localeCompare(b.ruleId);\n return a.message.localeCompare(b.message);\n}\n","/**\n * Kernel-side strings emitted by `kernel/adapters/sqlite/*` and the\n * scan-query parser (`kernel/scan/query.ts`). Same `tx(template, vars)`\n * convention as every other `kernel/i18n/*.texts.ts` peer.\n *\n * These are error messages from the storage adapter and the export-\n * query parser. Some of them surface as user-visible CLI errors via\n * `cli/commands/*` `formatErrorMessage(err)` paths; keeping them in\n * the catalog makes the future translator pipeline trivial.\n */\n\nexport const STORAGE_TEXTS = {\n scanPersistInvalidScannedAt:\n 'persistScanResult: invalid scannedAt {{value}} (expected non-negative integer ms)',\n\n findNodesInvalidSortBy:\n 'findNodes: invalid sortBy \"{{sortBy}}\". Allowed: {{allowed}}.',\n\n findNodesInvalidLimit:\n 'findNodes: invalid limit {{value}}; expected positive integer.',\n} as const;\n\nexport const QUERY_TEXTS = {\n exportQueryInvalidToken:\n 'invalid token \"{{token}}\": expected key=value (e.g. kind=skill, has=issues, path=foo/*).',\n\n exportQueryDuplicateKey:\n 'key \"{{key}}\" appears more than once; combine values with a comma instead (e.g. kind=skill,agent).',\n\n exportQueryEmptyValues: 'key \"{{key}}\" has no values.',\n\n exportQueryUnknownKey:\n 'unknown key \"{{key}}\". Valid keys: kind, has, path.',\n\n exportQueryEmptyKind:\n 'kind=\"\" is not a valid node kind (empty).',\n\n exportQueryUnsupportedHas:\n 'has=\"{{value}}\" is not supported. Valid: {{allowed}}. (findings / summary land at Steps 10 / 11.)',\n} as const;\n","/**\n * Export query — minimal filter language for `sm export <query>` (Step 8.3).\n *\n * Spec contract: `spec/cli-contract.md` line 190 says \"Query syntax is\n * implementation-defined pre-1.0\". This module defines the v0.5.0 syntax.\n *\n * **Grammar** (BNF-ish, intentionally tiny):\n *\n * query := token (WS+ token)*\n * token := key \"=\" value-list\n * key := \"kind\" | \"has\" | \"path\"\n * value-list := value (\",\" value)*\n * value := non-comma, non-whitespace string\n *\n * Tokens AND together; values within one token OR. An empty / whitespace-only\n * query is valid and matches every node (\"export everything\").\n *\n * **Filters**:\n *\n * - `kind=skill` / `kind=skill,agent` — node kind whitelist.\n * - `has=issues` — node must appear in some issue's `nodeIds`. (Future\n * expansion: `has=findings` / `has=summary` once Step 10 / 11 land.\n * Unknown values are a parse error today; we'll ratchet up the\n * accepted set additively.)\n * - `path=foo/*` / `path=.claude/agents/**` — POSIX glob over `node.path`.\n * Supports `*` (any chars except `/`) and `**` (any chars including `/`).\n *\n * **Subset semantics** (`applyExportQuery`):\n *\n * - Nodes pass when every specified filter matches (AND across keys,\n * OR within values).\n * - Links survive only when BOTH endpoints (`source` + `target`) belong\n * to the filtered node set. A subset that includes \"edges out to\n * unfiltered nodes\" would be confusing — the user asked for a focused\n * subgraph, not its boundary. External-URL pseudo-links are already\n * stripped by the orchestrator and never reach this layer.\n * - Issues survive when ANY of the issue's `nodeIds` is in the filtered\n * set. Issues span multiple nodes (e.g. `trigger-collision` over two\n * advertisers); dropping an issue when one of its nodes is outside\n * would hide cross-cutting problems the user is investigating.\n *\n * Pure: no IO, no DB, no FS.\n */\n\nimport type { Issue, Link, Node } from '../types.js';\nimport { QUERY_TEXTS } from '../i18n/storage.texts.js';\nimport { tx } from '../util/tx.js';\n\nconst HAS_VALUES = new Set(['issues']);\n\nexport interface IExportQuery {\n /** Original query string echoed back so consumers can render the header. */\n raw: string;\n /**\n * Whitelist of node kinds (`node.kind` is open string — built-in\n * Claude catalog `skill` / `agent` / `command` / `hook` / `note`,\n * plus whatever external Providers declare). The query parser does\n * not validate values against a closed enum; an unknown kind simply\n * yields zero matches at filter time.\n */\n kinds?: string[];\n hasIssues?: boolean;\n pathGlobs?: string[];\n}\n\nexport interface IExportSubset {\n query: IExportQuery;\n nodes: Node[];\n links: Link[];\n issues: Issue[];\n}\n\nexport class ExportQueryError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ExportQueryError';\n }\n}\n\n// Token-by-token parser with switch over keys + per-key validators.\n// Branching is intrinsic to the multi-key query grammar.\n// eslint-disable-next-line complexity\nexport function parseExportQuery(raw: string): IExportQuery {\n const trimmed = raw.trim();\n const out: IExportQuery = { raw: trimmed };\n if (trimmed.length === 0) return out;\n\n // Tokens are whitespace-separated key=value pairs. Values within one\n // token are comma-separated (multi-value OR). Keys repeated across\n // tokens are an error — the user should comma-separate within one\n // token instead, which is the documented form.\n const seen = new Set<string>();\n for (const token of trimmed.split(/\\s+/)) {\n const eq = token.indexOf('=');\n if (eq <= 0 || eq === token.length - 1) {\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryInvalidToken, { token }),\n );\n }\n const key = token.slice(0, eq).toLowerCase();\n const valuePart = token.slice(eq + 1);\n if (seen.has(key)) {\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryDuplicateKey, { key }),\n );\n }\n seen.add(key);\n\n const values = valuePart.split(',').map((v) => v.trim()).filter((v) => v.length > 0);\n if (values.length === 0) {\n throw new ExportQueryError(tx(QUERY_TEXTS.exportQueryEmptyValues, { key }));\n }\n\n switch (key) {\n case 'kind':\n out.kinds = parseKindValues(values);\n break;\n case 'has':\n if (parseHasValues(values)) out.hasIssues = true;\n break;\n case 'path':\n out.pathGlobs = values;\n break;\n default:\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryUnknownKey, { key }),\n );\n }\n }\n\n return out;\n}\n\n/**\n * Validate every token of a `kind=...` clause. Per\n * `node.schema.json#/properties/kind`, kinds are an open string — any\n * non-empty value is structurally valid. We still reject empty tokens\n * (a typo like `kind=,skill` shouldn't silently match every node).\n * Unknown-but-non-empty kinds simply yield zero matches at filter time.\n */\nfunction parseKindValues(values: string[]): string[] {\n for (const v of values) {\n if (v.length === 0) {\n throw new ExportQueryError(QUERY_TEXTS.exportQueryEmptyKind);\n }\n }\n return values;\n}\n\n/** Validate every token of a `has=...` clause; returns true iff `issues` is present. */\nfunction parseHasValues(values: string[]): boolean {\n for (const v of values) {\n if (!HAS_VALUES.has(v)) {\n throw new ExportQueryError(\n tx(QUERY_TEXTS.exportQueryUnsupportedHas, {\n value: v,\n allowed: [...HAS_VALUES].join(', '),\n }),\n );\n }\n }\n return values.includes('issues');\n}\n\nexport function applyExportQuery(\n scan: { nodes: Node[]; links: Link[]; issues: Issue[] },\n query: IExportQuery,\n): IExportSubset {\n const nodesWithIssues = query.hasIssues\n ? collectNodesWithIssues(scan.issues)\n : null;\n const compiledGlobs = query.pathGlobs\n ? query.pathGlobs.map(compileGlob)\n : null;\n\n const filteredNodes = scan.nodes.filter((node) => {\n if (query.kinds && !query.kinds.includes(node.kind)) return false;\n if (nodesWithIssues && !nodesWithIssues.has(node.path)) return false;\n if (compiledGlobs && !compiledGlobs.some((re) => re.test(node.path))) return false;\n return true;\n });\n\n const survivingPaths = new Set(filteredNodes.map((n) => n.path));\n\n // Links: both endpoints must survive. See module-level commentary on\n // why we close the subgraph instead of carrying boundary edges.\n const filteredLinks = scan.links.filter(\n (link) => survivingPaths.has(link.source) && survivingPaths.has(link.target),\n );\n\n // Issues: any node in the issue's nodeIds being in scope keeps the\n // issue. See module-level commentary on why we don't require all.\n const filteredIssues = scan.issues.filter((issue) =>\n issue.nodeIds.some((id) => survivingPaths.has(id)),\n );\n\n return {\n query,\n nodes: filteredNodes,\n links: filteredLinks,\n issues: filteredIssues,\n };\n}\n\nfunction collectNodesWithIssues(issues: Issue[]): Set<string> {\n const out = new Set<string>();\n for (const issue of issues) {\n for (const nodeId of issue.nodeIds) out.add(nodeId);\n }\n return out;\n}\n\n/**\n * Compile a minimal POSIX glob into a RegExp. Supports:\n *\n * - `*` — any sequence of chars except `/` (single segment wildcard).\n * - `**` — any sequence of chars including `/` (cross-segment wildcard).\n * - everything else is literal (regex metacharacters escaped).\n *\n * No `?`, no `[abc]`, no brace expansion. The grammar is explicitly\n * minimal so the spec doesn't bind us to a specific glob library before\n * v1.0; we can grow this when consumers ask for it.\n */\nfunction compileGlob(pattern: string): RegExp {\n // First escape every regex metachar EXCEPT `*` (which we'll process\n // in a second pass). A negated character class is the cleanest way\n // to enumerate \"everything that needs escaping in a path glob\".\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n // `**` first so the `*` pass below doesn't double-process it. Use a\n // sentinel that can't appear in user input post-escape.\n const withDouble = escaped.replace(/\\*\\*/g, '\u0000DOUBLESTAR\u0000');\n const withSingle = withDouble.replace(/\\*/g, '[^/]*');\n // Null-byte sentinel is intentional — guarantees the marker can't\n // collide with anything in user-supplied glob patterns post-escape.\n // eslint-disable-next-line no-control-regex\n const final = withSingle.replace(/\u0000DOUBLESTAR\u0000/g, '.*');\n return new RegExp(`^${final}$`);\n}\n","/**\n * `LoggerPort` — structured logging port for the kernel.\n *\n * The kernel must NOT write to stdout/stderr directly. Anything that\n * would historically have been a `console.log` / `console.error` goes\n * through this port; the adapter (CLI, server, test harness) decides\n * format, level filter, and destination.\n *\n * Levels follow the conventional ordering, lowest = most verbose:\n *\n * trace < debug < info < warn < error < silent\n *\n * `silent` is a sentinel for filtering only — it never appears as a\n * `LogRecord.level`. Setting an adapter to `silent` disables every\n * method.\n */\n\nexport type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';\n\nexport type LogMethodLevel = Exclude<LogLevel, 'silent'>;\n\nexport const LOG_LEVELS: readonly LogLevel[] = [\n 'trace',\n 'debug',\n 'info',\n 'warn',\n 'error',\n 'silent',\n] as const;\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n trace: 0,\n debug: 1,\n info: 2,\n warn: 3,\n error: 4,\n silent: 5,\n};\n\nexport function logLevelRank(level: LogLevel): number {\n return LEVEL_RANK[level];\n}\n\nexport function isLogLevel(value: unknown): value is LogLevel {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LEVEL_RANK, value);\n}\n\n/**\n * Parse a string into a `LogLevel`. Returns `null` for invalid input\n * (incl. `undefined` / `null` / empty). Case-insensitive; trims\n * whitespace.\n */\nexport function parseLogLevel(value: string | undefined | null): LogLevel | null {\n if (value === undefined || value === null) return null;\n const normalized = value.trim().toLowerCase();\n if (normalized === '') return null;\n return isLogLevel(normalized) ? normalized : null;\n}\n\nexport interface LogRecord {\n level: LogMethodLevel;\n /** ISO 8601 timestamp produced at the moment the log call was made. */\n timestamp: string;\n message: string;\n /** Optional structured context. Caller-owned; serialization is up to the formatter. */\n context?: Record<string, unknown>;\n}\n\nexport interface LoggerPort {\n trace(message: string, context?: Record<string, unknown>): void;\n debug(message: string, context?: Record<string, unknown>): void;\n info(message: string, context?: Record<string, unknown>): void;\n warn(message: string, context?: Record<string, unknown>): void;\n error(message: string, context?: Record<string, unknown>): void;\n}\n","/**\n * Kernel entry point. `createKernel()` returns a shell with an empty registry\n * and no bound ports. Driving adapters (CLI, Server, Skill) are expected to\n * wire adapters before invoking use cases.\n */\n\nimport { Registry } from './registry.js';\n\nexport interface Kernel {\n registry: Registry;\n}\n\nexport function createKernel(): Kernel {\n return { registry: new Registry() };\n}\n\n// Pre-1.0 export surface — every name is enumerated explicitly so a\n// rename / addition in any of the underlying modules requires an\n// explicit edit here. The previous `export type *` wildcards from\n// `./types.js` and `./ports/index.js` re-published every internal type\n// implicitly; pre-1.0 that's quiet drift, post-1.0 it would silently\n// turn refactors into major bumps. Group order: registry, domain\n// types, orchestrator, watcher / delta / query, ports, extension\n// kinds.\n\nexport { Registry, EXTENSION_KINDS, DuplicateExtensionError, qualifiedExtensionId } from './registry.js';\nexport type { Extension, ExtensionKind } from './registry.js';\n\n// --- domain types (./types.ts) -----------------------------------------\nexport type {\n // unions\n NodeKind,\n LinkKind,\n Confidence,\n Severity,\n Stability,\n TExecutionMode,\n ExecutionKind,\n ExecutionStatus,\n ExecutionFailureReason,\n ExecutionRunner,\n // value objects\n TripleSplit,\n LinkTrigger,\n LinkLocation,\n // graph\n Node,\n Link,\n IssueFix,\n Issue,\n ScanStats,\n ScanScannedBy,\n ScanResult,\n // history surface\n ExecutionRecord,\n HistoryStatsTotals,\n HistoryStatsTokensPerAction,\n HistoryStatsExecutionsPerPeriod,\n HistoryStatsTopNode,\n HistoryStatsPerActionRate,\n HistoryStatsErrorRates,\n HistoryStats,\n} from './types.js';\n\n// --- orchestrator (./orchestrator.ts) ---------------------------------\nexport {\n runScan,\n runScanWithRenames,\n detectRenamesAndOrphans,\n mergeNodeWithEnrichments,\n runExtractorsForNode,\n} from './orchestrator.js';\nexport type {\n RunScanOptions,\n RenameOp,\n IExtractorRunRecord,\n IEnrichmentRecord,\n IPersistedEnrichment,\n} from './orchestrator.js';\n\n// --- adapters (./adapters/...) -----------------------------------------\nexport { InMemoryProgressEmitter } from './adapters/in-memory-progress.js';\nexport {\n KV_SCHEMA_KEY,\n makeDedicatedStoreWrapper,\n makeKvStoreWrapper,\n makePluginStore,\n} from './adapters/plugin-store.js';\nexport type {\n IDedicatedStorePersist,\n IDedicatedStoreWrapper,\n IKvStorePersist,\n IKvStoreWrapper,\n IPluginStore,\n} from './adapters/plugin-store.js';\n\n// --- scan utilities (./scan/...) ---------------------------------------\nexport { createChokidarWatcher } from './scan/watcher.js';\nexport type {\n IFsWatcher,\n IWatchBatch,\n IWatchEvent,\n ICreateFsWatcherOptions,\n TWatchEventKind,\n} from './scan/watcher.js';\nexport { computeScanDelta, isEmptyDelta } from './scan/delta.js';\nexport type { IScanDelta, INodeChange, TNodeChangeReason } from './scan/delta.js';\nexport { parseExportQuery, applyExportQuery, ExportQueryError } from './scan/query.js';\nexport type { IExportQuery, IExportSubset } from './scan/query.js';\n\n// --- ports (./ports/...) -----------------------------------------------\nexport type { ITransactionalStorage, StoragePort } from './ports/storage.js';\nexport type {\n IIssueRow,\n INodeBundle,\n INodeCounts,\n INodeFilter,\n IPersistOptions,\n} from './types/storage.js';\nexport type { FilesystemPort, IWalkOptions, NodeStat } from './ports/filesystem.js';\nexport type {\n PluginLoaderPort,\n IDiscoveredPlugin,\n ILoadedExtension,\n IPluginManifest,\n IPluginStorageSchema,\n TGranularity,\n TPluginLoadStatus,\n TPluginStorage,\n} from './ports/plugin-loader.js';\nexport type { IRunOptions, IRunResult, RunnerPort } from './ports/runner.js';\nexport type {\n ProgressEmitterPort,\n ProgressEvent,\n ProgressListener,\n} from './ports/progress-emitter.js';\nexport type {\n LoggerPort,\n LogLevel,\n LogMethodLevel,\n LogRecord,\n} from './ports/logger.js';\nexport {\n LOG_LEVELS,\n isLogLevel,\n logLevelRank,\n parseLogLevel,\n} from './ports/logger.js';\nexport { SilentLogger } from './adapters/silent-logger.js';\nexport { log, configureLogger, resetLogger, getActiveLogger } from './util/logger.js';\n\n// --- extension kinds (./extensions/...) --------------------------------\nexport type {\n IProvider,\n IRawNode,\n IExtractor,\n IExtractorContext,\n IExtractorCallbacks,\n IRule,\n IRuleContext,\n IAction,\n IActionPrecondition,\n IFormatter,\n IFormatterContext,\n IHook,\n IHookContext,\n THookTrigger,\n THookFilter,\n IExtensionBase,\n} from './extensions/index.js';\nexport { HOOK_TRIGGERS } from './extensions/index.js';\n"],"mappings":"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","names":["existsSync","require","readFileSync","resolve","createRequire","Ajv2020","Ajv2020","resolve","readFileSync","require","createRequire","existsSync","byPath","resolve","relative"]}
|