@skill-map/cli 0.20.1 → 0.21.0
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/dist/cli/tutorial/sm-tutorial.md +93 -14
- package/dist/cli.js +1332 -339
- package/dist/cli.js.map +1 -1
- package/dist/index.js +300 -238
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +91 -11
- package/dist/kernel/index.js +300 -238
- package/dist/kernel/index.js.map +1 -1
- package/dist/migrations/001_initial.sql +13 -0
- package/dist/ui/chunk-25AWRVIC.js +965 -0
- package/dist/ui/chunk-6FTVUS57.js +123 -0
- package/dist/ui/{chunk-4NLC7QD2.js → chunk-GXRWH2VL.js} +1 -1
- package/dist/ui/chunk-MF2M6GYF.js +1 -0
- package/dist/ui/{chunk-EZZF5RL5.js → chunk-MPMBTIUR.js} +2 -2
- package/dist/ui/chunk-N366HMME.js +1 -0
- package/dist/ui/{chunk-6GUHSAP5.js → chunk-OPPQMCMQ.js} +1 -1
- package/dist/ui/chunk-V3SZQETX.js +61 -0
- package/dist/ui/{chunk-E4ALROJS.js → chunk-VVOEPDQD.js} +1 -1
- package/dist/ui/{chunk-6BZZQV42.js → chunk-W2EFGI3J.js} +1 -1
- package/dist/ui/chunk-W62WVNU4.js +251 -0
- package/dist/ui/index.html +2 -10
- package/dist/ui/main-NIYE2VFS.js +2 -0
- package/dist/ui/media/fa-brands-400-AHOAZHCU.woff2 +0 -0
- package/dist/ui/media/fa-regular-400-VRZYIBIZ.woff2 +0 -0
- package/dist/ui/media/fa-solid-900-MDEYK55F.woff2 +0 -0
- package/dist/ui/media/fa-v4compatibility-ETEVP6IB.woff2 +0 -0
- package/dist/ui/styles-M2FETVAG.css +1 -0
- package/migrations/001_initial.sql +13 -0
- package/package.json +2 -2
- package/dist/ui/chunk-FWX4RRDF.js +0 -125
- package/dist/ui/chunk-GGMXMGRJ.js +0 -1
- package/dist/ui/chunk-K5PULFK7.js +0 -1
- package/dist/ui/chunk-OJ6W6OIB.js +0 -61
- package/dist/ui/chunk-PTCD42GB.js +0 -247
- package/dist/ui/chunk-ZSRIBCAW.js +0 -965
- package/dist/ui/main-5FJWWH5I.js +0 -1
- package/dist/ui/styles-VJ5Q6D2X.css +0 -1
package/dist/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/sidecar/parse.ts","../kernel/util/ajv-interop.ts","../kernel/sidecar/drift.ts","../kernel/sidecar/discover-orphans.ts","../kernel/sidecar/store.ts","../kernel/adapters/in-memory-progress.ts","../kernel/adapters/silent-logger.ts","../kernel/util/logger.ts","../kernel/adapters/plugin-loader.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/util/format-error.ts","../kernel/scan/walk-content.ts","../kernel/scan/ignore.ts","../built-in-plugins/parsers/frontmatter-yaml/index.ts","../built-in-plugins/parsers/plain/index.ts","../kernel/scan/parsers/index.ts","../kernel/extensions/provider.ts","../kernel/extensions/hook-dispatcher.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 / analyzer / 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/annotations`, `core/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 | 'analyzer'\n | 'action'\n | 'formatter'\n | 'hook';\n\nexport const EXTENSION_KINDS: readonly ExtensionKind[] = Object.freeze([\n 'provider',\n 'extractor',\n 'analyzer',\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 → analyzer 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';\nimport { isAbsolute, resolve as resolvePath } from 'node:path';\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 {\n computeDriftStatus,\n discoverOrphanSidecars,\n readSidecarFor,\n type IOrphanSidecar,\n type IParsedSidecar,\n} from './sidecar/index.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 loadSchemaValidators,\n type IProviderFrontmatterValidator,\n} from './adapters/schema-validators.js';\nimport type { IContributionRecord } from './adapters/sqlite/contributions.js';\nimport { ORCHESTRATOR_TEXTS } from './i18n/orchestrator.texts.js';\nimport { qualifiedExtensionId } from './registry.js';\nimport { formatErrorMessage } from './util/format-error.js';\nimport { tx } from './util/tx.js';\nimport {\n resolveProviderWalk,\n type IProvider,\n type IRawNode,\n type IExtractorContext,\n type IExtractor,\n type IHook,\n type IAnalyzer,\n type THookTrigger,\n} from './extensions/index.js';\nimport {\n makeHookDispatcher,\n makeEvent,\n type IHookDispatcher,\n} from './extensions/hook-dispatcher.js';\nimport type { IRegisteredAnnotationKey } from './types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from './types/view-catalog.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 analyzers: IAnalyzer[];\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 * Step 9.6.6 — runtime catalog of plugin-contributed annotation keys\n * (the same shape `kernel.getRegisteredAnnotationKeys()` returns).\n * Threaded into the rule pass so `core/unknown-field` can\n * legitimise registered plugin namespaces / root keys without\n * re-walking the manifests. Absent → empty catalog (every plugin\n * key is treated as unknown). Built-in catalog from\n * `annotations.schema.json` is NOT included — that is hard-coded\n * inside the rule.\n */\n annotationContributions?: readonly IRegisteredAnnotationKey[];\n /**\n * Runtime catalog of plugin-contributed view contributions (the same\n * shape `kernel.getRegisteredViewContributions()` returns). Threaded\n * into the rule pass so:\n * - `core/unknown-slot` and `core/contribution-orphan` can\n * introspect the catalog (read-only).\n * - The orchestrator's per-rule emit closure can look up each\n * declared `(contributionId → slot)` pairing for AJV\n * payload validation.\n * Absent → empty catalog. Rules that emit contributions silently\n * drop emissions when the catalog has no entry for the rule's\n * declared contributionId.\n */\n viewContributions?: readonly IRegisteredViewContribution[];\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 * Pre-computed absolute paths of orphan job MD files (files under\n * `.skill-map/jobs/` whose absolute path appears nowhere in\n * `state_jobs.filePath`). Threaded into the rule pass so the\n * built-in `core/job-orphan-file` rule can project each as a `warn`\n * issue without the kernel reaching for the storage port or doing\n * its own FS walk. The driving adapter (CLI, BFF) computes this\n * inside its already-open storage transaction via\n * `findOrphanJobFiles(jobsDir, await port.jobs.listReferencedFilePaths())`\n * — mirrors the `orphanSidecars` model where detection lives\n * outside the rule and the rule only projects. Absent / empty when\n * the caller has no jobs context (out-of-band tests, fresh DB,\n * `--no-built-ins`).\n */\n orphanJobFiles?: readonly string[];\n /**\n * Side set of absolute file paths the operator opted into for\n * link-validation purposes via `scan.referencePaths`. Threaded\n * through to `IAnalyzerContext.referenceablePaths` so the built-in\n * `core/broken-ref` rule can suppress its `warn` for path-style\n * links whose target lands in the set. Files are NOT walked by\n * the kernel — the driving adapter populates the set before\n * calling `runScan`. Absent / empty when the operator left\n * `scan.referencePaths` unconfigured.\n */\n referenceablePaths?: ReadonlySet<string>;\n /**\n * Absolute path of the scan's cwd / project root. Threaded onto\n * `IAnalyzerContext.cwd` so rules that need to resolve a relative\n * `link.target` to an absolute filesystem path can do so without\n * heuristics. Absent for callers that don't track a cwd\n * concept (out-of-band tests, embedders).\n */\n cwd?: string;\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 * - 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 reserved: Extractors are deterministic-only, so\n * every record produced by the orchestrator sets it to `false`. The\n * field is kept on the record (and the row in `node_enrichments`) so a\n * future Action-issued enrichment can populate it without reshaping\n * the persistence contract — see spec `architecture.md`\n * §Extractor · enrichment layer.\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 contributions: IContributionRecord[];\n freshlyRunTuples: ReadonlySet<string>;\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 contributions: IContributionRecord[];\n freshlyRunTuples: ReadonlySet<string>;\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: [], analyzers: [] };\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 analyzers, 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 // Analyzers 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 analyzerResult = await runAnalyzers(\n exts.analyzers,\n walked.nodes,\n walked.internalLinks,\n walked.orphanSidecars,\n walked.sidecarRoots,\n options.annotationContributions ?? [],\n options.viewContributions ?? [],\n options.orphanJobFiles ?? [],\n options.referenceablePaths,\n options.cwd,\n emitter,\n hookDispatcher,\n );\n const issues = analyzerResult.issues;\n // Analyzer-emitted view contributions ride into the same per-scan buffer\n // that extractor-emitted contributions populate; both reach\n // `scan_contributions` through `replaceAllScanContributions`. The\n // walked.contributions array is already populated by the extractor\n // path inside `walked` — we append the analyzer emissions here.\n for (const c of analyzerResult.contributions) walked.contributions.push(c);\n // Phase 3 — analyzers ALWAYS run and see every node in the merged graph\n // (no per-(analyzer, node) cache like extractors have). Fold a tuple per\n // (analyzer × node) into the freshly-run set so the persist layer's\n // per-tuple sweep can drop stale analyzer-emitted rows when an analyzer\n // stops emitting for a previously-emitting node. Without this fold\n // the bug we just fixed for extractors re-emerges for analyzers.\n for (const analyzer of exts.analyzers ?? []) {\n if (analyzer.viewContributions === undefined) continue;\n for (const node of walked.nodes) {\n walked.freshlyRunTuples.add(`${analyzer.pluginId}/${analyzer.id}/${node.path}`);\n }\n }\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 analyzers 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 // `'markdown'`), 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 contributions: walked.contributions,\n freshlyRunTuples: walked.freshlyRunTuples,\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.analyzerId !== 'frontmatter-invalid' && issue.analyzerId !== '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 * Phase 3 / View contribution system — per-(plugin × extension ×\n * node × contribution) records collected from `ctx.emitContribution`\n * during the walk. AJV-validated at emit time against the slot's\n * payload schema; off-slot emissions are dropped silently before\n * landing here. The persistence layer flushes these via\n * `replaceAllScanContributions`. Empty for scans where no extension\n * declared `viewContributions` (the common case today).\n */\n contributions: IContributionRecord[];\n /**\n * Phase 3 / View contribution system — set of `(plugin, extension,\n * node)` tuples where `extract()` actually RAN this scan (cache\n * miss). Cached-extractor tuples are EXCLUDED so their prior rows\n * survive in `scan_contributions`. Format:\n * `<pluginId>/<extensionId>/<nodePath>`. Drives the per-tuple sweep\n * in the persistence layer (`IPersistOptions.freshlyRunTuples`).\n * Rules are folded in by the caller (`runScanInternal`) since they\n * always run; this set carries only the extractor-side tuples.\n */\n freshlyRunTuples: Set<string>;\n /**\n * Spec § 9.6.2 — orphan sidecar paths (`.sm` files without a sibling\n * `.md`). Discovered after the Provider walk completes so the rule\n * pass can emit `annotation-orphan` warnings. Survives across\n * scans only as derived state — no persistence, recomputed every\n * scan from the live filesystem.\n */\n orphanSidecars: IOrphanSidecar[];\n /**\n * Spec § 9.6.6 — raw parsed sidecar root keyed by `node.path`.\n * Plumbed through to the rule pass so semantic rules\n * (`core/unknown-field`) walk plugin namespaces / root keys without\n * re-reading `.sm` files from disk. Empty when no node carries a\n * parseable sidecar.\n */\n sidecarRoots: Map<string, Record<string, unknown>>;\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 contributions: IContributionRecord[];\n}> {\n const internalLinks: Link[] = [];\n const externalLinks: Link[] = [];\n const enrichmentBuffer = new Map<string, IEnrichmentRecord>();\n const contributions: IContributionRecord[] = [];\n // Schema validators are cached at module level (`loadSchemaValidators`),\n // so the cost of this lookup is module-scoped — pulling once per\n // node-extract pass keeps the closure capture clean without paying\n // per emission.\n const validators = loadSchemaValidators();\n\n for (const extractor of opts.extractors) {\n const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);\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 // Extractors are deterministic-only; `is_probabilistic` is\n // reserved on the row for future Action-issued enrichments.\n isProbabilistic: false,\n });\n }\n };\n // Phase 3 — view contributions emit-time wiring. Three drop reasons,\n // all silent + `extension.error` event (mirror of `emitLink`):\n // 1. Extractor never declared `viewContributions[<id>]` —\n // reason: `unknown-contribution-id`.\n // 2. Declared `slot` is not in the closed catalog (also\n // caught at AJV manifest load, but defence-in-depth) —\n // reason: `unknown-slot`.\n // 3. Payload fails the slot's payload schema —\n // reason: AJV error string.\n // Accepted emissions append a record to the buffer; persistence\n // happens later via `replaceAllScanContributions`.\n const declaredContributions = readDeclaredContributions(extractor);\n const emitContribution = (contributionId: string, payload: unknown): void => {\n const declared = declaredContributions.get(contributionId);\n if (!declared) {\n emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {\n phase: 'emitContribution',\n contributionId,\n reason: 'unknown-contribution-id',\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {\n extractorId: qualifiedId,\n contributionId,\n nodePath: opts.node.path,\n }),\n });\n return;\n }\n const result = validators.validateContributionPayload(declared.slot, payload);\n if (!result.ok) {\n emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {\n phase: 'emitContribution',\n contributionId,\n slot: declared.slot,\n reason: result.errors,\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {\n extractorId: qualifiedId,\n contributionId,\n nodePath: opts.node.path,\n slot: declared.slot,\n errors: result.errors,\n }),\n });\n return;\n }\n contributions.push({\n pluginId: extractor.pluginId,\n extensionId: extractor.id,\n nodePath: opts.node.path,\n contributionId,\n slot: declared.slot,\n payload,\n emittedAt: Date.now(),\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 emitContribution,\n store,\n );\n await extractor.extract(ctx);\n }\n\n return {\n internalLinks,\n externalLinks,\n enrichments: Array.from(enrichmentBuffer.values()),\n contributions,\n };\n}\n\n/**\n * Pull the manifest's `viewContributions` map into a `Map<contributionId,\n * { slot }>`. Called once per extractor per node — the result lives\n * for the duration of `runExtractorsForNode` and disappears with the\n * function frame, so no caching is required (the manifest is already\n * the canonical source).\n */\nfunction readDeclaredContributions(\n extension: { viewContributions?: unknown },\n): Map<string, { slot: string }> {\n const out = new Map<string, { slot: string }>();\n const raw = extension.viewContributions;\n if (typeof raw !== 'object' || raw === null) return out;\n for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) continue;\n const slot = (value as { slot?: unknown }).slot;\n if (typeof slot !== 'string') continue;\n out.set(id, { slot });\n }\n return out;\n}\n\n/**\n * Emit an `extension.error` event from the orchestrator's emit-time\n * drop paths (off-contract link, off-slot / unknown contribution\n * payload). Uses the same `makeEvent` shape as the rest of the file\n * so listeners (BFF SSE, CLI logger) see a uniform timestamp +\n * type + data envelope.\n */\nfunction emitExtensionError(\n emitter: ProgressEmitterPort,\n qualifiedId: string,\n nodePath: string,\n data: Record<string, unknown>,\n): void {\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'contribution-rejected',\n extensionId: qualifiedId,\n nodePath,\n ...data,\n }),\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 // Phase 3 / View contributions — flat buffer (no per-node dedup\n // because the qualified id `<pluginId>/<extensionId>/<contributionId>`\n // is structurally unique within a single scan). Per-(plugin × node ×\n // contribution) re-emissions inside one scan are caller-error and\n // simply produce two records — the persistence layer's PRIMARY KEY\n // takes the last-write-wins decision when the buffer flushes.\n const contributionsBuffer: IContributionRecord[] = [];\n // Phase 3 / View contributions — accumulator of (plugin, extension,\n // node) tuples where extract() actually RAN this scan (cache miss).\n // Cached extractors don't push here — their prior `scan_contributions`\n // rows must be preserved. Drives the per-tuple sweep documented in\n // `spec/architecture.md` §View contribution system → Persistence.\n // Format: `<pluginId>/<extensionId>/<nodePath>`.\n const freshlyRunTuples = new Set<string>();\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 // Spec § 9.6.6 — raw parsed sidecar root keyed by `node.path`.\n // Threaded through to the rule pass so semantic rules\n // (`core/unknown-field`) can reason about plugin namespaces and root\n // keys without re-reading the `.sm` file from disk. Populated by\n // `resolveAndApplySidecar` below.\n const sidecarRoots = new Map<string, Record<string, unknown>>();\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 (`'core/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 // Path-dedup across the multi-provider walk. Spec § Provider dispatch\n // (architecture.md): every Provider walks the full root, but each\n // file is offered to at most ONE Provider's `classify`. The first\n // Provider in iteration order whose `classify` returns non-null\n // claims the file; subsequent Providers see the path as\n // already-claimed and skip. Without this, the universal markdown\n // fallback (`core/markdown`, registered LAST) would re-claim every\n // file vendor Providers already classified, double-emitting nodes.\n // The Set is per-scan (rebuilt each call) so successive `runScan`\n // invocations start clean.\n const claimedPaths = new Set<string>();\n\n for (const provider of providers) {\n for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {\n filesWalked += 1;\n if (claimedPaths.has(raw.path)) continue;\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 if (kind === null) {\n // Provider disclaimed the file — another Provider may claim\n // it on its own walk pass, or the file is outside every\n // active Provider's territory. Wired to the `filesSkipped`\n // stat below would require threading a counter through\n // `walked`; for now the field stays at the legacy `0` and\n // the disclaim path is observed via test assertions.\n continue;\n }\n claimedPaths.add(raw.path);\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 // Spec § 9.6.2 — sidecars are read on EVERY scan (not cached)\n // because `.sm` lives outside the body/frontmatter hash domain:\n // a user can edit the sidecar without touching the `.md` and\n // the overlay should still reflect live status. Re-resolve the\n // sidecar overlay; the persistence layer projects `stability`\n // and `version` from `node.sidecar.annotations.*` directly when\n // it writes the indexed columns.\n const reusedSidecarIssues = resolveAndApplySidecar(\n reused.node, raw.path, roots, bodyHash, frontmatterHash, sidecarRoots,\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 issue of reusedSidecarIssues) 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 // Spec § 9.6.2 — sidecar overlay applies to BOTH freshly-built\n // and partial-cache nodes. Done after the node is in `nodes[]`\n // so a downstream consumer iterating `nodes` sees the overlay\n // applied (mutation is in-place on the same object reference).\n const sidecarIssues = resolveAndApplySidecar(\n node, raw.path, roots, bodyHash, frontmatterHash, sidecarRoots,\n );\n for (const issue of sidecarIssues) frontmatterIssues.push(issue);\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 // Phase 3 — record (plugin, extension, node) for every extractor\n // that actually runs against this node this scan. The persist\n // layer uses this to drop stale `scan_contributions` rows for\n // extractors that previously emitted but no longer do (e.g.\n // body change removes the trigger). Cached extractors are NOT\n // recorded — their rows must survive untouched.\n for (const ex of extractorsToRun) {\n freshlyRunTuples.add(`${ex.pluginId}/${ex.id}/${node.path}`);\n }\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 // Phase 3 — fold per-node view contributions into the scan-wide\n // buffer. The persistence layer flushes on transaction commit\n // via `replaceAllScanContributions`.\n for (const c of extractResult.contributions) contributionsBuffer.push(c);\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 // Spec § 9.6.2 — orphan sidecar sweep. Walks the same roots looking\n // for `*.sm` whose sibling `*.md` is missing. The list flows through\n // to the rule pass; `annotation-orphan` emits one warning per entry.\n const orphanSidecars = discoverOrphanSidecars(roots);\n\n return {\n nodes,\n internalLinks,\n externalLinks,\n cachedPaths,\n frontmatterIssues,\n filesWalked,\n enrichments: [...enrichmentBuffer.values()],\n extractorRuns,\n contributions: contributionsBuffer,\n freshlyRunTuples,\n orphanSidecars,\n sidecarRoots,\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 (`'core/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 analyzer over the merged graph. Analyzers see internal\n * links only — broken-ref / trigger-collision / superseded all reason\n * about graph relations, not URLs.\n *\n * Analyzers MAY emit per-node view contributions via\n * `ctx.emitContribution(nodePath, contributionId, payload)`. The\n * orchestrator validates each emission against the slot's payload\n * schema (mirror of the Extractor emit path) and silently drops\n * invalid emissions with an `extension.error` event. Accepted\n * emissions land on the returned `contributions[]` and reach\n * `scan_contributions` via the same persistence pipeline as\n * Extractor-emitted contributions.\n */\nasync function runAnalyzers(\n analyzers: IAnalyzer[],\n nodes: Node[],\n internalLinks: Link[],\n orphanSidecars: IOrphanSidecar[],\n sidecarRoots: ReadonlyMap<string, Record<string, unknown>>,\n annotationContributions: readonly IRegisteredAnnotationKey[],\n viewContributions: readonly IRegisteredViewContribution[],\n orphanJobFiles: readonly string[],\n referenceablePaths: ReadonlySet<string> | undefined,\n cwd: string | undefined,\n emitter: ProgressEmitterPort,\n hookDispatcher: IHookDispatcher,\n): Promise<{ issues: Issue[]; contributions: IContributionRecord[] }> {\n const issues: Issue[] = [];\n const contributions: IContributionRecord[] = [];\n const validators = loadSchemaValidators();\n // Project the kernel-internal `IOrphanSidecar` shape to the analyzer-\n // facing `IAnalyzerOrphanSidecar`: analyzers don't need the absolute\n // `.sm` path, just the relative path + the expected `.md`.\n const analyzerOrphans = orphanSidecars.map((o) => ({\n relativePath: o.relativePath,\n expectedMdPath: o.expectedMdPath,\n }));\n for (const analyzer of analyzers) {\n const qualifiedId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);\n const declaredContributions = readDeclaredContributions(analyzer);\n const emitContribution = (\n nodePath: string,\n contributionId: string,\n payload: unknown,\n ): void => {\n const declared = declaredContributions.get(contributionId);\n if (!declared) {\n emitExtensionError(emitter, qualifiedId, nodePath, {\n phase: 'emitContribution',\n contributionId,\n reason: 'unknown-contribution-id',\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {\n extractorId: qualifiedId,\n contributionId,\n nodePath,\n }),\n });\n return;\n }\n const result = validators.validateContributionPayload(declared.slot, payload);\n if (!result.ok) {\n emitExtensionError(emitter, qualifiedId, nodePath, {\n phase: 'emitContribution',\n contributionId,\n slot: declared.slot,\n reason: result.errors,\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {\n extractorId: qualifiedId,\n contributionId,\n nodePath,\n slot: declared.slot,\n errors: result.errors,\n }),\n });\n return;\n }\n contributions.push({\n pluginId: analyzer.pluginId,\n extensionId: analyzer.id,\n nodePath,\n contributionId,\n slot: declared.slot,\n payload,\n emittedAt: Date.now(),\n });\n };\n const emitted = await analyzer.evaluate({\n nodes,\n links: internalLinks,\n orphanSidecars: analyzerOrphans,\n sidecarRoots,\n annotationContributions,\n viewContributions,\n orphanJobFiles,\n ...(referenceablePaths ? { referenceablePaths } : {}),\n ...(cwd ? { cwd } : {}),\n emitContribution,\n });\n for (const issue of emitted) {\n const validated = validateIssue(analyzer, issue, emitter);\n if (validated) issues.push(validated);\n }\n // Spec § A.11 — `analyzer.completed`. Aggregated per Analyzer, after every\n // issue has been validated. Fan-out scope: one event per Analyzer per\n // scan. The payload carries the qualified analyzer id so a hook with\n // `filter: { analyzerId: '...' }` can scope to a single analyzer.\n const evt = makeEvent('analyzer.completed', { analyzerId: qualifiedId });\n emitter.emit(evt);\n await hookDispatcher.dispatch('analyzer.completed', evt);\n }\n return { issues, contributions };\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 analyzerId: '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 analyzerId: '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 analyzerId: '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 analyzer 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\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 // The Node surface no longer carries `title` / `description` /\n // `stability` / `version` denormalisations. Consumers that need\n // them read from the canonical sources directly:\n // - title / description → `node.frontmatter.{name,description}`\n // - stability / version → `node.sidecar.annotations.{stability,version}`\n // The persistence layer keeps these as denormalised SQL columns on\n // `scan_nodes` (used for `--sort-by`, faceted listings) and projects\n // them at write time from the same canonical sources.\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 };\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\n/**\n * Resolve and apply a co-located `.sm` sidecar onto a freshly built or\n * reused `Node` (Step 9.6.2). Tries each scan root in order to find\n * the absolute `.md` path the sidecar should accompany; reads the\n * `.sm`, validates it against the spec schemas, computes drift, and\n * mutates `node.{stability, version, author, sidecar}` accordingly.\n *\n * Schema-invalid or YAML-malformed sidecars yield an `invalid-sidecar`\n * issue but do not crash the scan: the node still scans with `present`\n * = true and `status` = null (the row remembers a sidecar exists, just\n * can't be parsed). On parse success, `annotations.{stability, version,\n * author}` overlay onto the node.\n */\nfunction resolveAndApplySidecar(\n node: Node,\n relativePath: string,\n roots: readonly string[],\n liveBodyHash: string,\n liveFrontmatterHash: string,\n sidecarRoots: Map<string, Record<string, unknown>>,\n): Issue[] {\n const issues: Issue[] = [];\n const mdAbs = resolveAbsoluteMdPath(relativePath, roots);\n if (mdAbs === null) {\n // Node yielded by Provider but its file isn't reachable through any\n // of the orchestrator's roots — sidecar lookup is impossible. The\n // node still scans with no overlay (sidecar.present = false).\n node.sidecar = { present: false };\n return issues;\n }\n\n const result = readSidecarFor(mdAbs);\n if (!result.present) {\n node.sidecar = { present: false };\n return issues;\n }\n\n // A sidecar file exists. Even if parsing failed we mark `present`\n // true so `scan_nodes.sidecar_present = 1`; status stays null.\n if (result.parsed === null) {\n node.sidecar = { present: true, status: null, annotations: null, root: null };\n for (const parseIssue of result.issues) {\n issues.push({\n analyzerId: 'invalid-sidecar',\n severity: 'warn',\n nodeIds: [node.path],\n message: parseIssue.message,\n data: { sidecarPath: relativePathFromRoots(mdAbs, roots) },\n });\n }\n return issues;\n }\n\n const status = computeDriftStatus({\n storedBodyHash: result.parsed.identityBodyHash,\n storedFrontmatterHash: result.parsed.identityFrontmatterHash,\n liveBodyHash,\n liveFrontmatterHash,\n });\n // R15 closure (2026-05-07) — surface the full parsed root on the\n // overlay so BFF consumers (UI inspector audit / plugin-contributions\n // / debug panels) can read `for.*`, `audit.*`, `settings.*`, and\n // plugin-namespaced sub-keys without re-reading the file. The\n // `annotations` field above stays — it duplicates `root.annotations`\n // by design so existing consumers keep working unchanged.\n node.sidecar = {\n present: true,\n status,\n annotations: result.parsed.annotations,\n root: result.parsed.raw,\n };\n // Step 9.6.6 — record the raw parsed sidecar root so the rule pass\n // (specifically `core/unknown-field`) can reason about plugin\n // namespaces and root keys without re-reading the file from disk.\n sidecarRoots.set(node.path, result.parsed.raw);\n return issues;\n}\n\n// `applyAnnotationsOverlay` was previously responsible for projecting\n// `annotations.{stability,version}` onto `node.{stability,version}`.\n// Those fields no longer exist on the Node surface — consumers read\n// from `node.sidecar.annotations.*` directly, and the persistence\n// layer projects to indexed SQL columns at write time. The function\n// is gone; the call site at `resolveAndApplySidecar` only attaches\n// the overlay (`node.sidecar = ...`).\n\nfunction resolveAbsoluteMdPath(\n relativePath: string,\n roots: readonly string[],\n): string | null {\n if (isAbsolute(relativePath)) {\n return existsSync(relativePath) ? relativePath : null;\n }\n for (const root of roots) {\n const candidate = resolvePath(root, relativePath);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\nfunction relativePathFromRoots(\n absolutePath: string,\n roots: readonly string[],\n): string {\n for (const root of roots) {\n const abs = resolvePath(root);\n if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\\\`)) {\n return absolutePath.slice(abs.length + 1).split(/[\\\\/]/).join('/');\n }\n }\n return absolutePath;\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 emitContribution: (contributionId: string, payload: unknown) => 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 emitContribution,\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 analyzerId: '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 analyzerId: '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(analyzer: IAnalyzer, issue: Issue, emitter: ProgressEmitterPort): Issue | null {\n const severity: Severity | undefined = issue.severity;\n if (severity !== 'error' && severity !== 'warn' && severity !== 'info') {\n // Analyzer 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 = `${analyzer.pluginId}/${analyzer.id}`;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'issue-invalid-severity',\n extensionId: qualifiedId,\n severity,\n issue: { analyzerId: issue.analyzerId || analyzer.id, message: issue.message, nodeIds: issue.nodeIds },\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {\n analyzerId: qualifiedId,\n severity: JSON.stringify(severity),\n }),\n }),\n );\n return null;\n }\n return { ...issue, analyzerId: issue.analyzerId || analyzer.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`. With Extractors deterministic-only no row is\n * stale-flagged in this revision; the filter is preserved for the\n * future Action-issued enrichment revision (queued LLM jobs whose\n * output must survive body changes), where stale visibility\n * belongs to the UI layer 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.20.1\",\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 \"reference\": \"node scripts/build-reference.js\",\n \"reference:check\": \"node scripts/build-reference.js --check\",\n \"validate\": \"npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run reference:check\",\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' 'server/**/*.test.ts'\",\n \"test:ci\": \"tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.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' 'server/**/*.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' 'server/**/*.test.ts'\",\n \"clean\": \"rm -rf dist coverage\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"2.0.1\",\n \"@skill-map/spec\": \"0.20.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 * Sidecar reader (Step 9.6.2).\n *\n * Parses a co-located `.sm` YAML sidecar next to a `.md` node and\n * validates it against `spec/schemas/sidecar.schema.json` and\n * `spec/schemas/annotations.schema.json`. Returns a typed\n * `IParsedSidecar` (or `null` if no sidecar accompanies the node) plus\n * a list of validation issues for the caller to fold into the scan\n * result.\n *\n * Design notes:\n *\n * - YAML parsing via `js-yaml` (already on the dependency tree, used\n * by the orchestrator's canonical-frontmatter helper).\n * - AJV validators are compiled once and cached in module scope (the\n * sidecar shape is static). The cache mirrors\n * `loadSchemaValidators` for the existing kernel schemas.\n * - Malformed YAML or schema-invalid sidecars do NOT crash the scan\n * — the caller emits an `invalid-sidecar` issue and proceeds with\n * no overlay (the node still scans with the new columns set to\n * `sidecarPresent = 1` / `sidecarStatus = null`).\n */\n\nimport { existsSync, 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';\nimport yaml from 'js-yaml';\n\nimport { applyAjvFormats } from '../util/ajv-interop.js';\n\nexport interface IParsedSidecar {\n /** Path to the `.sm` file on disk (absolute). */\n filePath: string;\n /** `identity.bodyHash` — sha256 of the body at the last bump. */\n identityBodyHash: string;\n /** `identity.frontmatterHash` — sha256 of the canonical frontmatter at the last bump. */\n identityFrontmatterHash: string;\n /** `identity.path` — relative path to the `.md` node. */\n identityPath: string;\n /** Parsed `annotations:` block. `null` if absent or empty. */\n annotations: Record<string, unknown> | null;\n /** Full parsed root object (for plugin namespace access). */\n raw: Record<string, unknown>;\n}\n\nexport interface ISidecarParseIssue {\n /** Human-readable reason. The orchestrator wraps this in the\n * `invalid-sidecar` rule message before it surfaces to the user. */\n message: string;\n}\n\nexport interface ISidecarReadResult {\n parsed: IParsedSidecar | null;\n /**\n * `true` when a `.sm` file existed at the resolved path (regardless of\n * parse success). Drives `scan_nodes.sidecar_present` so the row keeps\n * tracking the file's existence even when its contents are unusable.\n */\n present: boolean;\n issues: ISidecarParseIssue[];\n}\n\n/**\n * Resolve `<mdAbsolutePath>.replace(.md, .sm)` and read + validate\n * the sidecar at that location. The `.sm` file is optional — when\n * absent the result is `{ parsed: null, present: false, issues: [] }`.\n */\n// Linear pipeline with one branch per failure mode (file-missing,\n// read-error, YAML-parse-error, root-not-mapping, schema-invalid,\n// happy-path). Each branch returns directly; cyclomatic count\n// counts them all but there's no actual nested logic.\n// eslint-disable-next-line complexity\nexport function readSidecarFor(mdAbsolutePath: string): ISidecarReadResult {\n const sidecarPath = sidecarPathFor(mdAbsolutePath);\n if (!existsSync(sidecarPath)) {\n return { parsed: null, present: false, issues: [] };\n }\n\n let raw: string;\n try {\n raw = readFileSync(sidecarPath, 'utf8');\n } catch (err) {\n return {\n parsed: null,\n present: true,\n issues: [{ message: `cannot read ${sidecarPath}: ${(err as Error).message}` }],\n };\n }\n\n let parsedYaml: unknown;\n try {\n parsedYaml = yaml.load(raw);\n } catch (err) {\n return {\n parsed: null,\n present: true,\n issues: [{ message: `malformed YAML in ${sidecarPath}: ${(err as Error).message}` }],\n };\n }\n\n if (!isPlainObject(parsedYaml)) {\n return {\n parsed: null,\n present: true,\n issues: [{ message: `sidecar root must be a YAML mapping at ${sidecarPath}` }],\n };\n }\n\n const sidecarValidator = getSidecarValidator();\n if (!sidecarValidator(parsedYaml)) {\n const errors = (sidecarValidator.errors ?? [])\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n return {\n parsed: null,\n present: true,\n issues: [{ message: `sidecar schema validation failed at ${sidecarPath}: ${errors}` }],\n };\n }\n\n const root = parsedYaml as Record<string, unknown>;\n const identityBlock = root['identity'] as Record<string, unknown>;\n const annotationsRaw = root['annotations'];\n const annotations = isPlainObject(annotationsRaw)\n ? Object.keys(annotationsRaw).length === 0\n ? null\n : (annotationsRaw as Record<string, unknown>)\n : null;\n\n return {\n parsed: {\n filePath: sidecarPath,\n identityBodyHash: String(identityBlock['bodyHash']),\n identityFrontmatterHash: String(identityBlock['frontmatterHash']),\n identityPath: String(identityBlock['path']),\n annotations,\n raw: root,\n },\n present: true,\n issues: [],\n };\n}\n\n/**\n * Compute the sidecar path for a given `.md` file. Co-located: same\n * directory, same basename, extension swapped to `.sm`. Files that do\n * not end in `.md` (Provider future-proofing) get the `.sm` suffix\n * appended.\n */\nexport function sidecarPathFor(mdAbsolutePath: string): string {\n if (mdAbsolutePath.endsWith('.md')) {\n return `${mdAbsolutePath.slice(0, -'.md'.length)}.sm`;\n }\n return `${mdAbsolutePath}.sm`;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nlet cachedSidecarValidator: ValidateFunction | null = null;\n\nfunction getSidecarValidator(): ValidateFunction {\n if (cachedSidecarValidator) return cachedSidecarValidator;\n const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n\n const specRoot = resolveSpecRoot();\n const annotationsSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/annotations.schema.json'), 'utf8'),\n );\n const sidecarSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/sidecar.schema.json'), 'utf8'),\n );\n ajv.addSchema(annotationsSchema);\n cachedSidecarValidator = ajv.compile(sidecarSchema);\n return cachedSidecarValidator;\n}\n\n/**\n * Test-only escape hatch — drop the cached validator so a test can\n * rebuild it after monkey-patching the spec package.\n */\nexport function _resetSidecarValidatorCacheForTests(): void {\n cachedSidecarValidator = null;\n}\n\nfunction resolveSpecRoot(): string {\n const require = createRequire(import.meta.url);\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 — sidecar reader cannot load schemas.',\n );\n }\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 * Drift detection for sidecar `.sm` files (Step 9.6.2).\n *\n * Compares the hashes captured the last time a sidecar was bumped\n * (`for.bodyHash` / `for.frontmatterHash` from the parsed sidecar)\n * against the live node hashes computed during the current scan.\n *\n * Returns one of four states:\n *\n * - `'fresh'` — both hashes match; sidecar is up to date.\n * - `'stale-body'` — body changed since last bump.\n * - `'stale-frontmatter'` — frontmatter changed since last bump.\n * - `'stale-both'` — both changed since last bump.\n *\n * Stale state is **derived**, never stored persistently — pure\n * function over hashes already on the node. The `scan_nodes.sidecar_status`\n * column caches the result for fast queries but the kernel re-derives\n * it on every scan.\n */\n\nimport type { SidecarStatus } from '../types.js';\n\nexport function computeDriftStatus(args: {\n storedBodyHash: string;\n storedFrontmatterHash: string;\n liveBodyHash: string;\n liveFrontmatterHash: string;\n}): SidecarStatus {\n const bodyDrift = args.storedBodyHash !== args.liveBodyHash;\n const fmDrift = args.storedFrontmatterHash !== args.liveFrontmatterHash;\n if (bodyDrift && fmDrift) return 'stale-both';\n if (bodyDrift) return 'stale-body';\n if (fmDrift) return 'stale-frontmatter';\n return 'fresh';\n}\n","/**\n * Orphan sidecar discovery (Step 9.6.2).\n *\n * Walks the scan roots, finds every `*.sm` file, and returns the\n * paths whose accompanying `*.md` does NOT exist on disk. The\n * `annotation-orphan` built-in rule consumes the result and emits one\n * warning per stranded sidecar.\n *\n * Implementation is intentionally a fresh walk (rather than piggy-\n * backing on the Provider walk) — the Provider only yields `.md`\n * files; orphans are exactly the `.sm` files that have no corresponding\n * `.md` to anchor them, so we need an `.sm`-driven sweep.\n */\n\nimport { existsSync, readdirSync, statSync } from 'node:fs';\nimport { join, relative, sep } from 'node:path';\n\nexport interface IOrphanSidecar {\n /** Absolute path to the orphan `.sm` file. */\n sidecarPath: string;\n /** Relative path (POSIX-separated) from the root that contained it. */\n relativePath: string;\n /** Absolute path of the `.md` file the sidecar was expected to accompany. */\n expectedMdPath: string;\n}\n\n/**\n * Find orphaned `.sm` files across the supplied roots. A `.sm` is an\n * orphan when its sibling `<basename>.md` does not exist.\n *\n * Walks the filesystem directly. Symbolic links are skipped (mirrors\n * the Claude Provider's walk policy — audit M7). Errors reading a\n * directory are swallowed silently; the walk degrades to \"no orphans\n * found in that subtree\".\n */\nexport function discoverOrphanSidecars(\n roots: readonly string[],\n shouldSkip?: (relativePath: string) => boolean,\n): IOrphanSidecar[] {\n const out: IOrphanSidecar[] = [];\n for (const root of roots) {\n walk(root, root, shouldSkip ?? (() => false), out);\n }\n return out;\n}\n\n// Recursive directory walker with five guards (try/catch, skip filter,\n// symlink check, isDirectory recursion, isFile + extension check). The\n// shape mirrors the Claude Provider's walker — same tradeoff applies.\n// eslint-disable-next-line complexity\nfunction walk(\n root: string,\n current: string,\n shouldSkip: (relativePath: string) => boolean,\n out: IOrphanSidecar[],\n): void {\n let entries;\n try {\n entries = readdirSync(current, { withFileTypes: true, encoding: 'utf8' });\n } catch {\n return;\n }\n for (const entry of entries) {\n const full = join(current, entry.name);\n const rel = relative(root, full).split(sep).join('/');\n if (shouldSkip(rel)) continue;\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n walk(root, full, shouldSkip, out);\n continue;\n }\n if (!entry.isFile()) continue;\n if (!entry.name.endsWith('.sm')) continue;\n const expectedMd = `${full.slice(0, -'.sm'.length)}.md`;\n if (existsSync(expectedMd) && safeIsFile(expectedMd)) continue;\n out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });\n }\n}\n\nfunction safeIsFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n","/**\n * Sidecar write channel (Step 9.6.3, Decision #125).\n *\n * `ISidecarStore` is the kernel's port for materialising patches against\n * `<basename>.sm` files. Mirrors `StoragePort`'s shape (port + driving\n * adapter) but writes co-located YAML files in the repo rather than rows\n * in SQLite. The built-in `bump` Action returns a deep-merge patch\n * (`TActionWrite { kind: 'sidecar', path, changes }`) and the kernel\n * dispatches each entry through the active `ISidecarStore`.\n *\n * Atomicity is owned by the Store, not the Action: Actions stay pure\n * (testable / dry-runnable), and the read-modify-write critical section\n * lives inside `applyPatch()`. Two concurrent `applyPatch()` calls on the\n * same path are serialised via a path-keyed in-process mutex (chained\n * promise pattern, no external dep — mirrors `AsyncMutex` in\n * `adapters/sqlite/dialect.ts`).\n *\n * The on-disk write itself is atomic via the standard write-to-`.tmp`\n * + POSIX `rename` pattern. The `.tmp` file is a sibling of the target\n * (same directory) so the rename is guaranteed atomic on POSIX. Per\n * AGENTS.md this is the established atomic-write pattern; the AGENTS.md\n * `.tmp/` baseline applies to scratch / smoke-test directories, not to\n * sibling temp files used for atomic rename.\n *\n * Comment / key-order preservation is OUT OF SCOPE for 9.6.3 — `js-yaml`\n * loses comments and stable key order on round-trip. Flagged for the\n * Step 9.6 review queue (see ROADMAP §Step 9.6).\n */\n\nimport { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\nimport yaml from 'js-yaml';\n\nimport { applyAjvFormats } from '../util/ajv-interop.js';\n\n/**\n * Sidecar persistence port. Implementations MUST guarantee:\n *\n * 1. Two concurrent `applyPatch(samePath, ...)` calls are serialised.\n * 2. The read-modify-write cycle (read on-disk file → deep-merge patch\n * → schema-validate the merged result → write) is atomic from any\n * observer's view.\n * 3. A schema-invalid merge result throws and leaves the file\n * unchanged on disk — no partial writes.\n * 4. First-time bump (file did not exist) creates the `.sm` file.\n */\nexport interface ISidecarStore {\n /**\n * Apply a deep-merge patch to the sidecar at `sidecarAbsPath`.\n *\n * - `changes` is treated as a partial sidecar root. Object values\n * are merged recursively into the existing object at the same\n * path; array values REPLACE any existing array (no element-wise\n * merge — arrays in the annotation catalog are inherently\n * ordered or set-like and there is no safe element-merge\n * semantics).\n * - The merged result MUST validate against `sidecar.schema.json` +\n * `annotations.schema.json` via the kernel AJV stack. Validation\n * failure throws a structured `Error`; the file is unchanged.\n *\n * @param sidecarAbsPath absolute path to the `.sm` file to patch.\n * @param changes deep-merge patch; only the keys to set need be present.\n */\n applyPatch(sidecarAbsPath: string, changes: Record<string, unknown>): Promise<void>;\n}\n\n/**\n * Filesystem-backed `ISidecarStore`. Composed at the kernel boot site\n * and threaded through `IActionContext` consumers (the orchestrator's\n * Action dispatcher in 9.6.4 and beyond).\n */\nexport class FilesystemSidecarStore implements ISidecarStore {\n /**\n * Path-keyed in-process lock chain. Each path maps to the tail of a\n * promise chain; new requests await the tail and replace it with\n * their own completion promise. When the chain settles back to\n * `undefined`-equivalent the entry is GC-eligible (we don't bother\n * pruning because the keyspace is bounded by the number of `.sm`\n * files in the repo and entries are tiny).\n */\n #locks = new Map<string, Promise<void>>();\n\n async applyPatch(\n sidecarAbsPath: string,\n changes: Record<string, unknown>,\n ): Promise<void> {\n const prev = this.#locks.get(sidecarAbsPath) ?? Promise.resolve();\n let release: () => void;\n const settled = new Promise<void>((res) => {\n release = res;\n });\n // Chain: every newcomer waits for `prev` AND `settled` (this call\n // doing its work) before they enter. The chained tail is what we\n // store as the new tail, so the next caller waits for everything\n // that came before plus us.\n const tail = prev.then(() => settled);\n this.#locks.set(sidecarAbsPath, tail);\n try {\n await prev;\n this.#applyPatchSync(sidecarAbsPath, changes);\n } finally {\n release!();\n // If we are still the recorded tail, drop the entry to allow GC.\n if (this.#locks.get(sidecarAbsPath) === tail) {\n this.#locks.delete(sidecarAbsPath);\n }\n }\n }\n\n #applyPatchSync(sidecarAbsPath: string, changes: Record<string, unknown>): void {\n const current = readSidecarObject(sidecarAbsPath);\n const merged = deepMerge(current, changes);\n const validator = getSidecarValidator();\n if (!validator(merged)) {\n const errors = (validator.errors ?? [])\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n throw new Error(\n `sidecar patch produces a schema-invalid result at ${sidecarAbsPath}: ${errors}`,\n );\n }\n const yamlText = yaml.dump(merged, {\n sortKeys: true,\n lineWidth: -1,\n noRefs: true,\n noCompatMode: true,\n });\n atomicWriteFile(sidecarAbsPath, yamlText);\n }\n}\n\n/**\n * Deep-merge `patch` into `base`, returning a new object. Semantics:\n *\n * - Both values are plain objects → recurse key-by-key.\n * - `patch` is an array → REPLACES `base` at this position (no\n * element-wise merge).\n * - `patch` is `null` → DELETES the key from the result (whether or\n * not `base` had it). This is the patch's \"erase\" sentinel.\n * Persisted sidecars never contain literal nulls because the\n * schema rejects them on every typed property; the null only\n * ever lives in the in-flight patch object. Currently no caller;\n * retained as a generic primitive for future actions that need\n * per-write delete semantics.\n * - `patch` is any other primitive → REPLACES `base` at this position.\n * - Key only in `base` → carried through unchanged.\n * - Key only in `patch` (and not `null`) → set on the result.\n *\n * Mirrors `lodash.merge` for the object/array decisions but written by\n * hand to avoid pulling in the dep. Pure (no input mutation).\n */\nexport function deepMerge(\n base: Record<string, unknown>,\n patch: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = { ...base };\n for (const key of Object.keys(patch)) {\n const a = out[key];\n const b = patch[key];\n if (b === null) {\n delete out[key];\n continue;\n }\n if (isPlainObject(b)) {\n // Always recurse when the patch carries an object so the null-as-\n // delete sentinel applies at every depth. When the base lacks the\n // key (or holds a non-object), recurse against an empty object so\n // the patch's nested nulls do not leak into the result.\n const baseSub = isPlainObject(a) ? a : {};\n out[key] = deepMerge(baseSub, b);\n } else {\n out[key] = b;\n }\n }\n return out;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction readSidecarObject(sidecarAbsPath: string): Record<string, unknown> {\n if (!existsSync(sidecarAbsPath)) return {};\n const raw = readFileSync(sidecarAbsPath, 'utf8');\n const parsed = yaml.load(raw);\n if (parsed === null || parsed === undefined) return {};\n if (!isPlainObject(parsed)) {\n throw new Error(\n `sidecar at ${sidecarAbsPath} is not a YAML mapping; refusing to patch`,\n );\n }\n return parsed;\n}\n\nfunction atomicWriteFile(targetPath: string, content: string): void {\n const tmpPath = `${targetPath}.tmp`;\n try {\n writeFileSync(tmpPath, content, { encoding: 'utf8' });\n renameSync(tmpPath, targetPath);\n } catch (err) {\n // Best-effort cleanup; ignore secondary errors.\n try {\n if (existsSync(tmpPath)) unlinkSync(tmpPath);\n } catch {\n /* noop */\n }\n throw err;\n }\n}\n\nlet cachedValidator: ValidateFunction | null = null;\n\nfunction getSidecarValidator(): ValidateFunction {\n if (cachedValidator) return cachedValidator;\n const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n const specRoot = resolveSpecRoot();\n const annotationsSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/annotations.schema.json'), 'utf8'),\n );\n const sidecarSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/sidecar.schema.json'), 'utf8'),\n );\n ajv.addSchema(annotationsSchema);\n cachedValidator = ajv.compile(sidecarSchema);\n return cachedValidator;\n}\n\n/**\n * Test-only: drop the cached AJV validator. Mirrors\n * `_resetSidecarValidatorCacheForTests` in `parse.ts`.\n */\nexport function _resetSidecarStoreValidatorCacheForTests(): void {\n cachedValidator = null;\n}\n\nfunction resolveSpecRoot(): string {\n const require = createRequire(import.meta.url);\n try {\n const indexPath = require.resolve('@skill-map/spec/index.json');\n return dirname(indexPath);\n } catch {\n throw new Error('@skill-map/spec not resolvable — sidecar store cannot load schemas.');\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 TProgressListener,\n} from '../ports/progress-emitter.js';\n\nexport class InMemoryProgressEmitter implements ProgressEmitterPort {\n readonly #listeners = new Set<TProgressListener>();\n\n emit(event: ProgressEvent): void {\n for (const listener of this.#listeners) listener(event);\n }\n\n subscribe(listener: TProgressListener): () => 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 // Spec § 9.6.6 — per-extension annotation-contribution validation.\n // Two cross-cutting rules per entry: (a) `location: 'root'` REQUIRES\n // `ownership: 'exclusive'`, (b) the inline `schema` must be a valid\n // JSON Schema (compile with AJV). Cross-plugin collision detection\n // for `(key, location: 'root', ownership: 'exclusive')` runs later\n // at the orchestrator/composer level; this stage covers single-plugin\n // shape validation only.\n const contribFailure = validateAnnotationContributions(\n pluginPath,\n manifest,\n relEntry,\n manifestView,\n );\n if (contribFailure) return { ok: false, failure: contribFailure };\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 § 9.6.6 — Annotation-contribution validation. Runs AFTER the\n * kind-specific AJV manifest pass (the contribution shape — schema /\n * ownership / location — is already structurally validated by then via\n * the base schema). Two extra invariants:\n *\n * (a) `location: 'root'` REQUIRES `ownership: 'exclusive'` (a\n * top-level reserved key cannot be silently shared).\n * (b) The inline `schema` MUST AJV-compile cleanly (catch typos in\n * JSON-Schema-keyword usage at load time, not at first write).\n *\n * Returns a discovered-plugin failure (`invalid-manifest`) on either\n * violation, or `null` when the extension's contributions are well-formed.\n * Cross-plugin collision detection runs later in the runtime composer.\n */\n// Linear validator with one branch per failure mode (root-shared,\n// schema-not-object, schema-compile-fails) plus the per-entry guards.\n// Each branch returns directly; cyclomatic count comes from the guard\n// chain inside the entry loop, not from real nested logic.\n// eslint-disable-next-line complexity\nfunction validateAnnotationContributions(\n pluginPath: string,\n manifest: IPluginManifest,\n relEntry: string,\n manifestView: unknown,\n): IDiscoveredPlugin | null {\n if (!isRecord(manifestView)) return null;\n const raw = manifestView['annotationContributions'];\n if (raw === undefined) return null;\n if (!isRecord(raw)) return null;\n for (const [key, value] of Object.entries(raw)) {\n if (!isRecord(value)) continue;\n const location = (value['location'] as string | undefined) ?? 'namespaced';\n const ownership = (value['ownership'] as string | undefined) ?? 'shared';\n if (location === 'root' && ownership !== 'exclusive') {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestRootSharedAnnotation, {\n relEntry,\n key,\n ownership,\n }),\n ),\n manifest,\n };\n }\n const schema = value['schema'];\n if (!isRecord(schema)) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {\n relEntry,\n key,\n errDescription: 'schema must be an object literal',\n }),\n ),\n manifest,\n };\n }\n try {\n const ajv: TAjv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n ajv.compile(schema);\n } catch (err) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {\n relEntry,\n key,\n errDescription: describe(err),\n }),\n ),\n manifest,\n };\n }\n }\n return null;\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', 'analyzer', '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 * 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 — ten events. Eight\n * are pipeline-driven (emitted from inside `runScan`); two\n * (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving\n * binary before / after the verb runs, fire-and-forget so\n * `process.exit` is never blocked). The full `ProgressEmitterPort`\n * catalog (per-node `scan.progress`, `model.delta`, `run.*`, internal\n * job lifecycle) is deliberately not hookable: too verbose for a\n * reactive surface, internal to the runner, or covered elsewhere.\n * Declaring a trigger outside the curated set yields\n * `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 * 0. `boot` — once per CLI process, before verb routing.\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. `analyzer.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 * 9. `shutdown` — once per CLI process, after the verb's\n * exit code resolves and before\n * `process.exit`.\n */\n\nimport type { IExtensionBase } from './base.js';\nimport type { Node, TExecutionMode } from '../types.js';\n\n/**\n * The ten hookable lifecycle events. Mirrors the `triggers[]` enum in\n * `spec/schemas/extensions/hook.schema.json`. Eight are pipeline-driven\n * (emitted from inside `runScan`); two (`boot`, `shutdown`) are\n * CLI-process-driven (emitted by the driving binary before / after the\n * verb runs). Anything outside this set is rejected at load time as\n * `invalid-manifest`.\n */\nexport type THookTrigger =\n | 'boot'\n | 'scan.started'\n | 'scan.completed'\n | 'extractor.completed'\n | 'analyzer.completed'\n | 'action.completed'\n | 'job.spawning'\n | 'job.completed'\n | 'job.failed'\n | 'shutdown';\n\n/**\n * Frozen list mirror of `THookTrigger` for runtime introspection. The\n * loader validates `manifest.triggers[]` against this set; the\n * dispatcher iterates it in order when fanning an event out to\n * subscribed hooks. `boot` first / `shutdown` last so a debug log of\n * the array reads in lifecycle order.\n */\nexport const HOOK_TRIGGERS: readonly THookTrigger[] = Object.freeze([\n 'boot',\n 'scan.started',\n 'scan.completed',\n 'extractor.completed',\n 'analyzer.completed',\n 'action.completed',\n 'job.spawning',\n 'job.completed',\n 'job.failed',\n 'shutdown',\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 * / `analyzerId` / `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 `analyzer.completed` events. Qualified extension id of the Rule.\n */\n analyzerId?: 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-analyzer'\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-analyzer': 'schemas/extensions/analyzer.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 'schemas/view-slots.schema.json',\n 'schemas/input-types.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 * Validate a `ctx.emitContribution(id, payload)` payload against the\n * declared slot's payload schema in\n * `view-slots.schema.json#/$defs/payloads/<slot>`. Closed catalog:\n * passing an unknown slot returns `{ ok: false, errors:\n * 'unknown-slot' }` so the orchestrator can drop the emission\n * without crashing.\n */\n validateContributionPayload(\n slot: string,\n payload: unknown,\n ): { ok: true } | { 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 analyzer: 'extension-analyzer',\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 // Per-slot payload validators for `ctx.emitContribution`. Compiled\n // lazily on first use because not every CLI verb exercises the\n // contributions path; cold-CLI startup avoids paying for validators a\n // verb will never call. See `validateContributionPayload`.\n //\n // The closed catalog of slot ids mirrors\n // `view-slots.schema.json#/$defs/SlotName` exactly; entries inside\n // `$defs/payloads` whose key starts with an underscore (`_counter`,\n // `_tag`, `_TreeNode`) are internal `$ref` reuse targets, NOT slot\n // ids — querying them would compile but is meaningless at the\n // public API.\n const contributionValidators = new Map<string, ValidateFunction>();\n const VIEW_SLOTS_ID = 'https://skill-map.dev/spec/v0/view-slots.schema.json';\n const KNOWN_SLOTS = new Set<string>([\n 'card.title.right',\n 'card.subtitle.left',\n 'card.footer.left.counter',\n 'card.footer.right',\n 'graph.node.alert',\n 'inspector.header.badge.counter',\n 'inspector.header.badge.tag',\n 'inspector.body.panel.breakdown',\n 'inspector.body.panel.records',\n 'inspector.body.panel.tree',\n 'inspector.body.panel.key-values',\n 'inspector.body.panel.link-list',\n 'inspector.body.panel.markdown',\n 'topbar.actions.indicator',\n ]);\n\n function getContributionValidator(slot: string): ValidateFunction | null {\n if (!KNOWN_SLOTS.has(slot)) return null;\n const existing = contributionValidators.get(slot);\n if (existing) return existing;\n const ref = `${VIEW_SLOTS_ID}#/$defs/payloads/${slot}`;\n let compiled: ValidateFunction | undefined;\n try {\n compiled = ajv.compile({ $ref: ref });\n } catch {\n return null;\n }\n contributionValidators.set(slot, compiled);\n return compiled;\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 validateContributionPayload(slot: string, payload: unknown) {\n const validator = getContributionValidator(slot);\n if (!validator) {\n return { ok: false as const, errors: 'unknown-slot' };\n }\n if (validator(payload)) return { ok: true as const };\n const errors = (validator.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 registerProviderAuxiliarySchemas(ajv, providers);\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 * Register every Provider's auxiliary schemas (if any) on the AJV instance\n * BEFORE compiling per-kind schemas. Use case: Anthropic's merged\n * skill / command frontmatter — both kinds extend a shared\n * `skill-base.schema.json` declared as an auxiliary on the Provider, and\n * AJV resolves the cross-file `$ref` only after `addSchema` has registered\n * the auxiliary's `$id`.\n */\nfunction registerProviderAuxiliarySchemas(ajv: TAjv, providers: IProvider[]): void {\n for (const provider of providers) {\n if (!provider.schemas) continue;\n for (const aux of provider.schemas) {\n const auxJson = aux as { $id?: string };\n if (typeof auxJson.$id === 'string' && ajv.getSchema(auxJson.$id)) continue;\n ajv.addSchema(aux as object);\n }\n }\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 \"{{analyzerId}}\" emitted an issue with invalid severity {{severity}} ' +\n \"(allowed: 'error' | 'warn' | 'info'). Issue dropped.\",\n\n extensionErrorContributionUnknownId:\n 'Extractor \"{{extractorId}}\" emitted contribution \"{{contributionId}}\" on {{nodePath}} ' +\n 'but did not declare it in its `viewContributions` map. Contribution dropped.',\n\n extensionErrorContributionPayloadInvalid:\n 'Extractor \"{{extractorId}}\" emitted contribution \"{{contributionId}}\" on {{nodePath}}; ' +\n 'payload failed the \"{{slot}}\" schema: {{errors}}. Contribution 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 * Kernel-accessible counterpart of `cli/util/error-reporter.ts`'s\n * `formatErrorMessage`. The CLI helper now re-exports from here so the\n * historic CLI import path keeps working while kernel + BFF callers can\n * consume it directly without crossing the layering boundary.\n *\n * Kept deliberately tiny — same shape as the original CLI helper. The\n * surface grows (e.g. a `--verbose` stack mode, JSON envelope) only\n * when a concrete need surfaces.\n */\n\n/**\n * Compact error → string conversion.\n *\n * - `Error` → `err.message` verbatim. Callers wrap with their own\n * verb-specific context line via `tx(*_TEXTS.x, { message })` so\n * error catalogues stay greppable.\n * - Anything else → `String(value)`. Catches the rare throw-a-string\n * / throw-an-object path without exploding on `null`\n * (`String(null)` = `'null'`).\n */\nexport function formatErrorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","/**\n * Kernel walker. Discovers files inside one or more scope roots,\n * reads each, parses it via the configured parser, and yields\n * `IRawNode` records the orchestrator consumes.\n *\n * Owns the audit-cleared defences (every Provider that uses the walker\n * inherits these — no duplication needed in `Provider.walk`):\n *\n * - **Symlinks (audit M7)** — `entry.isSymbolicLink()` is checked\n * explicitly and the entry is skipped. Without this guard we relied\n * on `Dirent.isFile()` returning false for symlinks, which is an\n * implementation detail of node's `withFileTypes`. The explicit\n * skip is both self-documenting and resilient to future Dirent API\n * changes. The `followSymlinks?: false` option is reserved for a\n * future implementation that adds cycle detection + `realpath`-\n * resolved containment; until then the type forbids `true`.\n * - **TOCTOU race (audit M7)** — `readdir` reports a regular file →\n * `stat()` re-verifies before the read. Closes the window where the\n * entry could be swapped for a symlink between the two calls.\n * `stat` follows symlinks; rejecting non-regular results closes\n * that lane too.\n * - **Ignore filter** — every directory and file's path-relative-to-\n * root is checked against the project's `IIgnoreFilter`. When the\n * caller does not supply one, the walker falls back to bundled\n * defaults via `buildIgnoreFilter()` so direct test invocations\n * keep working without an explicit filter argument.\n *\n * Parser dispatch is by id: the walker resolves `options.parser`\n * against the kernel-internal parser registry once at the start of the\n * walk and throws `UnknownParserError` for unknown ids. Built-in\n * parsers ship with the kernel (`frontmatter-yaml`, `plain`); the set\n * is closed by design.\n */\n\nimport { readFile, readdir, stat } from 'node:fs/promises';\nimport { join, relative, sep } from 'node:path';\n\nimport type { IRawNode } from '../extensions/provider.js';\nimport { buildIgnoreFilter, type IIgnoreFilter } from './ignore.js';\nimport { getParser } from './parsers/index.js';\n\nexport interface IWalkContentOptions {\n /**\n * File extensions the walker yields. Strings include the leading dot\n * (e.g. `'.md'`, `'.mdc'`, `'.toml'`). Match is suffix-based; the\n * extension comparison is case-sensitive — Providers MUST list every\n * casing they want to match (today the kernel emits lowercase only,\n * matching the on-disk convention of every supported Provider).\n */\n extensions: readonly string[];\n /**\n * Parser id from the kernel-internal registry. Built-ins:\n * `'frontmatter-yaml'`, `'plain'`. Unknown ids throw at the start of\n * the walk; the orchestrator surfaces this as a Provider issue with\n * status `invalid-manifest`.\n */\n parser: string;\n /**\n * Project ignore filter. When omitted the walker uses the bundled\n * defaults (`buildIgnoreFilter()` with no extra layers), keeping\n * direct test invocations working without ceremony. Production callers\n * (the orchestrator) always pass a fully-composed filter.\n */\n ignoreFilter?: IIgnoreFilter;\n /**\n * Reserved escape hatch for a future symlink-follow implementation.\n * Today the walker hard-skips symlinks per audit M7. The type forbids\n * `true` until the audit-cleared follow path is actually built.\n */\n followSymlinks?: false;\n}\n\nexport class UnknownParserError extends Error {\n constructor(parserId: string) {\n super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);\n this.name = 'UnknownParserError';\n }\n}\n\n/**\n * Walk the given roots and yield one `IRawNode` per matching file.\n * Async generator so large scopes don't buffer in memory.\n */\nexport async function* walkContent(\n roots: readonly string[],\n options: IWalkContentOptions,\n): AsyncIterable<IRawNode> {\n const parser = getParser(options.parser);\n if (!parser) throw new UnknownParserError(options.parser);\n const filter: IIgnoreFilter = options.ignoreFilter ?? buildIgnoreFilter();\n const extensions = options.extensions;\n for (const root of roots) {\n for await (const file of walkRoot(root, root, filter, extensions)) {\n const relPath = relative(root, file).split(sep).join('/');\n let raw: string;\n try {\n raw = await readFile(file, 'utf8');\n } catch {\n // silently skip unreadable files\n continue;\n }\n const parsed = parser.parse(raw, relPath);\n yield {\n path: relPath,\n body: parsed.body,\n frontmatterRaw: parsed.frontmatterRaw,\n frontmatter: parsed.frontmatter,\n };\n }\n }\n}\n\n// eslint-disable-next-line complexity\nasync function* walkRoot(\n root: string,\n current: string,\n filter: IIgnoreFilter,\n extensions: readonly string[],\n): AsyncIterable<string> {\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true, encoding: 'utf8' });\n } catch {\n return;\n }\n for (const entry of entries) {\n const name = entry.name;\n const full = join(current, name);\n const rel = relative(root, full).split(sep).join('/');\n if (filter.ignores(rel)) continue;\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n yield* walkRoot(root, full, filter, extensions);\n } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {\n // TOCTOU re-check: readdir reported a regular file; verify before\n // reading. `stat` follows symlinks, so a swap between the two\n // calls is rejected here.\n try {\n const s = await stat(full);\n if (s.isFile()) yield full;\n } catch {\n // silently skip unreadable files\n }\n }\n }\n}\n\nfunction hasMatchingExtension(name: string, extensions: readonly string[]): boolean {\n for (const ext of extensions) {\n if (name.endsWith(ext)) return true;\n }\n return false;\n}\n","/**\n * `.skillmapignore` parser + filter facade. Wraps `ignore` (kaelzhang)\n * with the project-local layering: bundled defaults → `config.ignore`\n * (from `.skill-map/settings.json`) → `.skillmapignore` file content.\n *\n * Why a wrapper instead of exposing `ignore` directly:\n *\n * 1. Single-source defaults — `src/config/defaults/skillmapignore` is\n * the canonical default list, loaded once at module init (or at\n * explicit build time, depending on bundling). The runtime never\n * re-reads it per scan.\n * 2. Stable interface — Providers and the orchestrator depend on a\n * minimal `IIgnoreFilter` shape, so the underlying library can be\n * swapped without touching every consumer.\n * 3. Path normalization — every consumer passes the path RELATIVE to\n * the scan root (POSIX separators); the wrapper guarantees that\n * contract before delegating to `ignore`.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport ignoreFactory from 'ignore';\n\nexport interface IIgnoreFilter {\n /**\n * Returns `true` when `relativePath` should be skipped. The caller\n * MUST pass paths relative to the scan root, with POSIX separators\n * (forward slashes), no leading `/`. Directories MAY be passed with\n * or without trailing `/`; the wrapper does not require it.\n */\n ignores(relativePath: string): boolean;\n}\n\nexport interface IBuildIgnoreFilterOptions {\n /** Patterns from `config.ignore` in `.skill-map/settings.json`. */\n configIgnore?: string[] | undefined;\n /**\n * Raw text of the project's `.skillmapignore` file. Comments and\n * blank lines are tolerated by `ignore` itself; the caller does not\n * need to pre-process. Accepts `undefined` so callers can forward\n * `readIgnoreFileText()` directly without a guard.\n */\n ignoreFileText?: string | undefined;\n /**\n * When `false`, the bundled defaults are NOT pre-loaded. Default is\n * `true`. Tests use `false` to assert the precise effect of a single\n * pattern.\n */\n includeDefaults?: boolean | undefined;\n}\n\n/**\n * Build a filter from any combination of layers. Layer order is fixed:\n *\n * 1. bundled defaults (`src/config/defaults/skillmapignore`)\n * 2. `configIgnore`\n * 3. `ignoreFileText`\n *\n * Later layers override earlier ones via gitignore negation rules\n * (`!pattern` re-includes a path the prior layer excluded).\n */\nexport function buildIgnoreFilter(opts: IBuildIgnoreFilterOptions = {}): IIgnoreFilter {\n const ig = ignoreFactory();\n if (opts.includeDefaults !== false) {\n ig.add(loadDefaultsText());\n }\n if (opts.configIgnore && opts.configIgnore.length > 0) {\n ig.add(opts.configIgnore);\n }\n if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {\n ig.add(opts.ignoreFileText);\n }\n return {\n ignores(relativePath: string): boolean {\n // `ignore` requires a non-empty relative path; the empty string\n // (the root itself) MUST never be ignored.\n if (relativePath === '' || relativePath === '.' || relativePath === './') {\n return false;\n }\n const normalised = relativePath.replace(/^\\.\\//, '').replace(/\\\\/g, '/').replace(/^\\//, '');\n if (normalised === '') return false;\n return ig.ignores(normalised);\n },\n };\n}\n\n/**\n * Return the bundled defaults text. Useful for `sm init` (which writes\n * the file into the user's scope) and for tests. The same caching\n * logic backs `buildIgnoreFilter` so this never re-reads from disk on\n * a hot path.\n */\nexport function loadBundledIgnoreText(): string {\n return loadDefaultsText();\n}\n\n/**\n * Read `.skillmapignore` from `<root>/.skillmapignore` if it exists,\n * else return `undefined`. Caller passes the result as `ignoreFileText`\n * to `buildIgnoreFilter`.\n */\nexport function readIgnoreFileText(scopeRoot: string): string | undefined {\n const path = resolve(scopeRoot, '.skillmapignore');\n if (!existsSync(path)) return undefined;\n try {\n return readFileSync(path, 'utf8');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Async version of `readIgnoreFileText` that waits until the file's\n * content stops changing before returning. Used by the BFF + CLI\n * `sm watch` meta-file handlers when chokidar fires a `change` event\n * for `.skillmapignore`.\n *\n * Why: editors save in two motions — truncate (or rename-over) and\n * then write. chokidar emits the `change` event on the first motion\n * already, so a naive read can land while the file is empty or\n * partially flushed, rebuilding the ignore filter without the new\n * pattern. The user then has to save again to get the real effect.\n *\n * Strategy: read, sleep ~50 ms, read again. If both reads agree, the\n * file has settled — return that text. If they differ, retry up to\n * `maxAttempts` times. After the cap (~500 ms), use whatever the last\n * read produced; even partial content beats blocking the watcher.\n *\n * Default knobs (`pollMs: 50`, `maxAttempts: 10`) mirror the canonical\n * chokidar `awaitWriteFinish` recipe and were chosen because every\n * common editor (VS Code, vim, JetBrains, nano) settles inside that\n * window.\n */\nexport async function readIgnoreFileTextStable(\n scopeRoot: string,\n opts: { pollMs?: number; maxAttempts?: number } = {},\n): Promise<string | undefined> {\n const pollMs = opts.pollMs ?? 50;\n const maxAttempts = opts.maxAttempts ?? 10;\n let prev = readIgnoreFileText(scopeRoot);\n for (let i = 0; i < maxAttempts; i++) {\n await new Promise<void>((r) => setTimeout(r, pollMs));\n const curr = readIgnoreFileText(scopeRoot);\n if (curr === prev) return curr;\n prev = curr;\n }\n return prev;\n}\n\n// -----------------------------------------------------------------------------\n// Bundled defaults loader\n// -----------------------------------------------------------------------------\n\nlet cachedDefaults: string | null = null;\n\nfunction loadDefaultsText(): string {\n if (cachedDefaults !== null) return cachedDefaults;\n cachedDefaults = readDefaultsFromDisk();\n return cachedDefaults;\n}\n\n/** Test-only — drop the cache so a unit test can simulate a missing file. */\nexport function _resetDefaultsCacheForTests(): void {\n cachedDefaults = null;\n}\n\n/**\n * Resolve `src/config/defaults/skillmapignore` from disk. Walks a small\n * list of candidate locations relative to this module so the lookup\n * works in both the dev layout (`src/kernel/scan/ignore.ts` →\n * `src/config/defaults/`) and the bundled layout (single-file\n * `dist/...js` → `dist/config/defaults/`, populated by tsup `onSuccess`).\n */\nfunction readDefaultsFromDisk(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n resolve(here, '../../config/defaults/skillmapignore'), // src/kernel/scan/ → src/config/defaults/\n resolve(here, '../config/defaults/skillmapignore'), // dist/cli.js → dist/config/defaults/ (siblings)\n resolve(here, 'config/defaults/skillmapignore'),\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n try {\n return readFileSync(candidate, 'utf8');\n } catch {\n /* try next candidate */\n }\n }\n }\n // Fail soft: the scan still works without bundled defaults. The user's\n // own `.skillmapignore` + config.ignore still apply.\n return '';\n}\n","/**\n * `frontmatter-yaml` parser. Splits a `--- yaml --- body` document and\n * parses the frontmatter via `js-yaml`. Carries the audit-cleared\n * defences:\n *\n * - **Symlink / TOCTOU** — out of scope here (lives in the walker;\n * this parser receives the raw string after the kernel walker has\n * already vetted the file).\n * - **Prototype pollution (audit L2/L3)** — keys named `__proto__`,\n * `constructor`, `prototype` are stripped from the parsed object.\n * `js-yaml` stores `__proto__:` as an own data property (rather\n * than mutating `Object.prototype`), but the value still flows into\n * downstream `Object.assign`-style merges where the `__proto__`\n * setter fires. Stripping at parse time keeps the returned object\n * safe to spread, copy, and persist.\n * - **`!!js/function` & friends (audit L3)** — `yaml.load` runs with\n * `schema: JSON_SCHEMA` explicitly. js-yaml v4's default schema is\n * already safe (no `!!js/function` tag), but the explicit selection\n * documents intent and protects against an upstream default flip.\n * Frontmatter values that are valid JSON (string, number, bool,\n * null, sequence, mapping) round-trip unchanged; YAML-only\n * conveniences like unquoted timestamps degrade to strings, but the\n * kernel's node schema does not depend on parsed Date objects so\n * the tradeoff is safe.\n *\n * Lives under `src/built-in-plugins/parsers/` even though the parser\n * registry stays kernel-internal (no `kind: 'parser'` is exposed to\n * plugin authors). The relocation aligns the file layout with the\n * other built-ins (Provider / Extractor / Rule / Formatter / Action /\n * Hook) — every shipped extension-shaped artifact lives under\n * `built-in-plugins/`. The registry in `kernel/scan/parsers/index.ts`\n * imports from here and stays the single resolution surface.\n */\n\nimport yaml from 'js-yaml';\n\nimport type { IFileParser, IParsedFile } from '../../../kernel/scan/parsers/types.js';\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\nconst FORBIDDEN_FRONTMATTER_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport const frontmatterYamlParser: IFileParser = {\n id: 'frontmatter-yaml',\n parse(raw: string, _path: string): IParsedFile {\n const match = FRONTMATTER_RE.exec(raw);\n if (!match) return { frontmatterRaw: '', frontmatter: {}, body: raw };\n const frontmatterRaw = match[1]!;\n const body = match[2]!;\n const parsed: Record<string, unknown> = {};\n try {\n const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });\n if (doc && typeof doc === 'object' && !Array.isArray(doc)) {\n for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {\n if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;\n parsed[k] = v;\n }\n }\n } catch {\n // Malformed YAML — leave as empty object, keep the raw string for\n // downstream diagnostics.\n }\n return { frontmatterRaw, frontmatter: parsed, body };\n },\n};\n","/**\n * `plain` parser. Treats the entire raw as the body; emits an empty\n * frontmatter object and an empty `frontmatterRaw`. Pure pass-through:\n * the body is not normalised, line endings are preserved verbatim.\n *\n * Used by Providers that walk files carrying no frontmatter convention\n * (e.g. Roo Code rules at `.roo/rules/*.md`, Windsurf rules at\n * `.windsurf/rules/*.md`, plain `CONVENTIONS.md`). Such Providers MUST\n * derive `frontmatter.name` (and other base-required fields) from the\n * file path inside their `classify()` / Provider-side post-processing,\n * because the spec's `frontmatter/base.schema.json` requires `name`.\n *\n * Spec note: when the `frontmatter/base.schema.json` `name` requirement\n * is relaxed in a later phase, the path-derivation step becomes optional.\n *\n * Lives under `src/built-in-plugins/parsers/` for layout consistency\n * with the other built-ins; the parser registry in\n * `kernel/scan/parsers/index.ts` stays kernel-internal and imports\n * from here.\n */\n\nimport type { IFileParser, IParsedFile } from '../../../kernel/scan/parsers/types.js';\n\nexport const plainParser: IFileParser = {\n id: 'plain',\n parse(raw: string, _path: string): IParsedFile {\n return { frontmatter: {}, frontmatterRaw: '', body: raw };\n },\n};\n","/**\n * Kernel-internal parser registry. Built-ins are seeded at module load\n * time and frozen — user plugins cannot register their own parsers\n * (this module is NOT re-exported from `src/kernel/index.ts`).\n *\n * Provider manifests reference parsers by id via `read.parser`. The\n * walker calls `getParser(id)` once per scan when it resolves a\n * Provider's read config; the orchestrator never sees a parser\n * directly.\n *\n * Registry shape: a single `Map<id, IFileParser>` seeded from the two\n * built-in modules — both now living under `src/built-in-plugins/parsers/`\n * for layout consistency with the other shipped extensions, while the\n * registry itself stays kernel-internal (no `kind: 'parser'` is exposed\n * to plugin authors). The set of built-in ids is captured into\n * `FROZEN_IDS` at seed time; subsequent `registerParser` calls reject\n * collisions with frozen built-ins. The `registerParser` seam exists\n * for kernel-internal tests and future built-ins; it is not part of any\n * plugin-author API.\n */\n\nimport { frontmatterYamlParser } from '../../../built-in-plugins/parsers/frontmatter-yaml/index.js';\nimport { plainParser } from '../../../built-in-plugins/parsers/plain/index.js';\n\nimport type { IFileParser } from './types.js';\n\nexport type { IFileParser, IParsedFile } from './types.js';\n\nconst REGISTRY = new Map<string, IFileParser>([\n [frontmatterYamlParser.id, frontmatterYamlParser],\n [plainParser.id, plainParser],\n]);\nconst FROZEN_IDS: ReadonlySet<string> = new Set(REGISTRY.keys());\n\n/** Resolve a parser by id. Returns `undefined` for unknown ids. */\nexport function getParser(id: string): IFileParser | undefined {\n return REGISTRY.get(id);\n}\n\n/**\n * Kernel-internal seam for tests and future built-ins. Throws when the\n * id collides with a frozen built-in (`frontmatter-yaml`, `plain`).\n * NOT re-exported from `src/kernel/index.ts` — user plugins have no\n * public surface to call this.\n */\nexport function registerParser(parser: IFileParser): void {\n if (FROZEN_IDS.has(parser.id)) {\n throw new Error(\n `Cannot register parser with built-in id '${parser.id}'. Built-in parsers are frozen.`,\n );\n }\n REGISTRY.set(parser.id, parser);\n}\n\n/** Test-only — drop a non-built-in registration. Throws on a frozen id. */\nexport function _unregisterParserForTests(id: string): void {\n if (FROZEN_IDS.has(id)) {\n throw new Error(`Cannot unregister built-in parser '${id}'.`);\n }\n REGISTRY.delete(id);\n}\n","/**\n * Provider runtime contract. Walks filesystem roots and emits raw node\n * records; classification maps path conventions to a node kind.\n *\n * Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`,\n * `StoragePort.adapter`, etc.). A `Provider` is an extension kind authored\n * by plugins to declare a platform's universe (the catalog of kinds it\n * emits, the per-kind frontmatter schema, the filesystem directory it\n * owns); a hexagonal adapter is an internal implementation of a port.\n * Both can coexist without confusion because they live in different\n * namespaces.\n *\n * `walk()` is an async iterator so large scopes don't buffer in memory.\n * Each yielded `IRawNode` carries the full parsed frontmatter + body plus\n * the path relative to the scan root; the kernel computes hashes, bytes,\n * and tokens on top.\n *\n * **Spec 0.8.0**. Per-kind frontmatter schemas relocated from the spec\n * to the Provider that owns them. The flat\n * `defaultRefreshAction` map collapsed into the new `kinds` map: every\n * kind the Provider emits gets one entry that declares both its schema\n * and its refresh action. Spec keeps only `frontmatter/base.schema.json`\n * (universal); per-kind schemas live with the Provider.\n */\n\nimport type { IExtensionBase } from './base.js';\nimport type { IIgnoreFilter } from '../scan/ignore.js';\nimport { walkContent } from '../scan/walk-content.js';\n\nexport interface IRawNode {\n /** Path relative to the scan root that produced this node. */\n path: string;\n /** Raw markdown body (everything after the frontmatter fence). */\n body: string;\n /** Raw frontmatter text (between `---` fences). Empty string when absent. */\n frontmatterRaw: string;\n /** Parsed frontmatter, or `{}` when absent / unparseable. */\n frontmatter: Record<string, unknown>;\n}\n\n/**\n * One entry in a Provider's `kinds` map. Declares both the per-kind\n * frontmatter schema (path relative to the Provider's package dir, plus\n * the loaded JSON object the kernel passes to AJV) and the qualified\n * default refresh action id the UI dispatches for nodes of this kind.\n *\n * The split between `schema` (manifest-level path) and `schemaJson`\n * (runtime-loaded JSON) keeps the manifest shape spec-conformant while\n * letting the runtime instance carry the parsed schema without a second\n * filesystem read at scan time. Built-in Providers populate `schemaJson`\n * via `import schema from './schemas/skill.schema.json' with { type: 'json' }`;\n * user-plugin Providers loaded by `PluginLoader` will have it filled in\n * by the loader after manifest validation.\n */\nexport interface IProviderKind {\n /**\n * Path to the kind's frontmatter JSON Schema, relative to the\n * Provider's package directory. Mirrors the spec field of the same\n * name in `extensions/provider.schema.json#/properties/kinds/.../schema`.\n */\n schema: string;\n /**\n * Loaded JSON Schema document for the kind. The kernel registers this\n * with AJV at scan boot and validates each node's frontmatter against\n * it. The schema MUST extend the spec's\n * `frontmatter/base.schema.json` via `allOf` + `$ref` to base's\n * `$id`; the loader registers base into the same AJV instance so\n * cross-package `$ref`-by-`$id` resolves transparently.\n *\n * `unknown` rather than a stronger type because AJV consumes any JSON\n * Schema object; tightening to a concrete shape would require mirroring\n * the JSON Schema vocabulary in TypeScript.\n */\n schemaJson: unknown;\n /**\n * Qualified action id (`<plugin-id>/<action-id>`) the probabilistic-\n * refresh UI dispatches for nodes of this kind. The kernel resolves\n * the id against its qualified action registry; a dangling reference\n * disables the Provider with status `invalid-manifest`.\n */\n defaultRefreshAction: string;\n /**\n * Presentation metadata the UI consumes to render nodes of this kind\n * (palette swatches, list tags, graph nodes, filter chips). Required\n * so the UI never has to invent visuals for a Provider-declared kind.\n * Mirrors `extensions/provider.schema.json#/properties/kinds/.../ui`.\n */\n ui: IProviderKindUi;\n}\n\n/**\n * Presentation contract for one Provider kind. The Provider declares\n * intent (label + base color, optional dark variant + emoji + icon);\n * the UI derives `bg`/`fg` tints per theme via a deterministic helper\n * and reads the registry from the `kindRegistry` field embedded in REST\n * envelopes. Single source of truth for what a kind looks like — the\n * UI never hardcodes presentation for a built-in kind.\n */\nexport interface IProviderKindUi {\n /**\n * Plural human-readable label for groups of this kind (e.g. `'Skills'`,\n * `'Agents'`, `'Cursor Rules'`). Used in filter dropdowns, palette\n * tooltips, and any list grouping.\n */\n label: string;\n /**\n * Base hex color (`#RRGGBB`) for the light theme. The UI derives `bg`\n * and `fg` tints from this value at runtime via a deterministic\n * helper. Declaring one base value (instead of three) keeps the\n * manifest small and centralises accessibility-driven contrast in the\n * UI.\n */\n color: string;\n /**\n * Optional dark-theme variant of `color`. When absent, the UI falls\n * back to `color`. Declared explicitly because a luminosity flip\n * rarely matches the brand intent for kinds that should stand out in\n * dark mode.\n */\n colorDark?: string;\n /**\n * Optional decorative emoji used as a fallback when `icon` is absent\n * or fails to render. Length-bound so the UI can lay it out\n * predictably alongside text.\n */\n emoji?: string;\n /**\n * Optional discriminated icon descriptor. The UI prefers `icon` over\n * `emoji`; when both are absent, the UI falls back to the first\n * letter of `label` colored with `color`.\n */\n icon?: TProviderKindIcon;\n}\n\n/**\n * Discriminated icon contract. `pi` references a PrimeIcons identifier\n * (e.g. `'pi-cog'`); `svg` carries raw SVG path data the UI wraps in a\n * `<svg viewBox=\"0 0 24 24\"><path d=\"…\"/></svg>` element tinted with\n * `currentColor`. The discriminator (`kind`) keeps the UI dispatch\n * exhaustive without string-sniffing the payload.\n */\nexport type TProviderKindIcon =\n | { kind: 'pi'; id: string }\n | { kind: 'svg'; path: string };\n\nexport interface IProvider extends IExtensionBase {\n kind: 'provider';\n\n /**\n * Filesystem directory (relative to user home or project root) where this\n * Provider's content lives. Required. Examples: `'~/.claude'` for the\n * Claude Provider, `'~/.cursor'` for a hypothetical Cursor Provider.\n * The kernel walks this directory during boot/scan to discover nodes;\n * `sm doctor` validates the directory exists and emits a non-blocking\n * warning when it does not.\n */\n explorationDir: string;\n\n /**\n * Catalog of node kinds this Provider emits. Keyed by kind name. Every\n * kind the Provider can `classify()` MUST have an entry; an entry is\n * the union of the kind's frontmatter schema and its default refresh\n * action.\n *\n * The string keys are typed loosely (`string`) rather than `NodeKind`\n * because the value space is open by design: a future Cursor Provider\n * could declare `rule`, an Obsidian Provider could declare `daily`.\n * The kernel's hard-coded `NodeKind` union represents the kinds the\n * built-in Claude Provider emits; it is NOT the kernel-wide kind type\n * (see `kernel/types.ts:NodeKind` docstring). `Node.kind`, the AJV\n * `node.schema.json` validator, and the SQLite `scan_nodes.kind`\n * column all accept any non-empty string an enabled Provider returns.\n */\n kinds: Record<string, IProviderKind>;\n\n /**\n * Optional auxiliary JSON Schemas this Provider's per-kind schemas\n * `$ref` by `$id`. Registered with AJV via `addSchema` BEFORE the\n * per-kind schemas compile, so cross-file `$ref` resolution succeeds.\n *\n * Use case: when several kinds share a common base (e.g. Anthropic's\n * merged skill / command frontmatter — both extend a shared\n * `skill-base.schema.json`), the Provider declares the base here so\n * `skill.schema.json` and `command.schema.json` can `$ref` it without\n * duplicating fields.\n *\n * Runtime-only — does NOT appear in the spec's `provider.schema.json`\n * manifest. Manifest-validated schemas remain the per-kind ones in\n * `kinds[<kind>].schema`; auxiliary schemas are an implementation\n * concern of how the runtime composes those.\n */\n schemas?: unknown[];\n\n /**\n * Declarative file-discovery config consumed by the kernel walker.\n * When present, the kernel walks every root, includes files whose\n * extension matches `extensions`, parses each with the parser id\n * registered in the kernel-internal registry, and yields `IRawNode`\n * records the same shape `walk()` would.\n *\n * When neither `read` nor `walk` is declared, `resolveProviderWalk`\n * applies the default `{ extensions: ['.md'], parser: 'frontmatter-yaml' }`\n * so the most common Provider shape needs zero configuration.\n *\n * Precedence: when both `walk()` (runtime field) and `read` are\n * declared, `walk()` wins — `read` is ignored. The escape-hatch\n * relationship is intentional: most Providers should use `read`;\n * Providers with non-standard discovery requirements (custom file\n * naming, multi-pass walks, dynamic ignore logic) implement `walk()`\n * directly and accept the duplication of audit-cleared defences.\n *\n * Built-in parsers: `'frontmatter-yaml'` (markdown with `--- … ---`\n * YAML frontmatter; pollution-strip + JSON_SCHEMA-pinned), `'plain'`\n * (entire body, empty frontmatter). The set is closed; user plugins\n * cannot register their own.\n */\n read?: IProviderReadConfig;\n\n /**\n * Walk the given roots and yield every node the Provider recognises.\n * Non-matching files are silently skipped. Unreadable files produce\n * a diagnostic via the emitter but do not abort the walk.\n *\n * `options.ignoreFilter` — when supplied, the Provider MUST\n * skip every directory and file whose path-relative-to-root the\n * filter reports as ignored. Providers MAY also keep their own\n * hard-coded skip list (e.g. `.git`) as a defensive measure, but the\n * filter is the canonical source of user intent.\n *\n * Optional. When omitted, the Provider MUST declare `read` (or rely\n * on the default config). The orchestrator never calls `walk()`\n * directly — it goes through `resolveProviderWalk(provider)` which\n * picks `walk` over `read`.\n */\n walk?(\n roots: string[],\n options?: { ignoreFilter?: IIgnoreFilter },\n ): AsyncIterable<IRawNode>;\n\n /**\n * Given a path and its parsed frontmatter, decide the node kind — or\n * `null` to disclaim the file. The classifier is called after walk()\n * yields; with multiple Providers active, every Provider walks every\n * file matching its `read.extensions`, so each Provider MUST disclaim\n * paths it does not recognise. Returning the same path's kind from\n * two Providers fires the spec's `provider-ambiguous` issue and the\n * orchestrator drops the duplicate.\n *\n * Convention: a Provider's classify returns one of its own `kinds`\n * map keys for paths in its territory (`.claude/`, `.gemini/`,\n * `.agents/skills/`, etc.) and `null` elsewhere. External Providers\n * (Cursor, Obsidian, …) follow the same rule: claim what's yours,\n * disclaim everything else. The orchestrator does not validate the\n * kind against `NodeKind`.\n */\n classify(path: string, frontmatter: Record<string, unknown>): string | null;\n}\n\n/**\n * Declarative read config a Provider declares via `IProvider.read`.\n * Mirrors `extensions/provider.schema.json#/properties/read` at the\n * TypeScript level. Built-in parser ids: `'frontmatter-yaml'`, `'plain'`.\n */\nexport interface IProviderReadConfig {\n /**\n * File extensions the walker yields. Strings include the leading dot\n * (e.g. `'.md'`, `'.mdc'`, `'.toml'`). Match is suffix-based; the\n * comparison is case-sensitive.\n */\n extensions: string[];\n /**\n * Parser id from the kernel-internal registry. Built-ins:\n * `'frontmatter-yaml'`, `'plain'`. Unknown ids surface as\n * `UnknownParserError` from the walker; the orchestrator translates\n * the error into a Provider issue with status `invalid-manifest`.\n */\n parser: string;\n}\n\nconst DEFAULT_READ_CONFIG: IProviderReadConfig = Object.freeze({\n extensions: Object.freeze(['.md']) as unknown as string[],\n parser: 'frontmatter-yaml',\n});\n\n/**\n * Resolve how a Provider walks its roots. Precedence:\n *\n * 1. If the Provider declares `walk()` (runtime field), use it as-is.\n * Escape hatch for Providers with non-standard discovery logic.\n * 2. Else, use `provider.read` (declarative config) — or the default\n * `{ extensions: ['.md'], parser: 'frontmatter-yaml' }` when\n * `read` is also absent — and route through the kernel walker.\n *\n * Defaulting at the call site (rather than at manifest-load) keeps the\n * AJV-validated manifest equal to what the plugin author wrote — `read`\n * is not silently injected into a Provider's runtime shape.\n */\nexport function resolveProviderWalk(\n provider: IProvider,\n): (\n roots: string[],\n options?: { ignoreFilter?: IIgnoreFilter },\n) => AsyncIterable<IRawNode> {\n if (provider.walk) {\n const walk = provider.walk.bind(provider);\n return walk;\n }\n const read = provider.read ?? DEFAULT_READ_CONFIG;\n return (roots, options) => {\n // `ignoreFilter` is optional under `exactOptionalPropertyTypes`; only\n // include the key when the caller actually supplied a filter so the\n // walker's default-fallback path is preserved.\n const walkOptions: import('../scan/walk-content.js').IWalkContentOptions = {\n extensions: read.extensions,\n parser: read.parser,\n };\n if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;\n return walkContent(roots, walkOptions);\n };\n}\n","/**\n * Hook lifecycle dispatcher (spec § A.11). Indexes the supplied hooks\n * by 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 it ships (Decision #114).\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 * caller continues. A buggy hook MUST NOT block the main pipeline (or\n * the CLI exit path) — that would invert the design intent (hooks\n * REACT to events, they never steer them).\n *\n * The module lives under `kernel/extensions/` (alongside the `IHook`\n * contract itself) so two callers share it: `runScan` for the eight\n * pipeline-driven triggers (`scan.*`, `extractor.completed`,\n * `analyzer.completed`, `action.completed`, `job.*`) and the CLI entry\n * for the two CLI-process-driven triggers (`boot`, `shutdown`).\n * Pulling the dispatcher out of the orchestrator keeps both consumers\n * symmetric — same indexing, same filter semantics, same error\n * policy.\n */\n\nimport type { IHook, IHookContext, THookTrigger } from './hook.js';\nimport type { Node } from '../types.js';\nimport type { ProgressEmitterPort, ProgressEvent } from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport { formatErrorMessage } from '../util/format-error.js';\nimport { log } from '../util/logger.js';\n\nexport interface IHookDispatcher {\n /**\n * Fan the event out to every hook subscribed to `trigger`. Awaits each\n * hook's `on(ctx)` in registration order. Errors are caught and\n * logged via `extension.error`; they never propagate.\n */\n dispatch(trigger: THookTrigger, event: ProgressEvent): Promise<void>;\n}\n\n/**\n * Build a dispatcher over the given hooks. Empty `hooks` returns a\n * cheap no-op shape so the call sites can dispatch unconditionally.\n */\nexport function makeHookDispatcher(\n hooks: IHook[],\n emitter: ProgressEmitterPort,\n): 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\n // subsystem). Log once per hook at composition time — not\n // per-event — so a noisy scan doesn't flood the logger. The\n // hook still surfaces in `sm plugins list`; it just doesn't\n // 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 = formatErrorMessage(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\n/** Construct a `ProgressEvent` envelope. Mirrors the orchestrator helper. */\nexport function makeEvent(type: string, data: unknown): ProgressEvent {\n return { type, timestamp: new Date().toISOString(), data };\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['analyzerId'] === 'string') ctx.analyzerId = data['analyzerId'];\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","/**\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**: `(analyzerId, 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.analyzerId}\\x00${ids}\\x00${issue.message}`;\n}\n\nfunction byIssueSort(a: Issue, b: Issue): number {\n if (a.analyzerId !== b.analyzerId) return a.analyzerId.localeCompare(b.analyzerId);\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 TLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';\n\nexport type TLogMethodLevel = Exclude<TLogLevel, 'silent'>;\n\nexport const LOG_LEVELS: readonly TLogLevel[] = [\n 'trace',\n 'debug',\n 'info',\n 'warn',\n 'error',\n 'silent',\n] as const;\n\nconst LEVEL_RANK: Record<TLogLevel, 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: TLogLevel): number {\n return LEVEL_RANK[level];\n}\n\nexport function isLogLevel(value: unknown): value is TLogLevel {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LEVEL_RANK, value);\n}\n\n/**\n * Parse a string into a `TLogLevel`. Returns `null` for invalid input\n * (incl. `undefined` / `null` / empty). Case-insensitive; trims\n * whitespace.\n */\nexport function parseLogLevel(value: string | undefined | null): TLogLevel | 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: TLogMethodLevel;\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';\nimport type { IAnnotationContribution } from './extensions/base.js';\nimport type { IRegisteredAnnotationKey } from './types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from './types/view-catalog.js';\n\nexport type { IRegisteredAnnotationKey } from './types/annotation-catalog.js';\nexport type {\n IRegisteredViewContribution,\n IViewContribution,\n ISettingDeclaration,\n TSlotName,\n TInputTypeName,\n TSeverity,\n TSettingValue,\n} from './types/view-catalog.js';\n\nexport interface Kernel {\n registry: Registry;\n /**\n * Step 9.6.6 — read-only catalog of plugin-contributed annotation\n * keys, keyed by `(pluginId, key)`. Populated at plugin-load time;\n * pure read with no side effects. Built-in catalog (from\n * `annotations.schema.json`) is NOT included here.\n */\n getRegisteredAnnotationKeys: () => readonly IRegisteredAnnotationKey[];\n /**\n * Internal — replace the frozen catalog. Called once by the\n * plugin runtime composer after every plugin has loaded; consumers\n * MUST treat the resulting array as immutable.\n */\n setRegisteredAnnotationKeys: (entries: readonly IRegisteredAnnotationKey[]) => void;\n /**\n * Step 11.x — read-only catalog of plugin-contributed view\n * contributions, keyed by `(pluginId, extensionId, contributionId)`.\n * Populated at plugin-load time; pure read with no side effects.\n * Mirror of `getRegisteredAnnotationKeys` for the view contribution\n * surface (see `architecture.md` §View contribution system →\n * Runtime catalog).\n */\n getRegisteredViewContributions: () => readonly IRegisteredViewContribution[];\n /**\n * Internal — replace the frozen view-contribution catalog. Called\n * once by the plugin runtime composer after every plugin has loaded;\n * consumers MUST treat the resulting array as immutable.\n */\n setRegisteredViewContributions: (entries: readonly IRegisteredViewContribution[]) => void;\n}\n\nexport function createKernel(): Kernel {\n let annotationKeys: readonly IRegisteredAnnotationKey[] = Object.freeze([]);\n let viewContributions: readonly IRegisteredViewContribution[] = Object.freeze([]);\n return {\n registry: new Registry(),\n getRegisteredAnnotationKeys() { return annotationKeys; },\n setRegisteredAnnotationKeys(entries) {\n annotationKeys = Object.freeze([...entries]);\n },\n getRegisteredViewContributions() { return viewContributions; },\n setRegisteredViewContributions(entries) {\n viewContributions = Object.freeze([...entries]);\n },\n };\n}\n\nexport type { IAnnotationContribution } from './extensions/base.js';\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 TProgressListener,\n} from './ports/progress-emitter.js';\nexport type {\n LoggerPort,\n TLogLevel,\n TLogMethodLevel,\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 IAnalyzer,\n IAnalyzerContext,\n IAction,\n IActionPrecondition,\n IActionContext,\n IActionResult,\n TActionWrite,\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';\nexport {\n makeHookDispatcher,\n makeEvent,\n type IHookDispatcher,\n} from './extensions/hook-dispatcher.js';\n"],"mappings":"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","names":["existsSync","statSync","isAbsolute","yaml","require","existsSync","existsSync","readFileSync","dirname","resolve","createRequire","Ajv2020","yaml","createRequire","existsSync","readFileSync","readdirSync","join","relative","resolve","Ajv2020","require","createRequire","resolve","readFileSync","readFileSync","dirname","resolve","createRequire","Ajv2020","resolveSpecRoot","Ajv2020","resolve","readFileSync","require","createRequire","dirname","join","relative","sep","existsSync","readFileSync","dirname","resolve","dirname","resolve","existsSync","readFileSync","yaml","relative","sep","join","walk","existsSync","statSync","yaml","isAbsolute","byPath","resolve","relative","sep","relativePathFromRoots"]}
|
|
1
|
+
{"version":3,"sources":["../kernel/i18n/registry.texts.ts","../kernel/util/tx.ts","../kernel/registry.ts","../kernel/orchestrator.ts","../package.json","../kernel/sidecar/parse.ts","../kernel/util/ajv-interop.ts","../kernel/sidecar/drift.ts","../kernel/sidecar/discover-orphans.ts","../kernel/sidecar/store.ts","../core/config/helper.ts","../kernel/config/loader.ts","../kernel/adapters/schema-validators.ts","../kernel/util/format-error.ts","../kernel/util/skill-map-paths.ts","../core/paths/db-path.ts","../core/config/atomic-write.ts","../kernel/adapters/in-memory-progress.ts","../kernel/adapters/silent-logger.ts","../kernel/util/logger.ts","../kernel/adapters/plugin-loader.ts","../kernel/i18n/plugin-store.texts.ts","../kernel/adapters/plugin-store.ts","../kernel/extensions/hook.ts","../kernel/i18n/orchestrator.texts.ts","../kernel/scan/walk-content.ts","../kernel/scan/ignore.ts","../built-in-plugins/parsers/frontmatter-yaml/index.ts","../built-in-plugins/parsers/plain/index.ts","../kernel/scan/parsers/index.ts","../kernel/extensions/provider.ts","../kernel/extensions/hook-dispatcher.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 / analyzer / 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/annotations`, `core/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 | 'analyzer'\n | 'action'\n | 'formatter'\n | 'hook';\n\nexport const EXTENSION_KINDS: readonly ExtensionKind[] = Object.freeze([\n 'provider',\n 'extractor',\n 'analyzer',\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 → analyzer 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';\nimport { isAbsolute, resolve as resolvePath } from 'node:path';\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 {\n computeDriftStatus,\n discoverOrphanSidecars,\n readSidecarFor,\n type IOrphanSidecar,\n type IParsedSidecar,\n} from './sidecar/index.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 loadSchemaValidators,\n type IProviderFrontmatterValidator,\n} from './adapters/schema-validators.js';\nimport type { IContributionRecord } from './adapters/sqlite/contributions.js';\nimport type { IPriorExtractorRun } from './adapters/sqlite/scan-load.js';\nimport { ORCHESTRATOR_TEXTS } from './i18n/orchestrator.texts.js';\nimport { qualifiedExtensionId } from './registry.js';\nimport { formatErrorMessage } from './util/format-error.js';\nimport { tx } from './util/tx.js';\nimport {\n resolveProviderWalk,\n type IProvider,\n type IRawNode,\n type IExtractorContext,\n type IExtractor,\n type IHook,\n type IAnalyzer,\n type THookTrigger,\n} from './extensions/index.js';\nimport {\n makeHookDispatcher,\n makeEvent,\n type IHookDispatcher,\n} from './extensions/hook-dispatcher.js';\nimport type { IRegisteredAnnotationKey } from './types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from './types/view-catalog.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 analyzers: IAnalyzer[];\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 * Step 9.6.6 — runtime catalog of plugin-contributed annotation keys\n * (the same shape `kernel.getRegisteredAnnotationKeys()` returns).\n * Threaded into the rule pass so `core/unknown-field` can\n * legitimise registered plugin namespaces / root keys without\n * re-walking the manifests. Absent → empty catalog (every plugin\n * key is treated as unknown). Built-in catalog from\n * `annotations.schema.json` is NOT included — that is hard-coded\n * inside the rule.\n */\n annotationContributions?: readonly IRegisteredAnnotationKey[];\n /**\n * Runtime catalog of plugin-contributed view contributions (the same\n * shape `kernel.getRegisteredViewContributions()` returns). Threaded\n * into the rule pass so:\n * - `core/unknown-slot` and `core/contribution-orphan` can\n * introspect the catalog (read-only).\n * - The orchestrator's per-rule emit closure can look up each\n * declared `(contributionId → slot)` pairing for AJV\n * payload validation.\n * Absent → empty catalog. Rules that emit contributions silently\n * drop emissions when the catalog has no entry for the rule's\n * declared contributionId.\n */\n viewContributions?: readonly IRegisteredViewContribution[];\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, IPriorExtractorRun>>`.\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 or sidecar\n * annotations hash) → run only the missing extractors, drop prior\n * links whose `sources` map to any missing extractor or to an\n * extractor that is no longer registered.\n */\n priorExtractorRuns?: Map<string, Map<string, IPriorExtractorRun>>;\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 * Pre-computed absolute paths of orphan job MD files (files under\n * `.skill-map/jobs/` whose absolute path appears nowhere in\n * `state_jobs.filePath`). Threaded into the rule pass so the\n * built-in `core/job-orphan-file` rule can project each as a `warn`\n * issue without the kernel reaching for the storage port or doing\n * its own FS walk. The driving adapter (CLI, BFF) computes this\n * inside its already-open storage transaction via\n * `findOrphanJobFiles(jobsDir, await port.jobs.listReferencedFilePaths())`\n * — mirrors the `orphanSidecars` model where detection lives\n * outside the rule and the rule only projects. Absent / empty when\n * the caller has no jobs context (out-of-band tests, fresh DB,\n * `--no-built-ins`).\n */\n orphanJobFiles?: readonly string[];\n /**\n * Side set of absolute file paths the operator opted into for\n * link-validation purposes via `scan.referencePaths`. Threaded\n * through to `IAnalyzerContext.referenceablePaths` so the built-in\n * `core/broken-ref` rule can suppress its `warn` for path-style\n * links whose target lands in the set. Files are NOT walked by\n * the kernel — the driving adapter populates the set before\n * calling `runScan`. Absent / empty when the operator left\n * `scan.referencePaths` unconfigured.\n */\n referenceablePaths?: ReadonlySet<string>;\n /**\n * Absolute path of the scan's cwd / project root. Threaded onto\n * `IAnalyzerContext.cwd` so rules that need to resolve a relative\n * `link.target` to an absolute filesystem path can do so without\n * heuristics. Absent for callers that don't track a cwd\n * concept (out-of-band tests, embedders).\n */\n cwd?: string;\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 * sha256 of the canonical-form sidecar annotations the Extractor saw\n * at run time. Always populated (an absent sidecar canonicalises to\n * `{}` so the hash is stable). Used unconditionally by the cache\n * decision alongside `bodyHashAtRun`: a sidecar-only edit invalidates\n * the cached run for every applicable Extractor on that node.\n */\n sidecarAnnotationsHashAtRun: string;\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 * - 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 reserved: Extractors are deterministic-only, so\n * every record produced by the orchestrator sets it to `false`. The\n * field is kept on the record (and the row in `node_enrichments`) so a\n * future Action-issued enrichment can populate it without reshaping\n * the persistence contract — see spec `architecture.md`\n * §Extractor · enrichment layer.\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 contributions: IContributionRecord[];\n freshlyRunTuples: ReadonlySet<string>;\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 contributions: IContributionRecord[];\n freshlyRunTuples: ReadonlySet<string>;\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: [], analyzers: [] };\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 analyzers, 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 // Analyzers 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 analyzerResult = await runAnalyzers(\n exts.analyzers,\n walked.nodes,\n walked.internalLinks,\n walked.orphanSidecars,\n walked.sidecarRoots,\n options.annotationContributions ?? [],\n options.viewContributions ?? [],\n options.orphanJobFiles ?? [],\n options.referenceablePaths,\n options.cwd,\n emitter,\n hookDispatcher,\n );\n const issues = analyzerResult.issues;\n // Analyzer-emitted view contributions ride into the same per-scan buffer\n // that extractor-emitted contributions populate; both reach\n // `scan_contributions` through `replaceAllScanContributions`. The\n // walked.contributions array is already populated by the extractor\n // path inside `walked` — we append the analyzer emissions here.\n for (const c of analyzerResult.contributions) walked.contributions.push(c);\n // Phase 3 — analyzers ALWAYS run and see every node in the merged graph\n // (no per-(analyzer, node) cache like extractors have). Fold a tuple per\n // (analyzer × node) into the freshly-run set so the persist layer's\n // per-tuple sweep can drop stale analyzer-emitted rows when an analyzer\n // stops emitting for a previously-emitting node. Without this fold\n // the bug we just fixed for extractors re-emerges for analyzers.\n for (const analyzer of exts.analyzers ?? []) {\n if (analyzer.viewContributions === undefined) continue;\n for (const node of walked.nodes) {\n // NUL-separated so `nodePath` segments with slashes\n // (e.g. `.claude/agents/architect.md`) survive parsing in\n // `replaceAllScanContributions`. The `/`-separated form caused\n // `lastIndexOf('/')` to chop the wrong segment, leaving\n // analyzer-emitted rows orphaned on disable / state-flip.\n walked.freshlyRunTuples.add(`${analyzer.pluginId}\\0${analyzer.id}\\0${node.path}`);\n }\n }\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 analyzers 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 // `'markdown'`), 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 contributions: walked.contributions,\n freshlyRunTuples: walked.freshlyRunTuples,\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.analyzerId !== 'frontmatter-invalid' && issue.analyzerId !== '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 →\n * IPriorExtractorRun`. `undefined` opts out of the fine-grained\n * path (legacy callers that don't track the cache); the orchestrator\n * falls back to the pre-A.9 node-level cache check.\n */\n priorExtractorRuns: Map<string, Map<string, IPriorExtractorRun>> | 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 * Phase 3 / View contribution system — per-(plugin × extension ×\n * node × contribution) records collected from `ctx.emitContribution`\n * during the walk. AJV-validated at emit time against the slot's\n * payload schema; off-slot emissions are dropped silently before\n * landing here. The persistence layer flushes these via\n * `replaceAllScanContributions`. Empty for scans where no extension\n * declared `viewContributions` (the common case today).\n */\n contributions: IContributionRecord[];\n /**\n * Phase 3 / View contribution system — set of `(plugin, extension,\n * node)` tuples where `extract()` actually RAN this scan (cache\n * miss). Cached-extractor tuples are EXCLUDED so their prior rows\n * survive in `scan_contributions`. Format:\n * `<pluginId>/<extensionId>/<nodePath>`. Drives the per-tuple sweep\n * in the persistence layer (`IPersistOptions.freshlyRunTuples`).\n * Rules are folded in by the caller (`runScanInternal`) since they\n * always run; this set carries only the extractor-side tuples.\n */\n freshlyRunTuples: Set<string>;\n /**\n * Spec § 9.6.2 — orphan sidecar paths (`.sm` files without a sibling\n * `.md`). Discovered after the Provider walk completes so the rule\n * pass can emit `annotation-orphan` warnings. Survives across\n * scans only as derived state — no persistence, recomputed every\n * scan from the live filesystem.\n */\n orphanSidecars: IOrphanSidecar[];\n /**\n * Spec § 9.6.6 — raw parsed sidecar root keyed by `node.path`.\n * Plumbed through to the rule pass so semantic rules\n * (`core/unknown-field`) walk plugin namespaces / root keys without\n * re-reading `.sm` files from disk. Empty when no node carries a\n * parseable sidecar.\n */\n sidecarRoots: Map<string, Record<string, unknown>>;\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 contributions: IContributionRecord[];\n}> {\n const internalLinks: Link[] = [];\n const externalLinks: Link[] = [];\n const enrichmentBuffer = new Map<string, IEnrichmentRecord>();\n const contributions: IContributionRecord[] = [];\n // Schema validators are cached at module level (`loadSchemaValidators`),\n // so the cost of this lookup is module-scoped — pulling once per\n // node-extract pass keeps the closure capture clean without paying\n // per emission.\n const validators = loadSchemaValidators();\n\n for (const extractor of opts.extractors) {\n const qualifiedId = qualifiedExtensionId(extractor.pluginId, extractor.id);\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 // Extractors are deterministic-only; `is_probabilistic` is\n // reserved on the row for future Action-issued enrichments.\n isProbabilistic: false,\n });\n }\n };\n // Phase 3 — view contributions emit-time wiring. Three drop reasons,\n // all silent + `extension.error` event (mirror of `emitLink`):\n // 1. Extractor never declared `viewContributions[<id>]` —\n // reason: `unknown-contribution-id`.\n // 2. Declared `slot` is not in the closed catalog (also\n // caught at AJV manifest load, but defence-in-depth) —\n // reason: `unknown-slot`.\n // 3. Payload fails the slot's payload schema —\n // reason: AJV error string.\n // Accepted emissions append a record to the buffer; persistence\n // happens later via `replaceAllScanContributions`.\n const declaredContributions = readDeclaredContributions(extractor);\n const emitContribution = (contributionId: string, payload: unknown): void => {\n const declared = declaredContributions.get(contributionId);\n if (!declared) {\n emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {\n phase: 'emitContribution',\n contributionId,\n reason: 'unknown-contribution-id',\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {\n extractorId: qualifiedId,\n contributionId,\n nodePath: opts.node.path,\n }),\n });\n return;\n }\n const result = validators.validateContributionPayload(declared.slot, payload);\n if (!result.ok) {\n emitExtensionError(opts.emitter, qualifiedId, opts.node.path, {\n phase: 'emitContribution',\n contributionId,\n slot: declared.slot,\n reason: result.errors,\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {\n extractorId: qualifiedId,\n contributionId,\n nodePath: opts.node.path,\n slot: declared.slot,\n errors: result.errors,\n }),\n });\n return;\n }\n contributions.push({\n pluginId: extractor.pluginId,\n extensionId: extractor.id,\n nodePath: opts.node.path,\n contributionId,\n slot: declared.slot,\n payload,\n emittedAt: Date.now(),\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 emitContribution,\n store,\n );\n await extractor.extract(ctx);\n }\n\n return {\n internalLinks,\n externalLinks,\n enrichments: Array.from(enrichmentBuffer.values()),\n contributions,\n };\n}\n\n/**\n * Pull the manifest's `viewContributions` map into a `Map<contributionId,\n * { slot }>`. Called once per extractor per node — the result lives\n * for the duration of `runExtractorsForNode` and disappears with the\n * function frame, so no caching is required (the manifest is already\n * the canonical source).\n */\nfunction readDeclaredContributions(\n extension: { viewContributions?: unknown },\n): Map<string, { slot: string }> {\n const out = new Map<string, { slot: string }>();\n const raw = extension.viewContributions;\n if (typeof raw !== 'object' || raw === null) return out;\n for (const [id, value] of Object.entries(raw as Record<string, unknown>)) {\n if (typeof value !== 'object' || value === null) continue;\n const slot = (value as { slot?: unknown }).slot;\n if (typeof slot !== 'string') continue;\n out.set(id, { slot });\n }\n return out;\n}\n\n/**\n * Emit an `extension.error` event from the orchestrator's emit-time\n * drop paths (off-contract link, off-slot / unknown contribution\n * payload). Uses the same `makeEvent` shape as the rest of the file\n * so listeners (BFF SSE, CLI logger) see a uniform timestamp +\n * type + data envelope.\n */\nfunction emitExtensionError(\n emitter: ProgressEmitterPort,\n qualifiedId: string,\n nodePath: string,\n data: Record<string, unknown>,\n): void {\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'contribution-rejected',\n extensionId: qualifiedId,\n nodePath,\n ...data,\n }),\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 /**\n * sha256 of the canonical-form sidecar annotations for THIS node on\n * THIS scan. Consulted unconditionally — every Extractor's cached run\n * must have matched both this AND `bodyHash` to be reused. Always\n * populated by the caller; an absent sidecar canonicalises to `{}`.\n */\n sidecarAnnotationsHash: string;\n nodeHashCacheEligible: boolean;\n priorExtractorRuns: Map<string, Map<string, IPriorExtractorRun>> | 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 // Legacy fallback: caller did not load fine-grained breadcrumbs.\n // The sidecar-hash check would require per-extractor rows, so\n // every applicable extractor is assumed cached when the node-\n // level hashes match. Sidecar-edit invalidation is unavailable\n // on this code path; callers that need it must opt into the\n // fine-grained Map.\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 =\n opts.priorExtractorRuns.get(opts.nodePath) ?? new Map<string, IPriorExtractorRun>();\n for (const ex of applicableExtractors) {\n const qualified = qualifiedExtensionId(ex.pluginId, ex.id);\n const prior = priorRunsForNode.get(qualified);\n const bodyMatch = prior !== undefined && prior.bodyHash === opts.bodyHash;\n // Sidecar-hash gate applies to every extractor unconditionally.\n // The author-facing alternative (an opt-in `readsSidecar` flag)\n // was rejected because forgetting it produces a silent stale-data\n // bug — sidecar edits don't refresh until something in the `.md`\n // changes. Universal invalidation costs an extractor re-run per\n // node on `.sm` edits (negligible: sidecars change rarely and\n // extractors are pure-CPU); the gain is zero cognitive load for\n // plugin authors and zero correctness traps.\n const sidecarOk =\n prior !== undefined &&\n prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash;\n if (opts.nodeHashCacheEligible && bodyMatch && sidecarOk) {\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 sidecarAnnotationsHash: 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). Carry the live sidecar-annotations hash\n // on every record — non-sidecar-readers ignore it on the next cache\n // decision, sidecar-readers consult it.\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 sidecarAnnotationsHashAtRun: opts.sidecarAnnotationsHash,\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 // Phase 3 / View contributions — flat buffer (no per-node dedup\n // because the qualified id `<pluginId>/<extensionId>/<contributionId>`\n // is structurally unique within a single scan). Per-(plugin × node ×\n // contribution) re-emissions inside one scan are caller-error and\n // simply produce two records — the persistence layer's PRIMARY KEY\n // takes the last-write-wins decision when the buffer flushes.\n const contributionsBuffer: IContributionRecord[] = [];\n // Phase 3 / View contributions — accumulator of (plugin, extension,\n // node) tuples where extract() actually RAN this scan (cache miss).\n // Cached extractors don't push here — their prior `scan_contributions`\n // rows must be preserved. Drives the per-tuple sweep documented in\n // `spec/architecture.md` §View contribution system → Persistence.\n // Format: `<pluginId>/<extensionId>/<nodePath>`.\n const freshlyRunTuples = new Set<string>();\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 // Spec § 9.6.6 — raw parsed sidecar root keyed by `node.path`.\n // Threaded through to the rule pass so semantic rules\n // (`core/unknown-field`) can reason about plugin namespaces and root\n // keys without re-reading the `.sm` file from disk. Populated by\n // `resolveAndApplySidecar` below.\n const sidecarRoots = new Map<string, Record<string, unknown>>();\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 (`'core/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 // Path-dedup across the multi-provider walk. Spec § Provider dispatch\n // (architecture.md): every Provider walks the full root, but each\n // file is offered to at most ONE Provider's `classify`. The first\n // Provider in iteration order whose `classify` returns non-null\n // claims the file; subsequent Providers see the path as\n // already-claimed and skip. Without this, the universal markdown\n // fallback (`core/markdown`, registered LAST) would re-claim every\n // file vendor Providers already classified, double-emitting nodes.\n // The Set is per-scan (rebuilt each call) so successive `runScan`\n // invocations start clean.\n const claimedPaths = new Set<string>();\n\n for (const provider of providers) {\n for await (const raw of resolveProviderWalk(provider)(roots, walkOptions)) {\n filesWalked += 1;\n if (claimedPaths.has(raw.path)) continue;\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 if (kind === null) {\n // Provider disclaimed the file — another Provider may claim\n // it on its own walk pass, or the file is outside every\n // active Provider's territory. Wired to the `filesSkipped`\n // stat below would require threading a counter through\n // `walked`; for now the field stays at the legacy `0` and\n // the disclaim path is observed via test assertions.\n continue;\n }\n claimedPaths.add(raw.path);\n index += 1;\n\n // Resolve the sidecar overlay BEFORE the cache decision so we\n // can hash `overlay.annotations` and feed it into the cache key\n // alongside body+frontmatter. A sidecar edit changes neither the\n // body nor the frontmatter, so without this hash the cache would\n // silently reuse stale contributions for any extractor that read\n // the sidecar (`core/stability`, `core/annotations`, …). The hash\n // is consulted unconditionally — see `computeCacheDecision` for\n // the trade-off rationale.\n const sidecarResolution = resolveSidecarOverlay(\n raw.path, raw.path, roots, bodyHash, frontmatterHash,\n );\n const sidecarAnnotationsHash = sha256(\n canonicalSidecarAnnotations(sidecarResolution.overlay.annotations),\n );\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 sidecarAnnotationsHash,\n nodeHashCacheEligible,\n priorExtractorRuns,\n });\n const {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds,\n missingExtractors,\n fullCacheHit,\n } = cacheDecision;\n\n // Shared step: attach the freshly-resolved sidecar overlay to a\n // node and surface its issues + parsed root. Used by every\n // branch below so the apply path stays uniform.\n const attachSidecar = (node: Node): Issue[] => {\n node.sidecar = sidecarResolution.overlay;\n if (sidecarResolution.parsedRoot !== null) {\n sidecarRoots.set(node.path, sidecarResolution.parsedRoot);\n }\n return sidecarResolution.issues.map((i) =>\n i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node.path] },\n );\n };\n\n if (fullCacheHit && priorNode) {\n const reused = reusePriorNode({\n priorNode,\n bodyHash,\n sidecarAnnotationsHash,\n strict,\n cachedQualifiedIds,\n applicableQualifiedIds,\n shortIdToQualified,\n priorLinksByOriginating,\n priorFrontmatterIssuesByNode,\n });\n // Spec § 9.6.2 — sidecars are read on EVERY scan (not cached)\n // because `.sm` lives outside the body/frontmatter hash domain.\n // Attach the freshly-resolved overlay; the persistence layer\n // projects `stability` and `version` from\n // `node.sidecar.annotations.*` when it writes the indexed\n // columns.\n const reusedSidecarIssues = attachSidecar(reused.node);\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 issue of reusedSidecarIssues) 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 // Spec § 9.6.2 — sidecar overlay applies to BOTH freshly-built\n // and partial-cache nodes. Done after the node is in `nodes[]`\n // so a downstream consumer iterating `nodes` sees the overlay\n // applied (mutation is in-place on the same object reference).\n const sidecarIssues = attachSidecar(node);\n for (const issue of sidecarIssues) frontmatterIssues.push(issue);\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 // Phase 3 — record (plugin, extension, node) for every extractor\n // that actually runs against this node this scan. The persist\n // layer uses this to drop stale `scan_contributions` rows for\n // extractors that previously emitted but no longer do (e.g.\n // body change removes the trigger). Cached extractors are NOT\n // recorded — their rows must survive untouched.\n for (const ex of extractorsToRun) {\n // NUL-separated (see analyzer fold above for the rationale).\n freshlyRunTuples.add(`${ex.pluginId}\\0${ex.id}\\0${node.path}`);\n }\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 // Phase 3 — fold per-node view contributions into the scan-wide\n // buffer. The persistence layer flushes on transaction commit\n // via `replaceAllScanContributions`.\n for (const c of extractResult.contributions) contributionsBuffer.push(c);\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. Always populate\n // `sidecarAnnotationsHashAtRun`; non-sidecar-readers ignore it\n // on the next decision but the column is non-null going forward.\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 sidecarAnnotationsHashAtRun: sidecarAnnotationsHash,\n });\n }\n }\n }\n\n // Spec § 9.6.2 — orphan sidecar sweep. Walks the same roots looking\n // for `*.sm` whose sibling `*.md` is missing. The list flows through\n // to the rule pass; `annotation-orphan` emits one warning per entry.\n const orphanSidecars = discoverOrphanSidecars(roots);\n\n return {\n nodes,\n internalLinks,\n externalLinks,\n cachedPaths,\n frontmatterIssues,\n filesWalked,\n enrichments: [...enrichmentBuffer.values()],\n extractorRuns,\n contributions: contributionsBuffer,\n freshlyRunTuples,\n orphanSidecars,\n sidecarRoots,\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 (`'core/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 analyzer over the merged graph. Analyzers see internal\n * links only — broken-ref / trigger-collision / superseded all reason\n * about graph relations, not URLs.\n *\n * Analyzers MAY emit per-node view contributions via\n * `ctx.emitContribution(nodePath, contributionId, payload)`. The\n * orchestrator validates each emission against the slot's payload\n * schema (mirror of the Extractor emit path) and silently drops\n * invalid emissions with an `extension.error` event. Accepted\n * emissions land on the returned `contributions[]` and reach\n * `scan_contributions` via the same persistence pipeline as\n * Extractor-emitted contributions.\n */\nasync function runAnalyzers(\n analyzers: IAnalyzer[],\n nodes: Node[],\n internalLinks: Link[],\n orphanSidecars: IOrphanSidecar[],\n sidecarRoots: ReadonlyMap<string, Record<string, unknown>>,\n annotationContributions: readonly IRegisteredAnnotationKey[],\n viewContributions: readonly IRegisteredViewContribution[],\n orphanJobFiles: readonly string[],\n referenceablePaths: ReadonlySet<string> | undefined,\n cwd: string | undefined,\n emitter: ProgressEmitterPort,\n hookDispatcher: IHookDispatcher,\n): Promise<{ issues: Issue[]; contributions: IContributionRecord[] }> {\n const issues: Issue[] = [];\n const contributions: IContributionRecord[] = [];\n const validators = loadSchemaValidators();\n // Project the kernel-internal `IOrphanSidecar` shape to the analyzer-\n // facing `IAnalyzerOrphanSidecar`: analyzers don't need the absolute\n // `.sm` path, just the relative path + the expected `.md`.\n const analyzerOrphans = orphanSidecars.map((o) => ({\n relativePath: o.relativePath,\n expectedMdPath: o.expectedMdPath,\n }));\n for (const analyzer of analyzers) {\n const qualifiedId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);\n const declaredContributions = readDeclaredContributions(analyzer);\n const emitContribution = (\n nodePath: string,\n contributionId: string,\n payload: unknown,\n ): void => {\n const declared = declaredContributions.get(contributionId);\n if (!declared) {\n emitExtensionError(emitter, qualifiedId, nodePath, {\n phase: 'emitContribution',\n contributionId,\n reason: 'unknown-contribution-id',\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionUnknownId, {\n extractorId: qualifiedId,\n contributionId,\n nodePath,\n }),\n });\n return;\n }\n const result = validators.validateContributionPayload(declared.slot, payload);\n if (!result.ok) {\n emitExtensionError(emitter, qualifiedId, nodePath, {\n phase: 'emitContribution',\n contributionId,\n slot: declared.slot,\n reason: result.errors,\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorContributionPayloadInvalid, {\n extractorId: qualifiedId,\n contributionId,\n nodePath,\n slot: declared.slot,\n errors: result.errors,\n }),\n });\n return;\n }\n contributions.push({\n pluginId: analyzer.pluginId,\n extensionId: analyzer.id,\n nodePath,\n contributionId,\n slot: declared.slot,\n payload,\n emittedAt: Date.now(),\n });\n };\n const emitted = await analyzer.evaluate({\n nodes,\n links: internalLinks,\n orphanSidecars: analyzerOrphans,\n sidecarRoots,\n annotationContributions,\n viewContributions,\n orphanJobFiles,\n ...(referenceablePaths ? { referenceablePaths } : {}),\n ...(cwd ? { cwd } : {}),\n emitContribution,\n });\n for (const issue of emitted) {\n const validated = validateIssue(analyzer, issue, emitter);\n if (validated) issues.push(validated);\n }\n // Spec § A.11 — `analyzer.completed`. Aggregated per Analyzer, after every\n // issue has been validated. Fan-out scope: one event per Analyzer per\n // scan. The payload carries the qualified analyzer id so a hook with\n // `filter: { analyzerId: '...' }` can scope to a single analyzer.\n const evt = makeEvent('analyzer.completed', { analyzerId: qualifiedId });\n emitter.emit(evt);\n await hookDispatcher.dispatch('analyzer.completed', evt);\n }\n return { issues, contributions };\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 analyzerId: '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 analyzerId: '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 analyzerId: '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 analyzer 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\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 // The Node surface no longer carries `title` / `description` /\n // `stability` / `version` denormalisations. Consumers that need\n // them read from the canonical sources directly:\n // - title / description → `node.frontmatter.{name,description}`\n // - stability / version → `node.sidecar.annotations.{stability,version}`\n // The persistence layer keeps these as denormalised SQL columns on\n // `scan_nodes` (used for `--sort-by`, faceted listings) and projects\n // them at write time from the same canonical sources.\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 };\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\n/**\n * Canonical-form rationale — same deterministic-across-formatters story\n * as `canonicalFrontmatter`, applied to the `node.sidecar.annotations`\n * block. Used to hash the sidecar contribution that participates in\n * the per-`(node, extractor)` cache key alongside `bodyHash`.\n *\n * - Absent sidecar / present but no `annotations` block / annotations\n * literally `{}` all canonicalise to `{}` so the hash is stable\n * across \"no sidecar\" → \"empty annotations\" transitions and a\n * plain sidecar-less node never accidentally invalidates the cache.\n * - Object keys are sorted by `yaml.dump({ sortKeys: true })` so a\n * hand-edit that only re-orders keys produces the same hash.\n */\nfunction canonicalSidecarAnnotations(\n annotations: Record<string, unknown> | null | undefined,\n): string {\n if (!annotations || typeof annotations !== 'object' || Array.isArray(annotations)) {\n return yaml.dump({}, { sortKeys: true, lineWidth: -1, noRefs: true, noCompatMode: true });\n }\n return yaml.dump(annotations, {\n sortKeys: true,\n lineWidth: -1,\n noRefs: true,\n noCompatMode: true,\n });\n}\n\n/**\n * Pure overlay-resolution step (Step 9.6.2): tries each scan root in\n * order to find the absolute `.md` path the sidecar should accompany;\n * reads the `.sm`, validates it against the spec schemas, computes\n * drift, and produces an overlay value the caller then attaches to\n * the Node.\n *\n * Pulled apart from the original `resolveAndApplySidecar` so the\n * orchestrator can hash `overlay.annotations` BEFORE the cache\n * decision (the cache key includes the sidecar hash alongside body\n * and frontmatter). The previous \"all-in-one\" mutator survives as a\n * thin wrapper for call sites that don't need the hash early.\n *\n * Schema-invalid or YAML-malformed sidecars yield an `invalid-sidecar`\n * issue but do not crash the scan: the node still scans with `present`\n * = true and `status` = null. On parse success, `annotations` lands\n * on the overlay along with the full parsed root.\n */\ninterface ISidecarResolution {\n overlay: NonNullable<Node['sidecar']>;\n issues: Issue[];\n parsedRoot: Record<string, unknown> | null;\n}\n\nfunction resolveSidecarOverlay(\n relativePath: string,\n nodePathForIssue: string,\n roots: readonly string[],\n liveBodyHash: string,\n liveFrontmatterHash: string,\n): ISidecarResolution {\n const issues: Issue[] = [];\n const mdAbs = resolveAbsoluteMdPath(relativePath, roots);\n if (mdAbs === null) {\n return { overlay: { present: false }, issues, parsedRoot: null };\n }\n\n const result = readSidecarFor(mdAbs);\n if (!result.present) {\n return { overlay: { present: false }, issues, parsedRoot: null };\n }\n\n // A sidecar file exists. Even if parsing failed we mark `present`\n // true so `scan_nodes.sidecar_present = 1`; status stays null.\n if (result.parsed === null) {\n for (const parseIssue of result.issues) {\n issues.push({\n analyzerId: 'invalid-sidecar',\n severity: 'warn',\n nodeIds: [nodePathForIssue],\n message: parseIssue.message,\n data: { sidecarPath: relativePathFromRoots(mdAbs, roots) },\n });\n }\n return {\n overlay: { present: true, status: null, annotations: null, root: null },\n issues,\n parsedRoot: null,\n };\n }\n\n const status = computeDriftStatus({\n storedBodyHash: result.parsed.identityBodyHash,\n storedFrontmatterHash: result.parsed.identityFrontmatterHash,\n liveBodyHash,\n liveFrontmatterHash,\n });\n return {\n // R15 closure (2026-05-07) — surface the full parsed root on the\n // overlay so BFF consumers (UI inspector audit / plugin-contributions\n // / debug panels) can read `for.*`, `audit.*`, `settings.*`, and\n // plugin-namespaced sub-keys without re-reading the file. The\n // `annotations` field above stays — it duplicates `root.annotations`\n // by design so existing consumers keep working unchanged.\n overlay: {\n present: true,\n status,\n annotations: result.parsed.annotations,\n root: result.parsed.raw,\n },\n issues,\n parsedRoot: result.parsed.raw,\n };\n}\n\n// `applyAnnotationsOverlay` was previously responsible for projecting\n// `annotations.{stability,version}` onto `node.{stability,version}`.\n// Those fields no longer exist on the Node surface — consumers read\n// from `node.sidecar.annotations.*` directly, and the persistence\n// layer projects to indexed SQL columns at write time. The function\n// is gone; the orchestrator's main loop attaches the overlay\n// in-place via `attachSidecar`.\n\nfunction resolveAbsoluteMdPath(\n relativePath: string,\n roots: readonly string[],\n): string | null {\n if (isAbsolute(relativePath)) {\n return existsSync(relativePath) ? relativePath : null;\n }\n for (const root of roots) {\n const candidate = resolvePath(root, relativePath);\n if (existsSync(candidate)) return candidate;\n }\n return null;\n}\n\nfunction relativePathFromRoots(\n absolutePath: string,\n roots: readonly string[],\n): string {\n for (const root of roots) {\n const abs = resolvePath(root);\n if (absolutePath.startsWith(`${abs}/`) || absolutePath.startsWith(`${abs}\\\\`)) {\n return absolutePath.slice(abs.length + 1).split(/[\\\\/]/).join('/');\n }\n }\n return absolutePath;\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 emitContribution: (contributionId: string, payload: unknown) => 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 emitContribution,\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 analyzerId: '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 analyzerId: '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(analyzer: IAnalyzer, issue: Issue, emitter: ProgressEmitterPort): Issue | null {\n const severity: Severity | undefined = issue.severity;\n if (severity !== 'error' && severity !== 'warn' && severity !== 'info') {\n // Analyzer 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 = `${analyzer.pluginId}/${analyzer.id}`;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'issue-invalid-severity',\n extensionId: qualifiedId,\n severity,\n issue: { analyzerId: issue.analyzerId || analyzer.id, message: issue.message, nodeIds: issue.nodeIds },\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorIssueInvalidSeverity, {\n analyzerId: qualifiedId,\n severity: JSON.stringify(severity),\n }),\n }),\n );\n return null;\n }\n return { ...issue, analyzerId: issue.analyzerId || analyzer.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`. With Extractors deterministic-only no row is\n * stale-flagged in this revision; the filter is preserved for the\n * future Action-issued enrichment revision (queued LLM jobs whose\n * output must survive body changes), where stale visibility\n * belongs to the UI layer 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.21.0\",\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 \"reference\": \"node scripts/build-reference.js\",\n \"reference:check\": \"node scripts/build-reference.js --check\",\n \"validate\": \"npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run reference:check\",\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' 'server/**/*.test.ts'\",\n \"test:ci\": \"tsc --noEmit && node --import tsx --test 'test/**/*.test.ts' 'built-in-plugins/**/*.test.ts' 'kernel/**/*.test.ts' 'server/**/*.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' 'server/**/*.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' 'server/**/*.test.ts'\",\n \"clean\": \"rm -rf dist coverage\"\n },\n \"dependencies\": {\n \"@hono/node-server\": \"2.0.1\",\n \"@skill-map/spec\": \"0.21.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 * Sidecar reader (Step 9.6.2).\n *\n * Parses a co-located `.sm` YAML sidecar next to a `.md` node and\n * validates it against `spec/schemas/sidecar.schema.json` and\n * `spec/schemas/annotations.schema.json`. Returns a typed\n * `IParsedSidecar` (or `null` if no sidecar accompanies the node) plus\n * a list of validation issues for the caller to fold into the scan\n * result.\n *\n * Design notes:\n *\n * - YAML parsing via `js-yaml` (already on the dependency tree, used\n * by the orchestrator's canonical-frontmatter helper).\n * - AJV validators are compiled once and cached in module scope (the\n * sidecar shape is static). The cache mirrors\n * `loadSchemaValidators` for the existing kernel schemas.\n * - Malformed YAML or schema-invalid sidecars do NOT crash the scan\n * — the caller emits an `invalid-sidecar` issue and proceeds with\n * no overlay (the node still scans with the new columns set to\n * `sidecarPresent = 1` / `sidecarStatus = null`).\n */\n\nimport { existsSync, 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';\nimport yaml from 'js-yaml';\n\nimport { applyAjvFormats } from '../util/ajv-interop.js';\n\nexport interface IParsedSidecar {\n /** Path to the `.sm` file on disk (absolute). */\n filePath: string;\n /** `identity.bodyHash` — sha256 of the body at the last bump. */\n identityBodyHash: string;\n /** `identity.frontmatterHash` — sha256 of the canonical frontmatter at the last bump. */\n identityFrontmatterHash: string;\n /** `identity.path` — relative path to the `.md` node. */\n identityPath: string;\n /** Parsed `annotations:` block. `null` if absent or empty. */\n annotations: Record<string, unknown> | null;\n /** Full parsed root object (for plugin namespace access). */\n raw: Record<string, unknown>;\n}\n\nexport interface ISidecarParseIssue {\n /** Human-readable reason. The orchestrator wraps this in the\n * `invalid-sidecar` rule message before it surfaces to the user. */\n message: string;\n}\n\nexport interface ISidecarReadResult {\n parsed: IParsedSidecar | null;\n /**\n * `true` when a `.sm` file existed at the resolved path (regardless of\n * parse success). Drives `scan_nodes.sidecar_present` so the row keeps\n * tracking the file's existence even when its contents are unusable.\n */\n present: boolean;\n issues: ISidecarParseIssue[];\n}\n\n/**\n * Resolve `<mdAbsolutePath>.replace(.md, .sm)` and read + validate\n * the sidecar at that location. The `.sm` file is optional — when\n * absent the result is `{ parsed: null, present: false, issues: [] }`.\n */\n// Linear pipeline with one branch per failure mode (file-missing,\n// read-error, YAML-parse-error, root-not-mapping, schema-invalid,\n// happy-path). Each branch returns directly; cyclomatic count\n// counts them all but there's no actual nested logic.\n// eslint-disable-next-line complexity\nexport function readSidecarFor(mdAbsolutePath: string): ISidecarReadResult {\n const sidecarPath = sidecarPathFor(mdAbsolutePath);\n if (!existsSync(sidecarPath)) {\n return { parsed: null, present: false, issues: [] };\n }\n\n let raw: string;\n try {\n raw = readFileSync(sidecarPath, 'utf8');\n } catch (err) {\n return {\n parsed: null,\n present: true,\n issues: [{ message: `cannot read ${sidecarPath}: ${(err as Error).message}` }],\n };\n }\n\n let parsedYaml: unknown;\n try {\n parsedYaml = yaml.load(raw);\n } catch (err) {\n return {\n parsed: null,\n present: true,\n issues: [{ message: `malformed YAML in ${sidecarPath}: ${(err as Error).message}` }],\n };\n }\n\n if (!isPlainObject(parsedYaml)) {\n return {\n parsed: null,\n present: true,\n issues: [{ message: `sidecar root must be a YAML mapping at ${sidecarPath}` }],\n };\n }\n\n const sidecarValidator = getSidecarValidator();\n if (!sidecarValidator(parsedYaml)) {\n const errors = (sidecarValidator.errors ?? [])\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n return {\n parsed: null,\n present: true,\n issues: [{ message: `sidecar schema validation failed at ${sidecarPath}: ${errors}` }],\n };\n }\n\n const root = parsedYaml as Record<string, unknown>;\n const identityBlock = root['identity'] as Record<string, unknown>;\n const annotationsRaw = root['annotations'];\n const annotations = isPlainObject(annotationsRaw)\n ? Object.keys(annotationsRaw).length === 0\n ? null\n : (annotationsRaw as Record<string, unknown>)\n : null;\n\n return {\n parsed: {\n filePath: sidecarPath,\n identityBodyHash: String(identityBlock['bodyHash']),\n identityFrontmatterHash: String(identityBlock['frontmatterHash']),\n identityPath: String(identityBlock['path']),\n annotations,\n raw: root,\n },\n present: true,\n issues: [],\n };\n}\n\n/**\n * Compute the sidecar path for a given `.md` file. Co-located: same\n * directory, same basename, extension swapped to `.sm`. Files that do\n * not end in `.md` (Provider future-proofing) get the `.sm` suffix\n * appended.\n */\nexport function sidecarPathFor(mdAbsolutePath: string): string {\n if (mdAbsolutePath.endsWith('.md')) {\n return `${mdAbsolutePath.slice(0, -'.md'.length)}.sm`;\n }\n return `${mdAbsolutePath}.sm`;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nlet cachedSidecarValidator: ValidateFunction | null = null;\n\nfunction getSidecarValidator(): ValidateFunction {\n if (cachedSidecarValidator) return cachedSidecarValidator;\n const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n\n const specRoot = resolveSpecRoot();\n const annotationsSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/annotations.schema.json'), 'utf8'),\n );\n const sidecarSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/sidecar.schema.json'), 'utf8'),\n );\n ajv.addSchema(annotationsSchema);\n cachedSidecarValidator = ajv.compile(sidecarSchema);\n return cachedSidecarValidator;\n}\n\n/**\n * Test-only escape hatch — drop the cached validator so a test can\n * rebuild it after monkey-patching the spec package.\n */\nexport function _resetSidecarValidatorCacheForTests(): void {\n cachedSidecarValidator = null;\n}\n\nfunction resolveSpecRoot(): string {\n const require = createRequire(import.meta.url);\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 — sidecar reader cannot load schemas.',\n );\n }\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 * Drift detection for sidecar `.sm` files (Step 9.6.2).\n *\n * Compares the hashes captured the last time a sidecar was bumped\n * (`for.bodyHash` / `for.frontmatterHash` from the parsed sidecar)\n * against the live node hashes computed during the current scan.\n *\n * Returns one of four states:\n *\n * - `'fresh'` — both hashes match; sidecar is up to date.\n * - `'stale-body'` — body changed since last bump.\n * - `'stale-frontmatter'` — frontmatter changed since last bump.\n * - `'stale-both'` — both changed since last bump.\n *\n * Stale state is **derived**, never stored persistently — pure\n * function over hashes already on the node. The `scan_nodes.sidecar_status`\n * column caches the result for fast queries but the kernel re-derives\n * it on every scan.\n */\n\nimport type { SidecarStatus } from '../types.js';\n\nexport function computeDriftStatus(args: {\n storedBodyHash: string;\n storedFrontmatterHash: string;\n liveBodyHash: string;\n liveFrontmatterHash: string;\n}): SidecarStatus {\n const bodyDrift = args.storedBodyHash !== args.liveBodyHash;\n const fmDrift = args.storedFrontmatterHash !== args.liveFrontmatterHash;\n if (bodyDrift && fmDrift) return 'stale-both';\n if (bodyDrift) return 'stale-body';\n if (fmDrift) return 'stale-frontmatter';\n return 'fresh';\n}\n","/**\n * Orphan sidecar discovery (Step 9.6.2).\n *\n * Walks the scan roots, finds every `*.sm` file, and returns the\n * paths whose accompanying `*.md` does NOT exist on disk. The\n * `annotation-orphan` built-in rule consumes the result and emits one\n * warning per stranded sidecar.\n *\n * Implementation is intentionally a fresh walk (rather than piggy-\n * backing on the Provider walk) — the Provider only yields `.md`\n * files; orphans are exactly the `.sm` files that have no corresponding\n * `.md` to anchor them, so we need an `.sm`-driven sweep.\n */\n\nimport { existsSync, readdirSync, statSync } from 'node:fs';\nimport { join, relative, sep } from 'node:path';\n\nexport interface IOrphanSidecar {\n /** Absolute path to the orphan `.sm` file. */\n sidecarPath: string;\n /** Relative path (POSIX-separated) from the root that contained it. */\n relativePath: string;\n /** Absolute path of the `.md` file the sidecar was expected to accompany. */\n expectedMdPath: string;\n}\n\n/**\n * Find orphaned `.sm` files across the supplied roots. A `.sm` is an\n * orphan when its sibling `<basename>.md` does not exist.\n *\n * Walks the filesystem directly. Symbolic links are skipped (mirrors\n * the Claude Provider's walk policy — audit M7). Errors reading a\n * directory are swallowed silently; the walk degrades to \"no orphans\n * found in that subtree\".\n */\nexport function discoverOrphanSidecars(\n roots: readonly string[],\n shouldSkip?: (relativePath: string) => boolean,\n): IOrphanSidecar[] {\n const out: IOrphanSidecar[] = [];\n for (const root of roots) {\n walk(root, root, shouldSkip ?? (() => false), out);\n }\n return out;\n}\n\n// Recursive directory walker with five guards (try/catch, skip filter,\n// symlink check, isDirectory recursion, isFile + extension check). The\n// shape mirrors the Claude Provider's walker — same tradeoff applies.\n// eslint-disable-next-line complexity\nfunction walk(\n root: string,\n current: string,\n shouldSkip: (relativePath: string) => boolean,\n out: IOrphanSidecar[],\n): void {\n let entries;\n try {\n entries = readdirSync(current, { withFileTypes: true, encoding: 'utf8' });\n } catch {\n return;\n }\n for (const entry of entries) {\n const full = join(current, entry.name);\n const rel = relative(root, full).split(sep).join('/');\n if (shouldSkip(rel)) continue;\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n walk(root, full, shouldSkip, out);\n continue;\n }\n if (!entry.isFile()) continue;\n if (!entry.name.endsWith('.sm')) continue;\n const expectedMd = `${full.slice(0, -'.sm'.length)}.md`;\n if (existsSync(expectedMd) && safeIsFile(expectedMd)) continue;\n out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });\n }\n}\n\nfunction safeIsFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n","/**\n * Sidecar write channel (Step 9.6.3, Decision #125).\n *\n * `ISidecarStore` is the kernel's port for materialising patches against\n * `<basename>.sm` files. Mirrors `StoragePort`'s shape (port + driving\n * adapter) but writes co-located YAML files in the repo rather than rows\n * in SQLite. The built-in `bump` Action returns a deep-merge patch\n * (`TActionWrite { kind: 'sidecar', path, changes }`) and the kernel\n * dispatches each entry through the active `ISidecarStore`.\n *\n * Atomicity is owned by the Store, not the Action: Actions stay pure\n * (testable / dry-runnable), and the read-modify-write critical section\n * lives inside `applyPatch()`. Two concurrent `applyPatch()` calls on the\n * same path are serialised via a path-keyed in-process mutex (chained\n * promise pattern, no external dep — mirrors `AsyncMutex` in\n * `adapters/sqlite/dialect.ts`).\n *\n * The on-disk write itself is atomic via the standard write-to-`.tmp`\n * + POSIX `rename` pattern. The `.tmp` file is a sibling of the target\n * (same directory) so the rename is guaranteed atomic on POSIX. Per\n * AGENTS.md this is the established atomic-write pattern; the AGENTS.md\n * `.tmp/` baseline applies to scratch / smoke-test directories, not to\n * sibling temp files used for atomic rename.\n *\n * Comment / key-order preservation is OUT OF SCOPE for 9.6.3 — `js-yaml`\n * loses comments and stable key order on round-trip. Flagged for the\n * Step 9.6 review queue (see ROADMAP §Step 9.6).\n */\n\nimport { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\nimport yaml from 'js-yaml';\n\nimport { ensureSidecarWritesAllowed } from '../../core/config/sidecar-consent.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\n\n/**\n * Consent + runtime context required to gate a `.sm` write through\n * `ensureSidecarWritesAllowed` (per `spec/architecture.md` §Annotation\n * system · Write consent). The caller threads its own\n * `IRuntimeContext` (`cwd`, `homedir`) plus the operator's confirmation\n * signal — `true` when consent was already secured (`--yes` on the\n * CLI, `confirm: true` in the BFF body) and `false` otherwise.\n */\nexport interface ISidecarWriteConsent {\n confirm: boolean;\n cwd: string;\n homedir: string;\n}\n\n/**\n * Sidecar persistence port. Implementations MUST guarantee:\n *\n * 1. Two concurrent `applyPatch(samePath, ...)` calls are serialised.\n * 2. The read-modify-write cycle (read on-disk file → deep-merge patch\n * → schema-validate the merged result → write) is atomic from any\n * observer's view.\n * 3. A schema-invalid merge result throws and leaves the file\n * unchanged on disk — no partial writes.\n * 4. First-time bump (file did not exist) creates the `.sm` file.\n * 5. The consent gate runs BEFORE any disk I/O — when\n * `allowEditSmFiles` is false and `consent.confirm` is false, the\n * store throws `EConsentRequiredError` and the file is unchanged.\n */\nexport interface ISidecarStore {\n /**\n * Apply a deep-merge patch to the sidecar at `sidecarAbsPath`.\n *\n * - `changes` is treated as a partial sidecar root. Object values\n * are merged recursively into the existing object at the same\n * path; array values REPLACE any existing array (no element-wise\n * merge — arrays in the annotation catalog are inherently\n * ordered or set-like and there is no safe element-merge\n * semantics).\n * - The merged result MUST validate against `sidecar.schema.json` +\n * `annotations.schema.json` via the kernel AJV stack. Validation\n * failure throws a structured `Error`; the file is unchanged.\n * - The consent gate runs first: when `allowEditSmFiles` is false\n * and `consent.confirm` is false, the store throws\n * `EConsentRequiredError` and never touches disk. When\n * `consent.confirm` is true, the gate flips the flag to true in\n * `project-local` settings before the write proceeds.\n *\n * @param sidecarAbsPath absolute path to the `.sm` file to patch.\n * @param changes deep-merge patch; only the keys to set need be present.\n * @param consent confirm + runtime context bag — required; the\n * caller is the only party with the operator's intent.\n */\n applyPatch(\n sidecarAbsPath: string,\n changes: Record<string, unknown>,\n consent: ISidecarWriteConsent,\n ): Promise<void>;\n}\n\n/**\n * Filesystem-backed `ISidecarStore`. Composed at the kernel boot site\n * and threaded through `IActionContext` consumers (the orchestrator's\n * Action dispatcher in 9.6.4 and beyond).\n */\nexport class FilesystemSidecarStore implements ISidecarStore {\n /**\n * Path-keyed in-process lock chain. Each path maps to the tail of a\n * promise chain; new requests await the tail and replace it with\n * their own completion promise. When the chain settles back to\n * `undefined`-equivalent the entry is GC-eligible (we don't bother\n * pruning because the keyspace is bounded by the number of `.sm`\n * files in the repo and entries are tiny).\n */\n #locks = new Map<string, Promise<void>>();\n\n async applyPatch(\n sidecarAbsPath: string,\n changes: Record<string, unknown>,\n consent: ISidecarWriteConsent,\n ): Promise<void> {\n // Consent gate FIRST — if the operator has not granted permission\n // to write `.sm` files in this project, abort before taking the\n // path-keyed lock or touching disk. `ensureSidecarWritesAllowed`\n // throws `EConsentRequiredError`; the caller (CLI verb / BFF\n // route) catches and surfaces it as an interactive prompt or a\n // 412 envelope.\n ensureSidecarWritesAllowed({\n confirm: consent.confirm,\n cwd: consent.cwd,\n homedir: consent.homedir,\n });\n\n const prev = this.#locks.get(sidecarAbsPath) ?? Promise.resolve();\n let release: () => void;\n const settled = new Promise<void>((res) => {\n release = res;\n });\n // Chain: every newcomer waits for `prev` AND `settled` (this call\n // doing its work) before they enter. The chained tail is what we\n // store as the new tail, so the next caller waits for everything\n // that came before plus us.\n const tail = prev.then(() => settled);\n this.#locks.set(sidecarAbsPath, tail);\n try {\n await prev;\n this.#applyPatchSync(sidecarAbsPath, changes);\n } finally {\n release!();\n // If we are still the recorded tail, drop the entry to allow GC.\n if (this.#locks.get(sidecarAbsPath) === tail) {\n this.#locks.delete(sidecarAbsPath);\n }\n }\n }\n\n #applyPatchSync(sidecarAbsPath: string, changes: Record<string, unknown>): void {\n const current = readSidecarObject(sidecarAbsPath);\n const merged = deepMerge(current, changes);\n const validator = getSidecarValidator();\n if (!validator(merged)) {\n const errors = (validator.errors ?? [])\n .map((e) => `${e.instancePath || '(root)'} ${e.message ?? e.keyword}`)\n .join('; ');\n throw new Error(\n `sidecar patch produces a schema-invalid result at ${sidecarAbsPath}: ${errors}`,\n );\n }\n const yamlText = yaml.dump(merged, {\n sortKeys: true,\n lineWidth: -1,\n noRefs: true,\n noCompatMode: true,\n });\n atomicWriteFile(sidecarAbsPath, yamlText);\n }\n}\n\n/**\n * Deep-merge `patch` into `base`, returning a new object. Semantics:\n *\n * - Both values are plain objects → recurse key-by-key.\n * - `patch` is an array → REPLACES `base` at this position (no\n * element-wise merge).\n * - `patch` is `null` → DELETES the key from the result (whether or\n * not `base` had it). This is the patch's \"erase\" sentinel.\n * Persisted sidecars never contain literal nulls because the\n * schema rejects them on every typed property; the null only\n * ever lives in the in-flight patch object. Currently no caller;\n * retained as a generic primitive for future actions that need\n * per-write delete semantics.\n * - `patch` is any other primitive → REPLACES `base` at this position.\n * - Key only in `base` → carried through unchanged.\n * - Key only in `patch` (and not `null`) → set on the result.\n *\n * Mirrors `lodash.merge` for the object/array decisions but written by\n * hand to avoid pulling in the dep. Pure (no input mutation).\n */\nexport function deepMerge(\n base: Record<string, unknown>,\n patch: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = { ...base };\n for (const key of Object.keys(patch)) {\n const a = out[key];\n const b = patch[key];\n if (b === null) {\n delete out[key];\n continue;\n }\n if (isPlainObject(b)) {\n // Always recurse when the patch carries an object so the null-as-\n // delete sentinel applies at every depth. When the base lacks the\n // key (or holds a non-object), recurse against an empty object so\n // the patch's nested nulls do not leak into the result.\n const baseSub = isPlainObject(a) ? a : {};\n out[key] = deepMerge(baseSub, b);\n } else {\n out[key] = b;\n }\n }\n return out;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction readSidecarObject(sidecarAbsPath: string): Record<string, unknown> {\n if (!existsSync(sidecarAbsPath)) return {};\n const raw = readFileSync(sidecarAbsPath, 'utf8');\n const parsed = yaml.load(raw);\n if (parsed === null || parsed === undefined) return {};\n if (!isPlainObject(parsed)) {\n throw new Error(\n `sidecar at ${sidecarAbsPath} is not a YAML mapping; refusing to patch`,\n );\n }\n return parsed;\n}\n\nfunction atomicWriteFile(targetPath: string, content: string): void {\n const tmpPath = `${targetPath}.tmp`;\n try {\n writeFileSync(tmpPath, content, { encoding: 'utf8' });\n renameSync(tmpPath, targetPath);\n } catch (err) {\n // Best-effort cleanup; ignore secondary errors.\n try {\n if (existsSync(tmpPath)) unlinkSync(tmpPath);\n } catch {\n /* noop */\n }\n throw err;\n }\n}\n\nlet cachedValidator: ValidateFunction | null = null;\n\nfunction getSidecarValidator(): ValidateFunction {\n if (cachedValidator) return cachedValidator;\n const ajv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n const specRoot = resolveSpecRoot();\n const annotationsSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/annotations.schema.json'), 'utf8'),\n );\n const sidecarSchema = JSON.parse(\n readFileSync(resolve(specRoot, 'schemas/sidecar.schema.json'), 'utf8'),\n );\n ajv.addSchema(annotationsSchema);\n cachedValidator = ajv.compile(sidecarSchema);\n return cachedValidator;\n}\n\n/**\n * Test-only: drop the cached AJV validator. Mirrors\n * `_resetSidecarValidatorCacheForTests` in `parse.ts`.\n */\nexport function _resetSidecarStoreValidatorCacheForTests(): void {\n cachedValidator = null;\n}\n\nfunction resolveSpecRoot(): string {\n const require = createRequire(import.meta.url);\n try {\n const indexPath = require.resolve('@skill-map/spec/index.json');\n return dirname(indexPath);\n } catch {\n throw new Error('@skill-map/spec not resolvable — sidecar store cannot load schemas.');\n }\n}\n","/**\n * Typed read / write helper over the layered settings.json config.\n *\n * Composition pattern: this module is intentionally THIN. Every read\n * goes through `loadConfig` (the single source of truth for layer\n * merge + AJV validation + sources tracking); every write goes\n * through the `atomic-write` + `dot-path` helpers and re-validates\n * the merged file via the same AJV validators `loadConfig` uses, so\n * the disk can never end up with a config that the loader would\n * later reject.\n *\n * Key affordance — `USER_ONLY_KEYS`:\n * Some config keys describe **user preferences** (the user is\n * choosing how the tool behaves on their machine), not **project\n * contracts** (the project is declaring how the tool should walk\n * its content). `updateCheck.enabled` is the canonical example —\n * whether to see \"update available\" notifications is a per-user\n * call; switching projects shouldn't toggle it.\n *\n * The set of user-only keys is enforced HERE, in code, not in the\n * schema (the schema stays additive across layers so older installs\n * that wrote the key into a project file keep validating). The\n * helper:\n * - forces `scope: 'global'` on reads — a project-layer override\n * for a user-only key is silently ignored, which mirrors the\n * intent (\"this should not live in project\").\n * - rejects `target: 'project'` on writes with a directed error\n * so `sm config set` (and any future writer) can surface a\n * clear \"rerun with -g\" message.\n *\n * Lives under `src/core/config/` so both `cli/` and `server/` (BFF)\n * can import it. Receives `cwd` and `homedir` as explicit parameters\n * — the module reads no `process.env` / `process.cwd()`, so the\n * kernel-boundary lint rule (`src/eslint.config.js:233`) holds.\n */\n\nimport { isAbsolute, resolve } from 'node:path';\n\nimport {\n loadConfig,\n PROJECT_LOCAL_ONLY_KEYS,\n type ILoadedConfig,\n type TConfigLayer,\n} from '../../kernel/config/loader.js';\nimport { loadSchemaValidators } from '../../kernel/adapters/schema-validators.js';\nimport { defaultLocalSettingsPath, defaultSettingsPath } from '../paths/db-path.js';\nimport {\n getAtPath,\n setAtPath,\n deleteAtPath,\n ForbiddenSegmentError,\n} from './dot-path.js';\nimport { readJsonObjectOrEmpty, writeJsonAtomic } from './atomic-write.js';\n\n/**\n * Keys that MUST live in `~/.skill-map/settings.json` (user / global\n * scope) only. Reads force `scope: 'global'` regardless of the caller's\n * option; writes reject `target: 'project'` with `UserOnlyKeyError`.\n *\n * Adding a key here is a behavior change for anyone who set it in a\n * project file before — the value gets silently ignored at read time.\n * Document the migration in the changeset that adds the entry.\n */\nexport const USER_ONLY_KEYS: ReadonlySet<string> = new Set<string>([\n 'updateCheck.enabled',\n]);\n\n/**\n * Keys whose value can OPEN disk access outside the project root —\n * the operator must opt in via `--yes` (CLI) or a confirm dialog\n * (UI) before the write goes through. Surfaces:\n *\n * - `scan.includeHome`: boolean toggle that adds every active\n * Provider's `explorationDir` resolved against `~`.\n * - `scan.extraRoots`: string[] of additional directories to scan\n * as nodes.\n * - `scan.referencePaths`: string[] of directories walked for link\n * validation only.\n *\n * The CLI wrapper (`sm config set`) consults this set + the\n * \"expanding the surface?\" predicate to decide whether `--yes` is\n * required (writes that NARROW the surface — disabling\n * `includeHome`, removing paths — are not gated).\n */\nexport const PRIVACY_SENSITIVE_KEYS: ReadonlySet<string> = new Set<string>([\n 'scan.includeHome',\n 'scan.extraRoots',\n 'scan.referencePaths',\n]);\n\n/** Thrown when `writeConfigValue` is asked to write a user-only key to project. */\nexport class UserOnlyKeyError extends Error {\n constructor(public readonly key: string) {\n super(\n `Config key '${key}' is user-scope only. ` +\n `Pass { target: 'user' } (or rerun the CLI with -g) to write it to ~/.skill-map/settings.json.`,\n );\n this.name = 'UserOnlyKeyError';\n }\n}\n\n/**\n * Thrown when `writeConfigValue` (or `removeConfigValue`) is asked to\n * write a `PROJECT_LOCAL_ONLY_KEYS` member into the committed `project`\n * layer (`<cwd>/.skill-map/settings.json`). The loader strips these\n * keys from that layer at read time, so persisting them there is a\n * silent footgun — the value would never take effect. Surfaced as a\n * directed error so the writer can re-target `project-local`\n * (`<cwd>/.skill-map/settings.local.json`, gitignored) or `user` /\n * `user-local` (`~/.skill-map/...`).\n */\nexport class ProjectLocalOnlyKeyError extends Error {\n constructor(public readonly key: string) {\n super(\n `Config key '${key}' is project-local only. ` +\n `Pass { target: 'project-local' } to write it to .skill-map/settings.local.json (gitignored), ` +\n `or use -g for the user / user-local scope.`,\n );\n this.name = 'ProjectLocalOnlyKeyError';\n }\n}\n\n// Re-export the loader-side set so single-import consumers (the CLI's\n// `sm config set`, the sidecar-consent helper, the BFF's preferences\n// route) can both consume the catalogue and `instanceof`-match the\n// directed error against a single module path.\nexport { PROJECT_LOCAL_ONLY_KEYS };\n\nexport interface IReadConfigValueOpts<T> {\n /** Resolution scope. `'global'` skips project layers. `'project'` walks all six. */\n scope: 'project' | 'global';\n cwd: string;\n homedir: string;\n /** Returned when the key is absent across every layer. */\n default?: T;\n /**\n * Forwarded to `loadConfig`: when true, malformed JSON / schema\n * violations throw instead of degrading to a warning + skip. CLI\n * verbs flip this on with `--strict`; the BFF leaves it false so a\n * single bad layer never breaks the boot path.\n */\n strict?: boolean;\n}\n\nexport interface IWriteConfigValueOpts {\n /**\n * Which file to mutate.\n * - `'user'` → `~/.skill-map/settings.json`\n * - `'user-local'` → `~/.skill-map/settings.local.json`\n * - `'project'` → `<cwd>/.skill-map/settings.json`\n * - `'project-local'` → `<cwd>/.skill-map/settings.local.json`\n *\n * Rejected (UserOnlyKeyError) when `target === 'project'` and the\n * key is in `USER_ONLY_KEYS`.\n *\n * Rejected (ProjectLocalOnlyKeyError) when `target === 'project'`\n * and the key is in `PROJECT_LOCAL_ONLY_KEYS` — those keys must\n * land in `project-local`, `user`, or `user-local` so a teammate's\n * checkout never inherits per-machine state via the committed\n * `settings.json`.\n */\n target: 'project' | 'project-local' | 'user' | 'user-local';\n cwd: string;\n homedir: string;\n}\n\nexport type IRemoveConfigValueOpts = IWriteConfigValueOpts;\n\n/**\n * Resolve a single config key. Returns the merged value across all\n * eligible layers (or `opts.default` / `undefined` when absent).\n *\n * For `USER_ONLY_KEYS`, the scope is forced to `'global'` regardless\n * of `opts.scope` — the project file is intentionally invisible to\n * the read so a stray project-layer entry from an older install is a\n * no-op rather than a silent override.\n *\n * Type discipline: the return is `T | undefined`. The helper does NOT\n * validate the runtime shape of the value against the caller's `T` —\n * AJV at the layer-load step already enforces the schema, so the\n * value's shape matches `project-config.schema.json`. Callers that\n * declare a wrong `T` get an unsound cast; that is a programming\n * error, not a runtime concern of the helper.\n */\nexport function readConfigValue<T>(\n key: string,\n opts: IReadConfigValueOpts<T>,\n): T | undefined {\n const scope = USER_ONLY_KEYS.has(key) ? 'global' : opts.scope;\n const loaded = loadConfigForScope(scope, opts);\n const value = getAtPath(loaded.effective as unknown, key) as T | undefined;\n if (value === undefined) return opts.default;\n return value;\n}\n\n/**\n * Persist a value under `key` to the chosen layer's settings.json.\n *\n * Pipeline: read the current layer file → mutate via `setAtPath` →\n * AJV-revalidate the merged result against `project-config.schema.json`\n * → atomic write. The validate step uses the SAME validators\n * `loadConfig` uses, so a value the helper accepts is one the loader\n * will accept the next time it boots.\n *\n * Throws `UserOnlyKeyError` when the caller asks to write a user-only\n * key into the project layer.\n */\nexport function writeConfigValue(\n key: string,\n value: unknown,\n opts: IWriteConfigValueOpts,\n): void {\n if (USER_ONLY_KEYS.has(key) && opts.target === 'project') {\n throw new UserOnlyKeyError(key);\n }\n if (PROJECT_LOCAL_ONLY_KEYS.has(key) && opts.target === 'project') {\n throw new ProjectLocalOnlyKeyError(key);\n }\n const path = targetSettingsPath(opts.target, opts.cwd, opts.homedir);\n const merged = readJsonObjectOrEmpty(path);\n setAtPath(merged, key, value);\n validateOrThrow(merged);\n writeJsonAtomic(path, merged);\n}\n\n/**\n * Remove `key` from the chosen layer's settings.json. Returns `true`\n * when the key existed and was removed, `false` when it was already\n * absent (no-op, no write performed). Same UserOnlyKeyError guard as\n * `writeConfigValue` so a `sm config reset updateCheck.enabled`\n * (without `-g`) surfaces a directed error instead of silently\n * re-deleting a never-present project entry.\n */\nexport function removeConfigValue(key: string, opts: IRemoveConfigValueOpts): boolean {\n if (USER_ONLY_KEYS.has(key) && opts.target === 'project') {\n throw new UserOnlyKeyError(key);\n }\n if (PROJECT_LOCAL_ONLY_KEYS.has(key) && opts.target === 'project') {\n throw new ProjectLocalOnlyKeyError(key);\n }\n const path = targetSettingsPath(opts.target, opts.cwd, opts.homedir);\n const merged = readJsonObjectOrEmpty(path);\n const removed = deleteAtPath(merged, key);\n if (!removed) return false;\n validateOrThrow(merged);\n writeJsonAtomic(path, merged);\n return true;\n}\n\n/**\n * Return the layer that contributed the effective value for `key`, or\n * `undefined` when no layer set it (the value is the default from\n * `src/config/defaults.json` or absent entirely). Wraps `loadConfig`\n * + the `sources` map; honors the `USER_ONLY_KEYS` scope override\n * the read path uses.\n */\nexport function getValueSource(\n key: string,\n opts: { scope: 'project' | 'global'; cwd: string; homedir: string },\n): TConfigLayer | undefined {\n const scope = USER_ONLY_KEYS.has(key) ? 'global' : opts.scope;\n const loaded = loadConfigForScope(scope, opts);\n return loaded.sources.get(key);\n}\n\n// ---------------------------------------------------------------------------\n// internals\n// ---------------------------------------------------------------------------\n\nfunction loadConfigForScope(\n scope: 'project' | 'global',\n opts: { cwd: string; homedir: string; strict?: boolean },\n): ILoadedConfig {\n return loadConfig({\n scope,\n cwd: opts.cwd,\n homedir: opts.homedir,\n ...(opts.strict ? { strict: true } : {}),\n });\n}\n\nfunction targetSettingsPath(\n target: IWriteConfigValueOpts['target'],\n cwd: string,\n home: string,\n): string {\n switch (target) {\n case 'user':\n return defaultSettingsPath(home);\n case 'user-local':\n return defaultLocalSettingsPath(home);\n case 'project':\n return defaultSettingsPath(cwd);\n case 'project-local':\n return defaultLocalSettingsPath(cwd);\n }\n}\n\nfunction validateOrThrow(content: Record<string, unknown>): void {\n const validators = loadSchemaValidators();\n const result = validators.validate('project-config', content);\n if (result.ok) return;\n throw new ConfigValidationError(result.errors);\n}\n\n/**\n * Surfaces an AJV failure as a single message string. `sm config set`\n * (and any other writer) can render the message directly to the user\n * without hand-formatting the AJV `errors` array.\n */\nexport class ConfigValidationError extends Error {\n constructor(public readonly errors: string) {\n super(`Config validation failed: ${errors}`);\n this.name = 'ConfigValidationError';\n }\n}\n\n// Re-export the dot-path error so consumers can `instanceof` against a\n// single import path (`core/config/helper`) instead of reaching into\n// `core/config/dot-path`. Behavior is unchanged.\nexport { ForbiddenSegmentError };\n\n// ---------------------------------------------------------------------------\n// Privacy-sensitive write helpers\n// ---------------------------------------------------------------------------\n\nexport interface IPathExposureInputs {\n /** The dot-path being mutated (must be a member of `PRIVACY_SENSITIVE_KEYS`). */\n key: string;\n /** New value the operator wants to write. */\n value: unknown;\n /** Project working directory — used to decide whether a path is in-scope. */\n cwd: string;\n /** User home — used to expand `~/...` entries before the in-scope check. */\n homedir: string;\n}\n\nexport interface IPathExposureResult {\n /**\n * `true` when the new value introduces disk access OUTSIDE the\n * project root (toggling `scan.includeHome` `false`→`true`, or\n * adding paths to `scan.extraRoots` / `scan.referencePaths` that\n * resolve outside `cwd`). Drives the `--yes` requirement on\n * `sm config set`.\n *\n * Writes that NARROW the surface (disabling `includeHome`,\n * removing paths) return `false` so the user can revert the\n * exposure without a confirmation step.\n */\n expandsSurface: boolean;\n /**\n * Concrete absolute paths the new value will expose to the scan.\n * Empty when `expandsSurface === false`. Used by CLI / UI to\n * enumerate what the user is about to opt into.\n */\n exposedPaths: string[];\n}\n\n/**\n * Project the disk-access expansion of a privacy-sensitive write.\n * Returns `{ expandsSurface: false, exposedPaths: [] }` for keys\n * outside `PRIVACY_SENSITIVE_KEYS` — the caller can invoke this\n * unconditionally and only branch when `expandsSurface === true`.\n */\n// eslint-disable-next-line complexity\nexport function projectPathExposure(inputs: IPathExposureInputs): IPathExposureResult {\n const empty: IPathExposureResult = { expandsSurface: false, exposedPaths: [] };\n if (!PRIVACY_SENSITIVE_KEYS.has(inputs.key)) return empty;\n\n // Compare against the currently persisted value to gate only on\n // expansions. Read uses `scope: 'project'` because these keys live\n // in the project layer; reads through `readConfigValue` honour the\n // `USER_ONLY_KEYS` override too (this set has no overlap with\n // user-only keys today).\n if (inputs.key === 'scan.includeHome') {\n if (inputs.value !== true) return empty;\n const before = readConfigValue<boolean>('scan.includeHome', {\n scope: 'project',\n cwd: inputs.cwd,\n homedir: inputs.homedir,\n default: false,\n });\n if (before === true) return empty;\n return {\n expandsSurface: true,\n // The CLI / UI fills this with the concrete provider HOME dirs\n // because the helper has no access to the active extension set.\n exposedPaths: ['~ (per-provider explorationDir)'],\n };\n }\n\n // Both list-shaped keys: a value is \"expanding\" iff it adds at\n // least one out-of-project entry that wasn't present before.\n if (inputs.key === 'scan.extraRoots' || inputs.key === 'scan.referencePaths') {\n if (!Array.isArray(inputs.value)) return empty;\n const before = readConfigValue<string[]>(inputs.key, {\n scope: 'project',\n cwd: inputs.cwd,\n homedir: inputs.homedir,\n default: [],\n }) ?? [];\n const beforeSet = new Set(before);\n const added = (inputs.value as unknown[])\n .filter((entry): entry is string => typeof entry === 'string')\n .filter((entry) => !beforeSet.has(entry));\n const exposed = added\n .map((entry) => resolveScanPathForExposure(entry, inputs.cwd, inputs.homedir))\n .filter((abs) => abs !== null && !isUnderProject(abs, inputs.cwd)) as string[];\n if (exposed.length === 0) return empty;\n return { expandsSurface: true, exposedPaths: exposed };\n }\n\n return empty;\n}\n\nfunction resolveScanPathForExposure(raw: string, cwd: string, homedir: string): string | null {\n // Identical resolution rules as `core/runtime/reference-paths-walker:resolveScanPath`,\n // duplicated locally to avoid a circular dep between core/config and\n // core/runtime.\n if (raw.startsWith('~/')) return resolve(`${homedir}/${raw.slice(2)}`);\n if (raw === '~') return resolve(homedir);\n if (isAbsolute(raw)) return resolve(raw);\n return resolve(cwd, raw);\n}\n\nfunction isUnderProject(absPath: string, cwd: string): boolean {\n const projectRoot = resolve(cwd);\n // Containment check via prefix + path separator so `/projectRoot2`\n // never reads as \"under /projectRoot\".\n return absPath === projectRoot || absPath.startsWith(`${projectRoot}/`);\n}\n","/**\n * Layered config loader for `.skill-map/settings.json`. Walks the six\n * canonical layers (defaults → user → user-local → project → project-local\n * → overrides), deep-merges per key, validates each layer against the\n * `project-config` JSON schema, and skips offending keys (warning) or\n * fails fast (strict). The effective config plus a per-key sources map\n * are returned so `sm config show --source` can answer who set what.\n *\n * Layer semantics (low → high precedence):\n * 1. `defaults` — `src/config/defaults.json`, shipped in bundle.\n * 2. `user` — `~/.skill-map/settings.json`.\n * 3. `user-local` — `~/.skill-map/settings.local.json`.\n * 4. `project` — `<cwd>/.skill-map/settings.json`.\n * 5. `project-local` — `<cwd>/.skill-map/settings.local.json`.\n * 6. `override` — caller-supplied object (env vars / CLI flags).\n *\n * For scope === 'global', layers 4 and 5 resolve to the same files as 2/3\n * and are skipped to avoid double-merging the same source.\n *\n * Failure modes:\n * - missing file → silent skip (the layer is optional).\n * - malformed JSON → warning + skip whole layer (or throw if strict).\n * - schema violation → strip the offending key + warning (or throw\n * if strict). Per-key resilience: a single bad\n * value never invalidates the rest of the file.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\n\nimport { loadSchemaValidators, type ISchemaValidators } from '../adapters/schema-validators.js';\nimport { CONFIG_LOADER_TEXTS } from '../i18n/config-loader.texts.js';\nimport { formatErrorMessage } from '../util/format-error.js';\nimport {\n kernelLocalSettingsPath,\n kernelSettingsPath,\n} from '../util/skill-map-paths.js';\nimport { tx } from '../util/tx.js';\n\nimport DEFAULTS_RAW from '../../config/defaults.json' with { type: 'json' };\n\n// -----------------------------------------------------------------------------\n// Public types\n// -----------------------------------------------------------------------------\n\nexport interface IRetentionConfig {\n completed: number | null;\n failed: number | null;\n}\n\nexport interface IJobsConfig {\n ttlSeconds: number;\n graceMultiplier: number;\n minimumTtlSeconds: number;\n perActionTtl: Record<string, number>;\n perActionPriority: Record<string, number>;\n retention: IRetentionConfig;\n}\n\nexport interface IPluginConfigEntry {\n enabled?: boolean;\n config?: Record<string, unknown>;\n}\n\nexport interface IScanWatchConfig {\n debounceMs: number;\n}\n\nexport interface IScanConfig {\n tokenize: boolean;\n strict: boolean;\n /**\n * Reserved for a future implementation. The walker (built-in `claude`\n * Provider, `walkMarkdown`) currently always skips symlinks, regardless\n * of this flag's value. Following a symlink also requires cycle detection\n * and a `realpath`-resolved containment check, which is out of scope for\n * the current security pass — see audit M7. The schema field stays so\n * a settings.json that already opts in keeps validating; flipping it\n * to `true` is a no-op until the walker is extended.\n */\n followSymlinks: boolean;\n maxFileSizeBytes: number;\n watch: IScanWatchConfig;\n /**\n * **Privacy-sensitive** (per `project-config.schema.json` §scan.includeHome).\n * Default false. When true, `sm scan` (without `-g`) appends every\n * active Provider's `explorationDir` resolved against `~`\n * (`~/.claude`, `~/.gemini`, `~/.agents`, …) to the effective\n * scan roots. The reference impl gates writes that flip this\n * `false`→`true` behind `--yes` / a confirm dialog.\n */\n includeHome: boolean;\n /**\n * **Privacy-sensitive when entries point outside the project**\n * (per `project-config.schema.json` §scan.extraRoots). Default `[]`.\n * Additional directories appended to the scan roots — entries\n * starting with `~` resolve against the user home; relative entries\n * resolve against the project root.\n */\n extraRoots: string[];\n /**\n * **Privacy-sensitive when entries point outside the project**\n * (per `project-config.schema.json` §scan.referencePaths). Default\n * `[]`. Directories walked in parallel by the scan to collect\n * existing absolute paths into a side set. Files there are NOT\n * parsed and NOT indexed as nodes — the only effect is suppressing\n * `core/broken-ref` warnings for targets that exist on disk but\n * fall outside the indexed graph. The kernel passes the set to\n * rules via `IAnalyzerContext.referenceablePaths`.\n */\n referencePaths: string[];\n}\n\nexport interface IEffectiveConfig {\n schemaVersion: 1;\n autoMigrate: boolean;\n /**\n * **Project-local only** (per `PROJECT_LOCAL_ONLY_KEYS`). Grants this\n * project permission to create / modify `.sm` annotation sidecars\n * next to source files. Default `false`. The first time a verb or\n * BFF route attempts a `.sm` write while this is `false`, the kernel\n * raises `EConsentRequiredError`. The CLI surfaces it as an\n * interactive `confirm()` prompt (or `--yes` bypass); the BFF\n * returns 412 `confirm-required`. On accept the flag is persisted\n * to `<cwd>/.skill-map/settings.local.json` (gitignored,\n * per-checkout). Stripped with a warning when found in the\n * committed `project` layer — each developer consents\n * independently.\n */\n allowEditSmFiles: boolean;\n tokenizer: string;\n providers: string[];\n roots: string[];\n ignore: string[];\n scan: IScanConfig;\n plugins: Record<string, IPluginConfigEntry>;\n history: { share: boolean };\n jobs: IJobsConfig;\n i18n: { locale: string };\n}\n\n/**\n * Dot-paths that MUST NOT be loaded from the committed `project`\n * layer (`<cwd>/.skill-map/settings.json`). They remain valid in\n * `defaults`, `user`, `user-local`, `project-local`, and `override`.\n * When the loader finds one in the project file, it strips the key\n * (warning) before the deep-merge runs, so a shared checkout cannot\n * leak `~/...` exposure to every teammate.\n *\n * Keep in lock-step with the descriptions in\n * `spec/schemas/project-config.schema.json` (every entry here carries\n * a `Privacy-sensitive, project-local only` marker on its spec\n * description).\n */\nexport const PROJECT_LOCAL_ONLY_KEYS: ReadonlySet<string> = new Set<string>([\n 'allowEditSmFiles',\n 'scan.includeHome',\n 'scan.extraRoots',\n 'scan.referencePaths',\n]);\n\nexport type TConfigLayer =\n | 'defaults'\n | 'user'\n | 'user-local'\n | 'project'\n | 'project-local'\n | 'override';\n\nexport interface ILoadConfigOptions {\n /** Determines whether project-scoped layers are walked (`project`) or skipped (`global`). */\n scope: 'project' | 'global';\n /** Working directory used to resolve project-scoped config files. */\n cwd: string;\n /** User home directory used to resolve user-scoped config files. */\n homedir: string;\n /** Top layer applied after every file layer. Translates env vars / CLI flags into config keys. */\n overrides?: Record<string, unknown>;\n /** When true, every warning is thrown as an `Error` instead of being collected. */\n strict?: boolean;\n}\n\nexport interface ILoadedConfig {\n effective: IEffectiveConfig;\n /** Maps dot-path keys (e.g. `\"scan.strict\"`) to the layer that last wrote them. */\n sources: Map<string, TConfigLayer>;\n /** Accumulated warnings about malformed JSON, schema violations, or invalid values. */\n warnings: string[];\n}\n\n// -----------------------------------------------------------------------------\n// Public API\n// -----------------------------------------------------------------------------\n\nconst DEFAULTS = DEFAULTS_RAW as unknown as IEffectiveConfig;\n\n// Complexity comes from the six-layer walk: each branch (defaults\n// init, per-layer file iterator with strict / cleaned / strip /\n// merge / record, overrides) contributes one path. Splitting per\n// branch would scatter the layer-merge invariant across helpers\n// that have no other consumer.\n// eslint-disable-next-line complexity\nexport function loadConfig(opts: ILoadConfigOptions): ILoadedConfig {\n const cwd = opts.cwd;\n const home = opts.homedir;\n const strict = opts.strict ?? false;\n const warnings: string[] = [];\n const sources = new Map<string, TConfigLayer>();\n const validators = loadSchemaValidators();\n\n let effective = structuredClone(DEFAULTS);\n recordSources('', effective, sources, 'defaults');\n\n const filePairs: Array<{ path: string; layer: TConfigLayer }> = [\n { path: kernelSettingsPath(home), layer: 'user' },\n { path: kernelLocalSettingsPath(home), layer: 'user-local' },\n ];\n if (opts.scope === 'project') {\n filePairs.push(\n { path: kernelSettingsPath(cwd), layer: 'project' },\n { path: kernelLocalSettingsPath(cwd), layer: 'project-local' },\n );\n }\n\n for (const { path, layer } of filePairs) {\n if (!existsSync(path)) continue;\n const partial = readJsonSafe(path, layer, warnings, strict);\n if (partial === null) continue;\n const cleaned = validateAndStrip(validators, partial, layer, warnings, strict);\n if (layer === 'project') {\n stripProjectLocalOnlyKeys(cleaned, warnings, strict);\n }\n effective = deepMerge(effective as unknown as Record<string, unknown>, cleaned) as unknown as IEffectiveConfig;\n recordSources('', cleaned, sources, layer);\n }\n\n if (opts.overrides && Object.keys(opts.overrides).length > 0) {\n const cleaned = validateAndStrip(validators, opts.overrides, 'override', warnings, strict);\n effective = deepMerge(effective as unknown as Record<string, unknown>, cleaned) as unknown as IEffectiveConfig;\n recordSources('', cleaned, sources, 'override');\n }\n\n return { effective, sources, warnings };\n}\n\n// -----------------------------------------------------------------------------\n// Internals\n// -----------------------------------------------------------------------------\n\nfunction readJsonSafe(\n path: string,\n layer: TConfigLayer,\n warnings: string[],\n strict: boolean,\n): unknown | null {\n let text: string;\n try {\n text = readFileSync(path, 'utf8');\n } catch (err) {\n return reportAndSkip(\n tx(CONFIG_LOADER_TEXTS.readFailure, { layer, path, message: formatErrorMessage(err) }),\n warnings,\n strict,\n );\n }\n try {\n return JSON.parse(text);\n } catch (err) {\n return reportAndSkip(\n tx(CONFIG_LOADER_TEXTS.invalidJson, { layer, path, message: formatErrorMessage(err) }),\n warnings,\n strict,\n );\n }\n}\n\nfunction reportAndSkip(msg: string, warnings: string[], strict: boolean): null {\n if (strict) throw new Error(msg);\n warnings.push(msg);\n return null;\n}\n\n/**\n * Validate `raw` against the project-config schema and return a copy with\n * any offending keys removed. Errors are accumulated as warnings (or thrown\n * in strict mode). Continues per-key so a single bad value never invalidates\n * the rest of the file.\n */\nfunction validateAndStrip(\n validators: ISchemaValidators,\n raw: unknown,\n layer: TConfigLayer,\n warnings: string[],\n strict: boolean,\n): Record<string, unknown> {\n if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {\n const msg = tx(CONFIG_LOADER_TEXTS.expectedObject, { layer, type: describeJsonType(raw) });\n if (strict) throw new Error(msg);\n warnings.push(msg);\n return {};\n }\n\n const cloned = structuredClone(raw) as Record<string, unknown>;\n const validator = validators.getValidator('project-config');\n if (validator(cloned)) return cloned;\n\n for (const err of validator.errors ?? []) {\n applyValidationError(cloned, err, layer, warnings, strict);\n }\n return cloned;\n}\n\n/**\n * Apply one AJV error to the cloned config object: drop the offending\n * key (additionalProperties or invalid-value), then either throw (in\n * strict mode) or push a human-readable warning. Mutates `cloned` and\n * `warnings` in place.\n */\nfunction applyValidationError(\n cloned: Record<string, unknown>,\n err: { instancePath?: string; keyword: string; message?: string; params?: unknown },\n layer: TConfigLayer,\n warnings: string[],\n strict: boolean,\n): void {\n const path = err.instancePath ?? '';\n if (err.keyword === 'additionalProperties') {\n const extra = (err.params as { additionalProperty: string }).additionalProperty;\n deleteAtPath(cloned, path, extra);\n const msg = tx(CONFIG_LOADER_TEXTS.unknownKey, { layer, key: joinSegments(path, extra) });\n if (strict) throw new Error(msg);\n warnings.push(msg);\n return;\n }\n const segments = path.split('/').filter(Boolean);\n if (segments.length > 0) {\n const last = segments.pop() as string;\n deleteAtPath(cloned, '/' + segments.join('/'), last);\n }\n const msg = tx(CONFIG_LOADER_TEXTS.invalidValue, {\n layer,\n path: path || '(root)',\n message: err.message ?? err.keyword,\n });\n if (strict) throw new Error(msg);\n warnings.push(msg);\n}\n\nfunction describeJsonType(v: unknown): string {\n if (v === null) return 'null';\n if (Array.isArray(v)) return 'array';\n return typeof v;\n}\n\n/**\n * Names that, when used as object keys, manipulate the prototype chain.\n * Filtered everywhere user-controlled config data is walked or merged so\n * a hostile config layer cannot pollute `Object.prototype` or replace\n * the merged config's `[[Prototype]]`.\n */\nconst FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nfunction deleteAtPath(root: Record<string, unknown>, parentPath: string, key: string): void {\n if (containsForbidden(parentPath, key)) return;\n const segments = parentPath.split('/').filter(Boolean);\n let cur: unknown = root;\n for (const seg of segments) {\n if (!isPlainObject(cur)) return;\n cur = cur[seg];\n }\n if (isPlainObject(cur)) delete cur[key];\n}\n\n/**\n * Walk every `PROJECT_LOCAL_ONLY_KEYS` dot-path against `cloned` and\n * delete the leaf when present. Pushes a per-stripped-key warning\n * (or throws in strict mode). Used only for the `project` layer; the\n * other five layers carry these keys legitimately.\n */\nfunction stripProjectLocalOnlyKeys(\n cloned: Record<string, unknown>,\n warnings: string[],\n strict: boolean,\n): void {\n for (const dotKey of PROJECT_LOCAL_ONLY_KEYS) {\n const segments = dotKey.split('.').filter(Boolean);\n if (segments.length === 0) continue;\n const leaf = segments.pop() as string;\n if (!keyPresentAtPath(cloned, segments, leaf)) continue;\n const parentPath = '/' + segments.join('/');\n deleteAtPath(cloned, parentPath, leaf);\n const msg = tx(CONFIG_LOADER_TEXTS.projectLocalOnlyStripped, {\n layer: 'project',\n key: dotKey,\n });\n if (strict) throw new Error(msg);\n warnings.push(msg);\n }\n}\n\nfunction keyPresentAtPath(\n root: Record<string, unknown>,\n parentSegments: string[],\n leaf: string,\n): boolean {\n let cur: unknown = root;\n for (const seg of parentSegments) {\n if (!isPlainObject(cur)) return false;\n cur = cur[seg];\n }\n return isPlainObject(cur) && Object.prototype.hasOwnProperty.call(cur, leaf);\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v);\n}\n\nfunction containsForbidden(parentPath: string, leaf: string): boolean {\n if (FORBIDDEN_KEYS.has(leaf)) return true;\n for (const seg of parentPath.split('/')) {\n if (FORBIDDEN_KEYS.has(seg)) return true;\n }\n return false;\n}\n\nfunction joinSegments(instancePath: string, leaf: string): string {\n const segments = instancePath.split('/').filter(Boolean);\n return [...segments, leaf].join('.');\n}\n\nfunction deepMerge(\n target: Record<string, unknown>,\n source: Record<string, unknown>,\n): Record<string, unknown> {\n const out: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(source)) {\n if (FORBIDDEN_KEYS.has(k)) continue;\n out[k] = mergeValue(out[k], v);\n }\n return out;\n}\n\nfunction mergeValue(target: unknown, source: unknown): unknown {\n if (source === null || typeof source !== 'object' || Array.isArray(source)) {\n return source;\n }\n // When the source is a plain object — recurse even if the target slot\n // is empty, so nested `__proto__` / `constructor` / `prototype` keys\n // are filtered. Skipping the recursion in the empty-target case\n // (early version of the H1 fix) leaked pollution keys verbatim into\n // the merged config.\n const targetSlot =\n target !== null && typeof target === 'object' && !Array.isArray(target)\n ? (target as Record<string, unknown>)\n : {};\n return deepMerge(targetSlot, source as Record<string, unknown>);\n}\n\n// eslint-disable-next-line complexity\nfunction recordSources(\n prefix: string,\n value: unknown,\n map: Map<string, TConfigLayer>,\n layer: TConfigLayer,\n): void {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) {\n if (prefix) map.set(prefix, layer);\n return;\n }\n const entries = Object.entries(value as Record<string, unknown>);\n if (entries.length === 0 && prefix) {\n map.set(prefix, layer);\n return;\n }\n for (const [k, v] of entries) {\n const next = prefix ? `${prefix}.${k}` : k;\n recordSources(next, v, map, layer);\n }\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-analyzer'\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-analyzer': 'schemas/extensions/analyzer.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 'schemas/view-slots.schema.json',\n 'schemas/input-types.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 * Validate a `ctx.emitContribution(id, payload)` payload against the\n * declared slot's payload schema in\n * `view-slots.schema.json#/$defs/payloads/<slot>`. Closed catalog:\n * passing an unknown slot returns `{ ok: false, errors:\n * 'unknown-slot' }` so the orchestrator can drop the emission\n * without crashing.\n */\n validateContributionPayload(\n slot: string,\n payload: unknown,\n ): { ok: true } | { 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 analyzer: 'extension-analyzer',\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 // Per-slot payload validators for `ctx.emitContribution`. Compiled\n // lazily on first use because not every CLI verb exercises the\n // contributions path; cold-CLI startup avoids paying for validators a\n // verb will never call. See `validateContributionPayload`.\n //\n // The closed catalog of slot ids mirrors\n // `view-slots.schema.json#/$defs/SlotName` exactly; entries inside\n // `$defs/payloads` whose key starts with an underscore (`_counter`,\n // `_tag`, `_TreeNode`) are internal `$ref` reuse targets, NOT slot\n // ids — querying them would compile but is meaningless at the\n // public API.\n const contributionValidators = new Map<string, ValidateFunction>();\n const VIEW_SLOTS_ID = 'https://skill-map.dev/spec/v0/view-slots.schema.json';\n const KNOWN_SLOTS = new Set<string>([\n 'card.title.right',\n 'card.subtitle.left',\n 'card.footer.left',\n 'card.footer.right',\n 'graph.node.alert',\n 'inspector.header.badge.counter',\n 'inspector.header.badge.tag',\n 'inspector.body.panel.breakdown',\n 'inspector.body.panel.records',\n 'inspector.body.panel.tree',\n 'inspector.body.panel.key-values',\n 'inspector.body.panel.link-list',\n 'inspector.body.panel.markdown',\n 'topbar.nav.start',\n ]);\n\n function getContributionValidator(slot: string): ValidateFunction | null {\n if (!KNOWN_SLOTS.has(slot)) return null;\n const existing = contributionValidators.get(slot);\n if (existing) return existing;\n const ref = `${VIEW_SLOTS_ID}#/$defs/payloads/${slot}`;\n let compiled: ValidateFunction | undefined;\n try {\n compiled = ajv.compile({ $ref: ref });\n } catch {\n return null;\n }\n contributionValidators.set(slot, compiled);\n return compiled;\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 validateContributionPayload(slot: string, payload: unknown) {\n const validator = getContributionValidator(slot);\n if (!validator) {\n return { ok: false as const, errors: 'unknown-slot' };\n }\n if (validator(payload)) return { ok: true as const };\n const errors = (validator.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 registerProviderAuxiliarySchemas(ajv, providers);\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 * Register every Provider's auxiliary schemas (if any) on the AJV instance\n * BEFORE compiling per-kind schemas. Use case: Anthropic's merged\n * skill / command frontmatter — both kinds extend a shared\n * `skill-base.schema.json` declared as an auxiliary on the Provider, and\n * AJV resolves the cross-file `$ref` only after `addSchema` has registered\n * the auxiliary's `$id`.\n */\nfunction registerProviderAuxiliarySchemas(ajv: TAjv, providers: IProvider[]): void {\n for (const provider of providers) {\n if (!provider.schemas) continue;\n for (const aux of provider.schemas) {\n const auxJson = aux as { $id?: string };\n if (typeof auxJson.$id === 'string' && ajv.getSchema(auxJson.$id)) continue;\n ajv.addSchema(aux as object);\n }\n }\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-accessible counterpart of `cli/util/error-reporter.ts`'s\n * `formatErrorMessage`. The CLI helper now re-exports from here so the\n * historic CLI import path keeps working while kernel + BFF callers can\n * consume it directly without crossing the layering boundary.\n *\n * Kept deliberately tiny — same shape as the original CLI helper. The\n * surface grows (e.g. a `--verbose` stack mode, JSON envelope) only\n * when a concrete need surfaces.\n */\n\n/**\n * Compact error → string conversion.\n *\n * - `Error` → `err.message` verbatim. Callers wrap with their own\n * verb-specific context line via `tx(*_TEXTS.x, { message })` so\n * error catalogues stay greppable.\n * - Anything else → `String(value)`. Catches the rare throw-a-string\n * / throw-an-object path without exploding on `null`\n * (`String(null)` = `'null'`).\n */\nexport function formatErrorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","/**\n * Kernel-side helpers that compose the layered-config file paths from\n * the canonical `SKILL_MAP_DIR` literal.\n *\n * `SKILL_MAP_DIR` is exported once from `core/paths/db-path.ts` and\n * re-exported here as `KERNEL_SKILL_MAP_DIR` so kernel-side callers\n * keep their historic name without the literal living in two files\n * (audit m3 — one literal home, no `grep \"'\\.skill-map'\"` sweep\n * invariant to maintain across kernel + CLI).\n */\n\nimport { join } from 'node:path';\n\nimport { SKILL_MAP_DIR } from '../../core/paths/db-path.js';\n\n/**\n * Per-scope directory the kernel + CLI both store state under (DB file,\n * settings, plugins, etc.). Re-exported from `core/paths/db-path.ts`\n * — the single canonical source for the literal.\n */\nexport const KERNEL_SKILL_MAP_DIR = SKILL_MAP_DIR;\n\nconst SETTINGS_FILENAME = 'settings.json';\nconst LOCAL_SETTINGS_FILENAME = 'settings.local.json';\n\n/**\n * `<scopeRoot>/.skill-map/settings.json` — the canonical layered-config\n * file. Used by `kernel/config/loader.ts` to compose its user / project\n * walk.\n */\nexport function kernelSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, KERNEL_SKILL_MAP_DIR, SETTINGS_FILENAME);\n}\n\n/**\n * `<scopeRoot>/.skill-map/settings.local.json` — the local-overrides\n * companion to `settings.json`. Used by the same loader walk.\n */\nexport function kernelLocalSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, KERNEL_SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);\n}\n","/**\n * Pure path helpers for the on-disk skill-map scope layout. Moved out\n * of `cli/util/db-path.ts` so the BFF (`src/server/`) can consume them\n * without reaching into the CLI layer. The CLI-only siblings\n * (`assertDbExists`, `requireDbOrExit` — they take a stderr stream and\n * an `ExitCode`) stay in `cli/util/db-path.ts` and re-export the\n * primitives from here.\n *\n * Spec global flags (per `spec/cli-contract.md` §Global flags):\n * -g / --global operate on `~/.skill-map/` instead of `./.skill-map/`\n * --db <path> escape hatch for explicit DB file\n */\n\nimport { join, resolve } from 'node:path';\n\nimport type { IRuntimeContext } from '../runtime/runtime-context.js';\n\n/**\n * Per-scope directory the CLI stores its state under (DB file, settings,\n * plugins, etc.). Same name in project (`<cwd>/.skill-map/`) and global\n * (`~/.skill-map/`) scopes; the difference is the parent. Exported so\n * write-side scaffolding (`sm init`) and other helpers can reuse the\n * convention without duplicating the literal.\n */\nexport const SKILL_MAP_DIR = '.skill-map';\n\nconst DB_FILENAME = 'skill-map.db';\nconst JOBS_DIRNAME = 'jobs';\nconst PLUGINS_DIRNAME = 'plugins';\nconst SETTINGS_FILENAME = 'settings.json';\nconst LOCAL_SETTINGS_FILENAME = 'settings.local.json';\nconst IGNORE_FILENAME = '.skillmapignore';\n\n/**\n * Single source of truth for the relative DB path inside a scope\n * directory (`.skill-map/skill-map.db`). Same string in project and\n * global scope; the difference is the parent directory the helper\n * resolves against.\n */\nconst DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;\n\n/**\n * Entries `sm init` appends to the project `.gitignore`. Centralised\n * here (instead of the verb file) so the literals live alongside their\n * filename constants and the verb consumes them as a frozen list.\n */\nexport const GITIGNORE_ENTRIES: readonly string[] = [\n `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,\n `${SKILL_MAP_DIR}/${DB_FILENAME}`,\n];\n\n/**\n * Inputs for `resolveDbPath`. Extends `IRuntimeContext` so the helper\n * never reads `process.cwd()` / `homedir()` directly — every caller\n * threads the runtime context (mandatory) alongside the spec flags.\n * Pattern: `resolveDbPath({ global, db, ...defaultRuntimeContext() })`.\n */\nexport interface IDbLocationOptions extends IRuntimeContext {\n global: boolean;\n db: string | undefined;\n}\n\n/**\n * Resolve the DB file path from command-line options.\n *\n * Precedence: explicit `--db <path>` > `-g/--global` (~/.skill-map/) >\n * project default (cwd/.skill-map/).\n *\n * Always returns an absolute path. Does NOT verify existence — pair with\n * `assertDbExists` for read-side verbs.\n */\nexport function resolveDbPath(options: IDbLocationOptions): string {\n if (options.db) return resolve(options.db);\n if (options.global) return join(options.homedir, DEFAULT_DB_REL);\n return resolve(options.cwd, DEFAULT_DB_REL);\n}\n\n/**\n * Default project DB path (`<cwd>/.skill-map/skill-map.db`). Same effect\n * as `resolveDbPath({ global: false, db: undefined, ...ctx })`; this\n * helper is the cheaper and more explicit route for call sites that have\n * no `--global` / `--db` flags to honour (`sm scan`, `sm refresh`,\n * `sm watch`).\n */\nexport function defaultProjectDbPath(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, DEFAULT_DB_REL);\n}\n\n/**\n * Default project jobs directory (`<cwd>/.skill-map/jobs`). Used by the\n * `sm job prune` orphan-files pass and any other call site that needs\n * the project-scoped jobs spool.\n */\nexport function defaultProjectJobsDir(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);\n}\n\n/**\n * Default project plugins directory (`<cwd>/.skill-map/plugins`).\n * Project + user plugin discovery composes this with the user-scoped\n * `<homedir>/.skill-map/plugins` peer.\n */\nexport function defaultProjectPluginsDir(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);\n}\n\n/**\n * Default user (global) plugins directory (`<homedir>/.skill-map/plugins`).\n * Used alongside `defaultProjectPluginsDir` when discovery walks both\n * scopes.\n */\nexport function defaultUserPluginsDir(ctx: IRuntimeContext): string {\n return join(ctx.homedir, SKILL_MAP_DIR, PLUGINS_DIRNAME);\n}\n\n/**\n * Default DB path under an arbitrary scope root\n * (`<scopeRoot>/.skill-map/skill-map.db`). Companion to\n * `defaultProjectDbPath` for callers that already resolved the scope\n * root themselves (today: `sm init`, which switches between\n * `cwd`/`homedir` based on `--global`).\n */\nexport function defaultDbPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);\n}\n\n/**\n * Default settings file (`<scopeRoot>/.skill-map/settings.json`).\n */\nexport function defaultSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, SETTINGS_FILENAME);\n}\n\n/**\n * Default local-overrides settings file\n * (`<scopeRoot>/.skill-map/settings.local.json`).\n */\nexport function defaultLocalSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);\n}\n\n/**\n * Default `.skillmapignore` file path\n * (`<scopeRoot>/.skillmapignore`). Sits at the scope root, NOT inside\n * `.skill-map/` — `sm scan` reads it from the same level as `package.json`\n * etc. so authors can keep ignore rules visible in the project tree.\n */\nexport function defaultIgnoreFilePath(scopeRoot: string): string {\n return join(scopeRoot, IGNORE_FILENAME);\n}\n","/**\n * Atomic file I/O for `.skill-map/settings.json` writes.\n *\n * Promoted from `src/cli/commands/config.ts` (`writeJsonAtomic` +\n * `readJsonObjectOrEmpty`) so the config-helper module and any other\n * settings-mutating code path can share one implementation. Behavior\n * is unchanged from the previous inline definitions.\n *\n * Lives under `src/core/config/` so `cli/` and `server/` (BFF) can\n * both import it; the module reads no `process.env` /\n * `process.cwd()` (every input is an explicit parameter), so the\n * kernel-boundary lint rule (`src/eslint.config.js:233`) holds.\n */\n\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n\n/**\n * Read `path` as a JSON object. Returns `{}` when the file is absent,\n * malformed, or its top-level value is not a plain object (arrays /\n * scalars). Never throws — callers treat \"no settings here\" the same\n * as \"settings present but empty.\"\n */\nexport function readJsonObjectOrEmpty(path: string): Record<string, unknown> {\n if (!existsSync(path)) return {};\n try {\n const raw = JSON.parse(readFileSync(path, 'utf8'));\n if (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n return raw as Record<string, unknown>;\n }\n } catch {\n /* fall through to {} */\n }\n return {};\n}\n\n/**\n * Write `content` to `path` atomically. The body is staged into a\n * sibling `<path>.tmp.<pid>` file (same directory so the rename never\n * crosses filesystems) and `renameSync`'d into place — POSIX\n * guarantees rename is atomic on the same fs, so a crash mid-write\n * leaves the destination either at its prior content or at the new\n * content, never half-written.\n *\n * The pre-rename stage is owner-only (`writeFileSync` defaults to the\n * process umask; we do not chmod here because settings.json is not\n * security-critical, and tightening would diverge from `sm init`'s\n * behaviour).\n *\n * On failure the temp file is best-effort removed so we do not leak\n * `<path>.tmp.<pid>` siblings if e.g. the rename target is read-only.\n */\nexport function writeJsonAtomic(path: string, content: Record<string, unknown>): void {\n mkdirSync(dirname(path), { recursive: true });\n const tmp = `${path}.tmp.${process.pid}`;\n try {\n writeFileSync(tmp, JSON.stringify(content, null, 2) + '\\n', 'utf8');\n renameSync(tmp, path);\n } catch (err) {\n try {\n unlinkSync(tmp);\n } catch {\n // Best effort — the staged file may not exist (writeFileSync\n // could have failed before the inode was created).\n }\n throw err;\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 TProgressListener,\n} from '../ports/progress-emitter.js';\n\nexport class InMemoryProgressEmitter implements ProgressEmitterPort {\n readonly #listeners = new Set<TProgressListener>();\n\n emit(event: ProgressEvent): void {\n for (const listener of this.#listeners) listener(event);\n }\n\n subscribe(listener: TProgressListener): () => 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 // Spec § 9.6.6 — per-extension annotation-contribution validation.\n // Two cross-cutting rules per entry: (a) `location: 'root'` REQUIRES\n // `ownership: 'exclusive'`, (b) the inline `schema` must be a valid\n // JSON Schema (compile with AJV). Cross-plugin collision detection\n // for `(key, location: 'root', ownership: 'exclusive')` runs later\n // at the orchestrator/composer level; this stage covers single-plugin\n // shape validation only.\n const contribFailure = validateAnnotationContributions(\n pluginPath,\n manifest,\n relEntry,\n manifestView,\n );\n if (contribFailure) return { ok: false, failure: contribFailure };\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 § 9.6.6 — Annotation-contribution validation. Runs AFTER the\n * kind-specific AJV manifest pass (the contribution shape — schema /\n * ownership / location — is already structurally validated by then via\n * the base schema). Two extra invariants:\n *\n * (a) `location: 'root'` REQUIRES `ownership: 'exclusive'` (a\n * top-level reserved key cannot be silently shared).\n * (b) The inline `schema` MUST AJV-compile cleanly (catch typos in\n * JSON-Schema-keyword usage at load time, not at first write).\n *\n * Returns a discovered-plugin failure (`invalid-manifest`) on either\n * violation, or `null` when the extension's contributions are well-formed.\n * Cross-plugin collision detection runs later in the runtime composer.\n */\n// Linear validator with one branch per failure mode (root-shared,\n// schema-not-object, schema-compile-fails) plus the per-entry guards.\n// Each branch returns directly; cyclomatic count comes from the guard\n// chain inside the entry loop, not from real nested logic.\n// eslint-disable-next-line complexity\nfunction validateAnnotationContributions(\n pluginPath: string,\n manifest: IPluginManifest,\n relEntry: string,\n manifestView: unknown,\n): IDiscoveredPlugin | null {\n if (!isRecord(manifestView)) return null;\n const raw = manifestView['annotationContributions'];\n if (raw === undefined) return null;\n if (!isRecord(raw)) return null;\n for (const [key, value] of Object.entries(raw)) {\n if (!isRecord(value)) continue;\n const location = (value['location'] as string | undefined) ?? 'namespaced';\n const ownership = (value['ownership'] as string | undefined) ?? 'shared';\n if (location === 'root' && ownership !== 'exclusive') {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestRootSharedAnnotation, {\n relEntry,\n key,\n ownership,\n }),\n ),\n manifest,\n };\n }\n const schema = value['schema'];\n if (!isRecord(schema)) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {\n relEntry,\n key,\n errDescription: 'schema must be an object literal',\n }),\n ),\n manifest,\n };\n }\n try {\n const ajv: TAjv = new Ajv2020({ strict: false, allErrors: true, allowUnionTypes: true });\n applyAjvFormats(ajv);\n ajv.compile(schema);\n } catch (err) {\n return {\n ...fail(\n pluginPath,\n manifest.id,\n 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestAnnotationSchemaCompile, {\n relEntry,\n key,\n errDescription: describe(err),\n }),\n ),\n manifest,\n };\n }\n }\n return null;\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', 'analyzer', '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 * 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 — ten events. Eight\n * are pipeline-driven (emitted from inside `runScan`); two\n * (`boot`, `shutdown`) are CLI-process-driven (emitted by the driving\n * binary before / after the verb runs, fire-and-forget so\n * `process.exit` is never blocked). The full `ProgressEmitterPort`\n * catalog (per-node `scan.progress`, `model.delta`, `run.*`, internal\n * job lifecycle) is deliberately not hookable: too verbose for a\n * reactive surface, internal to the runner, or covered elsewhere.\n * Declaring a trigger outside the curated set yields\n * `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 * 0. `boot` — once per CLI process, before verb routing.\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. `analyzer.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 * 9. `shutdown` — once per CLI process, after the verb's\n * exit code resolves and before\n * `process.exit`.\n */\n\nimport type { IExtensionBase } from './base.js';\nimport type { Node, TExecutionMode } from '../types.js';\n\n/**\n * The ten hookable lifecycle events. Mirrors the `triggers[]` enum in\n * `spec/schemas/extensions/hook.schema.json`. Eight are pipeline-driven\n * (emitted from inside `runScan`); two (`boot`, `shutdown`) are\n * CLI-process-driven (emitted by the driving binary before / after the\n * verb runs). Anything outside this set is rejected at load time as\n * `invalid-manifest`.\n */\nexport type THookTrigger =\n | 'boot'\n | 'scan.started'\n | 'scan.completed'\n | 'extractor.completed'\n | 'analyzer.completed'\n | 'action.completed'\n | 'job.spawning'\n | 'job.completed'\n | 'job.failed'\n | 'shutdown';\n\n/**\n * Frozen list mirror of `THookTrigger` for runtime introspection. The\n * loader validates `manifest.triggers[]` against this set; the\n * dispatcher iterates it in order when fanning an event out to\n * subscribed hooks. `boot` first / `shutdown` last so a debug log of\n * the array reads in lifecycle order.\n */\nexport const HOOK_TRIGGERS: readonly THookTrigger[] = Object.freeze([\n 'boot',\n 'scan.started',\n 'scan.completed',\n 'extractor.completed',\n 'analyzer.completed',\n 'action.completed',\n 'job.spawning',\n 'job.completed',\n 'job.failed',\n 'shutdown',\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 * / `analyzerId` / `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 `analyzer.completed` events. Qualified extension id of the Rule.\n */\n analyzerId?: 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 * 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 \"{{analyzerId}}\" emitted an issue with invalid severity {{severity}} ' +\n \"(allowed: 'error' | 'warn' | 'info'). Issue dropped.\",\n\n extensionErrorContributionUnknownId:\n 'Extractor \"{{extractorId}}\" emitted contribution \"{{contributionId}}\" on {{nodePath}} ' +\n 'but did not declare it in its `viewContributions` map. Contribution dropped.',\n\n extensionErrorContributionPayloadInvalid:\n 'Extractor \"{{extractorId}}\" emitted contribution \"{{contributionId}}\" on {{nodePath}}; ' +\n 'payload failed the \"{{slot}}\" schema: {{errors}}. Contribution 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 * Kernel walker. Discovers files inside one or more scope roots,\n * reads each, parses it via the configured parser, and yields\n * `IRawNode` records the orchestrator consumes.\n *\n * Owns the audit-cleared defences (every Provider that uses the walker\n * inherits these — no duplication needed in `Provider.walk`):\n *\n * - **Symlinks (audit M7)** — `entry.isSymbolicLink()` is checked\n * explicitly and the entry is skipped. Without this guard we relied\n * on `Dirent.isFile()` returning false for symlinks, which is an\n * implementation detail of node's `withFileTypes`. The explicit\n * skip is both self-documenting and resilient to future Dirent API\n * changes. The `followSymlinks?: false` option is reserved for a\n * future implementation that adds cycle detection + `realpath`-\n * resolved containment; until then the type forbids `true`.\n * - **TOCTOU race (audit M7)** — `readdir` reports a regular file →\n * `stat()` re-verifies before the read. Closes the window where the\n * entry could be swapped for a symlink between the two calls.\n * `stat` follows symlinks; rejecting non-regular results closes\n * that lane too.\n * - **Ignore filter** — every directory and file's path-relative-to-\n * root is checked against the project's `IIgnoreFilter`. When the\n * caller does not supply one, the walker falls back to bundled\n * defaults via `buildIgnoreFilter()` so direct test invocations\n * keep working without an explicit filter argument.\n *\n * Parser dispatch is by id: the walker resolves `options.parser`\n * against the kernel-internal parser registry once at the start of the\n * walk and throws `UnknownParserError` for unknown ids. Built-in\n * parsers ship with the kernel (`frontmatter-yaml`, `plain`); the set\n * is closed by design.\n */\n\nimport { readFile, readdir, stat } from 'node:fs/promises';\nimport { join, relative, sep } from 'node:path';\n\nimport type { IRawNode } from '../extensions/provider.js';\nimport { buildIgnoreFilter, type IIgnoreFilter } from './ignore.js';\nimport { getParser } from './parsers/index.js';\n\nexport interface IWalkContentOptions {\n /**\n * File extensions the walker yields. Strings include the leading dot\n * (e.g. `'.md'`, `'.mdc'`, `'.toml'`). Match is suffix-based; the\n * extension comparison is case-sensitive — Providers MUST list every\n * casing they want to match (today the kernel emits lowercase only,\n * matching the on-disk convention of every supported Provider).\n */\n extensions: readonly string[];\n /**\n * Parser id from the kernel-internal registry. Built-ins:\n * `'frontmatter-yaml'`, `'plain'`. Unknown ids throw at the start of\n * the walk; the orchestrator surfaces this as a Provider issue with\n * status `invalid-manifest`.\n */\n parser: string;\n /**\n * Project ignore filter. When omitted the walker uses the bundled\n * defaults (`buildIgnoreFilter()` with no extra layers), keeping\n * direct test invocations working without ceremony. Production callers\n * (the orchestrator) always pass a fully-composed filter.\n */\n ignoreFilter?: IIgnoreFilter;\n /**\n * Reserved escape hatch for a future symlink-follow implementation.\n * Today the walker hard-skips symlinks per audit M7. The type forbids\n * `true` until the audit-cleared follow path is actually built.\n */\n followSymlinks?: false;\n}\n\nexport class UnknownParserError extends Error {\n constructor(parserId: string) {\n super(`Unknown parser id '${parserId}'. Built-in parsers: 'frontmatter-yaml', 'plain'.`);\n this.name = 'UnknownParserError';\n }\n}\n\n/**\n * Walk the given roots and yield one `IRawNode` per matching file.\n * Async generator so large scopes don't buffer in memory.\n */\nexport async function* walkContent(\n roots: readonly string[],\n options: IWalkContentOptions,\n): AsyncIterable<IRawNode> {\n const parser = getParser(options.parser);\n if (!parser) throw new UnknownParserError(options.parser);\n const filter: IIgnoreFilter = options.ignoreFilter ?? buildIgnoreFilter();\n const extensions = options.extensions;\n for (const root of roots) {\n for await (const file of walkRoot(root, root, filter, extensions)) {\n const relPath = relative(root, file).split(sep).join('/');\n let raw: string;\n try {\n raw = await readFile(file, 'utf8');\n } catch {\n // silently skip unreadable files\n continue;\n }\n const parsed = parser.parse(raw, relPath);\n yield {\n path: relPath,\n body: parsed.body,\n frontmatterRaw: parsed.frontmatterRaw,\n frontmatter: parsed.frontmatter,\n };\n }\n }\n}\n\n// eslint-disable-next-line complexity\nasync function* walkRoot(\n root: string,\n current: string,\n filter: IIgnoreFilter,\n extensions: readonly string[],\n): AsyncIterable<string> {\n let entries;\n try {\n entries = await readdir(current, { withFileTypes: true, encoding: 'utf8' });\n } catch {\n return;\n }\n for (const entry of entries) {\n const name = entry.name;\n const full = join(current, name);\n const rel = relative(root, full).split(sep).join('/');\n if (filter.ignores(rel)) continue;\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n yield* walkRoot(root, full, filter, extensions);\n } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {\n // TOCTOU re-check: readdir reported a regular file; verify before\n // reading. `stat` follows symlinks, so a swap between the two\n // calls is rejected here.\n try {\n const s = await stat(full);\n if (s.isFile()) yield full;\n } catch {\n // silently skip unreadable files\n }\n }\n }\n}\n\nfunction hasMatchingExtension(name: string, extensions: readonly string[]): boolean {\n for (const ext of extensions) {\n if (name.endsWith(ext)) return true;\n }\n return false;\n}\n","/**\n * `.skillmapignore` parser + filter facade. Wraps `ignore` (kaelzhang)\n * with the project-local layering: bundled defaults → `config.ignore`\n * (from `.skill-map/settings.json`) → `.skillmapignore` file content.\n *\n * Why a wrapper instead of exposing `ignore` directly:\n *\n * 1. Single-source defaults — `src/config/defaults/skillmapignore` is\n * the canonical default list, loaded once at module init (or at\n * explicit build time, depending on bundling). The runtime never\n * re-reads it per scan.\n * 2. Stable interface — Providers and the orchestrator depend on a\n * minimal `IIgnoreFilter` shape, so the underlying library can be\n * swapped without touching every consumer.\n * 3. Path normalization — every consumer passes the path RELATIVE to\n * the scan root (POSIX separators); the wrapper guarantees that\n * contract before delegating to `ignore`.\n */\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport ignoreFactory from 'ignore';\n\nexport interface IIgnoreFilter {\n /**\n * Returns `true` when `relativePath` should be skipped. The caller\n * MUST pass paths relative to the scan root, with POSIX separators\n * (forward slashes), no leading `/`. Directories MAY be passed with\n * or without trailing `/`; the wrapper does not require it.\n */\n ignores(relativePath: string): boolean;\n}\n\nexport interface IBuildIgnoreFilterOptions {\n /** Patterns from `config.ignore` in `.skill-map/settings.json`. */\n configIgnore?: string[] | undefined;\n /**\n * Raw text of the project's `.skillmapignore` file. Comments and\n * blank lines are tolerated by `ignore` itself; the caller does not\n * need to pre-process. Accepts `undefined` so callers can forward\n * `readIgnoreFileText()` directly without a guard.\n */\n ignoreFileText?: string | undefined;\n /**\n * When `false`, the bundled defaults are NOT pre-loaded. Default is\n * `true`. Tests use `false` to assert the precise effect of a single\n * pattern.\n */\n includeDefaults?: boolean | undefined;\n}\n\n/**\n * Build a filter from any combination of layers. Layer order is fixed:\n *\n * 1. bundled defaults (`src/config/defaults/skillmapignore`)\n * 2. `configIgnore`\n * 3. `ignoreFileText`\n *\n * Later layers override earlier ones via gitignore negation rules\n * (`!pattern` re-includes a path the prior layer excluded).\n */\nexport function buildIgnoreFilter(opts: IBuildIgnoreFilterOptions = {}): IIgnoreFilter {\n const ig = ignoreFactory();\n if (opts.includeDefaults !== false) {\n ig.add(loadDefaultsText());\n }\n if (opts.configIgnore && opts.configIgnore.length > 0) {\n ig.add(opts.configIgnore);\n }\n if (opts.ignoreFileText && opts.ignoreFileText.length > 0) {\n ig.add(opts.ignoreFileText);\n }\n return {\n ignores(relativePath: string): boolean {\n // `ignore` requires a non-empty relative path; the empty string\n // (the root itself) MUST never be ignored.\n if (relativePath === '' || relativePath === '.' || relativePath === './') {\n return false;\n }\n const normalised = relativePath.replace(/^\\.\\//, '').replace(/\\\\/g, '/').replace(/^\\//, '');\n if (normalised === '') return false;\n return ig.ignores(normalised);\n },\n };\n}\n\n/**\n * Return the bundled defaults text. Useful for `sm init` (which writes\n * the file into the user's scope) and for tests. The same caching\n * logic backs `buildIgnoreFilter` so this never re-reads from disk on\n * a hot path.\n */\nexport function loadBundledIgnoreText(): string {\n return loadDefaultsText();\n}\n\n/**\n * Read `.skillmapignore` from `<root>/.skillmapignore` if it exists,\n * else return `undefined`. Caller passes the result as `ignoreFileText`\n * to `buildIgnoreFilter`.\n */\nexport function readIgnoreFileText(scopeRoot: string): string | undefined {\n const path = resolve(scopeRoot, '.skillmapignore');\n if (!existsSync(path)) return undefined;\n try {\n return readFileSync(path, 'utf8');\n } catch {\n return undefined;\n }\n}\n\n/**\n * Async version of `readIgnoreFileText` that waits until the file's\n * content stops changing before returning. Used by the BFF + CLI\n * `sm watch` meta-file handlers when chokidar fires a `change` event\n * for `.skillmapignore`.\n *\n * Why: editors save in two motions — truncate (or rename-over) and\n * then write. chokidar emits the `change` event on the first motion\n * already, so a naive read can land while the file is empty or\n * partially flushed, rebuilding the ignore filter without the new\n * pattern. The user then has to save again to get the real effect.\n *\n * Strategy: read, sleep ~50 ms, read again. If both reads agree, the\n * file has settled — return that text. If they differ, retry up to\n * `maxAttempts` times. After the cap (~500 ms), use whatever the last\n * read produced; even partial content beats blocking the watcher.\n *\n * Default knobs (`pollMs: 50`, `maxAttempts: 10`) mirror the canonical\n * chokidar `awaitWriteFinish` recipe and were chosen because every\n * common editor (VS Code, vim, JetBrains, nano) settles inside that\n * window.\n */\nexport async function readIgnoreFileTextStable(\n scopeRoot: string,\n opts: { pollMs?: number; maxAttempts?: number } = {},\n): Promise<string | undefined> {\n const pollMs = opts.pollMs ?? 50;\n const maxAttempts = opts.maxAttempts ?? 10;\n let prev = readIgnoreFileText(scopeRoot);\n for (let i = 0; i < maxAttempts; i++) {\n await new Promise<void>((r) => setTimeout(r, pollMs));\n const curr = readIgnoreFileText(scopeRoot);\n if (curr === prev) return curr;\n prev = curr;\n }\n return prev;\n}\n\n// -----------------------------------------------------------------------------\n// Bundled defaults loader\n// -----------------------------------------------------------------------------\n\nlet cachedDefaults: string | null = null;\n\nfunction loadDefaultsText(): string {\n if (cachedDefaults !== null) return cachedDefaults;\n cachedDefaults = readDefaultsFromDisk();\n return cachedDefaults;\n}\n\n/** Test-only — drop the cache so a unit test can simulate a missing file. */\nexport function _resetDefaultsCacheForTests(): void {\n cachedDefaults = null;\n}\n\n/**\n * Resolve `src/config/defaults/skillmapignore` from disk. Walks a small\n * list of candidate locations relative to this module so the lookup\n * works in both the dev layout (`src/kernel/scan/ignore.ts` →\n * `src/config/defaults/`) and the bundled layout (single-file\n * `dist/...js` → `dist/config/defaults/`, populated by tsup `onSuccess`).\n */\nfunction readDefaultsFromDisk(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n const candidates = [\n resolve(here, '../../config/defaults/skillmapignore'), // src/kernel/scan/ → src/config/defaults/\n resolve(here, '../config/defaults/skillmapignore'), // dist/cli.js → dist/config/defaults/ (siblings)\n resolve(here, 'config/defaults/skillmapignore'),\n ];\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n try {\n return readFileSync(candidate, 'utf8');\n } catch {\n /* try next candidate */\n }\n }\n }\n // Fail soft: the scan still works without bundled defaults. The user's\n // own `.skillmapignore` + config.ignore still apply.\n return '';\n}\n","/**\n * `frontmatter-yaml` parser. Splits a `--- yaml --- body` document and\n * parses the frontmatter via `js-yaml`. Carries the audit-cleared\n * defences:\n *\n * - **Symlink / TOCTOU** — out of scope here (lives in the walker;\n * this parser receives the raw string after the kernel walker has\n * already vetted the file).\n * - **Prototype pollution (audit L2/L3)** — keys named `__proto__`,\n * `constructor`, `prototype` are stripped from the parsed object.\n * `js-yaml` stores `__proto__:` as an own data property (rather\n * than mutating `Object.prototype`), but the value still flows into\n * downstream `Object.assign`-style merges where the `__proto__`\n * setter fires. Stripping at parse time keeps the returned object\n * safe to spread, copy, and persist.\n * - **`!!js/function` & friends (audit L3)** — `yaml.load` runs with\n * `schema: JSON_SCHEMA` explicitly. js-yaml v4's default schema is\n * already safe (no `!!js/function` tag), but the explicit selection\n * documents intent and protects against an upstream default flip.\n * Frontmatter values that are valid JSON (string, number, bool,\n * null, sequence, mapping) round-trip unchanged; YAML-only\n * conveniences like unquoted timestamps degrade to strings, but the\n * kernel's node schema does not depend on parsed Date objects so\n * the tradeoff is safe.\n *\n * Lives under `src/built-in-plugins/parsers/` even though the parser\n * registry stays kernel-internal (no `kind: 'parser'` is exposed to\n * plugin authors). The relocation aligns the file layout with the\n * other built-ins (Provider / Extractor / Rule / Formatter / Action /\n * Hook) — every shipped extension-shaped artifact lives under\n * `built-in-plugins/`. The registry in `kernel/scan/parsers/index.ts`\n * imports from here and stays the single resolution surface.\n */\n\nimport yaml from 'js-yaml';\n\nimport type { IFileParser, IParsedFile } from '../../../kernel/scan/parsers/types.js';\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\nconst FORBIDDEN_FRONTMATTER_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nexport const frontmatterYamlParser: IFileParser = {\n id: 'frontmatter-yaml',\n parse(raw: string, _path: string): IParsedFile {\n const match = FRONTMATTER_RE.exec(raw);\n if (!match) return { frontmatterRaw: '', frontmatter: {}, body: raw };\n const frontmatterRaw = match[1]!;\n const body = match[2]!;\n const parsed: Record<string, unknown> = {};\n try {\n const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });\n if (doc && typeof doc === 'object' && !Array.isArray(doc)) {\n for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {\n if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;\n parsed[k] = v;\n }\n }\n } catch {\n // Malformed YAML — leave as empty object, keep the raw string for\n // downstream diagnostics.\n }\n return { frontmatterRaw, frontmatter: parsed, body };\n },\n};\n","/**\n * `plain` parser. Treats the entire raw as the body; emits an empty\n * frontmatter object and an empty `frontmatterRaw`. Pure pass-through:\n * the body is not normalised, line endings are preserved verbatim.\n *\n * Used by Providers that walk files carrying no frontmatter convention\n * (e.g. Roo Code rules at `.roo/rules/*.md`, Windsurf rules at\n * `.windsurf/rules/*.md`, plain `CONVENTIONS.md`). Such Providers MUST\n * derive `frontmatter.name` (and other base-required fields) from the\n * file path inside their `classify()` / Provider-side post-processing,\n * because the spec's `frontmatter/base.schema.json` requires `name`.\n *\n * Spec note: when the `frontmatter/base.schema.json` `name` requirement\n * is relaxed in a later phase, the path-derivation step becomes optional.\n *\n * Lives under `src/built-in-plugins/parsers/` for layout consistency\n * with the other built-ins; the parser registry in\n * `kernel/scan/parsers/index.ts` stays kernel-internal and imports\n * from here.\n */\n\nimport type { IFileParser, IParsedFile } from '../../../kernel/scan/parsers/types.js';\n\nexport const plainParser: IFileParser = {\n id: 'plain',\n parse(raw: string, _path: string): IParsedFile {\n return { frontmatter: {}, frontmatterRaw: '', body: raw };\n },\n};\n","/**\n * Kernel-internal parser registry. Built-ins are seeded at module load\n * time and frozen — user plugins cannot register their own parsers\n * (this module is NOT re-exported from `src/kernel/index.ts`).\n *\n * Provider manifests reference parsers by id via `read.parser`. The\n * walker calls `getParser(id)` once per scan when it resolves a\n * Provider's read config; the orchestrator never sees a parser\n * directly.\n *\n * Registry shape: a single `Map<id, IFileParser>` seeded from the two\n * built-in modules — both now living under `src/built-in-plugins/parsers/`\n * for layout consistency with the other shipped extensions, while the\n * registry itself stays kernel-internal (no `kind: 'parser'` is exposed\n * to plugin authors). The set of built-in ids is captured into\n * `FROZEN_IDS` at seed time; subsequent `registerParser` calls reject\n * collisions with frozen built-ins. The `registerParser` seam exists\n * for kernel-internal tests and future built-ins; it is not part of any\n * plugin-author API.\n */\n\nimport { frontmatterYamlParser } from '../../../built-in-plugins/parsers/frontmatter-yaml/index.js';\nimport { plainParser } from '../../../built-in-plugins/parsers/plain/index.js';\n\nimport type { IFileParser } from './types.js';\n\nexport type { IFileParser, IParsedFile } from './types.js';\n\nconst REGISTRY = new Map<string, IFileParser>([\n [frontmatterYamlParser.id, frontmatterYamlParser],\n [plainParser.id, plainParser],\n]);\nconst FROZEN_IDS: ReadonlySet<string> = new Set(REGISTRY.keys());\n\n/** Resolve a parser by id. Returns `undefined` for unknown ids. */\nexport function getParser(id: string): IFileParser | undefined {\n return REGISTRY.get(id);\n}\n\n/**\n * Kernel-internal seam for tests and future built-ins. Throws when the\n * id collides with a frozen built-in (`frontmatter-yaml`, `plain`).\n * NOT re-exported from `src/kernel/index.ts` — user plugins have no\n * public surface to call this.\n */\nexport function registerParser(parser: IFileParser): void {\n if (FROZEN_IDS.has(parser.id)) {\n throw new Error(\n `Cannot register parser with built-in id '${parser.id}'. Built-in parsers are frozen.`,\n );\n }\n REGISTRY.set(parser.id, parser);\n}\n\n/** Test-only — drop a non-built-in registration. Throws on a frozen id. */\nexport function _unregisterParserForTests(id: string): void {\n if (FROZEN_IDS.has(id)) {\n throw new Error(`Cannot unregister built-in parser '${id}'.`);\n }\n REGISTRY.delete(id);\n}\n","/**\n * Provider runtime contract. Walks filesystem roots and emits raw node\n * records; classification maps path conventions to a node kind.\n *\n * Distinct from the **hexagonal-architecture** 'adapter' (`RunnerPort.adapter`,\n * `StoragePort.adapter`, etc.). A `Provider` is an extension kind authored\n * by plugins to declare a platform's universe (the catalog of kinds it\n * emits, the per-kind frontmatter schema, the filesystem directory it\n * owns); a hexagonal adapter is an internal implementation of a port.\n * Both can coexist without confusion because they live in different\n * namespaces.\n *\n * `walk()` is an async iterator so large scopes don't buffer in memory.\n * Each yielded `IRawNode` carries the full parsed frontmatter + body plus\n * the path relative to the scan root; the kernel computes hashes, bytes,\n * and tokens on top.\n *\n * **Spec 0.8.0**. Per-kind frontmatter schemas relocated from the spec\n * to the Provider that owns them. The flat\n * `defaultRefreshAction` map collapsed into the new `kinds` map: every\n * kind the Provider emits gets one entry that declares both its schema\n * and its refresh action. Spec keeps only `frontmatter/base.schema.json`\n * (universal); per-kind schemas live with the Provider.\n */\n\nimport type { IExtensionBase } from './base.js';\nimport type { IIgnoreFilter } from '../scan/ignore.js';\nimport { walkContent } from '../scan/walk-content.js';\n\nexport interface IRawNode {\n /** Path relative to the scan root that produced this node. */\n path: string;\n /** Raw markdown body (everything after the frontmatter fence). */\n body: string;\n /** Raw frontmatter text (between `---` fences). Empty string when absent. */\n frontmatterRaw: string;\n /** Parsed frontmatter, or `{}` when absent / unparseable. */\n frontmatter: Record<string, unknown>;\n}\n\n/**\n * One entry in a Provider's `kinds` map. Declares both the per-kind\n * frontmatter schema (path relative to the Provider's package dir, plus\n * the loaded JSON object the kernel passes to AJV) and the qualified\n * default refresh action id the UI dispatches for nodes of this kind.\n *\n * The split between `schema` (manifest-level path) and `schemaJson`\n * (runtime-loaded JSON) keeps the manifest shape spec-conformant while\n * letting the runtime instance carry the parsed schema without a second\n * filesystem read at scan time. Built-in Providers populate `schemaJson`\n * via `import schema from './schemas/skill.schema.json' with { type: 'json' }`;\n * user-plugin Providers loaded by `PluginLoader` will have it filled in\n * by the loader after manifest validation.\n */\nexport interface IProviderKind {\n /**\n * Path to the kind's frontmatter JSON Schema, relative to the\n * Provider's package directory. Mirrors the spec field of the same\n * name in `extensions/provider.schema.json#/properties/kinds/.../schema`.\n */\n schema: string;\n /**\n * Loaded JSON Schema document for the kind. The kernel registers this\n * with AJV at scan boot and validates each node's frontmatter against\n * it. The schema MUST extend the spec's\n * `frontmatter/base.schema.json` via `allOf` + `$ref` to base's\n * `$id`; the loader registers base into the same AJV instance so\n * cross-package `$ref`-by-`$id` resolves transparently.\n *\n * `unknown` rather than a stronger type because AJV consumes any JSON\n * Schema object; tightening to a concrete shape would require mirroring\n * the JSON Schema vocabulary in TypeScript.\n */\n schemaJson: unknown;\n /**\n * Qualified action id (`<plugin-id>/<action-id>`) the probabilistic-\n * refresh UI dispatches for nodes of this kind. The kernel resolves\n * the id against its qualified action registry; a dangling reference\n * disables the Provider with status `invalid-manifest`.\n */\n defaultRefreshAction: string;\n /**\n * Presentation metadata the UI consumes to render nodes of this kind\n * (palette swatches, list tags, graph nodes, filter chips). Required\n * so the UI never has to invent visuals for a Provider-declared kind.\n * Mirrors `extensions/provider.schema.json#/properties/kinds/.../ui`.\n */\n ui: IProviderKindUi;\n}\n\n/**\n * Presentation contract for one Provider kind. The Provider declares\n * intent (label + base color, optional dark variant + emoji + icon);\n * the UI derives `bg`/`fg` tints per theme via a deterministic helper\n * and reads the registry from the `kindRegistry` field embedded in REST\n * envelopes. Single source of truth for what a kind looks like — the\n * UI never hardcodes presentation for a built-in kind.\n */\nexport interface IProviderKindUi {\n /**\n * Plural human-readable label for groups of this kind (e.g. `'Skills'`,\n * `'Agents'`, `'Cursor Rules'`). Used in filter dropdowns, palette\n * tooltips, and any list grouping.\n */\n label: string;\n /**\n * Base hex color (`#RRGGBB`) for the light theme. The UI derives `bg`\n * and `fg` tints from this value at runtime via a deterministic\n * helper. Declaring one base value (instead of three) keeps the\n * manifest small and centralises accessibility-driven contrast in the\n * UI.\n */\n color: string;\n /**\n * Optional dark-theme variant of `color`. When absent, the UI falls\n * back to `color`. Declared explicitly because a luminosity flip\n * rarely matches the brand intent for kinds that should stand out in\n * dark mode.\n */\n colorDark?: string;\n /**\n * Optional decorative emoji used as a fallback when `icon` is absent\n * or fails to render. Length-bound so the UI can lay it out\n * predictably alongside text.\n */\n emoji?: string;\n /**\n * Optional discriminated icon descriptor. The UI prefers `icon` over\n * `emoji`; when both are absent, the UI falls back to the first\n * letter of `label` colored with `color`.\n */\n icon?: TProviderKindIcon;\n}\n\n/**\n * Discriminated icon contract. `pi` references a PrimeIcons identifier\n * (e.g. `'pi-cog'`); `svg` carries raw SVG path data the UI wraps in a\n * `<svg viewBox=\"0 0 24 24\"><path d=\"…\"/></svg>` element tinted with\n * `currentColor`. The discriminator (`kind`) keeps the UI dispatch\n * exhaustive without string-sniffing the payload.\n */\nexport type TProviderKindIcon =\n | { kind: 'pi'; id: string }\n | { kind: 'svg'; path: string };\n\nexport interface IProvider extends IExtensionBase {\n kind: 'provider';\n\n /**\n * Filesystem directory (relative to user home or project root) where this\n * Provider's content lives. Required. Examples: `'~/.claude'` for the\n * Claude Provider, `'~/.cursor'` for a hypothetical Cursor Provider.\n * The kernel walks this directory during boot/scan to discover nodes;\n * `sm doctor` validates the directory exists and emits a non-blocking\n * warning when it does not.\n */\n explorationDir: string;\n\n /**\n * Catalog of node kinds this Provider emits. Keyed by kind name. Every\n * kind the Provider can `classify()` MUST have an entry; an entry is\n * the union of the kind's frontmatter schema and its default refresh\n * action.\n *\n * The string keys are typed loosely (`string`) rather than `NodeKind`\n * because the value space is open by design: a future Cursor Provider\n * could declare `rule`, an Obsidian Provider could declare `daily`.\n * The kernel's hard-coded `NodeKind` union represents the kinds the\n * built-in Claude Provider emits; it is NOT the kernel-wide kind type\n * (see `kernel/types.ts:NodeKind` docstring). `Node.kind`, the AJV\n * `node.schema.json` validator, and the SQLite `scan_nodes.kind`\n * column all accept any non-empty string an enabled Provider returns.\n */\n kinds: Record<string, IProviderKind>;\n\n /**\n * Optional auxiliary JSON Schemas this Provider's per-kind schemas\n * `$ref` by `$id`. Registered with AJV via `addSchema` BEFORE the\n * per-kind schemas compile, so cross-file `$ref` resolution succeeds.\n *\n * Use case: when several kinds share a common base (e.g. Anthropic's\n * merged skill / command frontmatter — both extend a shared\n * `skill-base.schema.json`), the Provider declares the base here so\n * `skill.schema.json` and `command.schema.json` can `$ref` it without\n * duplicating fields.\n *\n * Runtime-only — does NOT appear in the spec's `provider.schema.json`\n * manifest. Manifest-validated schemas remain the per-kind ones in\n * `kinds[<kind>].schema`; auxiliary schemas are an implementation\n * concern of how the runtime composes those.\n */\n schemas?: unknown[];\n\n /**\n * Declarative file-discovery config consumed by the kernel walker.\n * When present, the kernel walks every root, includes files whose\n * extension matches `extensions`, parses each with the parser id\n * registered in the kernel-internal registry, and yields `IRawNode`\n * records the same shape `walk()` would.\n *\n * When neither `read` nor `walk` is declared, `resolveProviderWalk`\n * applies the default `{ extensions: ['.md'], parser: 'frontmatter-yaml' }`\n * so the most common Provider shape needs zero configuration.\n *\n * Precedence: when both `walk()` (runtime field) and `read` are\n * declared, `walk()` wins — `read` is ignored. The escape-hatch\n * relationship is intentional: most Providers should use `read`;\n * Providers with non-standard discovery requirements (custom file\n * naming, multi-pass walks, dynamic ignore logic) implement `walk()`\n * directly and accept the duplication of audit-cleared defences.\n *\n * Built-in parsers: `'frontmatter-yaml'` (markdown with `--- … ---`\n * YAML frontmatter; pollution-strip + JSON_SCHEMA-pinned), `'plain'`\n * (entire body, empty frontmatter). The set is closed; user plugins\n * cannot register their own.\n */\n read?: IProviderReadConfig;\n\n /**\n * Walk the given roots and yield every node the Provider recognises.\n * Non-matching files are silently skipped. Unreadable files produce\n * a diagnostic via the emitter but do not abort the walk.\n *\n * `options.ignoreFilter` — when supplied, the Provider MUST\n * skip every directory and file whose path-relative-to-root the\n * filter reports as ignored. Providers MAY also keep their own\n * hard-coded skip list (e.g. `.git`) as a defensive measure, but the\n * filter is the canonical source of user intent.\n *\n * Optional. When omitted, the Provider MUST declare `read` (or rely\n * on the default config). The orchestrator never calls `walk()`\n * directly — it goes through `resolveProviderWalk(provider)` which\n * picks `walk` over `read`.\n */\n walk?(\n roots: string[],\n options?: { ignoreFilter?: IIgnoreFilter },\n ): AsyncIterable<IRawNode>;\n\n /**\n * Given a path and its parsed frontmatter, decide the node kind — or\n * `null` to disclaim the file. The classifier is called after walk()\n * yields; with multiple Providers active, every Provider walks every\n * file matching its `read.extensions`, so each Provider MUST disclaim\n * paths it does not recognise. Returning the same path's kind from\n * two Providers fires the spec's `provider-ambiguous` issue and the\n * orchestrator drops the duplicate.\n *\n * Convention: a Provider's classify returns one of its own `kinds`\n * map keys for paths in its territory (`.claude/`, `.gemini/`,\n * `.agents/skills/`, etc.) and `null` elsewhere. External Providers\n * (Cursor, Obsidian, …) follow the same rule: claim what's yours,\n * disclaim everything else. The orchestrator does not validate the\n * kind against `NodeKind`.\n */\n classify(path: string, frontmatter: Record<string, unknown>): string | null;\n}\n\n/**\n * Declarative read config a Provider declares via `IProvider.read`.\n * Mirrors `extensions/provider.schema.json#/properties/read` at the\n * TypeScript level. Built-in parser ids: `'frontmatter-yaml'`, `'plain'`.\n */\nexport interface IProviderReadConfig {\n /**\n * File extensions the walker yields. Strings include the leading dot\n * (e.g. `'.md'`, `'.mdc'`, `'.toml'`). Match is suffix-based; the\n * comparison is case-sensitive.\n */\n extensions: string[];\n /**\n * Parser id from the kernel-internal registry. Built-ins:\n * `'frontmatter-yaml'`, `'plain'`. Unknown ids surface as\n * `UnknownParserError` from the walker; the orchestrator translates\n * the error into a Provider issue with status `invalid-manifest`.\n */\n parser: string;\n}\n\nconst DEFAULT_READ_CONFIG: IProviderReadConfig = Object.freeze({\n extensions: Object.freeze(['.md']) as unknown as string[],\n parser: 'frontmatter-yaml',\n});\n\n/**\n * Resolve how a Provider walks its roots. Precedence:\n *\n * 1. If the Provider declares `walk()` (runtime field), use it as-is.\n * Escape hatch for Providers with non-standard discovery logic.\n * 2. Else, use `provider.read` (declarative config) — or the default\n * `{ extensions: ['.md'], parser: 'frontmatter-yaml' }` when\n * `read` is also absent — and route through the kernel walker.\n *\n * Defaulting at the call site (rather than at manifest-load) keeps the\n * AJV-validated manifest equal to what the plugin author wrote — `read`\n * is not silently injected into a Provider's runtime shape.\n */\nexport function resolveProviderWalk(\n provider: IProvider,\n): (\n roots: string[],\n options?: { ignoreFilter?: IIgnoreFilter },\n) => AsyncIterable<IRawNode> {\n if (provider.walk) {\n const walk = provider.walk.bind(provider);\n return walk;\n }\n const read = provider.read ?? DEFAULT_READ_CONFIG;\n return (roots, options) => {\n // `ignoreFilter` is optional under `exactOptionalPropertyTypes`; only\n // include the key when the caller actually supplied a filter so the\n // walker's default-fallback path is preserved.\n const walkOptions: import('../scan/walk-content.js').IWalkContentOptions = {\n extensions: read.extensions,\n parser: read.parser,\n };\n if (options?.ignoreFilter) walkOptions.ignoreFilter = options.ignoreFilter;\n return walkContent(roots, walkOptions);\n };\n}\n","/**\n * Hook lifecycle dispatcher (spec § A.11). Indexes the supplied hooks\n * by 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 it ships (Decision #114).\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 * caller continues. A buggy hook MUST NOT block the main pipeline (or\n * the CLI exit path) — that would invert the design intent (hooks\n * REACT to events, they never steer them).\n *\n * The module lives under `kernel/extensions/` (alongside the `IHook`\n * contract itself) so two callers share it: `runScan` for the eight\n * pipeline-driven triggers (`scan.*`, `extractor.completed`,\n * `analyzer.completed`, `action.completed`, `job.*`) and the CLI entry\n * for the two CLI-process-driven triggers (`boot`, `shutdown`).\n * Pulling the dispatcher out of the orchestrator keeps both consumers\n * symmetric — same indexing, same filter semantics, same error\n * policy.\n */\n\nimport type { IHook, IHookContext, THookTrigger } from './hook.js';\nimport type { Node } from '../types.js';\nimport type { ProgressEmitterPort, ProgressEvent } from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport { formatErrorMessage } from '../util/format-error.js';\nimport { log } from '../util/logger.js';\n\nexport interface IHookDispatcher {\n /**\n * Fan the event out to every hook subscribed to `trigger`. Awaits each\n * hook's `on(ctx)` in registration order. Errors are caught and\n * logged via `extension.error`; they never propagate.\n */\n dispatch(trigger: THookTrigger, event: ProgressEvent): Promise<void>;\n}\n\n/**\n * Build a dispatcher over the given hooks. Empty `hooks` returns a\n * cheap no-op shape so the call sites can dispatch unconditionally.\n */\nexport function makeHookDispatcher(\n hooks: IHook[],\n emitter: ProgressEmitterPort,\n): 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\n // subsystem). Log once per hook at composition time — not\n // per-event — so a noisy scan doesn't flood the logger. The\n // hook still surfaces in `sm plugins list`; it just doesn't\n // 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 = formatErrorMessage(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\n/** Construct a `ProgressEvent` envelope. Mirrors the orchestrator helper. */\nexport function makeEvent(type: string, data: unknown): ProgressEvent {\n return { type, timestamp: new Date().toISOString(), data };\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['analyzerId'] === 'string') ctx.analyzerId = data['analyzerId'];\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","/**\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**: `(analyzerId, 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.analyzerId}\\x00${ids}\\x00${issue.message}`;\n}\n\nfunction byIssueSort(a: Issue, b: Issue): number {\n if (a.analyzerId !== b.analyzerId) return a.analyzerId.localeCompare(b.analyzerId);\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 TLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';\n\nexport type TLogMethodLevel = Exclude<TLogLevel, 'silent'>;\n\nexport const LOG_LEVELS: readonly TLogLevel[] = [\n 'trace',\n 'debug',\n 'info',\n 'warn',\n 'error',\n 'silent',\n] as const;\n\nconst LEVEL_RANK: Record<TLogLevel, 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: TLogLevel): number {\n return LEVEL_RANK[level];\n}\n\nexport function isLogLevel(value: unknown): value is TLogLevel {\n return typeof value === 'string' && Object.prototype.hasOwnProperty.call(LEVEL_RANK, value);\n}\n\n/**\n * Parse a string into a `TLogLevel`. Returns `null` for invalid input\n * (incl. `undefined` / `null` / empty). Case-insensitive; trims\n * whitespace.\n */\nexport function parseLogLevel(value: string | undefined | null): TLogLevel | 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: TLogMethodLevel;\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';\nimport type { IAnnotationContribution } from './extensions/base.js';\nimport type { IRegisteredAnnotationKey } from './types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from './types/view-catalog.js';\n\nexport type { IRegisteredAnnotationKey } from './types/annotation-catalog.js';\nexport type {\n IRegisteredViewContribution,\n IViewContribution,\n ISettingDeclaration,\n TSlotName,\n TInputTypeName,\n TSeverity,\n TSettingValue,\n} from './types/view-catalog.js';\n\nexport interface Kernel {\n registry: Registry;\n /**\n * Step 9.6.6 — read-only catalog of plugin-contributed annotation\n * keys, keyed by `(pluginId, key)`. Populated at plugin-load time;\n * pure read with no side effects. Built-in catalog (from\n * `annotations.schema.json`) is NOT included here.\n */\n getRegisteredAnnotationKeys: () => readonly IRegisteredAnnotationKey[];\n /**\n * Internal — replace the frozen catalog. Called once by the\n * plugin runtime composer after every plugin has loaded; consumers\n * MUST treat the resulting array as immutable.\n */\n setRegisteredAnnotationKeys: (entries: readonly IRegisteredAnnotationKey[]) => void;\n /**\n * Step 11.x — read-only catalog of plugin-contributed view\n * contributions, keyed by `(pluginId, extensionId, contributionId)`.\n * Populated at plugin-load time; pure read with no side effects.\n * Mirror of `getRegisteredAnnotationKeys` for the view contribution\n * surface (see `architecture.md` §View contribution system →\n * Runtime catalog).\n */\n getRegisteredViewContributions: () => readonly IRegisteredViewContribution[];\n /**\n * Internal — replace the frozen view-contribution catalog. Called\n * once by the plugin runtime composer after every plugin has loaded;\n * consumers MUST treat the resulting array as immutable.\n */\n setRegisteredViewContributions: (entries: readonly IRegisteredViewContribution[]) => void;\n}\n\nexport function createKernel(): Kernel {\n let annotationKeys: readonly IRegisteredAnnotationKey[] = Object.freeze([]);\n let viewContributions: readonly IRegisteredViewContribution[] = Object.freeze([]);\n return {\n registry: new Registry(),\n getRegisteredAnnotationKeys() { return annotationKeys; },\n setRegisteredAnnotationKeys(entries) {\n annotationKeys = Object.freeze([...entries]);\n },\n getRegisteredViewContributions() { return viewContributions; },\n setRegisteredViewContributions(entries) {\n viewContributions = Object.freeze([...entries]);\n },\n };\n}\n\nexport type { IAnnotationContribution } from './extensions/base.js';\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 TProgressListener,\n} from './ports/progress-emitter.js';\nexport type {\n LoggerPort,\n TLogLevel,\n TLogMethodLevel,\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 IAnalyzer,\n IAnalyzerContext,\n IAction,\n IActionPrecondition,\n IActionContext,\n IActionResult,\n TActionWrite,\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';\nexport {\n makeHookDispatcher,\n makeEvent,\n type IHookDispatcher,\n} from './extensions/hook-dispatcher.js';\n"],"mappings":"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","names":["existsSync","statSync","isAbsolute","yaml","require","existsSync","existsSync","readFileSync","renameSync","writeFileSync","unlinkSync","dirname","resolve","createRequire","Ajv2020","yaml","resolve","existsSync","readFileSync","readFileSync","dirname","resolve","createRequire","Ajv2020","resolveSpecRoot","Ajv2020","resolve","readFileSync","require","createRequire","dirname","join","join","resolve","existsSync","readFileSync","dirname","createRequire","existsSync","readFileSync","readdirSync","isAbsolute","join","relative","resolve","Ajv2020","require","createRequire","resolve","readFileSync","join","relative","sep","existsSync","readFileSync","dirname","resolve","dirname","resolve","existsSync","readFileSync","yaml","relative","sep","join","walk","existsSync","statSync","node","yaml","isAbsolute","byPath","resolve","relative","sep","relativePathFromRoots"]}
|