@skill-map/cli 0.24.3 → 0.24.4

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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../kernel/i18n/registry.texts.ts","../kernel/util/tx.ts","../kernel/registry.ts","../kernel/orchestrator/index.ts","../package.json","../kernel/adapters/in-memory-progress.ts","../kernel/adapters/plugin-loader/index.ts","../kernel/adapters/plugin-loader/id-utils.ts","../kernel/adapters/plugin-loader/validation.ts","../kernel/util/ajv-interop.ts","../kernel/extensions/hook.ts","../kernel/adapters/plugin-loader/storage-schemas.ts","../kernel/i18n/plugin-store.texts.ts","../kernel/adapters/plugin-store.ts","../kernel/adapters/schema-validators.ts","../kernel/types/view-catalog.ts","../kernel/util/format-error.ts","../kernel/adapters/silent-logger.ts","../kernel/util/logger.ts","../kernel/extensions/hook-dispatcher.ts","../kernel/i18n/orchestrator.texts.ts","../kernel/orchestrator/extractors.ts","../kernel/orchestrator/analyzers.ts","../kernel/orchestrator/cache.ts","../kernel/orchestrator/renames.ts","../kernel/scan/walk-content.ts","../kernel/scan/ignore.ts","../built-in-plugins/parsers/frontmatter-yaml/index.ts","../kernel/util/strip-prototype-pollution.ts","../built-in-plugins/parsers/plain/index.ts","../kernel/scan/parsers/index.ts","../kernel/extensions/provider.ts","../kernel/sidecar/parse.ts","../kernel/sidecar/drift.ts","../kernel/sidecar/discover-orphans.ts","../kernel/sidecar/store.ts","../core/config/atomic-write.ts","../core/config/helper.ts","../kernel/config/loader.ts","../kernel/util/skill-map-paths.ts","../core/paths/db-path.ts","../kernel/orchestrator/node-build.ts","../kernel/orchestrator/frontmatter.ts","../kernel/orchestrator/walk.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 { existsSync, statSync } from 'node:fs';\n\n// js-tiktoken ships CJS subpaths without explicit `.cjs` in the import\n// specifier, the lint rule's hard-coded extension matrix doesn't model\n// dual-package CJS subpath exports.\n// eslint-disable-next-line import-x/extensions\nimport { Tiktoken } from 'js-tiktoken/lite';\n// eslint-disable-next-line import-x/extensions\nimport cl100k_base from 'js-tiktoken/ranks/cl100k_base';\n\nimport pkg from '../../package.json' with { type: 'json' };\n\nimport { InMemoryProgressEmitter } from '../adapters/in-memory-progress.js';\nimport { installedSpecVersion } from '../adapters/plugin-loader.js';\nimport type { IPluginStore } from '../adapters/plugin-store.js';\nimport {\n buildProviderFrontmatterValidator,\n type IProviderFrontmatterValidator,\n} from '../adapters/schema-validators.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport type { IPriorExtractorRun } from '../adapters/sqlite/scan-load.js';\nimport {\n makeHookDispatcher,\n makeEvent,\n type IHookDispatcher,\n} from '../extensions/hook-dispatcher.js';\nimport type {\n IAnalyzer,\n IExtractor,\n IHook,\n IProvider,\n} from '../extensions/index.js';\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type { Kernel } from '../index.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { IIgnoreFilter } from '../scan/ignore.js';\nimport type {\n Issue,\n ScanResult,\n ScanScannedBy,\n} from '../types.js';\nimport type { IRegisteredAnnotationKey } from '../types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from '../types/view-catalog.js';\nimport { tx } from '../util/tx.js';\nimport { runAnalyzers } from './analyzers.js';\nimport {\n indexPriorSnapshot,\n type IPriorIndex,\n} from './cache.js';\nimport {\n recomputeExternalRefsCount,\n recomputeLinkCounts,\n type IEnrichmentRecord,\n type IExtractorRunRecord,\n} from './extractors.js';\nimport {\n detectRenamesAndOrphans,\n type RenameOp,\n} from './renames.js';\nimport {\n walkAndExtract,\n type IWalkAndExtractResult,\n} from './walk.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\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/contribution-orphan` can introspect the catalog\n * (read-only) and join it with the live node set to flag\n * dangling emissions. Slot catalog drift is NOT a scan concern,\n * it lives at load time and surfaces via `sm plugins doctor`\n * (the kernel rejects unknown slots as `invalid-manifest` first,\n * doctor catches the catalog-version-skew tail).\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 * 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\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 setup = buildScanSetup(options);\n const { emitter, exts, hookDispatcher, encoder, prior, start } = setup;\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: setup.strict,\n enableCache: setup.enableCache,\n prior,\n priorIndex: setup.priorIndex,\n priorExtractorRuns: setup.priorExtractorRuns,\n providerFrontmatter: setup.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 await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);\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 registeredActionIds = new Set(\n _kernel.registry.all('action').map((a) => qualifiedExtensionId(a.pluginId, a.id)),\n );\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 registeredActionIds,\n emitter,\n hookDispatcher,\n );\n mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);\n const issues = analyzerResult.issues;\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 = buildScanStats(walked, issues, start);\n const scanCompletedEvent = makeEvent('scan.completed', { stats });\n emitter.emit(scanCompletedEvent);\n await hookDispatcher.dispatch('scan.completed', scanCompletedEvent);\n\n return buildScanReturn(walked, issues, renameOps, stats, options, setup);\n}\n\ninterface IScanSetup {\n start: number;\n scannedAt: number;\n emitter: ProgressEmitterPort;\n exts: NonNullable<RunScanOptions['extensions']>;\n hookDispatcher: IHookDispatcher;\n encoder: Tiktoken | null;\n prior: ScanResult | null;\n priorIndex: IPriorIndex;\n priorExtractorRuns: Map<string, Map<string, IPriorExtractorRun>> | undefined;\n providerFrontmatter: IProviderFrontmatterValidator;\n scope: 'project' | 'global';\n strict: boolean;\n enableCache: boolean;\n}\n\n/**\n * Resolve every per-scan invariant (emitter, encoder, prior index,\n * extension buckets, dispatcher) so `runScanInternal` stays a linear\n * sequence of phase calls instead of a 30-line setup preamble.\n *\n * Spec § A.9, `priorExtractorRuns === undefined` means the caller\n * doesn't track the fine-grained Extractor cache (legacy behaviour:\n * out-of-band tests, alternate driving adapters that have no DB).\n * That case falls back to the pre-A.9 model where the node-level body\n * / frontmatter hash check is sufficient. Passing an explicit\n * (possibly empty) Map opts the caller into the fine-grained path.\n */\nfunction buildScanSetup(options: RunScanOptions): IScanSetup {\n const start = Date.now();\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 // Encoder is heavyweight to construct (loads the cl100k_base BPE\n // table 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 priorIndex = indexPriorSnapshot(prior);\n // Spec 0.8.0: each Provider owns its per-kind frontmatter schemas.\n // 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 return {\n start,\n scannedAt: start,\n emitter,\n exts,\n hookDispatcher,\n encoder,\n prior,\n priorIndex,\n priorExtractorRuns: options.priorExtractorRuns,\n providerFrontmatter,\n scope: options.scope ?? 'project',\n strict: options.strict === true,\n enableCache: options.enableCache === true,\n };\n}\n\n/**\n * Spec § A.11, emit one `extractor.completed` event per registered\n * extractor after the full walk completes. Aggregated (no per-node\n * fan-out, that lives in `scan.progress` which is deliberately NOT\n * hookable).\n */\nasync function dispatchExtractorCompleted(\n extractors: readonly IExtractor[],\n emitter: ProgressEmitterPort,\n hookDispatcher: IHookDispatcher,\n): Promise<void> {\n for (const extractor of 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\n/**\n * Merge analyzer-side emissions into the walk's accumulators:\n *\n * - analyzer-emitted view contributions ride into the same per-scan\n * buffer extractor-emitted contributions populate.\n * - Phase 3: fold a tuple per `(analyzer × node)` into\n * `freshlyRunTuples` so the persist layer's per-tuple sweep can\n * drop stale analyzer-emitted rows when an analyzer stops emitting\n * for a previously-emitting node.\n */\nfunction mergeAnalyzerEmissions(\n walked: IWalkAndExtractResult,\n analyzerResult: { contributions: IContributionRecord[] },\n analyzers: readonly IAnalyzer[] | undefined,\n): void {\n for (const c of analyzerResult.contributions) walked.contributions.push(c);\n for (const analyzer of 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}\n\nfunction buildScanStats(\n walked: IWalkAndExtractResult,\n issues: Issue[],\n start: number,\n): ScanResult['stats'] {\n return {\n // `filesSkipped` is \"files walked but not classified by any\n // Provider\". Today every walked file IS classified by its Provider\n // (the `claude` Provider's `classify()` always returns a kind,\n // falling back to `'markdown'`), so this is always 0. Wired now\n // so the field shape is spec-conformant; meaningful once multiple\n // 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\nfunction buildScanReturn(\n walked: IWalkAndExtractResult,\n issues: Issue[],\n renameOps: RenameOp[],\n stats: ScanResult['stats'],\n options: RunScanOptions,\n setup: IScanSetup,\n): {\n result: ScanResult;\n renameOps: RenameOp[];\n extractorRuns: IExtractorRunRecord[];\n enrichments: IEnrichmentRecord[];\n contributions: IContributionRecord[];\n freshlyRunTuples: ReadonlySet<string>;\n} {\n return {\n result: {\n schemaVersion: 1,\n scannedAt: setup.scannedAt,\n scope: setup.scope,\n roots: options.roots,\n providers: setup.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","{\n \"name\": \"@skill-map/cli\",\n \"version\": \"0.24.3\",\n \"description\": \"skill-map reference implementation — kernel + CLI + adapters.\",\n \"license\": \"MIT\",\n \"type\": \"module\",\n \"homepage\": \"https://skill-map.dev\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+https://github.com/crystian/skill-map.git\",\n \"directory\": \"src\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/crystian/skill-map/issues\"\n },\n \"keywords\": [\n \"skill-map\",\n \"markdown\",\n \"ai-agents\",\n \"claude-code\",\n \"graph\"\n ],\n \"bin\": {\n \"sm\": \"bin/sm.js\",\n \"skill-map\": \"bin/sm.js\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"import\": \"./dist/index.js\"\n },\n \"./kernel\": {\n \"types\": \"./dist/kernel/index.d.ts\",\n \"import\": \"./dist/kernel/index.js\"\n },\n \"./conformance\": {\n \"types\": \"./dist/conformance/index.d.ts\",\n \"import\": \"./dist/conformance/index.js\"\n }\n },\n \"files\": [\n \"bin/\",\n \"dist/\",\n \"migrations/\",\n \"README.md\"\n ],\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"dev:serve\": \"node scripts/dev-serve.js\",\n \"typecheck\": \"tsc --noEmit\",\n \"lint\": \"eslint .\",\n \"lint:fix\": \"eslint . --fix\",\n \"reference\": \"node scripts/build-reference.js\",\n \"reference:check\": \"node scripts/build-reference.js --check\",\n \"validate\": \"pnpm validate:compile && pnpm validate:test\",\n \"validate:compile\": \"pnpm typecheck && pnpm lint && pnpm build && pnpm reference:check\",\n \"validate:test\": \"pnpm test:ci\",\n \"pretest\": \"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\": \"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\": \"workspace:*\",\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.18\",\n \"ignore\": \"7.0.5\",\n \"js-tiktoken\": \"1.0.21\",\n \"js-yaml\": \"4.1.1\",\n \"kysely\": \"0.28.17\",\n \"semver\": \"7.7.4\",\n \"typanion\": \"3.14.0\",\n \"ws\": \"8.20.0\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"10.0.1\",\n \"@stylistic/eslint-plugin\": \"5.10.0\",\n \"@types/js-yaml\": \"4.0.9\",\n \"@types/node\": \"24.12.2\",\n \"@types/semver\": \"7.7.1\",\n \"@types/ws\": \"8.18.1\",\n \"c8\": \"11.0.0\",\n \"eslint\": \"10.2.1\",\n \"eslint-plugin-import-x\": \"4.16.2\",\n \"tsup\": \"8.5.1\",\n \"tsx\": \"4.21.0\",\n \"typescript\": \"5.9.3\",\n \"typescript-eslint\": \"8.59.1\"\n },\n \"engines\": {\n \"node\": \">=24.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","/**\n * In-memory `ProgressEmitterPort` adapter. No network, no DB, just a\n * synchronous fan-out to registered listeners. Used by the default scan\n * orchestrator; the WebSocket-backed emitter that streams to\n * the Web UI lands.\n */\n\nimport type {\n ProgressEmitterPort,\n ProgressEvent,\n 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 * `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 { join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport semver from 'semver';\n\nimport type {\n IDiscoveredPlugin,\n ILoadedExtension,\n IPluginManifest,\n} from '../../types/plugin.js';\nimport type { PluginLoaderPort } from '../../ports/plugin-loader.js';\nimport { PLUGIN_LOADER_TEXTS } from '../../i18n/plugin-loader.texts.js';\nimport { tx } from '../../util/tx.js';\nimport type { ExtensionKind } from '../../registry.js';\nimport type { ISchemaValidators } from '../schema-validators.js';\n\nimport {\n applyIdCollisions,\n describe,\n fail,\n isInsidePlugin,\n isRecord,\n pathId,\n} from './id-utils.js';\nimport {\n extractDefault,\n importWithTimeout,\n stripFunctionsAndPluginId,\n} from './import-helpers.js';\nimport {\n KNOWN_KINDS,\n KNOWN_KINDS_LIST,\n validateAnnotationContributions,\n validateHookTriggers,\n} from './validation.js';\nimport { loadStorageSchemas } from './storage-schemas.js';\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 // Spec §architecture.md, \"AJV at three layers, manifest at load\n // (rejects unknown `slot` names with `invalid-manifest`)\". The\n // kind-specific schema validates the exported manifest shape\n // (e.g. `viewContributions[*].slot` against the closed catalog,\n // extractor's required `emitsLinkKinds`, etc.). Failures here are\n // structurally manifest-invalid, not module-load failures, the\n // module imported fine; the declared shape is wrong.\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 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { 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 * 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 * Shared id / path / type-guard helpers used across the loader's\n * validation pipeline. Kept tiny and dependency-free so every sibling\n * module (`validation.ts`, `import-helpers.ts`, `storage-schemas.ts`,\n * `index.ts`) can import without dragging in unrelated state.\n */\n\nimport { isAbsolute, relative, resolve } from 'node:path';\n\nimport type { IDiscoveredPlugin, TPluginLoadStatus } from '../../types/plugin.js';\nimport { PLUGIN_LOADER_TEXTS } from '../../i18n/plugin-loader.texts.js';\nimport { tx } from '../../util/tx.js';\n\n/**\n * Helper that builds the bare failure shape every error path returns.\n * Callers that have a parsed manifest layer it back on top via spread.\n */\nexport function 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 */\nexport function 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\nexport function 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\nexport function isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/** Fall-back plugin id derived from directory name when the manifest is unreadable. */\nexport function 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\nexport function 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 * Spec-driven per-extension validations the loader runs AFTER the\n * kind-specific AJV manifest pass.\n *\n * - `validateAnnotationContributions`, spec § 9.6.6: root keys must\n * be `exclusive`; every inline `schema` must AJV-compile.\n * - `validateHookTriggers`, spec § A.11: a hook MUST declare at\n * least one trigger and every trigger MUST appear in the curated\n * hookable set.\n *\n * Both return either a populated `IDiscoveredPlugin` failure row or\n * `null` when the extension is well-formed.\n */\n\nimport { Ajv2020 } from 'ajv/dist/2020.js';\n\nimport type { IDiscoveredPlugin, IPluginManifest } from '../../types/plugin.js';\nimport type { ExtensionKind } from '../../registry.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 { HOOK_TRIGGERS } from '../../extensions/hook.js';\nimport { describe, fail, isRecord } from './id-utils.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\nexport const KNOWN_KINDS = new Set<ExtensionKind>([\n 'provider',\n 'extractor',\n 'analyzer',\n 'action',\n 'formatter',\n 'hook',\n]);\nexport const 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 */\nexport const HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(', ');\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\nexport function 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 */\nexport function 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 * 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 * 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 * Spec § A.12, read and AJV-compile the storage output schemas a plugin\n * declares in its manifest. Mode A (`storage.schema`, single value-shape\n * under the KV sentinel) and Mode B (`storage.schemas`, per-table map)\n * share the same compile path; only the surrounding plumbing differs.\n *\n * Both helpers are pure functions that the loader's `loadOne` reaches\n * for in its last phase before declaring a plugin \"enabled\".\n */\n\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\n\nimport type { IPluginManifest, IPluginStorageSchema } from '../../types/plugin.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 { describe, isInsidePlugin } from './id-utils.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\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\nexport function 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 */\nexport function 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 * 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 * 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 { KNOWN_SLOT_NAMES } from '../types/view-catalog.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 (`KNOWN_SLOT_NAMES`\n // from `types/view-catalog.ts` is the single runtime source). Entries\n // inside `$defs/payloads` whose key starts with an underscore\n // (`_counter`, `_tag`, `_TreeNode`) are internal `$ref` reuse targets,\n // NOT slot 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\n function getContributionValidator(slot: string): ValidateFunction | null {\n if (!KNOWN_SLOT_NAMES.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 * Step 11.x, runtime view-contribution catalog types.\n *\n * Lives in its own module (rather than `kernel/index.ts`) so consumers\n * deep inside the kernel, `IAnalyzerContext`, the BFF route factories,\n * future Action contexts, can depend on the catalog shape without\n * dragging the whole kernel barrel and risking a cycle.\n *\n * Mirrors `annotation-catalog.ts` for the annotation contribution side\n * (Step 9.6.6). The two systems share the \"plugin contributes data,\n * kernel exposes catalog, UI renders\" pattern but never overlap in\n * storage or routing, see `architecture.md` §View contribution system\n * for the comparison table.\n *\n * **Closed catalog by design.** Both `TSlotName` and `TInputTypeName`\n * mirror the closed enums in `spec/schemas/view-slots.schema.json`\n * and `spec/schemas/input-types.schema.json`. Adding a member is a\n * coordinated kernel + spec + UI + scaffolder change. The closed-enum\n * shape lets TypeScript surface unknown slots at author time\n * (in plugin authors' editors when their plugin imports `@skill-map/cli`)\n * AND lets the runtime exhaustively dispatch slot → renderer in the\n * UI without `default:` fallbacks.\n */\n\n/**\n * Closed enum of view slot names. Mirror of\n * `spec/schemas/view-slots.schema.json#/$defs/SlotName`.\n *\n * Plugins pick one of these by name in their extension manifest's\n * `viewContributions[<contributionId>].slot` field. The kernel\n * validates each pick at load time (`invalid-manifest` on miss); the\n * slot fixes both the renderer and the payload shape.\n */\nexport type TSlotName =\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 * Runtime mirror of `TSlotName`. Single source of truth for every\n * consumer that needs to compare a string against the closed catalog\n * at runtime (manifest validation, `sm plugins doctor` drift checks,\n * `ctx.emitContribution` payload routing). Keep aligned with\n * `TSlotName` and `spec/schemas/view-slots.schema.json#/$defs/SlotName`\n * (the spec stays the formal source of truth; this list is the\n * TS / JS runtime mirror, no narrower).\n */\nexport const ALL_SLOT_NAMES: ReadonlyArray<TSlotName> = [\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/**\n * Set form of `ALL_SLOT_NAMES` for O(1) membership checks. Typed as\n * `ReadonlySet<string>` (not `ReadonlySet<TSlotName>`) because every\n * consumer feeds it untrusted strings pulled from plugin manifests\n * (`getContributionValidator`, `sm plugins doctor`'s slot-drift walk),\n * and TS `Set<T>.has(value: T)` would otherwise reject the call site.\n * The element values are still all `TSlotName` literals; the wider\n * key type only relaxes the `.has()` parameter.\n */\nexport const KNOWN_SLOT_NAMES: ReadonlySet<string> = new Set(ALL_SLOT_NAMES);\n\n/**\n * Closed enum of input-type names for plugin settings. Mirror of\n * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.\n *\n * Plugins pick one of these by name in their plugin manifest's\n * `settings[<settingId>].type` field. The kernel exposes the resolved\n * value via `ctx.settings.<settingId>` typed per the input-type's\n * value-type promise.\n */\nexport type TInputTypeName =\n | 'string-list'\n | 'single-string'\n | 'boolean-flag'\n | 'integer'\n | 'enum-pick'\n | 'enum-multipick'\n | 'path-glob'\n | 'regex'\n | 'secret'\n | 'key-value-list';\n\n/** Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>`. */\nexport type TSeverity = 'info' | 'warn' | 'success' | 'danger';\n\n/**\n * Manifest-side declaration of a single view contribution. The plugin\n * author writes one of these per Record key in\n * `IExtensionBase.viewContributions[<contributionId>]`.\n *\n * Mirror of `view-slots.schema.json#/$defs/IViewContribution`.\n */\nexport interface IViewContribution {\n /**\n * Required. Closed-catalog slot name. Unknown name rejects the\n * extension as `invalid-manifest` at load. The slot fixes both the\n * renderer and the payload shape; there is no separate \"contract\"\n * abstraction.\n */\n slot: TSlotName;\n /**\n * Optional human-readable label. English-only per `AGENTS.md`\n * (`Externalized texts, not internationalized`).\n */\n label?: string;\n /** Optional hover tooltip. English-only. */\n tooltip?: string;\n /**\n * Optional emoji codepoint OR PrimeIcons class id (without the\n * `pi-` prefix). The UI discriminates: matches Unicode\n * `\\p{Extended_Pictographic}` → emoji text, otherwise → PrimeIcon.\n * Required for counter slots and `card.title.right` (enforced by\n * the manifest-side conditional in `view-slots.schema.json`).\n */\n icon?: string;\n /**\n * Optional empty placeholder text shown when the payload is empty\n * AND `emitWhenEmpty` is true. Falls back to a UI-supplied generic\n * 'No data.' string. English-only.\n */\n emptyText?: string;\n /**\n * When false (default), the kernel drops emissions whose payload is\n * structurally empty so the slot stays silent. When true, the\n * renderer surfaces an empty placeholder. Per-slot definition of\n * \"empty\" lives in the slot's payload schema.\n */\n emitWhenEmpty?: boolean;\n /**\n * Optional ordering hint (default 100). Slots configured with\n * `order: 'priority'` sort contributions ASC by this value, with\n * alphabetical tie-break by qualified id. The plugin uses this to\n * suggest where its contribution belongs relative to others sharing\n * the same slot, the slot has the final say.\n */\n priority?: number;\n}\n\n/**\n * Single row of the runtime view-contribution catalog surfaced by\n * `kernel.getRegisteredViewContributions()`. One row per\n * `(pluginId × extensionId × contributionId)` tuple. Composed at boot\n * by `loadPluginRuntime` from every loaded extension's\n * `viewContributions` map.\n *\n * The qualified id is `<pluginId>/<extensionId>/<contributionId>`,\n * matches the qualified id pattern used elsewhere in the kernel\n * (`<pluginId>/<extensionId>` for extensions; this adds the third\n * segment for per-contribution identity).\n */\nexport interface IRegisteredViewContribution {\n pluginId: string;\n extensionId: string;\n contributionId: string;\n slot: TSlotName;\n /** Optional manifest-declared label (English-only). */\n label?: string;\n tooltip?: string;\n icon?: string;\n emptyText?: string;\n emitWhenEmpty: boolean;\n /** Manifest-declared ordering hint (default 100). See `IViewContribution.priority`. */\n priority?: number;\n}\n\n/**\n * Common fields on every setting declaration. The discriminated union\n * `ISettingDeclaration` extends one of these per `type` value.\n */\ninterface ISettingCommon {\n /** Required. Short human-readable label. English-only. */\n label: string;\n /** Optional helper text shown below the control. English-only. */\n description?: string;\n}\n\nexport interface ISetting_StringList extends ISettingCommon {\n type: 'string-list';\n default?: string[];\n min?: number;\n max?: number;\n itemMaxLength?: number;\n}\n\nexport interface ISetting_SingleString extends ISettingCommon {\n type: 'single-string';\n default?: string;\n minLength?: number;\n maxLength?: number;\n /** Optional ECMAScript regex pattern (no flags). */\n pattern?: string;\n}\n\nexport interface ISetting_BooleanFlag extends ISettingCommon {\n type: 'boolean-flag';\n default?: boolean;\n}\n\nexport interface ISetting_Integer extends ISettingCommon {\n type: 'integer';\n default?: number;\n min?: number;\n max?: number;\n step?: number;\n}\n\nexport interface ISetting_EnumOption {\n value: string;\n label: string;\n}\n\nexport interface ISetting_EnumPick extends ISettingCommon {\n type: 'enum-pick';\n options: ISetting_EnumOption[];\n default?: string;\n}\n\nexport interface ISetting_EnumMultipick extends ISettingCommon {\n type: 'enum-multipick';\n options: ISetting_EnumOption[];\n default?: string[];\n min?: number;\n max?: number;\n}\n\nexport interface ISetting_PathGlob extends ISettingCommon {\n type: 'path-glob';\n default?: string;\n /** When true, accepts string[]; when false (default), single string. */\n multiple?: boolean;\n}\n\nexport interface ISetting_Regex extends ISettingCommon {\n type: 'regex';\n default?: string;\n /** Subset of `gimsuy`. Default `''`. */\n flags?: string;\n}\n\nexport interface ISetting_Secret extends ISettingCommon {\n type: 'secret';\n /**\n * Optional uppercase-ASCII identifier. When set in the process\n * environment, that value wins over any stored value (lets CI\n * inject without writing to disk).\n */\n envVar?: string;\n}\n\nexport interface ISetting_KeyValueListEntry {\n key: string;\n value: string;\n}\n\nexport interface ISetting_KeyValueList extends ISettingCommon {\n type: 'key-value-list';\n keyLabel?: string;\n valueLabel?: string;\n default?: ISetting_KeyValueListEntry[];\n min?: number;\n max?: number;\n}\n\n/**\n * Discriminated union of every setting declaration shape. The plugin\n * author NEVER writes JSON Schema for settings, they pick one of\n * these `type` values and supply per-type parameters.\n *\n * Mirror of `input-types.schema.json#/$defs/ISettingDeclaration`.\n */\nexport type ISettingDeclaration =\n | ISetting_StringList\n | ISetting_SingleString\n | ISetting_BooleanFlag\n | ISetting_Integer\n | ISetting_EnumPick\n | ISetting_EnumMultipick\n | ISetting_PathGlob\n | ISetting_Regex\n | ISetting_Secret\n | ISetting_KeyValueList;\n\n/**\n * Runtime value type for a setting, derived from its declaration. The\n * kernel exposes settings to extractors as `Record<string, TSettingValue>`\n * via `ctx.settings.<settingId>`; consumers that want narrow typing\n * narrow at the call site by reading `manifest.settings[id].type`.\n */\nexport type TSettingValue =\n | string\n | string[]\n | boolean\n | number\n | ISetting_KeyValueListEntry[];\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 * 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 * 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// Builds a per-trigger context shape: each `THookTrigger` variant\n// pulls a different slice of the progress event. The switch IS the\n// contract; splitting per trigger scatters the dispatch table. Per\n// `context/lint.md` category 6 (discriminated-union dispatchers).\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 * 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 extensionErrorRecommendedActionMissing:\n 'Analyzer \"{{analyzerId}}\" declares recommendedAction \"{{actionId}}\" but no Action ' +\n 'is registered under that qualified id. The analyzer stays registered; the recommendation ' +\n 'will not surface in the inspector.',\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 * Per-node extractor invocation: build a fresh `IExtractorContext` for\n * each extractor, validate every emitted link / contribution against\n * the declared catalog, fold enrichment partials into per-`(node,\n * extractor)` records, and surface emit-time drops as\n * `extension.error` events.\n *\n * Also hosts the post-walk recompute helpers that re-derive\n * `linksOutCount` / `linksInCount` / `externalRefsCount` on every node\n * from the final merged link buffer, plus the `IExtractorRunRecord`\n * and `IEnrichmentRecord` types those records eventually persist as.\n */\n\nimport { makeEvent } from '../extensions/hook-dispatcher.js';\nimport type {\n IExtractor,\n IExtractorContext,\n} from '../extensions/index.js';\nimport type { IPluginStore } from '../adapters/plugin-store.js';\nimport { loadSchemaValidators } from '../adapters/schema-validators.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type {\n Confidence,\n Link,\n LinkKind,\n Node,\n} from '../types.js';\nimport { tx } from '../util/tx.js';\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 * 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; the\n // load-time catalog drift check lives in `sm plugins doctor`),\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 */\nexport function 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 */\nexport function 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\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\nexport function 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\nexport function 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 * 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","/**\n * Analyzer pass: runs every registered analyzer over the merged graph\n * after the walk completes. Mirrors the Extractor emit-time wiring for\n * `ctx.emitContribution` so analyzer-emitted view contributions\n * survive AJV validation against their slot's payload schema before\n * landing in `scan_contributions`.\n *\n * Issue validation (`validateIssue`) catches analyzers that emit an\n * out-of-spec `severity` and surfaces them as `extension.error` events\n * so plugin authors see why an issue silently disappeared.\n */\n\nimport {\n makeEvent,\n type IHookDispatcher,\n} from '../extensions/hook-dispatcher.js';\nimport type { IAnalyzer } from '../extensions/index.js';\nimport { loadSchemaValidators } from '../adapters/schema-validators.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport type { IOrphanSidecar } from '../sidecar/index.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { Issue, Link, Node, Severity } from '../types.js';\nimport type { IRegisteredAnnotationKey } from '../types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from '../types/view-catalog.js';\nimport { tx } from '../util/tx.js';\nimport { emitExtensionError, readDeclaredContributions } from './extractors.js';\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 */\nexport async 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 registeredActionIds: ReadonlySet<string>,\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 validateRecommendedActions(analyzers, registeredActionIds, emitter);\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 * Spec § extensions/analyzer.schema.json, every `recommendedActions`\n * entry MUST be the qualified id of a registered Action. The kernel\n * logs `recommended-action-missing` for unresolved entries but keeps\n * the analyzer registered (the analyzer still emits issues; only the\n * \"Recommended for issues\" hint in the inspector is dropped).\n *\n * Runs once per scan at the top of the analyzer pass, the action set\n * does not change during a scan and emitting per-analyzer-call would be\n * noise.\n */\nfunction validateRecommendedActions(\n analyzers: readonly IAnalyzer[],\n registeredActionIds: ReadonlySet<string>,\n emitter: ProgressEmitterPort,\n): void {\n for (const analyzer of analyzers) {\n const refs = analyzer.recommendedActions;\n if (refs === undefined || refs.length === 0) continue;\n const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);\n for (const actionId of refs) {\n if (registeredActionIds.has(actionId)) continue;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'recommended-action-missing',\n extensionId: analyzerId,\n actionId,\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {\n analyzerId,\n actionId,\n }),\n }),\n );\n }\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","/**\n * Per-`(node, extractor)` cache decision logic. Walks the Spec § A.9\n * `scan_extractor_runs` breadcrumbs alongside the node-level\n * body/frontmatter/sidecar-annotations hashes and decides:\n * - which extractors fully cache-hit (their prior contribution\n * survives unchanged);\n * - which extractors need to run for this scan;\n * - how each prior outbound internal link should be reshaped given\n * the cached / missing / obsolete state of its `sources`.\n *\n * Also indexes the prior `ScanResult` so the walker can look up prior\n * nodes / links / frontmatter issues by path in O(1).\n */\n\nimport type { Issue, Link, Node, ScanResult } from '../types.js';\nimport type { IExtractor } from '../extensions/index.js';\nimport type { IPriorExtractorRun } from '../adapters/sqlite/scan-load.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { IExtractorRunRecord } from './extractors.js';\n\nexport interface 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\nexport function 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 indexPriorNodes(prior.nodes, priorNodesByPath, priorNodePaths);\n indexPriorLinks(prior.links, priorNodePaths, priorLinksByOriginating);\n indexPriorFrontmatterIssues(prior.issues, priorFrontmatterIssuesByNode);\n return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };\n}\n\nfunction indexPriorNodes(\n nodes: readonly Node[],\n byPath: Map<string, Node>,\n paths: Set<string>,\n): void {\n for (const node of nodes) {\n byPath.set(node.path, node);\n paths.add(node.path);\n }\n}\n\nfunction indexPriorLinks(\n links: readonly Link[],\n priorNodePaths: Set<string>,\n byOriginating: Map<string, Link[]>,\n): void {\n for (const link of links) {\n const key = originatingNodeOf(link, priorNodePaths);\n const list = byOriginating.get(key);\n if (list) list.push(link);\n else byOriginating.set(key, [link]);\n }\n}\n\nconst FRONTMATTER_ISSUE_ANALYZERS: ReadonlySet<string> = new Set([\n 'frontmatter-invalid',\n 'frontmatter-malformed',\n // Audit L1: parser parse-error is emitted by\n // `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The\n // raw.parseIssues only flows through the non-cache path; a cached\n // node skips the rebuild, so the prior issue MUST survive the\n // incremental scan or the warning silently disappears on a clean\n // re-scan of an unchanged file.\n 'frontmatter-parse-error',\n]);\n\nfunction indexPriorFrontmatterIssues(\n issues: readonly Issue[],\n byNode: Map<string, Issue[]>,\n): void {\n for (const issue of issues) {\n if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;\n if (issue.nodeIds.length !== 1) continue;\n const path = issue.nodeIds[0]!;\n const list = byNode.get(path);\n if (list) list.push(issue);\n else byNode.set(path, [issue]);\n }\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 */\nexport function 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 * 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 */\nexport function 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\n // Two code paths, picked by whether the caller threaded fine-grained\n // breadcrumbs through. Legacy callers (no priorExtractorRuns) lose\n // sidecar-edit invalidation; modern callers get per-(node, extractor)\n // cache-hit precision. Split out so each branch is trivially small\n // and easy to reason about in isolation.\n const split = opts.priorExtractorRuns === undefined\n ? splitLegacy(applicableExtractors, applicableQualifiedIds, opts.nodeHashCacheEligible)\n : splitFineGrained(applicableExtractors, opts);\n\n return {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds: split.cachedQualifiedIds,\n missingExtractors: split.missingExtractors,\n fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0,\n };\n}\n\n/**\n * Pre-A.9 cache decision: caller did not load fine-grained\n * breadcrumbs, so we treat every applicable extractor as cached when\n * the node-level hashes match. Sidecar-edit invalidation is\n * unavailable on this path, callers that need it must opt into the\n * fine-grained Map.\n */\nfunction splitLegacy(\n applicableExtractors: IExtractor[],\n applicableQualifiedIds: Set<string>,\n nodeHashCacheEligible: boolean,\n): { cachedQualifiedIds: Set<string>; missingExtractors: IExtractor[] } {\n const cachedQualifiedIds = new Set<string>();\n const missingExtractors: IExtractor[] = [];\n if (nodeHashCacheEligible) {\n for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);\n } else {\n for (const ex of applicableExtractors) missingExtractors.push(ex);\n }\n return { cachedQualifiedIds, missingExtractors };\n}\n\n/**\n * Spec § A.9 cache decision: walk the fine-grained\n * `scan_extractor_runs` breadcrumbs and decide per-extractor whether\n * its prior run still applies to THIS body + THIS sidecar.\n *\n * Sidecar-hash gate applies unconditionally. The author-facing\n * alternative (an opt-in `readsSidecar` flag) was rejected because\n * forgetting it produces a silent stale-data bug, sidecar edits\n * don't refresh until something in the `.md` changes. Universal\n * invalidation costs an extractor re-run per node on `.sm` edits\n * (negligible: sidecars change rarely and extractors are pure-CPU);\n * the gain is zero cognitive load for plugin authors and zero\n * correctness traps.\n */\nfunction splitFineGrained(\n applicableExtractors: IExtractor[],\n opts: {\n nodePath: string;\n bodyHash: string;\n sidecarAnnotationsHash: string;\n nodeHashCacheEligible: boolean;\n priorExtractorRuns: Map<string, Map<string, IPriorExtractorRun>> | undefined;\n },\n): { cachedQualifiedIds: Set<string>; missingExtractors: IExtractor[] } {\n const cachedQualifiedIds = new Set<string>();\n const missingExtractors: IExtractor[] = [];\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 if (\n opts.nodeHashCacheEligible &&\n prior !== undefined &&\n prior.bodyHash === opts.bodyHash &&\n prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash\n ) {\n cachedQualifiedIds.add(qualified);\n } else {\n missingExtractors.push(ex);\n }\n }\n return { cachedQualifiedIds, missingExtractors };\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 */\nexport function 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 */\nexport function 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 * 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 */\nexport function 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 partition = partitionLinkSources(\n link.sources,\n shortIdToQualified,\n cachedQualifiedIds,\n applicableQualifiedIds,\n );\n if (partition.hasMissing) return null;\n if (partition.cached.length === 0) return null;\n if (partition.obsolete.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: partition.cached };\n}\n\n/**\n * Bucket every entry in `link.sources` into cached / missing /\n * obsolete per the contract documented on `reuseCachedLink`. `missing`\n * collapses into a single boolean (one missing source kills the link;\n * there's no point listing every missing one).\n */\nfunction partitionLinkSources(\n sources: readonly string[],\n shortIdToQualified: Map<string, string[]>,\n cachedQualifiedIds: Set<string>,\n applicableQualifiedIds: Set<string>,\n): { cached: string[]; obsolete: string[]; hasMissing: boolean } {\n const cached: string[] = [];\n const obsolete: string[] = [];\n let hasMissing = false;\n for (const source of sources) {\n const category = classifyLinkSource(\n source,\n shortIdToQualified,\n cachedQualifiedIds,\n applicableQualifiedIds,\n );\n if (category === 'cached') cached.push(source);\n else if (category === 'missing') hasMissing = true;\n else obsolete.push(source);\n }\n return { cached, obsolete, hasMissing };\n}\n\n/**\n * Decide how one entry in `link.sources` should be treated when its\n * owning node is a cache reuse candidate. Three buckets per the\n * `reuseCachedLink` contract:\n *\n * - `cached` , short id maps to a currently-registered qualified id\n * that has a matching `scan_extractor_runs` row for\n * this body hash. Contribution is fresh; survives.\n * - `missing` , registered for this kind but not cached for this\n * body. The missing extractor will re-emit, so the\n * prior link gets dropped to avoid duplicates.\n * - `obsolete`, no live extractor claims the short id, or the live\n * one is not applicable to this kind. Strip from\n * `sources`; keep the link if a cached source remains.\n */\nfunction classifyLinkSource(\n source: string,\n shortIdToQualified: Map<string, string[]>,\n cachedQualifiedIds: Set<string>,\n applicableQualifiedIds: Set<string>,\n): 'cached' | 'missing' | 'obsolete' {\n const candidates = shortIdToQualified.get(source);\n if (!candidates || candidates.length === 0) return 'obsolete';\n if (candidates.some((q) => cachedQualifiedIds.has(q))) return 'cached';\n if (candidates.some((q) => applicableQualifiedIds.has(q))) return 'missing';\n return 'obsolete';\n}\n","/**\n * Rename + orphan classification per `spec/db-schema.md` §Rename\n * detection. Pure: takes the prior `ScanResult` and the current node\n * set, mutates the supplied `issues` array in place, and returns the\n * `RenameOp[]` the persistence layer must apply inside the same tx as\n * the scan zone replace-all.\n */\n\nimport type { Issue, Node, ScanResult } from '../types.js';\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\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 * 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 / H1)**, `readdir` reports a regular file →\n * `lstat()` re-verifies before the read. Closes the window where the\n * entry could be swapped for a symlink between the two calls.\n * `lstat` does NOT follow symlinks (H1 fix, audit upgrade from\n * `stat`), so a `.md`→symlink race is rejected by `isFile()`\n * returning false on the symlink itself; non-regular types\n * (socket, FIFO, device) introduced in the race window are\n * rejected the same way.\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, lstat } 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 // Audit L1: forward parser diagnostics (e.g. malformed YAML)\n // through the IRawNode surface so the orchestrator can\n // convert them into warn-level kernel `Issue` rows. Omitted\n // when the parser reported no issues (happy path).\n ...(parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}),\n };\n }\n }\n}\n\n// Recursive directory walker: per-entry branches over symlink /\n// ignore-filter / kind (dir vs file) / extension allow-list. The\n// branching IS the walker; extraction yields helpers that all run\n// once per entry anyway. Per `context/lint.md` category 7 (recursive type-discriminator walkers).\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 (audit H1): readdir reported a regular file;\n // re-verify before reading. We use `lstat` (NOT `stat`) so a\n // symlink swapped in between `readdir` and the re-check is\n // detected here. `stat` follows symlinks, which would have let an\n // attacker race a benign `.md` → symlink to `~/.ssh/id_rsa` and\n // see the target's contents land in the SQLite body store and\n // /api/nodes response. `lstat` plus the `isFile()` predicate\n // rejects both symlinks and any non-regular type (socket, FIFO,\n // device) that appeared in the race window.\n try {\n const s = await lstat(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 + M2)**, the parsed object is\n * run through `stripPrototypePollution` so `__proto__`,\n * `constructor`, and `prototype` keys are removed at EVERY depth,\n * not just at the root. js-yaml v4 stores `__proto__:` as an own\n * data property at any nesting level (rather than mutating\n * `Object.prototype`), but the value still flows into downstream\n * `Object.assign`-style merges where the `__proto__` setter fires.\n * Deep stripping at parse time keeps the returned object safe to\n * spread, copy, and persist regardless of nesting.\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 * - **Malformed YAML surfacing (audit L1)**, when `yaml.load` throws\n * the parser still returns `frontmatter: {}` (the historic\n * fallback) so the scan keeps making progress, but it ALSO emits\n * an `IParseIssue` with code `frontmatter-parse-error` and the\n * sanitised `err.message`. The walker forwards it on `IRawNode`\n * and the orchestrator translates it into a warn-level kernel\n * `Issue` so authors see the typo instead of silently losing\n * their metadata.\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 {\n IFileParser,\n IParsedFile,\n IParseIssue,\n} from '../../../kernel/scan/parsers/types.js';\nimport { stripPrototypePollution } from '../../../kernel/util/strip-prototype-pollution.js';\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\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 let parsed: Record<string, unknown> = {};\n const issues: IParseIssue[] = [];\n try {\n const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });\n if (doc && typeof doc === 'object' && !Array.isArray(doc)) {\n // Deep strip (audit M2). The helper returns a fresh\n // own-property-clean object; nested `__proto__` / `constructor`\n // / `prototype` keys are dropped at every depth.\n parsed = stripPrototypePollution(doc as Record<string, unknown>);\n }\n } catch (err) {\n // Malformed YAML (audit L1), keep the historic `parsed = {}`\n // fallback so the scan keeps making progress, but surface a\n // diagnostic so the author sees the typo. Only the parser-error\n // message is interpolated; the raw frontmatter is NEVER folded\n // into the message (a hostile YAML could embed multi-line\n // garbage; `frontmatterRaw` stays available on `IParsedFile` for\n // downstream diagnostics that opt in).\n issues.push({\n code: 'frontmatter-parse-error',\n message: sanitiseParseErrorMessage(err),\n });\n }\n const out: IParsedFile = { frontmatterRaw, frontmatter: parsed, body };\n if (issues.length > 0) {\n return { ...out, issues };\n }\n return out;\n },\n};\n\n/**\n * Distil a `yaml.load` throw into a single-line, control-character-free\n * message. `js-yaml`'s `YAMLException.message` already excludes the\n * source position prefix; we strip CR/LF/tab and collapse runs of\n * whitespace so a multi-line \"reason\\n in ...\" string can never break\n * a single-line log render or smuggle ANSI escapes through a downstream\n * consumer.\n */\nfunction sanitiseParseErrorMessage(err: unknown): string {\n const raw = err instanceof Error ? err.message : String(err);\n // eslint-disable-next-line no-control-regex\n return raw.replace(/[\u0000-\u001f]+/g, ' ').replace(/\\s+/g, ' ').trim();\n}\n","/**\n * Shared prototype-pollution defence used at every trust boundary where\n * untrusted JSON / YAML enters the runtime (settings, sidecars, plugin\n * manifests).\n *\n * Two surfaces:\n *\n * 1. `FORBIDDEN_KEYS`, the closed set of key names that manipulate\n * the prototype chain. Merge functions consult this directly to\n * skip keys without cloning (see `kernel/config/loader.ts` and\n * `kernel/sidecar/store.ts`).\n *\n * 2. `stripPrototypePollution(value)`, pure: returns a deep-cloned\n * copy of `value` with every forbidden key removed at every depth.\n * Primitives pass through unchanged. Arrays recurse element-wise.\n * Plain objects have their entries filtered and recursed.\n * Non-plain objects (Date, Map, class instances) are returned\n * as-is, since they are never produced by `JSON.parse` /\n * `yaml.load` on the data we accept.\n *\n * Use the strip helper at the read boundary; consult `FORBIDDEN_KEYS`\n * inside merge primitives. Centralising both means the day a fourth\n * forbidden name surfaces (rare but possible, engine-specific\n * accessors), we update one file.\n */\n\nexport const FORBIDDEN_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\nexport function stripPrototypePollution<T>(value: T): T {\n return strip(value) as T;\n}\n\nfunction strip(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n if (Array.isArray(value)) return value.map(strip);\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (FORBIDDEN_KEYS.has(k)) continue;\n out[k] = strip(v);\n }\n return out;\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 type { IParseIssue } from '../scan/parsers/types.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 * Parser diagnostics (audit L1). Populated by the walker when the\n * parser surfaced `IParseIssue` entries (e.g. malformed YAML).\n * Carried through `processRawNode` and converted into warn-level\n * kernel `Issue` rows inside `buildFreshNodeAndValidateFrontmatter`.\n * Empty / undefined on the happy path.\n */\n parseIssues?: readonly IParseIssue[];\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 * 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 * 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 { stripPrototypePollution } from '../util/strip-prototype-pollution.js';\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 // Explicit JSON_SCHEMA to match the frontmatter parser and harden\n // against a future js-yaml default-schema loosening (audit M1).\n parsedYaml = yaml.load(raw, { schema: yaml.JSON_SCHEMA });\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 // Trust boundary: strip prototype-pollution keys at every depth before\n // AJV ever sees the document, so a tainted `.sm` cannot survive the\n // round-trip through `IParsedSidecar.raw` into plugin Action contexts.\n parsedYaml = stripPrototypePollution(parsedYaml);\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 * 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 } 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 { writeFileAtomicExclusive } from '../../core/config/atomic-write.js';\nimport { ensureSidecarWritesAllowed } from '../../core/config/sidecar-consent.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\nimport {\n FORBIDDEN_KEYS,\n stripPrototypePollution,\n} from '../util/strip-prototype-pollution.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 // Trust boundary: a hostile sidecar (or a future Action emitting a\n // patch derived from `node.sidecar.raw`) must not be able to set\n // `__proto__` / `constructor` / `prototype` on the merged result.\n // The parse boundary in `parse.ts` already strips these keys, but\n // the merge primitive enforces it independently so future callers\n // do not have to remember to pre-filter.\n if (FORBIDDEN_KEYS.has(key)) continue;\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 // Explicit JSON_SCHEMA to match the frontmatter parser and harden\n // against a future js-yaml default-schema loosening (audit M1).\n const parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });\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 // Trust boundary: strip prototype-pollution keys before the value\n // seeds the `current` argument to `deepMerge`. Defence-in-depth on top\n // of the merge primitive's own skip-on-forbidden-key filter.\n return stripPrototypePollution(parsed);\n}\n\nfunction atomicWriteFile(targetPath: string, content: string): void {\n // Audit M1 + L1: stage to a sibling temp file opened with\n // `O_EXCL | O_NOFOLLOW` and a CSPRNG-random suffix (no longer\n // pid + Date.now(), which was predictable, so a local attacker\n // could pre-plant a symlink at the temp path). `writeFileAtomicExclusive`\n // is shared with `writeJsonAtomic` (settings) so both surfaces\n // get the same hardening. Mode 0o600 is applied at open time and\n // survives the rename (POSIX rename preserves the inode + its mode).\n writeFileAtomicExclusive(targetPath, content);\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 * 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 closeSync,\n constants as fsConstants,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from 'node:fs';\nimport { randomBytes } from 'node:crypto';\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 * Stage `content` under `path` via an exclusive, no-follow open and\n * `renameSync` the result into place. Shared by `writeJsonAtomic`\n * (settings) and `kernel/sidecar/store.ts:atomicWriteFile` (`.sm`\n * sidecars), both of which previously composed a predictable temp\n * filename (`<path>.tmp.<pid>` / `<path>.tmp.<pid>.<Date.now()>`)\n * and called `writeFileSync`, which follows symlinks. A local\n * attacker who pre-planted a symlink at the predicted temp path\n * would have redirected the write to the symlink's target.\n *\n * Fix (audit M1):\n *\n * - The temp name embeds a cryptographically-random suffix\n * (`randomBytes(8).toString('hex')`) so the path is\n * unpredictable across invocations.\n * - `openSync` uses `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW` with\n * mode `0o600`. `O_EXCL` makes the syscall fail with `EEXIST` if\n * anything (file, symlink, directory) already lives at the temp\n * path; `O_NOFOLLOW` makes it fail with `ELOOP` if the leaf is a\n * symlink. Together they close the race the previous\n * `writeFileSync`-with-predictable-name pattern left open.\n * - The write goes through the returned fd; the rename is the\n * standard POSIX same-filesystem atomic rename. Mode `0o600`\n * survives the rename (POSIX rename preserves the inode + its\n * mode), which is what the settings / sidecar privacy guarantee\n * relies on.\n *\n * On failure the temp file is best-effort removed so we do not leak\n * `<path>.tmp.<random>` siblings if the rename target is read-only.\n */\nexport function writeFileAtomicExclusive(path: string, content: string): void {\n // 16 hex chars (64 bits of entropy). The `node:crypto` source is\n // CSPRNG-backed; an attacker cannot pre-plant a symlink at a\n // predicted temp path because they cannot predict the suffix.\n const tmp = `${path}.tmp.${process.pid}.${randomBytes(8).toString('hex')}`;\n let fd: number | null = null;\n try {\n // O_EXCL fails with EEXIST if the path already exists (file,\n // symlink, directory). O_NOFOLLOW fails with ELOOP if the final\n // path component is a symlink. Together they close the audit M1\n // race window. mode 0o600 is set at create time so the inode\n // never carries broader perms, even briefly.\n fd = openSync(\n tmp,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,\n 0o600,\n );\n writeSync(fd, content);\n closeSync(fd);\n fd = null;\n renameSync(tmp, path);\n } catch (err) {\n // Ensure the fd is released even if the rename threw.\n if (fd !== null) {\n try {\n closeSync(fd);\n } catch {\n /* best-effort */\n }\n }\n try {\n unlinkSync(tmp);\n } catch {\n // Best effort, the staged file may not exist (open could have\n // failed before the inode was created).\n }\n throw err;\n }\n}\n\n/**\n * Write `content` to `path` atomically. The body is staged into a\n * sibling `<path>.tmp.<pid>.<random>` file (same directory so the\n * rename never crosses filesystems) and `renameSync`'d into place,\n * POSIX guarantees rename is atomic on the same fs, so a crash\n * mid-write leaves the destination either at its prior content or\n * at the new content, never half-written.\n *\n * The pre-rename stage is owner-only (`mode: 0o600`, audit M1) and\n * opened with `O_EXCL | O_NOFOLLOW` (audit M1) so a pre-planted\n * symlink at the predicted temp path cannot redirect the write.\n * Settings files (`settings.json`, `settings.local.json`) carry\n * privacy-sensitive paths from `scan.extraFolders` / `referencePaths`\n * and the per-plugin config; on multi-user hosts the default umask\n * would leave them world-readable. `db restore` already uses 0o600\n * for the same reason. The mode is set on the temp file and survives\n * the rename (POSIX rename preserves the inode + its mode).\n */\nexport function writeJsonAtomic(path: string, content: Record<string, unknown>): void {\n mkdirSync(dirname(path), { recursive: true });\n writeFileAtomicExclusive(path, JSON.stringify(content, null, 2) + '\\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.extraFolders`: string[] of additional directories to scan\n * as nodes (the only way to extend the scan beyond the project).\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, removing paths, are\n * not gated).\n */\nexport const PRIVACY_SENSITIVE_KEYS: ReadonlySet<string> = new Set<string>([\n 'scan.extraFolders',\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 (adding paths to `scan.extraFolders` /\n * `scan.referencePaths` that resolve outside `cwd`). Drives the\n * `--yes` requirement on `sm config set`.\n *\n * Writes that NARROW the surface (removing paths) return `false` so\n * the user can revert the 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 \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 // 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 // Read uses `scope: 'project'` because these keys live in the\n // 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.extraFolders' || 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 { FORBIDDEN_KEYS } from '../util/strip-prototype-pollution.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 when entries point outside the project**\n * (per `project-config.schema.json` §scan.extraFolders). 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. This is the only mechanism to\n * extend the scan beyond the project root: there is no implicit HOME\n * walk and Providers cannot opt their own directory in.\n */\n extraFolders: 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.extraFolders',\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 // Strip `PROJECT_LOCAL_ONLY_KEYS` from every layer EXCEPT\n // `project-local`, that is the only legitimate home for them.\n // See `stripProjectLocalOnlyKeys` for the security rationale.\n if (layer !== 'project-local') {\n stripProjectLocalOnlyKeys(cleaned, layer, 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 stripProjectLocalOnlyKeys(cleaned, '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// `FORBIDDEN_KEYS` is the shared closed set from `kernel/util/strip-prototype-pollution.ts`;\n// this module consults it inside the merge primitive (skip-on-key) and inside\n// `containsForbidden` to also reject pollution-via-AJV-instancePath.\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). Invoked for every layer except\n * `project-local` (the only legitimate home for these keys).\n *\n * Why every non-project-local layer: the spec analyzer says\n * `allowEditSmFiles`, `scan.extraFolders`, and `scan.referencePaths`\n * are per-checkout, a \"yes\" in project A must not extend to project\n * B, and privacy-sensitive paths must not travel via the repo. The\n * original strip only covered the `project` layer (the committed\n * file), so a value in `~/.skill-map/settings.json` (the `user`\n * layer) would silently leak across every project, for\n * `allowEditSmFiles` that translates to \"consent gate bypassed\n * everywhere without a prompt.\" The strip now also covers `user`,\n * `user-local`, and `override`.\n */\nfunction stripProjectLocalOnlyKeys(\n cloned: Record<string, unknown>,\n layer: TConfigLayer,\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,\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// Recursive descent over the layered config, recording the source\n// layer of each leaf into a flat map. Primitive / array / object /\n// null branches are the type discriminator. Per `context/lint.md`\n// category 7 (recursive type-discriminator walkers).\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 * 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 * Node-construction helpers: hash a body, canonicalise frontmatter /\n * sidecar annotations, resolve the sidecar overlay for a given relative\n * path, and produce a fresh `Node` (validating its frontmatter on the\n * way out). Also hosts `mergeNodeWithEnrichments` + `IPersistedEnrichment`\n * the read-time merge of author frontmatter with the A.8 enrichment\n * layer.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync } 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';\nimport yaml from 'js-yaml';\n\nimport type { IProvider, IRawNode } from '../extensions/index.js';\nimport type { IProviderFrontmatterValidator } from '../adapters/schema-validators.js';\nimport {\n computeDriftStatus,\n readSidecarFor,\n} from '../sidecar/index.js';\nimport type { Issue, Node, TripleSplit } from '../types.js';\nimport { stripPrototypePollution } from '../util/strip-prototype-pollution.js';\nimport { detectMalformedFrontmatter, validateFrontmatter } from './frontmatter.js';\n\nexport interface 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\nexport function 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\nexport function 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\nexport function 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 */\nexport function 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 */\nexport function 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\nexport function 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\n// `pickMetadata` / `pickString` / `pickStability` are retained for\n// potential future re-use by extractors that need to project legacy\n// `metadata.*` fields out of frontmatter. They are not consumed\n// anywhere in source today; preserved alongside `buildNode` because\n// that is the historical neighbourhood.\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\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 */\nexport function 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 // Audit L1: parser-emitted diagnostics (e.g. malformed YAML) surface\n // here as warn-level kernel issues. Strict mode lifts them to error\n // alongside the existing schema-validation / malformed-fence paths.\n if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {\n for (const pi of opts.raw.parseIssues) {\n frontmatterIssues.push({\n analyzerId: pi.code,\n severity: opts.strict ? 'error' : 'warn',\n nodeIds: [opts.raw.path],\n message: pi.message,\n });\n }\n }\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/**\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\n/**\n * Copy every own enumerable property of `source` onto `target`, with a\n * deep prototype-pollution strip (audit M2). The shallow filter we used\n * to host inline would skip a root-level `__proto__` key, but\n * `meta: { __proto__: { polluted: true } }` survived because the nested\n * `__proto__` still landed on `target.meta`. Routing `source` through\n * `stripPrototypePollution` first removes the forbidden names at every\n * depth and returns a fresh own-property-clean object, so the subsequent\n * own-key copy is safe.\n *\n * The deep clone runs once per source per merge, the cost is O(size of\n * source) but bounded by the AJV-validated frontmatter / enrichment row\n * shapes, which the spec already caps below the K-key range.\n */\nfunction assignSafe(target: Record<string, unknown>, source: Record<string, unknown>): void {\n const safe = stripPrototypePollution(source);\n for (const [k, v] of Object.entries(safe)) {\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 * Frontmatter validation + malformed-fence detection helpers used by\n * `node-build.ts`. Pulled out of the monolith so the per-kind AJV\n * validation pass and the malformed-fence heuristic live next to each\n * other (they form a single conceptual surface: \"did the frontmatter\n * arrive intact?\").\n */\n\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type { IProvider } from '../extensions/index.js';\nimport type { IProviderFrontmatterValidator } from '../adapters/schema-validators.js';\nimport { tx } from '../util/tx.js';\nimport type { Issue } from '../types.js';\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 */\nexport function 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 */\nexport function 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\nexport type 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","/**\n * Scan main loop. For each provider × raw node: hash body, classify,\n * resolve sidecar, compute the per-(node, extractor) cache decision,\n * dispatch to the full-cache-hit branch or the extract-path branch,\n * and record the resulting `scan_extractor_runs` rows.\n *\n * The cache decision lives in `./cache.js`, the per-node extractor\n * invocation in `./extractors.js`, the fresh-node construction (plus\n * frontmatter validation) in `./node-build.js`. This file orchestrates\n * them.\n */\n\n// js-tiktoken ships CJS subpaths without explicit `.cjs` in the import\n// specifier; type-only imports survive lint without the disable that\n// the value-import sites need.\nimport type { Tiktoken } from 'js-tiktoken/lite';\n\nimport type { IPluginStore } from '../adapters/plugin-store.js';\nimport type { IProviderFrontmatterValidator } from '../adapters/schema-validators.js';\nimport type { IPriorExtractorRun } from '../adapters/sqlite/scan-load.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport { makeEvent } from '../extensions/hook-dispatcher.js';\nimport {\n resolveProviderWalk,\n type IExtractor,\n type IProvider,\n type IRawNode,\n} from '../extensions/index.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { IIgnoreFilter } from '../scan/ignore.js';\nimport {\n discoverOrphanSidecars,\n type IOrphanSidecar,\n} from '../sidecar/index.js';\nimport type { Issue, Link, Node, ScanResult } from '../types.js';\nimport {\n cloneNodeAndReshapeLinks,\n computeCacheDecision,\n reusePriorNode,\n type IPriorIndex,\n} from './cache.js';\nimport {\n runExtractorsForNode,\n type IEnrichmentRecord,\n type IExtractorRunRecord,\n} from './extractors.js';\nimport {\n buildFreshNodeAndValidateFrontmatter,\n canonicalFrontmatter,\n canonicalSidecarAnnotations,\n resolveSidecarOverlay,\n sha256,\n} from './node-build.js';\n\nexport interface 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\nexport interface 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 * Per-scan accumulators bundled into one object so the per-node\n * helpers (`processRawNode`, `applyFullCacheHit`, `applyExtractPath`)\n * mutate a single reference instead of taking 10+ buffer parameters.\n *\n * Every field is documented at the field site below; the docs that\n * used to live inline on each `const` declaration moved here.\n */\ninterface IWalkAccumulators {\n nodes: Node[];\n internalLinks: Link[];\n externalLinks: Link[];\n cachedPaths: Set<string>;\n frontmatterIssues: Issue[];\n /**\n * A.8 enrichment buffer. `ctx.enrichNode(partial)` calls fold into\n * a per-Extractor entry keyed by `(nodePath, qualifiedExtractorId)`\n * so the persistence layer can upsert exactly one row per pair into\n * `node_enrichments`. Attribution survives across scans, which\n * 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\n * fold into the same record's `value` (last-write-wins per field).\n */\n enrichmentBuffer: Map<string, IEnrichmentRecord>;\n /**\n * Phase 3 / View contributions, flat buffer (no per-node dedup\n * because the qualified id\n * `<pluginId>/<extensionId>/<contributionId>` is structurally\n * unique within a single scan).\n */\n contributionsBuffer: IContributionRecord[];\n /**\n * Phase 3 / View contributions, accumulator of (plugin, extension,\n * node) tuples where extract() actually RAN this scan (cache\n * miss). Cached extractors don't push here, their prior\n * `scan_contributions` rows must be preserved. Format:\n * `<pluginId>/<extensionId>/<nodePath>`.\n */\n freshlyRunTuples: Set<string>;\n /**\n * Spec § A.9, accumulator for `scan_extractor_runs`. One row per\n * (nodePath, qualifiedExtractorId) pair the orchestrator decided\n * \"this extractor is current for this body\". Includes both\n * freshly-run pairs and pairs whose prior run was reused intact via\n * the cache.\n */\n extractorRuns: IExtractorRunRecord[];\n /**\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\n * root keys without re-reading the `.sm` file from disk.\n */\n sidecarRoots: Map<string, Record<string, unknown>>;\n}\n\n/**\n * Per-scan immutable context derived from `IWalkAndExtractOptions`.\n * Build once at the top of `walkAndExtract`, pass to helpers by\n * reference. Mirror of the function-level destructure that the\n * pre-refactor monolith opened with.\n */\ninterface IWalkContext {\n opts: IWalkAndExtractOptions;\n priorNodesByPath: Map<string, Node>;\n priorLinksByOriginating: Map<string, Link[]>;\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n /**\n * Short→qualified id map built 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\n * cache bookkeeping. Multiple plugins can in theory expose\n * extractors with the same short id; we keep all qualifieds per\n * short id so the partial-cache filter recognises any of them as\n * \"still cached\".\n */\n shortIdToQualified: Map<string, string[]>;\n}\n\n/**\n * Main scan loop. For each provider × raw node: hash, classify,\n * decide cache (full / partial / none), reuse or build, run\n * extractors, record runs. Helpers\n * (`computeCacheDecision`, `cloneNodeAndReshapeLinks`,\n * `reusePriorNode`, `buildFreshNodeAndValidateFrontmatter`,\n * `runExtractorsForNode`) encapsulate the heavy lift; this function\n * is the dispatch glue.\n *\n * Per-iteration work split into `processRawNode` so the loop body\n * stays linear and the lint cap is satisfied without an\n * `eslint-disable`.\n */\nexport async function walkAndExtract(opts: IWalkAndExtractOptions): Promise<IWalkAndExtractResult> {\n const accum = createWalkAccumulators();\n const wctx = buildWalkContext(opts);\n\n // Path-dedup across the multi-provider walk. Spec § Provider\n // dispatch (architecture.md): every Provider walks the full root,\n // but each file is offered to at most ONE Provider's `classify`.\n // The first Provider in iteration order whose `classify` returns\n // non-null 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 const claimedPaths = new Set<string>();\n const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};\n let filesWalked = 0;\n let index = 0;\n\n for (const provider of opts.providers) {\n for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {\n filesWalked += 1;\n if (claimedPaths.has(raw.path)) continue;\n const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);\n if (advanced) index += 1;\n }\n }\n\n // Spec § 9.6.2, orphan sidecar sweep. Walks the same roots\n // looking for `*.sm` whose sibling `*.md` is missing. The list\n // flows through to the rule pass; `annotation-orphan` emits one\n // warning per entry.\n const orphanSidecars = discoverOrphanSidecars(opts.roots);\n\n return {\n nodes: accum.nodes,\n internalLinks: accum.internalLinks,\n externalLinks: accum.externalLinks,\n cachedPaths: accum.cachedPaths,\n frontmatterIssues: accum.frontmatterIssues,\n filesWalked,\n enrichments: [...accum.enrichmentBuffer.values()],\n extractorRuns: accum.extractorRuns,\n contributions: accum.contributionsBuffer,\n freshlyRunTuples: accum.freshlyRunTuples,\n orphanSidecars,\n sidecarRoots: accum.sidecarRoots,\n };\n}\n\nfunction createWalkAccumulators(): IWalkAccumulators {\n return {\n nodes: [],\n internalLinks: [],\n externalLinks: [],\n cachedPaths: new Set(),\n frontmatterIssues: [],\n enrichmentBuffer: new Map(),\n contributionsBuffer: [],\n freshlyRunTuples: new Set(),\n extractorRuns: [],\n sidecarRoots: new Map(),\n };\n}\n\nfunction buildWalkContext(opts: IWalkAndExtractOptions): IWalkContext {\n const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = opts.priorIndex;\n const shortIdToQualified = new Map<string, string[]>();\n for (const ex of opts.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 return { opts, priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode, shortIdToQualified };\n}\n\n/**\n * Process one raw-node yielded by a Provider's `walk()`. Returns\n * `true` if the node was claimed (classify produced a kind), `false`\n * if disclaimed (another Provider may claim it on its own pass).\n *\n * Folds the per-node pipeline into one function so `walkAndExtract`'s\n * outer loop body stays a 2-liner:\n *\n * - hash body / frontmatter\n * - classify (early-return on `null`, disclaimed)\n * - resolve sidecar + hash\n * - compute cache decision\n * - dispatch full-cache-hit vs partial/fresh branches\n */\nasync function processRawNode(\n raw: IRawNode,\n provider: IProvider,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n claimedPaths: Set<string>,\n nextIndex: number,\n): Promise<boolean> {\n const bodyHash = sha256(raw.body);\n // Canonical-form rationale, hash a CANONICAL form of the\n // frontmatter so a YAML formatter pass (re-indent, sort keys,\n // normalise trailing newline, swap single↔double quotes) doesn't\n // break the medium-confidence rename heuristic.\n const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));\n\n const kind = provider.classify(raw.path, raw.frontmatter);\n if (kind === null) {\n // Provider disclaimed the file, another Provider may claim it\n // on its own walk pass, or the file is outside every active\n // Provider's territory.\n return false;\n }\n claimedPaths.add(raw.path);\n\n const priorNode = wctx.priorNodesByPath.get(raw.path);\n // Cache reuse is gated on the explicit `enableCache` option. The\n // presence of a `prior` alone is no longer enough, a plain\n // `sm scan` always re-walks deterministically; only\n // `sm scan --changed` flips `enableCache` on. The rename heuristic\n // uses `prior` independently of `enableCache`.\n const nodeHashCacheEligible =\n wctx.opts.enableCache &&\n wctx.opts.prior !== null &&\n priorNode !== undefined &&\n priorNode.bodyHash === bodyHash &&\n priorNode.frontmatterHash === frontmatterHash;\n\n // Resolve the sidecar overlay BEFORE the cache decision so we can\n // 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 (e.g. `core/annotations`). Analyzers that read the\n // sidecar (`core/stability`, `core/annotation-stale`, …) re-run\n // every pass regardless, but the hash still matters for the\n // extract-phase cache.\n const sidecarResolution = resolveSidecarOverlay(\n raw.path, raw.path, wctx.opts.roots, bodyHash, frontmatterHash,\n );\n const sidecarAnnotationsHash = sha256(\n canonicalSidecarAnnotations(sidecarResolution.overlay.annotations),\n );\n\n const cacheDecision = computeCacheDecision({\n extractors: wctx.opts.extractors,\n kind,\n nodePath: raw.path,\n bodyHash,\n sidecarAnnotationsHash,\n nodeHashCacheEligible,\n priorExtractorRuns: wctx.opts.priorExtractorRuns,\n });\n\n const ctx: IProcessNodeContext = {\n raw, provider, kind, bodyHash, frontmatterHash, sidecarResolution,\n sidecarAnnotationsHash, nodeHashCacheEligible, cacheDecision, priorNode,\n index: nextIndex,\n };\n\n if (cacheDecision.fullCacheHit && priorNode) {\n applyFullCacheHit(ctx, wctx, accum);\n } else {\n await applyExtractPath(ctx, wctx, accum);\n }\n return true;\n}\n\n/**\n * Bag of per-iteration state shared by `applyFullCacheHit` and\n * `applyExtractPath`. Built inside `processRawNode`; never escapes\n * that scope.\n */\ninterface IProcessNodeContext {\n raw: IRawNode;\n provider: IProvider;\n kind: string;\n bodyHash: string;\n frontmatterHash: string;\n sidecarResolution: ReturnType<typeof resolveSidecarOverlay>;\n sidecarAnnotationsHash: string;\n nodeHashCacheEligible: boolean;\n cacheDecision: ReturnType<typeof computeCacheDecision>;\n priorNode: Node | undefined;\n index: number;\n}\n\n/**\n * Attach the freshly-resolved sidecar overlay to a node and surface\n * its issues + parsed root. Used by both apply paths so the apply\n * step stays uniform.\n */\nfunction attachSidecar(\n node: Node,\n resolution: ReturnType<typeof resolveSidecarOverlay>,\n sidecarRoots: Map<string, Record<string, unknown>>,\n): Issue[] {\n node.sidecar = resolution.overlay;\n if (resolution.parsedRoot !== null) {\n sidecarRoots.set(node.path, resolution.parsedRoot);\n }\n return resolution.issues.map((i) =>\n i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node.path] },\n );\n}\n\n/**\n * Full-cache-hit branch: reuse the prior node + its links + its\n * frontmatter issues + its extractor runs. Sidecars are re-resolved\n * on every scan (not cached) since `.sm` lives outside the body /\n * frontmatter hash domain.\n */\nfunction applyFullCacheHit(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n): void {\n const reused = reusePriorNode({\n priorNode: ctx.priorNode!,\n bodyHash: ctx.bodyHash,\n sidecarAnnotationsHash: ctx.sidecarAnnotationsHash,\n strict: wctx.opts.strict,\n cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,\n applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,\n shortIdToQualified: wctx.shortIdToQualified,\n priorLinksByOriginating: wctx.priorLinksByOriginating,\n priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode,\n });\n const reusedSidecarIssues = attachSidecar(reused.node, ctx.sidecarResolution, accum.sidecarRoots);\n accum.nodes.push(reused.node);\n accum.cachedPaths.add(reused.node.path);\n for (const link of reused.internalLinks) accum.internalLinks.push(link);\n for (const issue of reused.frontmatterIssues) accum.frontmatterIssues.push(issue);\n for (const issue of reusedSidecarIssues) accum.frontmatterIssues.push(issue);\n for (const run of reused.extractorRuns) accum.extractorRuns.push(run);\n wctx.opts.emitter.emit(makeEvent('scan.progress', {\n index: ctx.index, path: ctx.raw.path, kind: ctx.kind, cached: true,\n }));\n}\n\n/**\n * Partial- or full-re-extract branch. Either a brand-new node, a\n * node whose body / frontmatter changed, or a node whose hashes\n * match but at least one applicable extractor lacks a matching\n * `scan_extractor_runs` row (newly registered, or its prior run was\n * against a different body hash).\n */\nasync function applyExtractPath(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n): Promise<void> {\n const node = buildOrReuseNode(ctx, wctx, accum);\n // Spec § 9.6.2, sidecar overlay applies to BOTH freshly-built and\n // partial-cache nodes. Done after the node is in `accum.nodes` so a\n // downstream consumer iterating `nodes` sees the overlay applied\n // (mutation is in-place on the same object reference).\n const sidecarIssues = attachSidecar(node, ctx.sidecarResolution, accum.sidecarRoots);\n for (const issue of sidecarIssues) accum.frontmatterIssues.push(issue);\n\n const partialCacheHit = isPartialCacheHit(ctx);\n emitExtractProgress(ctx, wctx, partialCacheHit);\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\n ? ctx.cacheDecision.missingExtractors\n : ctx.cacheDecision.applicableExtractors;\n recordFreshlyRunTuples(extractorsToRun, node.path, accum);\n\n const extractResult = await runExtractorsForNode({\n extractors: extractorsToRun,\n node,\n body: ctx.raw.body,\n frontmatter: ctx.raw.frontmatter,\n bodyHash: ctx.bodyHash,\n emitter: wctx.opts.emitter,\n ...(wctx.opts.pluginStores ? { pluginStores: wctx.opts.pluginStores } : {}),\n });\n mergeExtractResult(extractResult, accum);\n recordExtractorRuns(node.path, ctx, accum);\n}\n\nfunction emitExtractProgress(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n partialCacheHit: boolean,\n): void {\n wctx.opts.emitter.emit(makeEvent('scan.progress', {\n index: ctx.index, path: ctx.raw.path, kind: ctx.kind, cached: false,\n ...(partialCacheHit ? { partialCache: true } : {}),\n }));\n}\n\n/**\n * Phase 3, record (plugin, extension, node) tuples for every\n * extractor that actually runs against this node this scan. The\n * persist layer uses these to drop stale `scan_contributions` rows\n * for extractors that previously emitted but no longer do (e.g. body\n * change removes the trigger). NUL-separated to survive `nodePath`\n * segments with slashes.\n */\nfunction recordFreshlyRunTuples(\n extractors: readonly IExtractor[],\n nodePath: string,\n accum: IWalkAccumulators,\n): void {\n for (const ex of extractors) {\n accum.freshlyRunTuples.add(`${ex.pluginId}\\0${ex.id}\\0${nodePath}`);\n }\n}\n\n/**\n * Fold the per-node extract result into the scan-wide accumulators:\n * links (internal + external), enrichments (last-write-wins per\n * `(nodePath, extractorId)` pair), and view contributions.\n */\nfunction mergeExtractResult(\n extractResult: Awaited<ReturnType<typeof runExtractorsForNode>>,\n accum: IWalkAccumulators,\n): void {\n for (const link of extractResult.internalLinks) accum.internalLinks.push(link);\n for (const link of extractResult.externalLinks) accum.externalLinks.push(link);\n for (const enr of extractResult.enrichments) {\n accum.enrichmentBuffer.set(`${enr.nodePath}\\x00${enr.extractorId}`, enr);\n }\n for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);\n}\n\nfunction isPartialCacheHit(ctx: IProcessNodeContext): boolean {\n return (\n ctx.nodeHashCacheEligible &&\n ctx.cacheDecision.cachedQualifiedIds.size > 0 &&\n ctx.priorNode !== undefined\n );\n}\n\n/**\n * Build the node row for the extract path: clone the prior node when\n * we have a partial-cache hit (body/frontmatter unchanged + at least\n * one cached extractor), otherwise build a fresh node from the raw\n * file. NOT marking the path as `cachedPaths` because some extraction\n * is happening, the `externalRefsCount` recompute wants the node\n * re-derived from a fresh extractor pass (the missing extractor may\n * emit URLs).\n */\nfunction buildOrReuseNode(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n): Node {\n if (isPartialCacheHit(ctx) && ctx.priorNode) {\n const partial = cloneNodeAndReshapeLinks({\n priorNode: ctx.priorNode,\n strict: wctx.opts.strict,\n cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,\n applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,\n shortIdToQualified: wctx.shortIdToQualified,\n priorLinksByOriginating: wctx.priorLinksByOriginating,\n priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode,\n });\n for (const link of partial.internalLinks) accum.internalLinks.push(link);\n for (const issue of partial.frontmatterIssues) accum.frontmatterIssues.push(issue);\n accum.nodes.push(partial.node);\n return partial.node;\n }\n const fresh = buildFreshNodeAndValidateFrontmatter({\n raw: ctx.raw,\n kind: ctx.kind,\n provider: ctx.provider,\n bodyHash: ctx.bodyHash,\n frontmatterHash: ctx.frontmatterHash,\n encoder: wctx.opts.encoder,\n providerFrontmatter: wctx.opts.providerFrontmatter,\n strict: wctx.opts.strict,\n });\n accum.nodes.push(fresh.node);\n for (const issue of fresh.frontmatterIssues) accum.frontmatterIssues.push(issue);\n return fresh.node;\n}\n\n/**\n * Persist a `scan_extractor_runs` row for every applicable extractor\n * (both freshly-run AND cached ones whose contribution we reused).\n * Skipping cached entries here would let the replace-all persist\n * forget them, defeating the whole point of the partial-cache path.\n * Always populate `sidecarAnnotationsHashAtRun`; non-sidecar-readers\n * ignore it on the next decision but the column is non-null going\n * forward.\n */\nfunction recordExtractorRuns(\n nodePath: string,\n ctx: IProcessNodeContext,\n accum: IWalkAccumulators,\n): void {\n const ranAt = Date.now();\n for (const ex of ctx.cacheDecision.applicableExtractors) {\n accum.extractorRuns.push({\n nodePath,\n extractorId: qualifiedExtensionId(ex.pluginId, ex.id),\n bodyHashAtRun: ctx.bodyHash,\n ranAt,\n sidecarAnnotationsHashAtRun: ctx.sidecarAnnotationsHash,\n });\n }\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 /**\n * Maximum directory traversal depth. `undefined` (default) walks the\n * tree recursively without bound; `0` limits the watch to the\n * literal `roots` entries (no descent), which is the right setting\n * when watching a directory only to catch changes to specific\n * top-level files (see `subscribeMeta` in `core/watcher/runtime.ts`).\n * Forwarded verbatim to chokidar's `depth` option.\n */\n depth?: number;\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 ...(opts.depth !== undefined ? { depth: opts.depth } : {}),\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","Tiktoken","readFileSync","resolve","resolve","Ajv2020","require","resolve","readFileSync","readFileSync","resolve","createRequire","Ajv2020","Ajv2020","resolve","readFileSync","require","createRequire","byPath","byPath","join","relative","existsSync","readFileSync","dirname","resolve","dirname","resolve","existsSync","readFileSync","relative","join","walk","existsSync","readFileSync","dirname","resolve","createRequire","Ajv2020","yaml","existsSync","readFileSync","yaml","Ajv2020","resolveSpecRoot","resolve","resolveSpecRoot","require","createRequire","dirname","existsSync","readdirSync","join","relative","sep","existsSync","readFileSync","dirname","resolve","createRequire","Ajv2020","yaml","existsSync","readFileSync","dirname","isAbsolute","resolve","existsSync","readFileSync","join","join","resolve","existsSync","isAbsolute","yaml","yaml","isAbsolute","existsSync","Tiktoken","existsSync","statSync","resolve","relative","sep","relativePathFromRoots"]}
1
+ {"version":3,"sources":["../kernel/i18n/registry.texts.ts","../kernel/util/tx.ts","../kernel/registry.ts","../kernel/orchestrator/index.ts","../package.json","../kernel/adapters/in-memory-progress.ts","../kernel/adapters/plugin-loader/index.ts","../kernel/adapters/plugin-loader/id-utils.ts","../kernel/adapters/plugin-loader/validation.ts","../kernel/util/ajv-interop.ts","../kernel/extensions/hook.ts","../kernel/adapters/plugin-loader/storage-schemas.ts","../kernel/i18n/plugin-store.texts.ts","../kernel/adapters/plugin-store.ts","../kernel/adapters/schema-validators.ts","../kernel/types/view-catalog.ts","../kernel/util/format-error.ts","../kernel/adapters/silent-logger.ts","../kernel/util/logger.ts","../kernel/extensions/hook-dispatcher.ts","../kernel/i18n/orchestrator.texts.ts","../kernel/orchestrator/extractors.ts","../kernel/orchestrator/analyzers.ts","../kernel/orchestrator/cache.ts","../kernel/orchestrator/renames.ts","../kernel/scan/walk-content.ts","../kernel/scan/ignore.ts","../built-in-plugins/parsers/frontmatter-yaml/index.ts","../kernel/util/strip-prototype-pollution.ts","../built-in-plugins/parsers/plain/index.ts","../kernel/scan/parsers/index.ts","../kernel/extensions/provider.ts","../kernel/sidecar/parse.ts","../kernel/sidecar/drift.ts","../kernel/sidecar/discover-orphans.ts","../kernel/sidecar/store.ts","../core/config/atomic-write.ts","../core/config/helper.ts","../kernel/config/loader.ts","../kernel/util/skill-map-paths.ts","../core/paths/db-path.ts","../kernel/orchestrator/node-build.ts","../kernel/orchestrator/frontmatter.ts","../kernel/orchestrator/walk.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 { existsSync, statSync } from 'node:fs';\n\n// js-tiktoken ships CJS subpaths without explicit `.cjs` in the import\n// specifier, the lint rule's hard-coded extension matrix doesn't model\n// dual-package CJS subpath exports.\n// eslint-disable-next-line import-x/extensions\nimport { Tiktoken } from 'js-tiktoken/lite';\n// eslint-disable-next-line import-x/extensions\nimport cl100k_base from 'js-tiktoken/ranks/cl100k_base';\n\nimport pkg from '../../package.json' with { type: 'json' };\n\nimport { InMemoryProgressEmitter } from '../adapters/in-memory-progress.js';\nimport { installedSpecVersion } from '../adapters/plugin-loader.js';\nimport type { IPluginStore } from '../adapters/plugin-store.js';\nimport {\n buildProviderFrontmatterValidator,\n type IProviderFrontmatterValidator,\n} from '../adapters/schema-validators.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport type { IPriorExtractorRun } from '../adapters/sqlite/scan-load.js';\nimport {\n makeHookDispatcher,\n makeEvent,\n type IHookDispatcher,\n} from '../extensions/hook-dispatcher.js';\nimport type {\n IAnalyzer,\n IExtractor,\n IHook,\n IProvider,\n} from '../extensions/index.js';\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type { Kernel } from '../index.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { IIgnoreFilter } from '../scan/ignore.js';\nimport type {\n Issue,\n ScanResult,\n ScanScannedBy,\n} from '../types.js';\nimport type { IRegisteredAnnotationKey } from '../types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from '../types/view-catalog.js';\nimport { tx } from '../util/tx.js';\nimport { runAnalyzers } from './analyzers.js';\nimport {\n indexPriorSnapshot,\n type IPriorIndex,\n} from './cache.js';\nimport {\n recomputeExternalRefsCount,\n recomputeLinkCounts,\n type IEnrichmentRecord,\n type IExtractorRunRecord,\n} from './extractors.js';\nimport {\n detectRenamesAndOrphans,\n type RenameOp,\n} from './renames.js';\nimport {\n walkAndExtract,\n type IWalkAndExtractResult,\n} from './walk.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\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/contribution-orphan` can introspect the catalog\n * (read-only) and join it with the live node set to flag\n * dangling emissions. Slot catalog drift is NOT a scan concern,\n * it lives at load time and surfaces via `sm plugins doctor`\n * (the kernel rejects unknown slots as `invalid-manifest` first,\n * doctor catches the catalog-version-skew tail).\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 * 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\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 setup = buildScanSetup(options);\n const { emitter, exts, hookDispatcher, encoder, prior, start } = setup;\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: setup.strict,\n enableCache: setup.enableCache,\n prior,\n priorIndex: setup.priorIndex,\n priorExtractorRuns: setup.priorExtractorRuns,\n providerFrontmatter: setup.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 await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);\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 registeredActionIds = new Set(\n _kernel.registry.all('action').map((a) => qualifiedExtensionId(a.pluginId, a.id)),\n );\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 registeredActionIds,\n emitter,\n hookDispatcher,\n );\n mergeAnalyzerEmissions(walked, analyzerResult, exts.analyzers);\n const issues = analyzerResult.issues;\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 = buildScanStats(walked, issues, start);\n const scanCompletedEvent = makeEvent('scan.completed', { stats });\n emitter.emit(scanCompletedEvent);\n await hookDispatcher.dispatch('scan.completed', scanCompletedEvent);\n\n return buildScanReturn(walked, issues, renameOps, stats, options, setup);\n}\n\ninterface IScanSetup {\n start: number;\n scannedAt: number;\n emitter: ProgressEmitterPort;\n exts: NonNullable<RunScanOptions['extensions']>;\n hookDispatcher: IHookDispatcher;\n encoder: Tiktoken | null;\n prior: ScanResult | null;\n priorIndex: IPriorIndex;\n priorExtractorRuns: Map<string, Map<string, IPriorExtractorRun>> | undefined;\n providerFrontmatter: IProviderFrontmatterValidator;\n scope: 'project' | 'global';\n strict: boolean;\n enableCache: boolean;\n}\n\n/**\n * Resolve every per-scan invariant (emitter, encoder, prior index,\n * extension buckets, dispatcher) so `runScanInternal` stays a linear\n * sequence of phase calls instead of a 30-line setup preamble.\n *\n * Spec § A.9, `priorExtractorRuns === undefined` means the caller\n * doesn't track the fine-grained Extractor cache (legacy behaviour:\n * out-of-band tests, alternate driving adapters that have no DB).\n * That case falls back to the pre-A.9 model where the node-level body\n * / frontmatter hash check is sufficient. Passing an explicit\n * (possibly empty) Map opts the caller into the fine-grained path.\n */\nfunction buildScanSetup(options: RunScanOptions): IScanSetup {\n const start = Date.now();\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 // Encoder is heavyweight to construct (loads the cl100k_base BPE\n // table 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 priorIndex = indexPriorSnapshot(prior);\n // Spec 0.8.0: each Provider owns its per-kind frontmatter schemas.\n // 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 return {\n start,\n scannedAt: start,\n emitter,\n exts,\n hookDispatcher,\n encoder,\n prior,\n priorIndex,\n priorExtractorRuns: options.priorExtractorRuns,\n providerFrontmatter,\n scope: options.scope ?? 'project',\n strict: options.strict === true,\n enableCache: options.enableCache === true,\n };\n}\n\n/**\n * Spec § A.11, emit one `extractor.completed` event per registered\n * extractor after the full walk completes. Aggregated (no per-node\n * fan-out, that lives in `scan.progress` which is deliberately NOT\n * hookable).\n */\nasync function dispatchExtractorCompleted(\n extractors: readonly IExtractor[],\n emitter: ProgressEmitterPort,\n hookDispatcher: IHookDispatcher,\n): Promise<void> {\n for (const extractor of 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\n/**\n * Merge analyzer-side emissions into the walk's accumulators:\n *\n * - analyzer-emitted view contributions ride into the same per-scan\n * buffer extractor-emitted contributions populate.\n * - Phase 3: fold a tuple per `(analyzer × node)` into\n * `freshlyRunTuples` so the persist layer's per-tuple sweep can\n * drop stale analyzer-emitted rows when an analyzer stops emitting\n * for a previously-emitting node.\n */\nfunction mergeAnalyzerEmissions(\n walked: IWalkAndExtractResult,\n analyzerResult: { contributions: IContributionRecord[] },\n analyzers: readonly IAnalyzer[] | undefined,\n): void {\n for (const c of analyzerResult.contributions) walked.contributions.push(c);\n for (const analyzer of 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}\n\nfunction buildScanStats(\n walked: IWalkAndExtractResult,\n issues: Issue[],\n start: number,\n): ScanResult['stats'] {\n return {\n // `filesSkipped` is \"files walked but not classified by any\n // Provider\". Today every walked file IS classified by its Provider\n // (the `claude` Provider's `classify()` always returns a kind,\n // falling back to `'markdown'`), so this is always 0. Wired now\n // so the field shape is spec-conformant; meaningful once multiple\n // 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\nfunction buildScanReturn(\n walked: IWalkAndExtractResult,\n issues: Issue[],\n renameOps: RenameOp[],\n stats: ScanResult['stats'],\n options: RunScanOptions,\n setup: IScanSetup,\n): {\n result: ScanResult;\n renameOps: RenameOp[];\n extractorRuns: IExtractorRunRecord[];\n enrichments: IEnrichmentRecord[];\n contributions: IContributionRecord[];\n freshlyRunTuples: ReadonlySet<string>;\n} {\n return {\n result: {\n schemaVersion: 1,\n scannedAt: setup.scannedAt,\n scope: setup.scope,\n roots: options.roots,\n providers: setup.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","{\n \"name\": \"@skill-map/cli\",\n \"version\": \"0.24.4\",\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\": \"pnpm validate:compile && pnpm validate:test\",\n \"validate:compile\": \"pnpm typecheck && pnpm lint && pnpm build && pnpm reference:check\",\n \"validate:test\": \"pnpm test:ci\",\n \"pretest\": \"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\": \"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\": \"workspace:*\",\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.18\",\n \"ignore\": \"7.0.5\",\n \"js-tiktoken\": \"1.0.21\",\n \"js-yaml\": \"4.1.1\",\n \"kysely\": \"0.28.17\",\n \"semver\": \"7.7.4\",\n \"typanion\": \"3.14.0\",\n \"ws\": \"8.20.0\"\n },\n \"devDependencies\": {\n \"@eslint/js\": \"10.0.1\",\n \"@stylistic/eslint-plugin\": \"5.10.0\",\n \"@types/js-yaml\": \"4.0.9\",\n \"@types/node\": \"24.12.2\",\n \"@types/semver\": \"7.7.1\",\n \"@types/ws\": \"8.18.1\",\n \"c8\": \"11.0.0\",\n \"eslint\": \"10.2.1\",\n \"eslint-plugin-import-x\": \"4.16.2\",\n \"tsup\": \"8.5.1\",\n \"tsx\": \"4.21.0\",\n \"typescript\": \"5.9.3\",\n \"typescript-eslint\": \"8.59.1\"\n },\n \"engines\": {\n \"node\": \">=24.0\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","/**\n * In-memory `ProgressEmitterPort` adapter. No network, no DB, just a\n * synchronous fan-out to registered listeners. Used by the default scan\n * orchestrator; the WebSocket-backed emitter that streams to\n * the Web UI lands.\n */\n\nimport type {\n ProgressEmitterPort,\n ProgressEvent,\n 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 * `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 { join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\nimport semver from 'semver';\n\nimport type {\n IDiscoveredPlugin,\n ILoadedExtension,\n IPluginManifest,\n} from '../../types/plugin.js';\nimport type { PluginLoaderPort } from '../../ports/plugin-loader.js';\nimport { PLUGIN_LOADER_TEXTS } from '../../i18n/plugin-loader.texts.js';\nimport { tx } from '../../util/tx.js';\nimport type { ExtensionKind } from '../../registry.js';\nimport type { ISchemaValidators } from '../schema-validators.js';\n\nimport {\n applyIdCollisions,\n describe,\n fail,\n isInsidePlugin,\n isRecord,\n pathId,\n} from './id-utils.js';\nimport {\n extractDefault,\n importWithTimeout,\n stripFunctionsAndPluginId,\n} from './import-helpers.js';\nimport {\n KNOWN_KINDS,\n KNOWN_KINDS_LIST,\n validateAnnotationContributions,\n validateHookTriggers,\n} from './validation.js';\nimport { loadStorageSchemas } from './storage-schemas.js';\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 // Spec §architecture.md, \"AJV at three layers, manifest at load\n // (rejects unknown `slot` names with `invalid-manifest`)\". The\n // kind-specific schema validates the exported manifest shape\n // (e.g. `viewContributions[*].slot` against the closed catalog,\n // extractor's required `emitsLinkKinds`, etc.). Failures here are\n // structurally manifest-invalid, not module-load failures, the\n // module imported fine; the declared shape is wrong.\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 'invalid-manifest',\n tx(PLUGIN_LOADER_TEXTS.invalidManifestExtensionShape, { 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 * 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 * Shared id / path / type-guard helpers used across the loader's\n * validation pipeline. Kept tiny and dependency-free so every sibling\n * module (`validation.ts`, `import-helpers.ts`, `storage-schemas.ts`,\n * `index.ts`) can import without dragging in unrelated state.\n */\n\nimport { isAbsolute, relative, resolve } from 'node:path';\n\nimport type { IDiscoveredPlugin, TPluginLoadStatus } from '../../types/plugin.js';\nimport { PLUGIN_LOADER_TEXTS } from '../../i18n/plugin-loader.texts.js';\nimport { tx } from '../../util/tx.js';\n\n/**\n * Helper that builds the bare failure shape every error path returns.\n * Callers that have a parsed manifest layer it back on top via spread.\n */\nexport function 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 */\nexport function 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\nexport function 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\nexport function isRecord(v: unknown): v is Record<string, unknown> {\n return typeof v === 'object' && v !== null && !Array.isArray(v);\n}\n\n/** Fall-back plugin id derived from directory name when the manifest is unreadable. */\nexport function 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\nexport function 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 * Spec-driven per-extension validations the loader runs AFTER the\n * kind-specific AJV manifest pass.\n *\n * - `validateAnnotationContributions`, spec § 9.6.6: root keys must\n * be `exclusive`; every inline `schema` must AJV-compile.\n * - `validateHookTriggers`, spec § A.11: a hook MUST declare at\n * least one trigger and every trigger MUST appear in the curated\n * hookable set.\n *\n * Both return either a populated `IDiscoveredPlugin` failure row or\n * `null` when the extension is well-formed.\n */\n\nimport { Ajv2020 } from 'ajv/dist/2020.js';\n\nimport type { IDiscoveredPlugin, IPluginManifest } from '../../types/plugin.js';\nimport type { ExtensionKind } from '../../registry.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 { HOOK_TRIGGERS } from '../../extensions/hook.js';\nimport { describe, fail, isRecord } from './id-utils.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\n\nexport const KNOWN_KINDS = new Set<ExtensionKind>([\n 'provider',\n 'extractor',\n 'analyzer',\n 'action',\n 'formatter',\n 'hook',\n]);\nexport const 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 */\nexport const HOOKABLE_TRIGGERS_LIST = HOOK_TRIGGERS.join(', ');\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\nexport function 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 */\nexport function 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 * 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 * 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 * Spec § A.12, read and AJV-compile the storage output schemas a plugin\n * declares in its manifest. Mode A (`storage.schema`, single value-shape\n * under the KV sentinel) and Mode B (`storage.schemas`, per-table map)\n * share the same compile path; only the surrounding plumbing differs.\n *\n * Both helpers are pure functions that the loader's `loadOne` reaches\n * for in its last phase before declaring a plugin \"enabled\".\n */\n\nimport { readFileSync } from 'node:fs';\nimport { resolve } from 'node:path';\n\nimport { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js';\n\nimport type { IPluginManifest, IPluginStorageSchema } from '../../types/plugin.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 { describe, isInsidePlugin } from './id-utils.js';\n\ntype TAjv = InstanceType<typeof Ajv2020>;\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\nexport function 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 */\nexport function 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 * 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 * 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 { KNOWN_SLOT_NAMES } from '../types/view-catalog.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 (`KNOWN_SLOT_NAMES`\n // from `types/view-catalog.ts` is the single runtime source). Entries\n // inside `$defs/payloads` whose key starts with an underscore\n // (`_counter`, `_tag`, `_TreeNode`) are internal `$ref` reuse targets,\n // NOT slot 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\n function getContributionValidator(slot: string): ValidateFunction | null {\n if (!KNOWN_SLOT_NAMES.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 * Step 11.x, runtime view-contribution catalog types.\n *\n * Lives in its own module (rather than `kernel/index.ts`) so consumers\n * deep inside the kernel, `IAnalyzerContext`, the BFF route factories,\n * future Action contexts, can depend on the catalog shape without\n * dragging the whole kernel barrel and risking a cycle.\n *\n * Mirrors `annotation-catalog.ts` for the annotation contribution side\n * (Step 9.6.6). The two systems share the \"plugin contributes data,\n * kernel exposes catalog, UI renders\" pattern but never overlap in\n * storage or routing, see `architecture.md` §View contribution system\n * for the comparison table.\n *\n * **Closed catalog by design.** Both `TSlotName` and `TInputTypeName`\n * mirror the closed enums in `spec/schemas/view-slots.schema.json`\n * and `spec/schemas/input-types.schema.json`. Adding a member is a\n * coordinated kernel + spec + UI + scaffolder change. The closed-enum\n * shape lets TypeScript surface unknown slots at author time\n * (in plugin authors' editors when their plugin imports `@skill-map/cli`)\n * AND lets the runtime exhaustively dispatch slot → renderer in the\n * UI without `default:` fallbacks.\n */\n\n/**\n * Closed enum of view slot names. Mirror of\n * `spec/schemas/view-slots.schema.json#/$defs/SlotName`.\n *\n * Plugins pick one of these by name in their extension manifest's\n * `viewContributions[<contributionId>].slot` field. The kernel\n * validates each pick at load time (`invalid-manifest` on miss); the\n * slot fixes both the renderer and the payload shape.\n */\nexport type TSlotName =\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 * Runtime mirror of `TSlotName`. Single source of truth for every\n * consumer that needs to compare a string against the closed catalog\n * at runtime (manifest validation, `sm plugins doctor` drift checks,\n * `ctx.emitContribution` payload routing). Keep aligned with\n * `TSlotName` and `spec/schemas/view-slots.schema.json#/$defs/SlotName`\n * (the spec stays the formal source of truth; this list is the\n * TS / JS runtime mirror, no narrower).\n */\nexport const ALL_SLOT_NAMES: ReadonlyArray<TSlotName> = [\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/**\n * Set form of `ALL_SLOT_NAMES` for O(1) membership checks. Typed as\n * `ReadonlySet<string>` (not `ReadonlySet<TSlotName>`) because every\n * consumer feeds it untrusted strings pulled from plugin manifests\n * (`getContributionValidator`, `sm plugins doctor`'s slot-drift walk),\n * and TS `Set<T>.has(value: T)` would otherwise reject the call site.\n * The element values are still all `TSlotName` literals; the wider\n * key type only relaxes the `.has()` parameter.\n */\nexport const KNOWN_SLOT_NAMES: ReadonlySet<string> = new Set(ALL_SLOT_NAMES);\n\n/**\n * Closed enum of input-type names for plugin settings. Mirror of\n * `spec/schemas/input-types.schema.json#/$defs/InputTypeName`.\n *\n * Plugins pick one of these by name in their plugin manifest's\n * `settings[<settingId>].type` field. The kernel exposes the resolved\n * value via `ctx.settings.<settingId>` typed per the input-type's\n * value-type promise.\n */\nexport type TInputTypeName =\n | 'string-list'\n | 'single-string'\n | 'boolean-flag'\n | 'integer'\n | 'enum-pick'\n | 'enum-multipick'\n | 'path-glob'\n | 'regex'\n | 'secret'\n | 'key-value-list';\n\n/** Closed severity palette aligned with PrimeNG `<p-tag>` / `<p-message>`. */\nexport type TSeverity = 'info' | 'warn' | 'success' | 'danger';\n\n/**\n * Manifest-side declaration of a single view contribution. The plugin\n * author writes one of these per Record key in\n * `IExtensionBase.viewContributions[<contributionId>]`.\n *\n * Mirror of `view-slots.schema.json#/$defs/IViewContribution`.\n */\nexport interface IViewContribution {\n /**\n * Required. Closed-catalog slot name. Unknown name rejects the\n * extension as `invalid-manifest` at load. The slot fixes both the\n * renderer and the payload shape; there is no separate \"contract\"\n * abstraction.\n */\n slot: TSlotName;\n /**\n * Optional human-readable label. English-only per `AGENTS.md`\n * (`Externalized texts, not internationalized`).\n */\n label?: string;\n /** Optional hover tooltip. English-only. */\n tooltip?: string;\n /**\n * Optional emoji codepoint OR PrimeIcons class id (without the\n * `pi-` prefix). The UI discriminates: matches Unicode\n * `\\p{Extended_Pictographic}` → emoji text, otherwise → PrimeIcon.\n * Required for counter slots and `card.title.right` (enforced by\n * the manifest-side conditional in `view-slots.schema.json`).\n */\n icon?: string;\n /**\n * Optional empty placeholder text shown when the payload is empty\n * AND `emitWhenEmpty` is true. Falls back to a UI-supplied generic\n * 'No data.' string. English-only.\n */\n emptyText?: string;\n /**\n * When false (default), the kernel drops emissions whose payload is\n * structurally empty so the slot stays silent. When true, the\n * renderer surfaces an empty placeholder. Per-slot definition of\n * \"empty\" lives in the slot's payload schema.\n */\n emitWhenEmpty?: boolean;\n /**\n * Optional ordering hint (default 100). Slots configured with\n * `order: 'priority'` sort contributions ASC by this value, with\n * alphabetical tie-break by qualified id. The plugin uses this to\n * suggest where its contribution belongs relative to others sharing\n * the same slot, the slot has the final say.\n */\n priority?: number;\n}\n\n/**\n * Single row of the runtime view-contribution catalog surfaced by\n * `kernel.getRegisteredViewContributions()`. One row per\n * `(pluginId × extensionId × contributionId)` tuple. Composed at boot\n * by `loadPluginRuntime` from every loaded extension's\n * `viewContributions` map.\n *\n * The qualified id is `<pluginId>/<extensionId>/<contributionId>`,\n * matches the qualified id pattern used elsewhere in the kernel\n * (`<pluginId>/<extensionId>` for extensions; this adds the third\n * segment for per-contribution identity).\n */\nexport interface IRegisteredViewContribution {\n pluginId: string;\n extensionId: string;\n contributionId: string;\n slot: TSlotName;\n /** Optional manifest-declared label (English-only). */\n label?: string;\n tooltip?: string;\n icon?: string;\n emptyText?: string;\n emitWhenEmpty: boolean;\n /** Manifest-declared ordering hint (default 100). See `IViewContribution.priority`. */\n priority?: number;\n}\n\n/**\n * Common fields on every setting declaration. The discriminated union\n * `ISettingDeclaration` extends one of these per `type` value.\n */\ninterface ISettingCommon {\n /** Required. Short human-readable label. English-only. */\n label: string;\n /** Optional helper text shown below the control. English-only. */\n description?: string;\n}\n\nexport interface ISetting_StringList extends ISettingCommon {\n type: 'string-list';\n default?: string[];\n min?: number;\n max?: number;\n itemMaxLength?: number;\n}\n\nexport interface ISetting_SingleString extends ISettingCommon {\n type: 'single-string';\n default?: string;\n minLength?: number;\n maxLength?: number;\n /** Optional ECMAScript regex pattern (no flags). */\n pattern?: string;\n}\n\nexport interface ISetting_BooleanFlag extends ISettingCommon {\n type: 'boolean-flag';\n default?: boolean;\n}\n\nexport interface ISetting_Integer extends ISettingCommon {\n type: 'integer';\n default?: number;\n min?: number;\n max?: number;\n step?: number;\n}\n\nexport interface ISetting_EnumOption {\n value: string;\n label: string;\n}\n\nexport interface ISetting_EnumPick extends ISettingCommon {\n type: 'enum-pick';\n options: ISetting_EnumOption[];\n default?: string;\n}\n\nexport interface ISetting_EnumMultipick extends ISettingCommon {\n type: 'enum-multipick';\n options: ISetting_EnumOption[];\n default?: string[];\n min?: number;\n max?: number;\n}\n\nexport interface ISetting_PathGlob extends ISettingCommon {\n type: 'path-glob';\n default?: string;\n /** When true, accepts string[]; when false (default), single string. */\n multiple?: boolean;\n}\n\nexport interface ISetting_Regex extends ISettingCommon {\n type: 'regex';\n default?: string;\n /** Subset of `gimsuy`. Default `''`. */\n flags?: string;\n}\n\nexport interface ISetting_Secret extends ISettingCommon {\n type: 'secret';\n /**\n * Optional uppercase-ASCII identifier. When set in the process\n * environment, that value wins over any stored value (lets CI\n * inject without writing to disk).\n */\n envVar?: string;\n}\n\nexport interface ISetting_KeyValueListEntry {\n key: string;\n value: string;\n}\n\nexport interface ISetting_KeyValueList extends ISettingCommon {\n type: 'key-value-list';\n keyLabel?: string;\n valueLabel?: string;\n default?: ISetting_KeyValueListEntry[];\n min?: number;\n max?: number;\n}\n\n/**\n * Discriminated union of every setting declaration shape. The plugin\n * author NEVER writes JSON Schema for settings, they pick one of\n * these `type` values and supply per-type parameters.\n *\n * Mirror of `input-types.schema.json#/$defs/ISettingDeclaration`.\n */\nexport type ISettingDeclaration =\n | ISetting_StringList\n | ISetting_SingleString\n | ISetting_BooleanFlag\n | ISetting_Integer\n | ISetting_EnumPick\n | ISetting_EnumMultipick\n | ISetting_PathGlob\n | ISetting_Regex\n | ISetting_Secret\n | ISetting_KeyValueList;\n\n/**\n * Runtime value type for a setting, derived from its declaration. The\n * kernel exposes settings to extractors as `Record<string, TSettingValue>`\n * via `ctx.settings.<settingId>`; consumers that want narrow typing\n * narrow at the call site by reading `manifest.settings[id].type`.\n */\nexport type TSettingValue =\n | string\n | string[]\n | boolean\n | number\n | ISetting_KeyValueListEntry[];\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 * 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 * 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// Builds a per-trigger context shape: each `THookTrigger` variant\n// pulls a different slice of the progress event. The switch IS the\n// contract; splitting per trigger scatters the dispatch table. Per\n// `context/lint.md` category 6 (discriminated-union dispatchers).\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 * 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 extensionErrorRecommendedActionMissing:\n 'Analyzer \"{{analyzerId}}\" declares recommendedAction \"{{actionId}}\" but no Action ' +\n 'is registered under that qualified id. The analyzer stays registered; the recommendation ' +\n 'will not surface in the inspector.',\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 * Per-node extractor invocation: build a fresh `IExtractorContext` for\n * each extractor, validate every emitted link / contribution against\n * the declared catalog, fold enrichment partials into per-`(node,\n * extractor)` records, and surface emit-time drops as\n * `extension.error` events.\n *\n * Also hosts the post-walk recompute helpers that re-derive\n * `linksOutCount` / `linksInCount` / `externalRefsCount` on every node\n * from the final merged link buffer, plus the `IExtractorRunRecord`\n * and `IEnrichmentRecord` types those records eventually persist as.\n */\n\nimport { makeEvent } from '../extensions/hook-dispatcher.js';\nimport type {\n IExtractor,\n IExtractorContext,\n} from '../extensions/index.js';\nimport type { IPluginStore } from '../adapters/plugin-store.js';\nimport { loadSchemaValidators } from '../adapters/schema-validators.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type {\n Confidence,\n Link,\n LinkKind,\n Node,\n} from '../types.js';\nimport { tx } from '../util/tx.js';\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 * 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; the\n // load-time catalog drift check lives in `sm plugins doctor`),\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 */\nexport function 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 */\nexport function 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\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\nexport function 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\nexport function 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 * 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","/**\n * Analyzer pass: runs every registered analyzer over the merged graph\n * after the walk completes. Mirrors the Extractor emit-time wiring for\n * `ctx.emitContribution` so analyzer-emitted view contributions\n * survive AJV validation against their slot's payload schema before\n * landing in `scan_contributions`.\n *\n * Issue validation (`validateIssue`) catches analyzers that emit an\n * out-of-spec `severity` and surfaces them as `extension.error` events\n * so plugin authors see why an issue silently disappeared.\n */\n\nimport {\n makeEvent,\n type IHookDispatcher,\n} from '../extensions/hook-dispatcher.js';\nimport type { IAnalyzer } from '../extensions/index.js';\nimport { loadSchemaValidators } from '../adapters/schema-validators.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport type { IOrphanSidecar } from '../sidecar/index.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { Issue, Link, Node, Severity } from '../types.js';\nimport type { IRegisteredAnnotationKey } from '../types/annotation-catalog.js';\nimport type { IRegisteredViewContribution } from '../types/view-catalog.js';\nimport { tx } from '../util/tx.js';\nimport { emitExtensionError, readDeclaredContributions } from './extractors.js';\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 */\nexport async 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 registeredActionIds: ReadonlySet<string>,\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 validateRecommendedActions(analyzers, registeredActionIds, emitter);\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 * Spec § extensions/analyzer.schema.json, every `recommendedActions`\n * entry MUST be the qualified id of a registered Action. The kernel\n * logs `recommended-action-missing` for unresolved entries but keeps\n * the analyzer registered (the analyzer still emits issues; only the\n * \"Recommended for issues\" hint in the inspector is dropped).\n *\n * Runs once per scan at the top of the analyzer pass, the action set\n * does not change during a scan and emitting per-analyzer-call would be\n * noise.\n */\nfunction validateRecommendedActions(\n analyzers: readonly IAnalyzer[],\n registeredActionIds: ReadonlySet<string>,\n emitter: ProgressEmitterPort,\n): void {\n for (const analyzer of analyzers) {\n const refs = analyzer.recommendedActions;\n if (refs === undefined || refs.length === 0) continue;\n const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);\n for (const actionId of refs) {\n if (registeredActionIds.has(actionId)) continue;\n emitter.emit(\n makeEvent('extension.error', {\n kind: 'recommended-action-missing',\n extensionId: analyzerId,\n actionId,\n message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {\n analyzerId,\n actionId,\n }),\n }),\n );\n }\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","/**\n * Per-`(node, extractor)` cache decision logic. Walks the Spec § A.9\n * `scan_extractor_runs` breadcrumbs alongside the node-level\n * body/frontmatter/sidecar-annotations hashes and decides:\n * - which extractors fully cache-hit (their prior contribution\n * survives unchanged);\n * - which extractors need to run for this scan;\n * - how each prior outbound internal link should be reshaped given\n * the cached / missing / obsolete state of its `sources`.\n *\n * Also indexes the prior `ScanResult` so the walker can look up prior\n * nodes / links / frontmatter issues by path in O(1).\n */\n\nimport type { Issue, Link, Node, ScanResult } from '../types.js';\nimport type { IExtractor } from '../extensions/index.js';\nimport type { IPriorExtractorRun } from '../adapters/sqlite/scan-load.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { IExtractorRunRecord } from './extractors.js';\n\nexport interface 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\nexport function 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 indexPriorNodes(prior.nodes, priorNodesByPath, priorNodePaths);\n indexPriorLinks(prior.links, priorNodePaths, priorLinksByOriginating);\n indexPriorFrontmatterIssues(prior.issues, priorFrontmatterIssuesByNode);\n return { priorNodesByPath, priorNodePaths, priorLinksByOriginating, priorFrontmatterIssuesByNode };\n}\n\nfunction indexPriorNodes(\n nodes: readonly Node[],\n byPath: Map<string, Node>,\n paths: Set<string>,\n): void {\n for (const node of nodes) {\n byPath.set(node.path, node);\n paths.add(node.path);\n }\n}\n\nfunction indexPriorLinks(\n links: readonly Link[],\n priorNodePaths: Set<string>,\n byOriginating: Map<string, Link[]>,\n): void {\n for (const link of links) {\n const key = originatingNodeOf(link, priorNodePaths);\n const list = byOriginating.get(key);\n if (list) list.push(link);\n else byOriginating.set(key, [link]);\n }\n}\n\nconst FRONTMATTER_ISSUE_ANALYZERS: ReadonlySet<string> = new Set([\n 'frontmatter-invalid',\n 'frontmatter-malformed',\n // Audit L1: parser parse-error is emitted by\n // `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The\n // raw.parseIssues only flows through the non-cache path; a cached\n // node skips the rebuild, so the prior issue MUST survive the\n // incremental scan or the warning silently disappears on a clean\n // re-scan of an unchanged file.\n 'frontmatter-parse-error',\n]);\n\nfunction indexPriorFrontmatterIssues(\n issues: readonly Issue[],\n byNode: Map<string, Issue[]>,\n): void {\n for (const issue of issues) {\n if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;\n if (issue.nodeIds.length !== 1) continue;\n const path = issue.nodeIds[0]!;\n const list = byNode.get(path);\n if (list) list.push(issue);\n else byNode.set(path, [issue]);\n }\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 */\nexport function 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 * 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 */\nexport function 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\n // Two code paths, picked by whether the caller threaded fine-grained\n // breadcrumbs through. Legacy callers (no priorExtractorRuns) lose\n // sidecar-edit invalidation; modern callers get per-(node, extractor)\n // cache-hit precision. Split out so each branch is trivially small\n // and easy to reason about in isolation.\n const split = opts.priorExtractorRuns === undefined\n ? splitLegacy(applicableExtractors, applicableQualifiedIds, opts.nodeHashCacheEligible)\n : splitFineGrained(applicableExtractors, opts);\n\n return {\n applicableExtractors,\n applicableQualifiedIds,\n cachedQualifiedIds: split.cachedQualifiedIds,\n missingExtractors: split.missingExtractors,\n fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0,\n };\n}\n\n/**\n * Pre-A.9 cache decision: caller did not load fine-grained\n * breadcrumbs, so we treat every applicable extractor as cached when\n * the node-level hashes match. Sidecar-edit invalidation is\n * unavailable on this path, callers that need it must opt into the\n * fine-grained Map.\n */\nfunction splitLegacy(\n applicableExtractors: IExtractor[],\n applicableQualifiedIds: Set<string>,\n nodeHashCacheEligible: boolean,\n): { cachedQualifiedIds: Set<string>; missingExtractors: IExtractor[] } {\n const cachedQualifiedIds = new Set<string>();\n const missingExtractors: IExtractor[] = [];\n if (nodeHashCacheEligible) {\n for (const id of applicableQualifiedIds) cachedQualifiedIds.add(id);\n } else {\n for (const ex of applicableExtractors) missingExtractors.push(ex);\n }\n return { cachedQualifiedIds, missingExtractors };\n}\n\n/**\n * Spec § A.9 cache decision: walk the fine-grained\n * `scan_extractor_runs` breadcrumbs and decide per-extractor whether\n * its prior run still applies to THIS body + THIS sidecar.\n *\n * Sidecar-hash gate applies unconditionally. The author-facing\n * alternative (an opt-in `readsSidecar` flag) was rejected because\n * forgetting it produces a silent stale-data bug, sidecar edits\n * don't refresh until something in the `.md` changes. Universal\n * invalidation costs an extractor re-run per node on `.sm` edits\n * (negligible: sidecars change rarely and extractors are pure-CPU);\n * the gain is zero cognitive load for plugin authors and zero\n * correctness traps.\n */\nfunction splitFineGrained(\n applicableExtractors: IExtractor[],\n opts: {\n nodePath: string;\n bodyHash: string;\n sidecarAnnotationsHash: string;\n nodeHashCacheEligible: boolean;\n priorExtractorRuns: Map<string, Map<string, IPriorExtractorRun>> | undefined;\n },\n): { cachedQualifiedIds: Set<string>; missingExtractors: IExtractor[] } {\n const cachedQualifiedIds = new Set<string>();\n const missingExtractors: IExtractor[] = [];\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 if (\n opts.nodeHashCacheEligible &&\n prior !== undefined &&\n prior.bodyHash === opts.bodyHash &&\n prior.sidecarAnnotationsHash === opts.sidecarAnnotationsHash\n ) {\n cachedQualifiedIds.add(qualified);\n } else {\n missingExtractors.push(ex);\n }\n }\n return { cachedQualifiedIds, missingExtractors };\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 */\nexport function 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 */\nexport function 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 * 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 */\nexport function 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 partition = partitionLinkSources(\n link.sources,\n shortIdToQualified,\n cachedQualifiedIds,\n applicableQualifiedIds,\n );\n if (partition.hasMissing) return null;\n if (partition.cached.length === 0) return null;\n if (partition.obsolete.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: partition.cached };\n}\n\n/**\n * Bucket every entry in `link.sources` into cached / missing /\n * obsolete per the contract documented on `reuseCachedLink`. `missing`\n * collapses into a single boolean (one missing source kills the link;\n * there's no point listing every missing one).\n */\nfunction partitionLinkSources(\n sources: readonly string[],\n shortIdToQualified: Map<string, string[]>,\n cachedQualifiedIds: Set<string>,\n applicableQualifiedIds: Set<string>,\n): { cached: string[]; obsolete: string[]; hasMissing: boolean } {\n const cached: string[] = [];\n const obsolete: string[] = [];\n let hasMissing = false;\n for (const source of sources) {\n const category = classifyLinkSource(\n source,\n shortIdToQualified,\n cachedQualifiedIds,\n applicableQualifiedIds,\n );\n if (category === 'cached') cached.push(source);\n else if (category === 'missing') hasMissing = true;\n else obsolete.push(source);\n }\n return { cached, obsolete, hasMissing };\n}\n\n/**\n * Decide how one entry in `link.sources` should be treated when its\n * owning node is a cache reuse candidate. Three buckets per the\n * `reuseCachedLink` contract:\n *\n * - `cached` , short id maps to a currently-registered qualified id\n * that has a matching `scan_extractor_runs` row for\n * this body hash. Contribution is fresh; survives.\n * - `missing` , registered for this kind but not cached for this\n * body. The missing extractor will re-emit, so the\n * prior link gets dropped to avoid duplicates.\n * - `obsolete`, no live extractor claims the short id, or the live\n * one is not applicable to this kind. Strip from\n * `sources`; keep the link if a cached source remains.\n */\nfunction classifyLinkSource(\n source: string,\n shortIdToQualified: Map<string, string[]>,\n cachedQualifiedIds: Set<string>,\n applicableQualifiedIds: Set<string>,\n): 'cached' | 'missing' | 'obsolete' {\n const candidates = shortIdToQualified.get(source);\n if (!candidates || candidates.length === 0) return 'obsolete';\n if (candidates.some((q) => cachedQualifiedIds.has(q))) return 'cached';\n if (candidates.some((q) => applicableQualifiedIds.has(q))) return 'missing';\n return 'obsolete';\n}\n","/**\n * Rename + orphan classification per `spec/db-schema.md` §Rename\n * detection. Pure: takes the prior `ScanResult` and the current node\n * set, mutates the supplied `issues` array in place, and returns the\n * `RenameOp[]` the persistence layer must apply inside the same tx as\n * the scan zone replace-all.\n */\n\nimport type { Issue, Node, ScanResult } from '../types.js';\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\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 * 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 / H1)**, `readdir` reports a regular file →\n * `lstat()` re-verifies before the read. Closes the window where the\n * entry could be swapped for a symlink between the two calls.\n * `lstat` does NOT follow symlinks (H1 fix, audit upgrade from\n * `stat`), so a `.md`→symlink race is rejected by `isFile()`\n * returning false on the symlink itself; non-regular types\n * (socket, FIFO, device) introduced in the race window are\n * rejected the same way.\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, lstat } 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 // Audit L1: forward parser diagnostics (e.g. malformed YAML)\n // through the IRawNode surface so the orchestrator can\n // convert them into warn-level kernel `Issue` rows. Omitted\n // when the parser reported no issues (happy path).\n ...(parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}),\n };\n }\n }\n}\n\n// Recursive directory walker: per-entry branches over symlink /\n// ignore-filter / kind (dir vs file) / extension allow-list. The\n// branching IS the walker; extraction yields helpers that all run\n// once per entry anyway. Per `context/lint.md` category 7 (recursive type-discriminator walkers).\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 (audit H1): readdir reported a regular file;\n // re-verify before reading. We use `lstat` (NOT `stat`) so a\n // symlink swapped in between `readdir` and the re-check is\n // detected here. `stat` follows symlinks, which would have let an\n // attacker race a benign `.md` → symlink to `~/.ssh/id_rsa` and\n // see the target's contents land in the SQLite body store and\n // /api/nodes response. `lstat` plus the `isFile()` predicate\n // rejects both symlinks and any non-regular type (socket, FIFO,\n // device) that appeared in the race window.\n try {\n const s = await lstat(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 + M2)**, the parsed object is\n * run through `stripPrototypePollution` so `__proto__`,\n * `constructor`, and `prototype` keys are removed at EVERY depth,\n * not just at the root. js-yaml v4 stores `__proto__:` as an own\n * data property at any nesting level (rather than mutating\n * `Object.prototype`), but the value still flows into downstream\n * `Object.assign`-style merges where the `__proto__` setter fires.\n * Deep stripping at parse time keeps the returned object safe to\n * spread, copy, and persist regardless of nesting.\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 * - **Malformed YAML surfacing (audit L1)**, when `yaml.load` throws\n * the parser still returns `frontmatter: {}` (the historic\n * fallback) so the scan keeps making progress, but it ALSO emits\n * an `IParseIssue` with code `frontmatter-parse-error` and the\n * sanitised `err.message`. The walker forwards it on `IRawNode`\n * and the orchestrator translates it into a warn-level kernel\n * `Issue` so authors see the typo instead of silently losing\n * their metadata.\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 {\n IFileParser,\n IParsedFile,\n IParseIssue,\n} from '../../../kernel/scan/parsers/types.js';\nimport { stripPrototypePollution } from '../../../kernel/util/strip-prototype-pollution.js';\n\nconst FRONTMATTER_RE = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?([\\s\\S]*)$/;\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 let parsed: Record<string, unknown> = {};\n const issues: IParseIssue[] = [];\n try {\n const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });\n if (doc && typeof doc === 'object' && !Array.isArray(doc)) {\n // Deep strip (audit M2). The helper returns a fresh\n // own-property-clean object; nested `__proto__` / `constructor`\n // / `prototype` keys are dropped at every depth.\n parsed = stripPrototypePollution(doc as Record<string, unknown>);\n }\n } catch (err) {\n // Malformed YAML (audit L1), keep the historic `parsed = {}`\n // fallback so the scan keeps making progress, but surface a\n // diagnostic so the author sees the typo. Only the parser-error\n // message is interpolated; the raw frontmatter is NEVER folded\n // into the message (a hostile YAML could embed multi-line\n // garbage; `frontmatterRaw` stays available on `IParsedFile` for\n // downstream diagnostics that opt in).\n issues.push({\n code: 'frontmatter-parse-error',\n message: sanitiseParseErrorMessage(err),\n });\n }\n const out: IParsedFile = { frontmatterRaw, frontmatter: parsed, body };\n if (issues.length > 0) {\n return { ...out, issues };\n }\n return out;\n },\n};\n\n/**\n * Distil a `yaml.load` throw into a single-line, control-character-free\n * message. `js-yaml`'s `YAMLException.message` already excludes the\n * source position prefix; we strip CR/LF/tab and collapse runs of\n * whitespace so a multi-line \"reason\\n in ...\" string can never break\n * a single-line log render or smuggle ANSI escapes through a downstream\n * consumer.\n */\nfunction sanitiseParseErrorMessage(err: unknown): string {\n const raw = err instanceof Error ? err.message : String(err);\n // eslint-disable-next-line no-control-regex\n return raw.replace(/[\u0000-\u001f]+/g, ' ').replace(/\\s+/g, ' ').trim();\n}\n","/**\n * Shared prototype-pollution defence used at every trust boundary where\n * untrusted JSON / YAML enters the runtime (settings, sidecars, plugin\n * manifests).\n *\n * Two surfaces:\n *\n * 1. `FORBIDDEN_KEYS`, the closed set of key names that manipulate\n * the prototype chain. Merge functions consult this directly to\n * skip keys without cloning (see `kernel/config/loader.ts` and\n * `kernel/sidecar/store.ts`).\n *\n * 2. `stripPrototypePollution(value)`, pure: returns a deep-cloned\n * copy of `value` with every forbidden key removed at every depth.\n * Primitives pass through unchanged. Arrays recurse element-wise.\n * Plain objects have their entries filtered and recursed.\n * Non-plain objects (Date, Map, class instances) are returned\n * as-is, since they are never produced by `JSON.parse` /\n * `yaml.load` on the data we accept.\n *\n * Use the strip helper at the read boundary; consult `FORBIDDEN_KEYS`\n * inside merge primitives. Centralising both means the day a fourth\n * forbidden name surfaces (rare but possible, engine-specific\n * accessors), we update one file.\n */\n\nexport const FORBIDDEN_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\nexport function stripPrototypePollution<T>(value: T): T {\n return strip(value) as T;\n}\n\nfunction strip(value: unknown): unknown {\n if (value === null || value === undefined) return value;\n if (typeof value !== 'object') return value;\n if (Array.isArray(value)) return value.map(strip);\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (FORBIDDEN_KEYS.has(k)) continue;\n out[k] = strip(v);\n }\n return out;\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 type { IParseIssue } from '../scan/parsers/types.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 * Parser diagnostics (audit L1). Populated by the walker when the\n * parser surfaced `IParseIssue` entries (e.g. malformed YAML).\n * Carried through `processRawNode` and converted into warn-level\n * kernel `Issue` rows inside `buildFreshNodeAndValidateFrontmatter`.\n * Empty / undefined on the happy path.\n */\n parseIssues?: readonly IParseIssue[];\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 * 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 * 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 { stripPrototypePollution } from '../util/strip-prototype-pollution.js';\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 // Explicit JSON_SCHEMA to match the frontmatter parser and harden\n // against a future js-yaml default-schema loosening (audit M1).\n parsedYaml = yaml.load(raw, { schema: yaml.JSON_SCHEMA });\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 // Trust boundary: strip prototype-pollution keys at every depth before\n // AJV ever sees the document, so a tainted `.sm` cannot survive the\n // round-trip through `IParsedSidecar.raw` into plugin Action contexts.\n parsedYaml = stripPrototypePollution(parsedYaml);\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 * 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 } 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 { writeFileAtomicExclusive } from '../../core/config/atomic-write.js';\nimport { ensureSidecarWritesAllowed } from '../../core/config/sidecar-consent.js';\nimport { applyAjvFormats } from '../util/ajv-interop.js';\nimport {\n FORBIDDEN_KEYS,\n stripPrototypePollution,\n} from '../util/strip-prototype-pollution.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 // Trust boundary: a hostile sidecar (or a future Action emitting a\n // patch derived from `node.sidecar.raw`) must not be able to set\n // `__proto__` / `constructor` / `prototype` on the merged result.\n // The parse boundary in `parse.ts` already strips these keys, but\n // the merge primitive enforces it independently so future callers\n // do not have to remember to pre-filter.\n if (FORBIDDEN_KEYS.has(key)) continue;\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 // Explicit JSON_SCHEMA to match the frontmatter parser and harden\n // against a future js-yaml default-schema loosening (audit M1).\n const parsed = yaml.load(raw, { schema: yaml.JSON_SCHEMA });\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 // Trust boundary: strip prototype-pollution keys before the value\n // seeds the `current` argument to `deepMerge`. Defence-in-depth on top\n // of the merge primitive's own skip-on-forbidden-key filter.\n return stripPrototypePollution(parsed);\n}\n\nfunction atomicWriteFile(targetPath: string, content: string): void {\n // Audit M1 + L1: stage to a sibling temp file opened with\n // `O_EXCL | O_NOFOLLOW` and a CSPRNG-random suffix (no longer\n // pid + Date.now(), which was predictable, so a local attacker\n // could pre-plant a symlink at the temp path). `writeFileAtomicExclusive`\n // is shared with `writeJsonAtomic` (settings) so both surfaces\n // get the same hardening. Mode 0o600 is applied at open time and\n // survives the rename (POSIX rename preserves the inode + its mode).\n writeFileAtomicExclusive(targetPath, content);\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 * 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 closeSync,\n constants as fsConstants,\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeSync,\n} from 'node:fs';\nimport { randomBytes } from 'node:crypto';\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 * Stage `content` under `path` via an exclusive, no-follow open and\n * `renameSync` the result into place. Shared by `writeJsonAtomic`\n * (settings) and `kernel/sidecar/store.ts:atomicWriteFile` (`.sm`\n * sidecars), both of which previously composed a predictable temp\n * filename (`<path>.tmp.<pid>` / `<path>.tmp.<pid>.<Date.now()>`)\n * and called `writeFileSync`, which follows symlinks. A local\n * attacker who pre-planted a symlink at the predicted temp path\n * would have redirected the write to the symlink's target.\n *\n * Fix (audit M1):\n *\n * - The temp name embeds a cryptographically-random suffix\n * (`randomBytes(8).toString('hex')`) so the path is\n * unpredictable across invocations.\n * - `openSync` uses `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW` with\n * mode `0o600`. `O_EXCL` makes the syscall fail with `EEXIST` if\n * anything (file, symlink, directory) already lives at the temp\n * path; `O_NOFOLLOW` makes it fail with `ELOOP` if the leaf is a\n * symlink. Together they close the race the previous\n * `writeFileSync`-with-predictable-name pattern left open.\n * - The write goes through the returned fd; the rename is the\n * standard POSIX same-filesystem atomic rename. Mode `0o600`\n * survives the rename (POSIX rename preserves the inode + its\n * mode), which is what the settings / sidecar privacy guarantee\n * relies on.\n *\n * On failure the temp file is best-effort removed so we do not leak\n * `<path>.tmp.<random>` siblings if the rename target is read-only.\n */\nexport function writeFileAtomicExclusive(path: string, content: string): void {\n // 16 hex chars (64 bits of entropy). The `node:crypto` source is\n // CSPRNG-backed; an attacker cannot pre-plant a symlink at a\n // predicted temp path because they cannot predict the suffix.\n const tmp = `${path}.tmp.${process.pid}.${randomBytes(8).toString('hex')}`;\n let fd: number | null = null;\n try {\n // O_EXCL fails with EEXIST if the path already exists (file,\n // symlink, directory). O_NOFOLLOW fails with ELOOP if the final\n // path component is a symlink. Together they close the audit M1\n // race window. mode 0o600 is set at create time so the inode\n // never carries broader perms, even briefly.\n fd = openSync(\n tmp,\n fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,\n 0o600,\n );\n writeSync(fd, content);\n closeSync(fd);\n fd = null;\n renameSync(tmp, path);\n } catch (err) {\n // Ensure the fd is released even if the rename threw.\n if (fd !== null) {\n try {\n closeSync(fd);\n } catch {\n /* best-effort */\n }\n }\n try {\n unlinkSync(tmp);\n } catch {\n // Best effort, the staged file may not exist (open could have\n // failed before the inode was created).\n }\n throw err;\n }\n}\n\n/**\n * Write `content` to `path` atomically. The body is staged into a\n * sibling `<path>.tmp.<pid>.<random>` file (same directory so the\n * rename never crosses filesystems) and `renameSync`'d into place,\n * POSIX guarantees rename is atomic on the same fs, so a crash\n * mid-write leaves the destination either at its prior content or\n * at the new content, never half-written.\n *\n * The pre-rename stage is owner-only (`mode: 0o600`, audit M1) and\n * opened with `O_EXCL | O_NOFOLLOW` (audit M1) so a pre-planted\n * symlink at the predicted temp path cannot redirect the write.\n * Settings files (`settings.json`, `settings.local.json`) carry\n * privacy-sensitive paths from `scan.extraFolders` / `referencePaths`\n * and the per-plugin config; on multi-user hosts the default umask\n * would leave them world-readable. `db restore` already uses 0o600\n * for the same reason. The mode is set on the temp file and survives\n * the rename (POSIX rename preserves the inode + its mode).\n */\nexport function writeJsonAtomic(path: string, content: Record<string, unknown>): void {\n mkdirSync(dirname(path), { recursive: true });\n writeFileAtomicExclusive(path, JSON.stringify(content, null, 2) + '\\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.extraFolders`: string[] of additional directories to scan\n * as nodes (the only way to extend the scan beyond the project).\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, removing paths, are\n * not gated).\n */\nexport const PRIVACY_SENSITIVE_KEYS: ReadonlySet<string> = new Set<string>([\n 'scan.extraFolders',\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 (adding paths to `scan.extraFolders` /\n * `scan.referencePaths` that resolve outside `cwd`). Drives the\n * `--yes` requirement on `sm config set`.\n *\n * Writes that NARROW the surface (removing paths) return `false` so\n * the user can revert the 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 \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 // 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 // Read uses `scope: 'project'` because these keys live in the\n // 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.extraFolders' || 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 { FORBIDDEN_KEYS } from '../util/strip-prototype-pollution.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 when entries point outside the project**\n * (per `project-config.schema.json` §scan.extraFolders). 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. This is the only mechanism to\n * extend the scan beyond the project root: there is no implicit HOME\n * walk and Providers cannot opt their own directory in.\n */\n extraFolders: 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.extraFolders',\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 // Strip `PROJECT_LOCAL_ONLY_KEYS` from every layer EXCEPT\n // `project-local`, that is the only legitimate home for them.\n // See `stripProjectLocalOnlyKeys` for the security rationale.\n if (layer !== 'project-local') {\n stripProjectLocalOnlyKeys(cleaned, layer, 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 stripProjectLocalOnlyKeys(cleaned, '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// `FORBIDDEN_KEYS` is the shared closed set from `kernel/util/strip-prototype-pollution.ts`;\n// this module consults it inside the merge primitive (skip-on-key) and inside\n// `containsForbidden` to also reject pollution-via-AJV-instancePath.\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). Invoked for every layer except\n * `project-local` (the only legitimate home for these keys).\n *\n * Why every non-project-local layer: the spec analyzer says\n * `allowEditSmFiles`, `scan.extraFolders`, and `scan.referencePaths`\n * are per-checkout, a \"yes\" in project A must not extend to project\n * B, and privacy-sensitive paths must not travel via the repo. The\n * original strip only covered the `project` layer (the committed\n * file), so a value in `~/.skill-map/settings.json` (the `user`\n * layer) would silently leak across every project, for\n * `allowEditSmFiles` that translates to \"consent gate bypassed\n * everywhere without a prompt.\" The strip now also covers `user`,\n * `user-local`, and `override`.\n */\nfunction stripProjectLocalOnlyKeys(\n cloned: Record<string, unknown>,\n layer: TConfigLayer,\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,\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// Recursive descent over the layered config, recording the source\n// layer of each leaf into a flat map. Primitive / array / object /\n// null branches are the type discriminator. Per `context/lint.md`\n// category 7 (recursive type-discriminator walkers).\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 * 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 * Node-construction helpers: hash a body, canonicalise frontmatter /\n * sidecar annotations, resolve the sidecar overlay for a given relative\n * path, and produce a fresh `Node` (validating its frontmatter on the\n * way out). Also hosts `mergeNodeWithEnrichments` + `IPersistedEnrichment`\n * the read-time merge of author frontmatter with the A.8 enrichment\n * layer.\n */\n\nimport { createHash } from 'node:crypto';\nimport { existsSync } 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';\nimport yaml from 'js-yaml';\n\nimport type { IProvider, IRawNode } from '../extensions/index.js';\nimport type { IProviderFrontmatterValidator } from '../adapters/schema-validators.js';\nimport {\n computeDriftStatus,\n readSidecarFor,\n} from '../sidecar/index.js';\nimport type { Issue, Node, TripleSplit } from '../types.js';\nimport { stripPrototypePollution } from '../util/strip-prototype-pollution.js';\nimport { detectMalformedFrontmatter, validateFrontmatter } from './frontmatter.js';\n\nexport interface 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\nexport function 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\nexport function 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\nexport function 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 */\nexport function 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 */\nexport function 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\nexport function 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\n// `pickMetadata` / `pickString` / `pickStability` are retained for\n// potential future re-use by extractors that need to project legacy\n// `metadata.*` fields out of frontmatter. They are not consumed\n// anywhere in source today; preserved alongside `buildNode` because\n// that is the historical neighbourhood.\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\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 */\nexport function 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 // Audit L1: parser-emitted diagnostics (e.g. malformed YAML) surface\n // here as warn-level kernel issues. Strict mode lifts them to error\n // alongside the existing schema-validation / malformed-fence paths.\n if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {\n for (const pi of opts.raw.parseIssues) {\n frontmatterIssues.push({\n analyzerId: pi.code,\n severity: opts.strict ? 'error' : 'warn',\n nodeIds: [opts.raw.path],\n message: pi.message,\n });\n }\n }\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/**\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\n/**\n * Copy every own enumerable property of `source` onto `target`, with a\n * deep prototype-pollution strip (audit M2). The shallow filter we used\n * to host inline would skip a root-level `__proto__` key, but\n * `meta: { __proto__: { polluted: true } }` survived because the nested\n * `__proto__` still landed on `target.meta`. Routing `source` through\n * `stripPrototypePollution` first removes the forbidden names at every\n * depth and returns a fresh own-property-clean object, so the subsequent\n * own-key copy is safe.\n *\n * The deep clone runs once per source per merge, the cost is O(size of\n * source) but bounded by the AJV-validated frontmatter / enrichment row\n * shapes, which the spec already caps below the K-key range.\n */\nfunction assignSafe(target: Record<string, unknown>, source: Record<string, unknown>): void {\n const safe = stripPrototypePollution(source);\n for (const [k, v] of Object.entries(safe)) {\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 * Frontmatter validation + malformed-fence detection helpers used by\n * `node-build.ts`. Pulled out of the monolith so the per-kind AJV\n * validation pass and the malformed-fence heuristic live next to each\n * other (they form a single conceptual surface: \"did the frontmatter\n * arrive intact?\").\n */\n\nimport { ORCHESTRATOR_TEXTS } from '../i18n/orchestrator.texts.js';\nimport type { IProvider } from '../extensions/index.js';\nimport type { IProviderFrontmatterValidator } from '../adapters/schema-validators.js';\nimport { tx } from '../util/tx.js';\nimport type { Issue } from '../types.js';\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 */\nexport function 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 */\nexport function 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\nexport type 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","/**\n * Scan main loop. For each provider × raw node: hash body, classify,\n * resolve sidecar, compute the per-(node, extractor) cache decision,\n * dispatch to the full-cache-hit branch or the extract-path branch,\n * and record the resulting `scan_extractor_runs` rows.\n *\n * The cache decision lives in `./cache.js`, the per-node extractor\n * invocation in `./extractors.js`, the fresh-node construction (plus\n * frontmatter validation) in `./node-build.js`. This file orchestrates\n * them.\n */\n\n// js-tiktoken ships CJS subpaths without explicit `.cjs` in the import\n// specifier; type-only imports survive lint without the disable that\n// the value-import sites need.\nimport type { Tiktoken } from 'js-tiktoken/lite';\n\nimport type { IPluginStore } from '../adapters/plugin-store.js';\nimport type { IProviderFrontmatterValidator } from '../adapters/schema-validators.js';\nimport type { IPriorExtractorRun } from '../adapters/sqlite/scan-load.js';\nimport type { IContributionRecord } from '../adapters/sqlite/contributions.js';\nimport { makeEvent } from '../extensions/hook-dispatcher.js';\nimport {\n resolveProviderWalk,\n type IExtractor,\n type IProvider,\n type IRawNode,\n} from '../extensions/index.js';\nimport type {\n ProgressEmitterPort,\n} from '../ports/progress-emitter.js';\nimport { qualifiedExtensionId } from '../registry.js';\nimport type { IIgnoreFilter } from '../scan/ignore.js';\nimport {\n discoverOrphanSidecars,\n type IOrphanSidecar,\n} from '../sidecar/index.js';\nimport type { Issue, Link, Node, ScanResult } from '../types.js';\nimport {\n cloneNodeAndReshapeLinks,\n computeCacheDecision,\n reusePriorNode,\n type IPriorIndex,\n} from './cache.js';\nimport {\n runExtractorsForNode,\n type IEnrichmentRecord,\n type IExtractorRunRecord,\n} from './extractors.js';\nimport {\n buildFreshNodeAndValidateFrontmatter,\n canonicalFrontmatter,\n canonicalSidecarAnnotations,\n resolveSidecarOverlay,\n sha256,\n} from './node-build.js';\n\nexport interface 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\nexport interface 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 * Per-scan accumulators bundled into one object so the per-node\n * helpers (`processRawNode`, `applyFullCacheHit`, `applyExtractPath`)\n * mutate a single reference instead of taking 10+ buffer parameters.\n *\n * Every field is documented at the field site below; the docs that\n * used to live inline on each `const` declaration moved here.\n */\ninterface IWalkAccumulators {\n nodes: Node[];\n internalLinks: Link[];\n externalLinks: Link[];\n cachedPaths: Set<string>;\n frontmatterIssues: Issue[];\n /**\n * A.8 enrichment buffer. `ctx.enrichNode(partial)` calls fold into\n * a per-Extractor entry keyed by `(nodePath, qualifiedExtractorId)`\n * so the persistence layer can upsert exactly one row per pair into\n * `node_enrichments`. Attribution survives across scans, which\n * 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\n * fold into the same record's `value` (last-write-wins per field).\n */\n enrichmentBuffer: Map<string, IEnrichmentRecord>;\n /**\n * Phase 3 / View contributions, flat buffer (no per-node dedup\n * because the qualified id\n * `<pluginId>/<extensionId>/<contributionId>` is structurally\n * unique within a single scan).\n */\n contributionsBuffer: IContributionRecord[];\n /**\n * Phase 3 / View contributions, accumulator of (plugin, extension,\n * node) tuples where extract() actually RAN this scan (cache\n * miss). Cached extractors don't push here, their prior\n * `scan_contributions` rows must be preserved. Format:\n * `<pluginId>/<extensionId>/<nodePath>`.\n */\n freshlyRunTuples: Set<string>;\n /**\n * Spec § A.9, accumulator for `scan_extractor_runs`. One row per\n * (nodePath, qualifiedExtractorId) pair the orchestrator decided\n * \"this extractor is current for this body\". Includes both\n * freshly-run pairs and pairs whose prior run was reused intact via\n * the cache.\n */\n extractorRuns: IExtractorRunRecord[];\n /**\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\n * root keys without re-reading the `.sm` file from disk.\n */\n sidecarRoots: Map<string, Record<string, unknown>>;\n}\n\n/**\n * Per-scan immutable context derived from `IWalkAndExtractOptions`.\n * Build once at the top of `walkAndExtract`, pass to helpers by\n * reference. Mirror of the function-level destructure that the\n * pre-refactor monolith opened with.\n */\ninterface IWalkContext {\n opts: IWalkAndExtractOptions;\n priorNodesByPath: Map<string, Node>;\n priorLinksByOriginating: Map<string, Link[]>;\n priorFrontmatterIssuesByNode: Map<string, Issue[]>;\n /**\n * Short→qualified id map built 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\n * cache bookkeeping. Multiple plugins can in theory expose\n * extractors with the same short id; we keep all qualifieds per\n * short id so the partial-cache filter recognises any of them as\n * \"still cached\".\n */\n shortIdToQualified: Map<string, string[]>;\n}\n\n/**\n * Main scan loop. For each provider × raw node: hash, classify,\n * decide cache (full / partial / none), reuse or build, run\n * extractors, record runs. Helpers\n * (`computeCacheDecision`, `cloneNodeAndReshapeLinks`,\n * `reusePriorNode`, `buildFreshNodeAndValidateFrontmatter`,\n * `runExtractorsForNode`) encapsulate the heavy lift; this function\n * is the dispatch glue.\n *\n * Per-iteration work split into `processRawNode` so the loop body\n * stays linear and the lint cap is satisfied without an\n * `eslint-disable`.\n */\nexport async function walkAndExtract(opts: IWalkAndExtractOptions): Promise<IWalkAndExtractResult> {\n const accum = createWalkAccumulators();\n const wctx = buildWalkContext(opts);\n\n // Path-dedup across the multi-provider walk. Spec § Provider\n // dispatch (architecture.md): every Provider walks the full root,\n // but each file is offered to at most ONE Provider's `classify`.\n // The first Provider in iteration order whose `classify` returns\n // non-null 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 const claimedPaths = new Set<string>();\n const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};\n let filesWalked = 0;\n let index = 0;\n\n for (const provider of opts.providers) {\n for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {\n filesWalked += 1;\n if (claimedPaths.has(raw.path)) continue;\n const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);\n if (advanced) index += 1;\n }\n }\n\n // Spec § 9.6.2, orphan sidecar sweep. Walks the same roots\n // looking for `*.sm` whose sibling `*.md` is missing. The list\n // flows through to the rule pass; `annotation-orphan` emits one\n // warning per entry.\n const orphanSidecars = discoverOrphanSidecars(opts.roots);\n\n return {\n nodes: accum.nodes,\n internalLinks: accum.internalLinks,\n externalLinks: accum.externalLinks,\n cachedPaths: accum.cachedPaths,\n frontmatterIssues: accum.frontmatterIssues,\n filesWalked,\n enrichments: [...accum.enrichmentBuffer.values()],\n extractorRuns: accum.extractorRuns,\n contributions: accum.contributionsBuffer,\n freshlyRunTuples: accum.freshlyRunTuples,\n orphanSidecars,\n sidecarRoots: accum.sidecarRoots,\n };\n}\n\nfunction createWalkAccumulators(): IWalkAccumulators {\n return {\n nodes: [],\n internalLinks: [],\n externalLinks: [],\n cachedPaths: new Set(),\n frontmatterIssues: [],\n enrichmentBuffer: new Map(),\n contributionsBuffer: [],\n freshlyRunTuples: new Set(),\n extractorRuns: [],\n sidecarRoots: new Map(),\n };\n}\n\nfunction buildWalkContext(opts: IWalkAndExtractOptions): IWalkContext {\n const { priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode } = opts.priorIndex;\n const shortIdToQualified = new Map<string, string[]>();\n for (const ex of opts.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 return { opts, priorNodesByPath, priorLinksByOriginating, priorFrontmatterIssuesByNode, shortIdToQualified };\n}\n\n/**\n * Process one raw-node yielded by a Provider's `walk()`. Returns\n * `true` if the node was claimed (classify produced a kind), `false`\n * if disclaimed (another Provider may claim it on its own pass).\n *\n * Folds the per-node pipeline into one function so `walkAndExtract`'s\n * outer loop body stays a 2-liner:\n *\n * - hash body / frontmatter\n * - classify (early-return on `null`, disclaimed)\n * - resolve sidecar + hash\n * - compute cache decision\n * - dispatch full-cache-hit vs partial/fresh branches\n */\nasync function processRawNode(\n raw: IRawNode,\n provider: IProvider,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n claimedPaths: Set<string>,\n nextIndex: number,\n): Promise<boolean> {\n const bodyHash = sha256(raw.body);\n // Canonical-form rationale, hash a CANONICAL form of the\n // frontmatter so a YAML formatter pass (re-indent, sort keys,\n // normalise trailing newline, swap single↔double quotes) doesn't\n // break the medium-confidence rename heuristic.\n const frontmatterHash = sha256(canonicalFrontmatter(raw.frontmatter, raw.frontmatterRaw));\n\n const kind = provider.classify(raw.path, raw.frontmatter);\n if (kind === null) {\n // Provider disclaimed the file, another Provider may claim it\n // on its own walk pass, or the file is outside every active\n // Provider's territory.\n return false;\n }\n claimedPaths.add(raw.path);\n\n const priorNode = wctx.priorNodesByPath.get(raw.path);\n // Cache reuse is gated on the explicit `enableCache` option. The\n // presence of a `prior` alone is no longer enough, a plain\n // `sm scan` always re-walks deterministically; only\n // `sm scan --changed` flips `enableCache` on. The rename heuristic\n // uses `prior` independently of `enableCache`.\n const nodeHashCacheEligible =\n wctx.opts.enableCache &&\n wctx.opts.prior !== null &&\n priorNode !== undefined &&\n priorNode.bodyHash === bodyHash &&\n priorNode.frontmatterHash === frontmatterHash;\n\n // Resolve the sidecar overlay BEFORE the cache decision so we can\n // 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 (e.g. `core/annotations`). Analyzers that read the\n // sidecar (`core/stability`, `core/annotation-stale`, …) re-run\n // every pass regardless, but the hash still matters for the\n // extract-phase cache.\n const sidecarResolution = resolveSidecarOverlay(\n raw.path, raw.path, wctx.opts.roots, bodyHash, frontmatterHash,\n );\n const sidecarAnnotationsHash = sha256(\n canonicalSidecarAnnotations(sidecarResolution.overlay.annotations),\n );\n\n const cacheDecision = computeCacheDecision({\n extractors: wctx.opts.extractors,\n kind,\n nodePath: raw.path,\n bodyHash,\n sidecarAnnotationsHash,\n nodeHashCacheEligible,\n priorExtractorRuns: wctx.opts.priorExtractorRuns,\n });\n\n const ctx: IProcessNodeContext = {\n raw, provider, kind, bodyHash, frontmatterHash, sidecarResolution,\n sidecarAnnotationsHash, nodeHashCacheEligible, cacheDecision, priorNode,\n index: nextIndex,\n };\n\n if (cacheDecision.fullCacheHit && priorNode) {\n applyFullCacheHit(ctx, wctx, accum);\n } else {\n await applyExtractPath(ctx, wctx, accum);\n }\n return true;\n}\n\n/**\n * Bag of per-iteration state shared by `applyFullCacheHit` and\n * `applyExtractPath`. Built inside `processRawNode`; never escapes\n * that scope.\n */\ninterface IProcessNodeContext {\n raw: IRawNode;\n provider: IProvider;\n kind: string;\n bodyHash: string;\n frontmatterHash: string;\n sidecarResolution: ReturnType<typeof resolveSidecarOverlay>;\n sidecarAnnotationsHash: string;\n nodeHashCacheEligible: boolean;\n cacheDecision: ReturnType<typeof computeCacheDecision>;\n priorNode: Node | undefined;\n index: number;\n}\n\n/**\n * Attach the freshly-resolved sidecar overlay to a node and surface\n * its issues + parsed root. Used by both apply paths so the apply\n * step stays uniform.\n */\nfunction attachSidecar(\n node: Node,\n resolution: ReturnType<typeof resolveSidecarOverlay>,\n sidecarRoots: Map<string, Record<string, unknown>>,\n): Issue[] {\n node.sidecar = resolution.overlay;\n if (resolution.parsedRoot !== null) {\n sidecarRoots.set(node.path, resolution.parsedRoot);\n }\n return resolution.issues.map((i) =>\n i.nodeIds.length > 0 ? i : { ...i, nodeIds: [node.path] },\n );\n}\n\n/**\n * Full-cache-hit branch: reuse the prior node + its links + its\n * frontmatter issues + its extractor runs. Sidecars are re-resolved\n * on every scan (not cached) since `.sm` lives outside the body /\n * frontmatter hash domain.\n */\nfunction applyFullCacheHit(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n): void {\n const reused = reusePriorNode({\n priorNode: ctx.priorNode!,\n bodyHash: ctx.bodyHash,\n sidecarAnnotationsHash: ctx.sidecarAnnotationsHash,\n strict: wctx.opts.strict,\n cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,\n applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,\n shortIdToQualified: wctx.shortIdToQualified,\n priorLinksByOriginating: wctx.priorLinksByOriginating,\n priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode,\n });\n const reusedSidecarIssues = attachSidecar(reused.node, ctx.sidecarResolution, accum.sidecarRoots);\n accum.nodes.push(reused.node);\n accum.cachedPaths.add(reused.node.path);\n for (const link of reused.internalLinks) accum.internalLinks.push(link);\n for (const issue of reused.frontmatterIssues) accum.frontmatterIssues.push(issue);\n for (const issue of reusedSidecarIssues) accum.frontmatterIssues.push(issue);\n for (const run of reused.extractorRuns) accum.extractorRuns.push(run);\n wctx.opts.emitter.emit(makeEvent('scan.progress', {\n index: ctx.index, path: ctx.raw.path, kind: ctx.kind, cached: true,\n }));\n}\n\n/**\n * Partial- or full-re-extract branch. Either a brand-new node, a\n * node whose body / frontmatter changed, or a node whose hashes\n * match but at least one applicable extractor lacks a matching\n * `scan_extractor_runs` row (newly registered, or its prior run was\n * against a different body hash).\n */\nasync function applyExtractPath(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n): Promise<void> {\n const node = buildOrReuseNode(ctx, wctx, accum);\n // Spec § 9.6.2, sidecar overlay applies to BOTH freshly-built and\n // partial-cache nodes. Done after the node is in `accum.nodes` so a\n // downstream consumer iterating `nodes` sees the overlay applied\n // (mutation is in-place on the same object reference).\n const sidecarIssues = attachSidecar(node, ctx.sidecarResolution, accum.sidecarRoots);\n for (const issue of sidecarIssues) accum.frontmatterIssues.push(issue);\n\n const partialCacheHit = isPartialCacheHit(ctx);\n emitExtractProgress(ctx, wctx, partialCacheHit);\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\n ? ctx.cacheDecision.missingExtractors\n : ctx.cacheDecision.applicableExtractors;\n recordFreshlyRunTuples(extractorsToRun, node.path, accum);\n\n const extractResult = await runExtractorsForNode({\n extractors: extractorsToRun,\n node,\n body: ctx.raw.body,\n frontmatter: ctx.raw.frontmatter,\n bodyHash: ctx.bodyHash,\n emitter: wctx.opts.emitter,\n ...(wctx.opts.pluginStores ? { pluginStores: wctx.opts.pluginStores } : {}),\n });\n mergeExtractResult(extractResult, accum);\n recordExtractorRuns(node.path, ctx, accum);\n}\n\nfunction emitExtractProgress(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n partialCacheHit: boolean,\n): void {\n wctx.opts.emitter.emit(makeEvent('scan.progress', {\n index: ctx.index, path: ctx.raw.path, kind: ctx.kind, cached: false,\n ...(partialCacheHit ? { partialCache: true } : {}),\n }));\n}\n\n/**\n * Phase 3, record (plugin, extension, node) tuples for every\n * extractor that actually runs against this node this scan. The\n * persist layer uses these to drop stale `scan_contributions` rows\n * for extractors that previously emitted but no longer do (e.g. body\n * change removes the trigger). NUL-separated to survive `nodePath`\n * segments with slashes.\n */\nfunction recordFreshlyRunTuples(\n extractors: readonly IExtractor[],\n nodePath: string,\n accum: IWalkAccumulators,\n): void {\n for (const ex of extractors) {\n accum.freshlyRunTuples.add(`${ex.pluginId}\\0${ex.id}\\0${nodePath}`);\n }\n}\n\n/**\n * Fold the per-node extract result into the scan-wide accumulators:\n * links (internal + external), enrichments (last-write-wins per\n * `(nodePath, extractorId)` pair), and view contributions.\n */\nfunction mergeExtractResult(\n extractResult: Awaited<ReturnType<typeof runExtractorsForNode>>,\n accum: IWalkAccumulators,\n): void {\n for (const link of extractResult.internalLinks) accum.internalLinks.push(link);\n for (const link of extractResult.externalLinks) accum.externalLinks.push(link);\n for (const enr of extractResult.enrichments) {\n accum.enrichmentBuffer.set(`${enr.nodePath}\\x00${enr.extractorId}`, enr);\n }\n for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);\n}\n\nfunction isPartialCacheHit(ctx: IProcessNodeContext): boolean {\n return (\n ctx.nodeHashCacheEligible &&\n ctx.cacheDecision.cachedQualifiedIds.size > 0 &&\n ctx.priorNode !== undefined\n );\n}\n\n/**\n * Build the node row for the extract path: clone the prior node when\n * we have a partial-cache hit (body/frontmatter unchanged + at least\n * one cached extractor), otherwise build a fresh node from the raw\n * file. NOT marking the path as `cachedPaths` because some extraction\n * is happening, the `externalRefsCount` recompute wants the node\n * re-derived from a fresh extractor pass (the missing extractor may\n * emit URLs).\n */\nfunction buildOrReuseNode(\n ctx: IProcessNodeContext,\n wctx: IWalkContext,\n accum: IWalkAccumulators,\n): Node {\n if (isPartialCacheHit(ctx) && ctx.priorNode) {\n const partial = cloneNodeAndReshapeLinks({\n priorNode: ctx.priorNode,\n strict: wctx.opts.strict,\n cachedQualifiedIds: ctx.cacheDecision.cachedQualifiedIds,\n applicableQualifiedIds: ctx.cacheDecision.applicableQualifiedIds,\n shortIdToQualified: wctx.shortIdToQualified,\n priorLinksByOriginating: wctx.priorLinksByOriginating,\n priorFrontmatterIssuesByNode: wctx.priorFrontmatterIssuesByNode,\n });\n for (const link of partial.internalLinks) accum.internalLinks.push(link);\n for (const issue of partial.frontmatterIssues) accum.frontmatterIssues.push(issue);\n accum.nodes.push(partial.node);\n return partial.node;\n }\n const fresh = buildFreshNodeAndValidateFrontmatter({\n raw: ctx.raw,\n kind: ctx.kind,\n provider: ctx.provider,\n bodyHash: ctx.bodyHash,\n frontmatterHash: ctx.frontmatterHash,\n encoder: wctx.opts.encoder,\n providerFrontmatter: wctx.opts.providerFrontmatter,\n strict: wctx.opts.strict,\n });\n accum.nodes.push(fresh.node);\n for (const issue of fresh.frontmatterIssues) accum.frontmatterIssues.push(issue);\n return fresh.node;\n}\n\n/**\n * Persist a `scan_extractor_runs` row for every applicable extractor\n * (both freshly-run AND cached ones whose contribution we reused).\n * Skipping cached entries here would let the replace-all persist\n * forget them, defeating the whole point of the partial-cache path.\n * Always populate `sidecarAnnotationsHashAtRun`; non-sidecar-readers\n * ignore it on the next decision but the column is non-null going\n * forward.\n */\nfunction recordExtractorRuns(\n nodePath: string,\n ctx: IProcessNodeContext,\n accum: IWalkAccumulators,\n): void {\n const ranAt = Date.now();\n for (const ex of ctx.cacheDecision.applicableExtractors) {\n accum.extractorRuns.push({\n nodePath,\n extractorId: qualifiedExtensionId(ex.pluginId, ex.id),\n bodyHashAtRun: ctx.bodyHash,\n ranAt,\n sidecarAnnotationsHashAtRun: ctx.sidecarAnnotationsHash,\n });\n }\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 /**\n * Maximum directory traversal depth. `undefined` (default) walks the\n * tree recursively without bound; `0` limits the watch to the\n * literal `roots` entries (no descent), which is the right setting\n * when watching a directory only to catch changes to specific\n * top-level files (see `subscribeMeta` in `core/watcher/runtime.ts`).\n * Forwarded verbatim to chokidar's `depth` option.\n */\n depth?: number;\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 ...(opts.depth !== undefined ? { depth: opts.depth } : {}),\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","Tiktoken","readFileSync","resolve","resolve","Ajv2020","require","resolve","readFileSync","readFileSync","resolve","createRequire","Ajv2020","Ajv2020","resolve","readFileSync","require","createRequire","byPath","byPath","join","relative","existsSync","readFileSync","dirname","resolve","dirname","resolve","existsSync","readFileSync","relative","join","walk","existsSync","readFileSync","dirname","resolve","createRequire","Ajv2020","yaml","existsSync","readFileSync","yaml","Ajv2020","resolveSpecRoot","resolve","resolveSpecRoot","require","createRequire","dirname","existsSync","readdirSync","join","relative","sep","existsSync","readFileSync","dirname","resolve","createRequire","Ajv2020","yaml","existsSync","readFileSync","dirname","isAbsolute","resolve","existsSync","readFileSync","join","join","resolve","existsSync","isAbsolute","yaml","yaml","isAbsolute","existsSync","Tiktoken","existsSync","statSync","resolve","relative","sep","relativePathFromRoots"]}