@skill-map/cli 0.22.0 → 0.23.1

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Contract runner executes the conformance cases shipped with
2
+ * Contract runner, executes the conformance cases shipped with
3
3
  * `@skill-map/spec` against an installed binary and emits a pass/fail result
4
4
  * per case.
5
5
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../conformance/index.ts","../../kernel/util/format-error.ts","../../kernel/util/skill-map-paths.ts","../../core/paths/db-path.ts","../../kernel/util/tx.ts","../../conformance/i18n/runner.texts.ts"],"sourcesContent":["/**\n * Contract runner — executes the conformance cases shipped with\n * `@skill-map/spec` against an installed binary and emits a pass/fail result\n * per case.\n *\n * Implements the six assertion types from `spec/schemas/conformance-case.schema.json`.\n * Provisions a clean tmp scope per case, optionally pre-populated with the\n * referenced fixture corpus.\n *\n * Step 0b scope: single-case dispatch. Suite-level runner + reporter land\n * alongside Step 2 extensions.\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { cpSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\n\nimport { formatErrorMessage } from '../kernel/util/format-error.js';\nimport { KERNEL_SKILL_MAP_DIR } from '../kernel/util/skill-map-paths.js';\nimport { tx } from '../kernel/util/tx.js';\nimport { CONFORMANCE_RUNNER_TEXTS } from './i18n/runner.texts.js';\n\nexport type IAssertionResult =\n | { ok: true; type: string }\n | { ok: false; type: string; reason: string };\n\nexport interface IRunCaseResult {\n caseId: string;\n passed: boolean;\n exitCode: number;\n stdout: string;\n stderr: string;\n assertions: IAssertionResult[];\n}\n\nexport interface IRunCaseOptions {\n /** Absolute path to the binary wrapper (e.g. `bin/sm.js`). */\n binary: string;\n /** Absolute path to the `@skill-map/spec` root. */\n specRoot: string;\n /** Absolute path to the case JSON under `<conformance-root>/cases/`. */\n casePath: string;\n /**\n * Absolute path to the `<conformance-root>/fixtures/` directory backing\n * this case (or the parent conformance suite).\n *\n * Phase 5 / A.13 introduced per-Provider conformance directories that\n * live outside the spec tree (Claude-specific cases moved to\n * `src/extensions/providers/claude/conformance/`). Cases reference\n * fixtures by directory name; the runner resolves them under\n * `fixturesRoot` so the spec-agnostic kernel-empty-boot case and the\n * Claude `basic-scan` / `rename-high` / `orphan-detection` cases can\n * coexist without colliding fixture namespaces. Defaults to\n * `<specRoot>/conformance/fixtures` for the legacy spec layout.\n */\n fixturesRoot?: string;\n /** Extra env vars passed to the child. */\n env?: NodeJS.ProcessEnv;\n}\n\ninterface IConformanceCase {\n id: string;\n description: string;\n fixture?: string;\n setup?: {\n disableAllProviders?: boolean;\n disableAllExtractors?: boolean;\n disableAllAnalyzers?: boolean;\n priorScans?: Array<{ fixture: string; flags?: string[] }>;\n };\n invoke: {\n verb: string;\n sub?: string;\n args?: string[];\n flags?: string[];\n };\n assertions: IAssertion[];\n}\n\n/**\n * Build the env-var bag a case's `setup.disableAll*` toggles inject into\n * every child invocation (priorScans + the main `invoke`). The CLI's scan\n * composer (`composeScanExtensions`) reads these vars and drops every\n * extension of the matching kind from the in-scan pipeline.\n */\nfunction disableEnv(setup: IConformanceCase['setup']): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n if (setup?.disableAllProviders) env['SKILL_MAP_DISABLE_ALL_PROVIDERS'] = '1';\n if (setup?.disableAllExtractors) env['SKILL_MAP_DISABLE_ALL_EXTRACTORS'] = '1';\n if (setup?.disableAllAnalyzers) env['SKILL_MAP_DISABLE_ALL_ANALYZERS'] = '1';\n return env;\n}\n\nexport type IAssertion =\n | { type: 'exit-code'; value: number }\n | {\n type: 'json-path';\n path: string;\n equals?: unknown;\n greaterThan?: number;\n lessThan?: number;\n matches?: string;\n }\n | { type: 'file-exists'; path: string }\n | { type: 'file-contains-verbatim'; path: string; fixture: string }\n | { type: 'file-matches-schema'; path: string; schema: string }\n | { type: 'stderr-matches'; pattern: string };\n\n// eslint-disable-next-line complexity\nexport function runConformanceCase(options: IRunCaseOptions): IRunCaseResult {\n const raw = readFileSync(options.casePath, 'utf8');\n const c: IConformanceCase = JSON.parse(raw);\n\n const fixturesRoot = options.fixturesRoot ?? join(options.specRoot, 'conformance', 'fixtures');\n\n // Defence in depth (audit L5): the conformance case id is JSON-author-\n // controlled. Replace anything that isn't a safe filesystem char and\n // cap the length so an over-long id (or one carrying path separators\n // / control bytes) can't escape `tmpdir()` or grow the prefix beyond\n // a reasonable bound.\n const safeId = c.id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 32);\n const scope = mkdtempSync(join(tmpdir(), `sm-conformance-${safeId}-`));\n const setupEnv = disableEnv(c.setup);\n try {\n // 1. Replay every `setup.priorScans` step into the scope DB before\n // the main invoke runs. Returns the failure result early if any\n // step exits non-zero.\n const priorFailure = runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv);\n if (priorFailure) return priorFailure;\n\n // 2. Copy the main fixture (replacing prior fixture content but\n // preserving the DB), then run the case's `invoke`.\n if (c.fixture) {\n replaceFixture(scope, fixturesRoot, c.fixture);\n }\n\n const argv = [c.invoke.verb];\n if (c.invoke.sub) argv.push(c.invoke.sub);\n if (c.invoke.args) argv.push(...c.invoke.args);\n if (c.invoke.flags) argv.push(...c.invoke.flags);\n\n const child = spawnSync(process.execPath, [options.binary, ...argv], {\n cwd: scope,\n env: { ...process.env, ...options.env, ...setupEnv },\n encoding: 'utf8',\n });\n\n const stdout = child.stdout ?? '';\n const stderr = child.stderr ?? '';\n const exitCode = child.status ?? 0;\n\n const assertions = c.assertions.map((a) =>\n evaluateAssertion(a, {\n exitCode,\n stdout,\n stderr,\n scope,\n specRoot: options.specRoot,\n fixturesRoot,\n }),\n );\n const passed = assertions.every((a) => a.ok);\n\n return { caseId: c.id, passed, exitCode, stdout, stderr, assertions };\n } finally {\n rmSync(scope, { recursive: true, force: true });\n }\n}\n\n/**\n * Phase 1 of `runConformanceCase` — replay every `setup.priorScans`\n * step in order. Each step replaces every non-`.skill-map/` directory\n * with the named fixture, then runs `sm scan` so the snapshot persists\n * into the scope DB. The scope DB survives across steps (we never\n * delete `.skill-map/`).\n *\n * Returns `null` on success (caller continues) or a `IRunCaseResult`\n * with a single `priorScan` failure assertion (caller returns it\n * unchanged).\n */\n// Per-step replay: replace fixture, spawn `sm scan`, check exit. The\n// failure-result construction is verbose because it carries every\n// stream the caller reports back.\n// eslint-disable-next-line complexity\nfunction runPriorScansSetup(\n c: IConformanceCase,\n options: IRunCaseOptions,\n scope: string,\n fixturesRoot: string,\n setupEnv: NodeJS.ProcessEnv,\n): IRunCaseResult | null {\n for (const step of c.setup?.priorScans ?? []) {\n replaceFixture(scope, fixturesRoot, step.fixture);\n const stepArgv = ['scan', ...(step.flags ?? [])];\n const stepChild = spawnSync(process.execPath, [options.binary, ...stepArgv], {\n cwd: scope,\n env: { ...process.env, ...options.env, ...setupEnv },\n encoding: 'utf8',\n });\n if ((stepChild.status ?? 0) !== 0) {\n return {\n caseId: c.id,\n passed: false,\n exitCode: stepChild.status ?? 0,\n stdout: stepChild.stdout ?? '',\n stderr: stepChild.stderr ?? '',\n assertions: [\n {\n ok: false,\n type: 'priorScan',\n reason: tx(CONFORMANCE_RUNNER_TEXTS.priorScanFailed, {\n fixture: step.fixture,\n exit: stepChild.status ?? 0,\n stderr: stepChild.stderr ?? '',\n }),\n },\n ],\n };\n }\n }\n return null;\n}\n\n/**\n * Replace every top-level entry in `scope` EXCEPT `.skill-map/` (which\n * holds the kernel DB and persists across staging steps), then copy\n * the fixture's contents on top. Used by `priorScans` and the main\n * fixture phase to swap Provider content while keeping the DB stable.\n *\n * `fixturesRoot` is the absolute path to the `fixtures/` directory of\n * the conformance suite hosting the case (spec-owned for kernel cases,\n * Provider-owned for Provider cases — see `IRunCaseOptions.fixturesRoot`).\n */\nfunction replaceFixture(scope: string, fixturesRoot: string, fixture: string): void {\n assertContained(fixturesRoot, fixture, 'fixture');\n for (const entry of readdirSync(scope)) {\n if (entry === KERNEL_SKILL_MAP_DIR) continue;\n rmSync(join(scope, entry), { recursive: true, force: true });\n }\n const src = join(fixturesRoot, fixture);\n cpSync(src, scope, { recursive: true });\n}\n\n/**\n * Reject case-supplied path strings that escape the directory tree they\n * are anchored to. A hostile case JSON would otherwise be able to copy\n * arbitrary filesystem content into the tmp scope (`fixture: \"../..\"`)\n * or read files outside the conformance sandbox via `file-exists` /\n * `file-contains-verbatim` assertions.\n */\nfunction assertContained(root: string, rel: string, label: string): void {\n if (isAbsolute(rel)) {\n throw new Error(\n tx(CONFORMANCE_RUNNER_TEXTS.pathMustBeRelative, { label, path: rel, anchor: root }),\n );\n }\n const abs = resolve(root, rel);\n const r = relative(root, abs);\n if (r.startsWith('..') || isAbsolute(r)) {\n throw new Error(\n tx(CONFORMANCE_RUNNER_TEXTS.pathEscapesAnchor, { label, path: rel, anchor: root }),\n );\n }\n}\n\ninterface IAssertionContext {\n exitCode: number;\n stdout: string;\n stderr: string;\n scope: string;\n specRoot: string;\n fixturesRoot: string;\n}\n\n// Switch over assertion types (`exit-code` / `stdout-matches` /\n// `file-exists` / `file-contains-verbatim` / `file-matches-schema` /\n// `stderr-matches` / `json-path`) with one branch per type. Splitting\n// per type would scatter the discriminated-union dispatch.\n// eslint-disable-next-line complexity\nfunction evaluateAssertion(a: IAssertion, ctx: IAssertionContext): IAssertionResult {\n switch (a.type) {\n case 'exit-code':\n return ctx.exitCode === a.value\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.expectedExitCode, {\n expected: a.value,\n actual: ctx.exitCode,\n }),\n };\n case 'json-path':\n return evaluateJsonPath(a, ctx);\n case 'file-exists': {\n try {\n assertContained(ctx.scope, a.path, 'file-exists');\n } catch (err) {\n return { ok: false, type: a.type, reason: formatErrorMessage(err) };\n }\n const abs = resolve(ctx.scope, a.path);\n return existsSync(abs)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path }),\n };\n }\n case 'file-contains-verbatim': {\n try {\n assertContained(ctx.fixturesRoot, a.fixture, 'file-contains-verbatim/fixture');\n assertContained(ctx.scope, a.path, 'file-contains-verbatim/path');\n } catch (err) {\n return { ok: false, type: a.type, reason: formatErrorMessage(err) };\n }\n const fixturePath = join(ctx.fixturesRoot, a.fixture);\n const targetPath = resolve(ctx.scope, a.path);\n if (!existsSync(targetPath)) {\n return {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.targetNotFound, { path: a.path }),\n };\n }\n const needle = readFileSync(fixturePath);\n const haystack = readFileSync(targetPath);\n return haystack.includes(needle)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.targetMissingFixture, { fixture: a.fixture }),\n };\n }\n case 'file-matches-schema':\n return {\n ok: false,\n type: a.type,\n reason: CONFORMANCE_RUNNER_TEXTS.fileMatchesSchemaUnimplemented,\n };\n case 'stderr-matches': {\n const re = new RegExp(a.pattern);\n return re.test(ctx.stderr)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.stderrDidNotMatch, { pattern: a.pattern }),\n };\n }\n }\n}\n\n/**\n * Minimal JSONPath evaluator — supports only the subset used by the stub\n * conformance suite: `$.foo`, `$.foo.bar`, `$.foo.length`, `$[0]`.\n * The full RFC 9535 implementation lands with Step 2.\n */\nfunction evaluateJsonPath(\n a: Extract<IAssertion, { type: 'json-path' }>,\n ctx: IAssertionContext,\n): IAssertionResult {\n let doc: unknown;\n try {\n doc = JSON.parse(ctx.stdout);\n } catch (err) {\n return {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.stdoutNotJson, { message: formatErrorMessage(err) }),\n };\n }\n\n const segments = parsePath(a.path);\n if (!segments) {\n return {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.unsupportedJsonPath, { path: a.path }),\n };\n }\n\n const walked = traverseJsonPath(doc, segments, a.path);\n if (!walked.ok) return { ok: false, type: a.type, reason: walked.reason };\n\n return applyJsonPathComparator(a, walked.value);\n}\n\n/**\n * Walk a parsed JSONPath segment list against a JSON document. Returns\n * the resolved value or a structured failure (caller maps to\n * `IAssertionResult`). Pure — no IO, no shared state.\n */\nfunction traverseJsonPath(\n doc: unknown,\n segments: Array<string | number>,\n path: string,\n): { ok: true; value: unknown } | { ok: false; reason: string } {\n let current: unknown = doc;\n for (const seg of segments) {\n if (typeof seg === 'number') {\n if (!Array.isArray(current)) {\n return { ok: false, reason: tx(CONFORMANCE_RUNNER_TEXTS.expectedArrayAtPath, { path }) };\n }\n current = current[seg];\n } else if (seg === 'length' && Array.isArray(current)) {\n current = current.length;\n } else if (typeof current === 'object' && current !== null) {\n current = (current as Record<string, unknown>)[seg];\n } else {\n return {\n ok: false,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.cannotTraverseSegment, {\n type: typeof current,\n segment: String(seg),\n }),\n };\n }\n }\n return { ok: true, value: current };\n}\n\n/**\n * Apply the comparator clause (`equals` / `greaterThan` / `lessThan` /\n * `matches`) of a `json-path` assertion against the value resolved at\n * the requested path. Returns the final `IAssertionResult` directly.\n *\n * Complexity from the four parallel comparator branches; splitting into\n * one helper per comparator would be ceremony.\n */\n// eslint-disable-next-line complexity\nfunction applyJsonPathComparator(\n a: Extract<IAssertion, { type: 'json-path' }>,\n current: unknown,\n): IAssertionResult {\n if ('equals' in a && a.equals !== undefined) {\n return deepEqual(current, a.equals)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathEqualsMismatch, {\n path: a.path,\n actual: JSON.stringify(current),\n expected: JSON.stringify(a.equals),\n }),\n };\n }\n if ('greaterThan' in a && typeof a.greaterThan === 'number') {\n return typeof current === 'number' && current > a.greaterThan\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathNotGreaterThan, {\n path: a.path,\n value: a.greaterThan,\n }),\n };\n }\n if ('lessThan' in a && typeof a.lessThan === 'number') {\n return typeof current === 'number' && current < a.lessThan\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathNotLessThan, {\n path: a.path,\n value: a.lessThan,\n }),\n };\n }\n if ('matches' in a && typeof a.matches === 'string') {\n const re = new RegExp(a.matches);\n return typeof current === 'string' && re.test(current)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathDidNotMatch, {\n path: a.path,\n pattern: a.matches,\n }),\n };\n }\n return { ok: false, type: a.type, reason: CONFORMANCE_RUNNER_TEXTS.jsonPathNoComparator };\n}\n\nfunction parsePath(path: string): Array<string | number> | null {\n if (!path.startsWith('$')) return null;\n const tail = path.slice(1);\n const segments: Array<string | number> = [];\n const re = /\\.([a-zA-Z_][a-zA-Z0-9_-]*)|\\[(\\d+)\\]/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = re.exec(tail)) !== null) {\n if (match.index !== lastIndex) return null;\n if (match[1] !== undefined) segments.push(match[1]);\n else if (match[2] !== undefined) segments.push(Number.parseInt(match[2], 10));\n lastIndex = re.lastIndex;\n }\n if (lastIndex !== tail.length) return null;\n return segments;\n}\n\n// eslint-disable-next-line complexity\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a && b && typeof a === 'object' && typeof b === 'object') {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const ak = Object.keys(a as object);\n const bk = Object.keys(b as object);\n if (ak.length !== bk.length) return false;\n for (const k of ak) {\n if (\n !deepEqual(\n (a as Record<string, unknown>)[k],\n (b as Record<string, unknown>)[k],\n )\n )\n {return false;}\n }\n return true;\n }\n return false;\n}\n\n/** Verifies the spec root looks sane (contains `index.json`). */\nexport function assertSpecRoot(specRoot: string): void {\n const indexPath = join(specRoot, 'index.json');\n if (!existsSync(indexPath) || !statSync(indexPath).isFile()) {\n throw new Error(tx(CONFORMANCE_RUNNER_TEXTS.specRootMissingIndex, { specRoot }));\n }\n}\n","/**\n * Kernel-accessible counterpart of `cli/util/error-reporter.ts`'s\n * `formatErrorMessage`. The CLI helper now re-exports from here so the\n * historic CLI import path keeps working while kernel + BFF callers can\n * consume it directly without crossing the layering boundary.\n *\n * Kept deliberately tiny — same shape as the original CLI helper. The\n * surface grows (e.g. a `--verbose` stack mode, JSON envelope) only\n * when a concrete need surfaces.\n */\n\n/**\n * Compact error → string conversion.\n *\n * - `Error` → `err.message` verbatim. Callers wrap with their own\n * verb-specific context line via `tx(*_TEXTS.x, { message })` so\n * error catalogues stay greppable.\n * - Anything else → `String(value)`. Catches the rare throw-a-string\n * / throw-an-object path without exploding on `null`\n * (`String(null)` = `'null'`).\n */\nexport function formatErrorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","/**\n * Kernel-side helpers that compose the layered-config file paths from\n * the canonical `SKILL_MAP_DIR` literal.\n *\n * `SKILL_MAP_DIR` is exported once from `core/paths/db-path.ts` and\n * re-exported here as `KERNEL_SKILL_MAP_DIR` so kernel-side callers\n * keep their historic name without the literal living in two files\n * (audit m3 — one literal home, no `grep \"'\\.skill-map'\"` sweep\n * invariant to maintain across kernel + CLI).\n */\n\nimport { join } from 'node:path';\n\nimport { SKILL_MAP_DIR } from '../../core/paths/db-path.js';\n\n/**\n * Per-scope directory the kernel + CLI both store state under (DB file,\n * settings, plugins, etc.). Re-exported from `core/paths/db-path.ts`\n * — the single canonical source for the literal.\n */\nexport const KERNEL_SKILL_MAP_DIR = SKILL_MAP_DIR;\n\nconst SETTINGS_FILENAME = 'settings.json';\nconst LOCAL_SETTINGS_FILENAME = 'settings.local.json';\n\n/**\n * `<scopeRoot>/.skill-map/settings.json` — the canonical layered-config\n * file. Used by `kernel/config/loader.ts` to compose its user / project\n * walk.\n */\nexport function kernelSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, KERNEL_SKILL_MAP_DIR, SETTINGS_FILENAME);\n}\n\n/**\n * `<scopeRoot>/.skill-map/settings.local.json` — the local-overrides\n * companion to `settings.json`. Used by the same loader walk.\n */\nexport function kernelLocalSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, KERNEL_SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);\n}\n","/**\n * Pure path helpers for the on-disk skill-map scope layout. Moved out\n * of `cli/util/db-path.ts` so the BFF (`src/server/`) can consume them\n * without reaching into the CLI layer. The CLI-only siblings\n * (`assertDbExists`, `requireDbOrExit` — they take a stderr stream and\n * an `ExitCode`) stay in `cli/util/db-path.ts` and re-export the\n * primitives from here.\n *\n * Spec global flags (per `spec/cli-contract.md` §Global flags):\n * -g / --global operate on `~/.skill-map/` instead of `./.skill-map/`\n * --db <path> escape hatch for explicit DB file\n */\n\nimport { join, resolve } from 'node:path';\n\nimport type { IRuntimeContext } from '../runtime/runtime-context.js';\n\n/**\n * Per-scope directory the CLI stores its state under (DB file, settings,\n * plugins, etc.). Same name in project (`<cwd>/.skill-map/`) and global\n * (`~/.skill-map/`) scopes; the difference is the parent. Exported so\n * write-side scaffolding (`sm init`) and other helpers can reuse the\n * convention without duplicating the literal.\n */\nexport const SKILL_MAP_DIR = '.skill-map';\n\nconst DB_FILENAME = 'skill-map.db';\nconst JOBS_DIRNAME = 'jobs';\nconst PLUGINS_DIRNAME = 'plugins';\nconst SETTINGS_FILENAME = 'settings.json';\nconst LOCAL_SETTINGS_FILENAME = 'settings.local.json';\nconst IGNORE_FILENAME = '.skillmapignore';\n\n/**\n * Single source of truth for the relative DB path inside a scope\n * directory (`.skill-map/skill-map.db`). Same string in project and\n * global scope; the difference is the parent directory the helper\n * resolves against.\n */\nconst DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;\n\n/**\n * Entries `sm init` appends to the project `.gitignore`. Centralised\n * here (instead of the verb file) so the literals live alongside their\n * filename constants and the verb consumes them as a frozen list.\n */\nexport const GITIGNORE_ENTRIES: readonly string[] = [\n `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,\n `${SKILL_MAP_DIR}/${DB_FILENAME}`,\n];\n\n/**\n * Inputs for `resolveDbPath`. Extends `IRuntimeContext` so the helper\n * never reads `process.cwd()` / `homedir()` directly — every caller\n * threads the runtime context (mandatory) alongside the spec flags.\n * Pattern: `resolveDbPath({ global, db, ...defaultRuntimeContext() })`.\n */\nexport interface IDbLocationOptions extends IRuntimeContext {\n global: boolean;\n db: string | undefined;\n}\n\n/**\n * Resolve the DB file path from command-line options.\n *\n * Precedence: explicit `--db <path>` > `-g/--global` (~/.skill-map/) >\n * project default (cwd/.skill-map/).\n *\n * Always returns an absolute path. Does NOT verify existence — pair with\n * `assertDbExists` for read-side verbs.\n */\nexport function resolveDbPath(options: IDbLocationOptions): string {\n if (options.db) return resolve(options.db);\n if (options.global) return join(options.homedir, DEFAULT_DB_REL);\n return resolve(options.cwd, DEFAULT_DB_REL);\n}\n\n/**\n * Default project DB path (`<cwd>/.skill-map/skill-map.db`). Same effect\n * as `resolveDbPath({ global: false, db: undefined, ...ctx })`; this\n * helper is the cheaper and more explicit route for call sites that have\n * no `--global` / `--db` flags to honour (`sm scan`, `sm refresh`,\n * `sm watch`).\n */\nexport function defaultProjectDbPath(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, DEFAULT_DB_REL);\n}\n\n/**\n * Default project jobs directory (`<cwd>/.skill-map/jobs`). Used by the\n * `sm job prune` orphan-files pass and any other call site that needs\n * the project-scoped jobs spool.\n */\nexport function defaultProjectJobsDir(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);\n}\n\n/**\n * Default project plugins directory (`<cwd>/.skill-map/plugins`).\n * Project + user plugin discovery composes this with the user-scoped\n * `<homedir>/.skill-map/plugins` peer.\n */\nexport function defaultProjectPluginsDir(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);\n}\n\n/**\n * Default user (global) plugins directory (`<homedir>/.skill-map/plugins`).\n * Used alongside `defaultProjectPluginsDir` when discovery walks both\n * scopes.\n */\nexport function defaultUserPluginsDir(ctx: IRuntimeContext): string {\n return join(ctx.homedir, SKILL_MAP_DIR, PLUGINS_DIRNAME);\n}\n\n/**\n * Default DB path under an arbitrary scope root\n * (`<scopeRoot>/.skill-map/skill-map.db`). Companion to\n * `defaultProjectDbPath` for callers that already resolved the scope\n * root themselves (today: `sm init`, which switches between\n * `cwd`/`homedir` based on `--global`).\n */\nexport function defaultDbPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);\n}\n\n/**\n * Default settings file (`<scopeRoot>/.skill-map/settings.json`).\n */\nexport function defaultSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, SETTINGS_FILENAME);\n}\n\n/**\n * Default local-overrides settings file\n * (`<scopeRoot>/.skill-map/settings.local.json`).\n */\nexport function defaultLocalSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);\n}\n\n/**\n * Default `.skillmapignore` file path\n * (`<scopeRoot>/.skillmapignore`). Sits at the scope root, NOT inside\n * `.skill-map/` — `sm scan` reads it from the same level as `package.json`\n * etc. so authors can keep ignore rules visible in the project tree.\n */\nexport function defaultIgnoreFilePath(scopeRoot: string): string {\n return join(scopeRoot, IGNORE_FILENAME);\n}\n","/**\n * `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 * Strings emitted by the conformance runner (`conformance/index.ts`).\n * Same `tx(template, vars)` convention as every other `*.texts.ts` peer.\n *\n * Reasons surface in `IAssertionResult.reason` — visible to anyone\n * reading the runner output (CI logs, `sm conformance run --json`).\n * Keeping them in the catalog unblocks a future Transloco migration and\n * keeps the wording in one place.\n */\n\nexport const CONFORMANCE_RUNNER_TEXTS = {\n priorScanFailed:\n 'setup.priorScans step `{{fixture}}` failed with exit {{exit}}: {{stderr}}',\n\n pathMustBeRelative:\n 'conformance: {{label}} path \"{{path}}\" must be relative to its anchor ({{anchor}})',\n\n pathEscapesAnchor:\n 'conformance: {{label}} path \"{{path}}\" escapes its anchor ({{anchor}})',\n\n expectedExitCode:\n 'expected exit {{expected}}, got {{actual}}',\n\n fileNotFound:\n 'file not found: {{path}}',\n\n targetNotFound:\n 'target not found: {{path}}',\n\n targetMissingFixture:\n 'target does not contain fixture {{fixture}} verbatim',\n\n fileMatchesSchemaUnimplemented:\n 'file-matches-schema not yet implemented (requires ajv; lands with Step 2)',\n\n stderrDidNotMatch:\n 'stderr did not match /{{pattern}}/',\n\n stdoutNotJson:\n 'stdout is not valid JSON: {{message}}',\n\n unsupportedJsonPath:\n 'unsupported jsonpath: {{path}}',\n\n expectedArrayAtPath:\n 'expected array at {{path}}',\n\n cannotTraverseSegment:\n \"cannot traverse {{type}} at segment '{{segment}}'\",\n\n jsonPathEqualsMismatch:\n '{{path}} = {{actual}}, expected {{expected}}',\n\n jsonPathNotGreaterThan:\n '{{path}} not > {{value}}',\n\n jsonPathNotLessThan:\n '{{path}} not < {{value}}',\n\n jsonPathDidNotMatch:\n '{{path}} did not match /{{pattern}}/',\n\n jsonPathNoComparator:\n 'no comparator on json-path assertion',\n\n specRootMissingIndex:\n 'spec root missing index.json at {{specRoot}}',\n} as const;\n"],"mappings":"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","names":["join","resolve","join","join","resolve"]}
1
+ {"version":3,"sources":["../../conformance/index.ts","../../kernel/util/format-error.ts","../../kernel/util/skill-map-paths.ts","../../core/paths/db-path.ts","../../kernel/util/tx.ts","../../conformance/i18n/runner.texts.ts"],"sourcesContent":["/**\n * Contract runner, executes the conformance cases shipped with\n * `@skill-map/spec` against an installed binary and emits a pass/fail result\n * per case.\n *\n * Implements the six assertion types from `spec/schemas/conformance-case.schema.json`.\n * Provisions a clean tmp scope per case, optionally pre-populated with the\n * referenced fixture corpus.\n *\n * Step 0b scope: single-case dispatch. Suite-level runner + reporter land\n * alongside Step 2 extensions.\n */\n\nimport { spawnSync } from 'node:child_process';\nimport { cpSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { isAbsolute, join, relative, resolve } from 'node:path';\n\nimport { formatErrorMessage } from '../kernel/util/format-error.js';\nimport { KERNEL_SKILL_MAP_DIR } from '../kernel/util/skill-map-paths.js';\nimport { tx } from '../kernel/util/tx.js';\nimport { CONFORMANCE_RUNNER_TEXTS } from './i18n/runner.texts.js';\n\nexport type IAssertionResult =\n | { ok: true; type: string }\n | { ok: false; type: string; reason: string };\n\nexport interface IRunCaseResult {\n caseId: string;\n passed: boolean;\n exitCode: number;\n stdout: string;\n stderr: string;\n assertions: IAssertionResult[];\n}\n\nexport interface IRunCaseOptions {\n /** Absolute path to the binary wrapper (e.g. `bin/sm.js`). */\n binary: string;\n /** Absolute path to the `@skill-map/spec` root. */\n specRoot: string;\n /** Absolute path to the case JSON under `<conformance-root>/cases/`. */\n casePath: string;\n /**\n * Absolute path to the `<conformance-root>/fixtures/` directory backing\n * this case (or the parent conformance suite).\n *\n * Phase 5 / A.13 introduced per-Provider conformance directories that\n * live outside the spec tree (Claude-specific cases moved to\n * `src/extensions/providers/claude/conformance/`). Cases reference\n * fixtures by directory name; the runner resolves them under\n * `fixturesRoot` so the spec-agnostic kernel-empty-boot case and the\n * Claude `basic-scan` / `rename-high` / `orphan-detection` cases can\n * coexist without colliding fixture namespaces. Defaults to\n * `<specRoot>/conformance/fixtures` for the legacy spec layout.\n */\n fixturesRoot?: string;\n /** Extra env vars passed to the child. */\n env?: NodeJS.ProcessEnv;\n}\n\ninterface IConformanceCase {\n id: string;\n description: string;\n fixture?: string;\n setup?: {\n disableAllProviders?: boolean;\n disableAllExtractors?: boolean;\n disableAllAnalyzers?: boolean;\n priorScans?: Array<{ fixture: string; flags?: string[] }>;\n };\n invoke: {\n verb: string;\n sub?: string;\n args?: string[];\n flags?: string[];\n };\n assertions: IAssertion[];\n}\n\n/**\n * Build the env-var bag a case's `setup.disableAll*` toggles inject into\n * every child invocation (priorScans + the main `invoke`). The CLI's scan\n * composer (`composeScanExtensions`) reads these vars and drops every\n * extension of the matching kind from the in-scan pipeline.\n */\nfunction disableEnv(setup: IConformanceCase['setup']): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {};\n if (setup?.disableAllProviders) env['SKILL_MAP_DISABLE_ALL_PROVIDERS'] = '1';\n if (setup?.disableAllExtractors) env['SKILL_MAP_DISABLE_ALL_EXTRACTORS'] = '1';\n if (setup?.disableAllAnalyzers) env['SKILL_MAP_DISABLE_ALL_ANALYZERS'] = '1';\n return env;\n}\n\nexport type IAssertion =\n | { type: 'exit-code'; value: number }\n | {\n type: 'json-path';\n path: string;\n equals?: unknown;\n greaterThan?: number;\n lessThan?: number;\n matches?: string;\n }\n | { type: 'file-exists'; path: string }\n | { type: 'file-contains-verbatim'; path: string; fixture: string }\n | { type: 'file-matches-schema'; path: string; schema: string }\n | { type: 'stderr-matches'; pattern: string };\n\n// Conformance runner orchestrates: case parse, setup steps, scope\n// provision, sm invocation, assert dispatch over the closed assertion\n// type union. Each step is one cyclomatic point; splitting hides the\n// pipeline. Per `context/lint.md` category 1 (CLI orchestrators).\n// eslint-disable-next-line complexity\nexport function runConformanceCase(options: IRunCaseOptions): IRunCaseResult {\n const raw = readFileSync(options.casePath, 'utf8');\n const c: IConformanceCase = JSON.parse(raw);\n\n const fixturesRoot = options.fixturesRoot ?? join(options.specRoot, 'conformance', 'fixtures');\n\n // Defence in depth (audit L5): the conformance case id is JSON-author-\n // controlled. Replace anything that isn't a safe filesystem char and\n // cap the length so an over-long id (or one carrying path separators\n // / control bytes) can't escape `tmpdir()` or grow the prefix beyond\n // a reasonable bound.\n const safeId = c.id.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 32);\n const scope = mkdtempSync(join(tmpdir(), `sm-conformance-${safeId}-`));\n const setupEnv = disableEnv(c.setup);\n try {\n // 1. Replay every `setup.priorScans` step into the scope DB before\n // the main invoke runs. Returns the failure result early if any\n // step exits non-zero.\n const priorFailure = runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv);\n if (priorFailure) return priorFailure;\n\n // 2. Copy the main fixture (replacing prior fixture content but\n // preserving the DB), then run the case's `invoke`.\n if (c.fixture) {\n replaceFixture(scope, fixturesRoot, c.fixture);\n }\n\n const argv = [c.invoke.verb];\n if (c.invoke.sub) argv.push(c.invoke.sub);\n if (c.invoke.args) argv.push(...c.invoke.args);\n if (c.invoke.flags) argv.push(...c.invoke.flags);\n\n const child = spawnSync(process.execPath, [options.binary, ...argv], {\n cwd: scope,\n env: { ...process.env, ...options.env, ...setupEnv },\n encoding: 'utf8',\n });\n\n const stdout = child.stdout ?? '';\n const stderr = child.stderr ?? '';\n const exitCode = child.status ?? 0;\n\n const assertions = c.assertions.map((a) =>\n evaluateAssertion(a, {\n exitCode,\n stdout,\n stderr,\n scope,\n specRoot: options.specRoot,\n fixturesRoot,\n }),\n );\n const passed = assertions.every((a) => a.ok);\n\n return { caseId: c.id, passed, exitCode, stdout, stderr, assertions };\n } finally {\n rmSync(scope, { recursive: true, force: true });\n }\n}\n\n/**\n * Phase 1 of `runConformanceCase`, replay every `setup.priorScans`\n * step in order. Each step replaces every non-`.skill-map/` directory\n * with the named fixture, then runs `sm scan` so the snapshot persists\n * into the scope DB. The scope DB survives across steps (we never\n * delete `.skill-map/`).\n *\n * Returns `null` on success (caller continues) or a `IRunCaseResult`\n * with a single `priorScan` failure assertion (caller returns it\n * unchanged).\n */\n// Per-step replay: replace fixture, spawn `sm scan`, check exit. The\n// failure-result construction is verbose because it carries every\n// stream the caller reports back.\n// eslint-disable-next-line complexity\nfunction runPriorScansSetup(\n c: IConformanceCase,\n options: IRunCaseOptions,\n scope: string,\n fixturesRoot: string,\n setupEnv: NodeJS.ProcessEnv,\n): IRunCaseResult | null {\n for (const step of c.setup?.priorScans ?? []) {\n replaceFixture(scope, fixturesRoot, step.fixture);\n const stepArgv = ['scan', ...(step.flags ?? [])];\n const stepChild = spawnSync(process.execPath, [options.binary, ...stepArgv], {\n cwd: scope,\n env: { ...process.env, ...options.env, ...setupEnv },\n encoding: 'utf8',\n });\n if ((stepChild.status ?? 0) !== 0) {\n return {\n caseId: c.id,\n passed: false,\n exitCode: stepChild.status ?? 0,\n stdout: stepChild.stdout ?? '',\n stderr: stepChild.stderr ?? '',\n assertions: [\n {\n ok: false,\n type: 'priorScan',\n reason: tx(CONFORMANCE_RUNNER_TEXTS.priorScanFailed, {\n fixture: step.fixture,\n exit: stepChild.status ?? 0,\n stderr: stepChild.stderr ?? '',\n }),\n },\n ],\n };\n }\n }\n return null;\n}\n\n/**\n * Replace every top-level entry in `scope` EXCEPT `.skill-map/` (which\n * holds the kernel DB and persists across staging steps), then copy\n * the fixture's contents on top. Used by `priorScans` and the main\n * fixture phase to swap Provider content while keeping the DB stable.\n *\n * `fixturesRoot` is the absolute path to the `fixtures/` directory of\n * the conformance suite hosting the case (spec-owned for kernel cases,\n * Provider-owned for Provider cases, see `IRunCaseOptions.fixturesRoot`).\n */\nfunction replaceFixture(scope: string, fixturesRoot: string, fixture: string): void {\n assertContained(fixturesRoot, fixture, 'fixture');\n for (const entry of readdirSync(scope)) {\n if (entry === KERNEL_SKILL_MAP_DIR) continue;\n rmSync(join(scope, entry), { recursive: true, force: true });\n }\n const src = join(fixturesRoot, fixture);\n cpSync(src, scope, { recursive: true });\n}\n\n/**\n * Reject case-supplied path strings that escape the directory tree they\n * are anchored to. A hostile case JSON would otherwise be able to copy\n * arbitrary filesystem content into the tmp scope (`fixture: \"../..\"`)\n * or read files outside the conformance sandbox via `file-exists` /\n * `file-contains-verbatim` assertions.\n */\nfunction assertContained(root: string, rel: string, label: string): void {\n if (isAbsolute(rel)) {\n throw new Error(\n tx(CONFORMANCE_RUNNER_TEXTS.pathMustBeRelative, { label, path: rel, anchor: root }),\n );\n }\n const abs = resolve(root, rel);\n const r = relative(root, abs);\n if (r.startsWith('..') || isAbsolute(r)) {\n throw new Error(\n tx(CONFORMANCE_RUNNER_TEXTS.pathEscapesAnchor, { label, path: rel, anchor: root }),\n );\n }\n}\n\ninterface IAssertionContext {\n exitCode: number;\n stdout: string;\n stderr: string;\n scope: string;\n specRoot: string;\n fixturesRoot: string;\n}\n\n// Switch over assertion types (`exit-code` / `stdout-matches` /\n// `file-exists` / `file-contains-verbatim` / `file-matches-schema` /\n// `stderr-matches` / `json-path`) with one branch per type. Splitting\n// per type would scatter the discriminated-union dispatch.\n// eslint-disable-next-line complexity\nfunction evaluateAssertion(a: IAssertion, ctx: IAssertionContext): IAssertionResult {\n switch (a.type) {\n case 'exit-code':\n return ctx.exitCode === a.value\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.expectedExitCode, {\n expected: a.value,\n actual: ctx.exitCode,\n }),\n };\n case 'json-path':\n return evaluateJsonPath(a, ctx);\n case 'file-exists': {\n try {\n assertContained(ctx.scope, a.path, 'file-exists');\n } catch (err) {\n return { ok: false, type: a.type, reason: formatErrorMessage(err) };\n }\n const abs = resolve(ctx.scope, a.path);\n return existsSync(abs)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path }),\n };\n }\n case 'file-contains-verbatim': {\n try {\n assertContained(ctx.fixturesRoot, a.fixture, 'file-contains-verbatim/fixture');\n assertContained(ctx.scope, a.path, 'file-contains-verbatim/path');\n } catch (err) {\n return { ok: false, type: a.type, reason: formatErrorMessage(err) };\n }\n const fixturePath = join(ctx.fixturesRoot, a.fixture);\n const targetPath = resolve(ctx.scope, a.path);\n if (!existsSync(targetPath)) {\n return {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.targetNotFound, { path: a.path }),\n };\n }\n const needle = readFileSync(fixturePath);\n const haystack = readFileSync(targetPath);\n return haystack.includes(needle)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.targetMissingFixture, { fixture: a.fixture }),\n };\n }\n case 'file-matches-schema':\n return {\n ok: false,\n type: a.type,\n reason: CONFORMANCE_RUNNER_TEXTS.fileMatchesSchemaUnimplemented,\n };\n case 'stderr-matches': {\n const re = new RegExp(a.pattern);\n return re.test(ctx.stderr)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.stderrDidNotMatch, { pattern: a.pattern }),\n };\n }\n }\n}\n\n/**\n * Minimal JSONPath evaluator, supports only the subset used by the stub\n * conformance suite: `$.foo`, `$.foo.bar`, `$.foo.length`, `$[0]`.\n * The full RFC 9535 implementation lands with Step 2.\n */\nfunction evaluateJsonPath(\n a: Extract<IAssertion, { type: 'json-path' }>,\n ctx: IAssertionContext,\n): IAssertionResult {\n let doc: unknown;\n try {\n doc = JSON.parse(ctx.stdout);\n } catch (err) {\n return {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.stdoutNotJson, { message: formatErrorMessage(err) }),\n };\n }\n\n const segments = parsePath(a.path);\n if (!segments) {\n return {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.unsupportedJsonPath, { path: a.path }),\n };\n }\n\n const walked = traverseJsonPath(doc, segments, a.path);\n if (!walked.ok) return { ok: false, type: a.type, reason: walked.reason };\n\n return applyJsonPathComparator(a, walked.value);\n}\n\n/**\n * Walk a parsed JSONPath segment list against a JSON document. Returns\n * the resolved value or a structured failure (caller maps to\n * `IAssertionResult`). Pure, no IO, no shared state.\n */\nfunction traverseJsonPath(\n doc: unknown,\n segments: Array<string | number>,\n path: string,\n): { ok: true; value: unknown } | { ok: false; reason: string } {\n let current: unknown = doc;\n for (const seg of segments) {\n if (typeof seg === 'number') {\n if (!Array.isArray(current)) {\n return { ok: false, reason: tx(CONFORMANCE_RUNNER_TEXTS.expectedArrayAtPath, { path }) };\n }\n current = current[seg];\n } else if (seg === 'length' && Array.isArray(current)) {\n current = current.length;\n } else if (typeof current === 'object' && current !== null) {\n current = (current as Record<string, unknown>)[seg];\n } else {\n return {\n ok: false,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.cannotTraverseSegment, {\n type: typeof current,\n segment: String(seg),\n }),\n };\n }\n }\n return { ok: true, value: current };\n}\n\n/**\n * Apply the comparator clause (`equals` / `greaterThan` / `lessThan` /\n * `matches`) of a `json-path` assertion against the value resolved at\n * the requested path. Returns the final `IAssertionResult` directly.\n *\n * Complexity from the four parallel comparator branches; splitting into\n * one helper per comparator would be ceremony.\n */\n// eslint-disable-next-line complexity\nfunction applyJsonPathComparator(\n a: Extract<IAssertion, { type: 'json-path' }>,\n current: unknown,\n): IAssertionResult {\n if ('equals' in a && a.equals !== undefined) {\n return deepEqual(current, a.equals)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathEqualsMismatch, {\n path: a.path,\n actual: JSON.stringify(current),\n expected: JSON.stringify(a.equals),\n }),\n };\n }\n if ('greaterThan' in a && typeof a.greaterThan === 'number') {\n return typeof current === 'number' && current > a.greaterThan\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathNotGreaterThan, {\n path: a.path,\n value: a.greaterThan,\n }),\n };\n }\n if ('lessThan' in a && typeof a.lessThan === 'number') {\n return typeof current === 'number' && current < a.lessThan\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathNotLessThan, {\n path: a.path,\n value: a.lessThan,\n }),\n };\n }\n if ('matches' in a && typeof a.matches === 'string') {\n const re = new RegExp(a.matches);\n return typeof current === 'string' && re.test(current)\n ? { ok: true, type: a.type }\n : {\n ok: false,\n type: a.type,\n reason: tx(CONFORMANCE_RUNNER_TEXTS.jsonPathDidNotMatch, {\n path: a.path,\n pattern: a.matches,\n }),\n };\n }\n return { ok: false, type: a.type, reason: CONFORMANCE_RUNNER_TEXTS.jsonPathNoComparator };\n}\n\nfunction parsePath(path: string): Array<string | number> | null {\n if (!path.startsWith('$')) return null;\n const tail = path.slice(1);\n const segments: Array<string | number> = [];\n const re = /\\.([a-zA-Z_][a-zA-Z0-9_-]*)|\\[(\\d+)\\]/g;\n let lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = re.exec(tail)) !== null) {\n if (match.index !== lastIndex) return null;\n if (match[1] !== undefined) segments.push(match[1]);\n else if (match[2] !== undefined) segments.push(Number.parseInt(match[2], 10));\n lastIndex = re.lastIndex;\n }\n if (lastIndex !== tail.length) return null;\n return segments;\n}\n\n// Structural equality over arbitrary JSON values: primitive / null /\n// array / object branches plus per-branch length / key-set checks.\n// The branching IS the type table. Per `context/lint.md` category 7\n// (recursive type-discriminator walkers).\n// eslint-disable-next-line complexity\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a !== typeof b) return false;\n if (a && b && typeof a === 'object' && typeof b === 'object') {\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n const ak = Object.keys(a as object);\n const bk = Object.keys(b as object);\n if (ak.length !== bk.length) return false;\n for (const k of ak) {\n if (\n !deepEqual(\n (a as Record<string, unknown>)[k],\n (b as Record<string, unknown>)[k],\n )\n )\n {return false;}\n }\n return true;\n }\n return false;\n}\n\n/** Verifies the spec root looks sane (contains `index.json`). */\nexport function assertSpecRoot(specRoot: string): void {\n const indexPath = join(specRoot, 'index.json');\n if (!existsSync(indexPath) || !statSync(indexPath).isFile()) {\n throw new Error(tx(CONFORMANCE_RUNNER_TEXTS.specRootMissingIndex, { specRoot }));\n }\n}\n","/**\n * Kernel-accessible counterpart of `cli/util/error-reporter.ts`'s\n * `formatErrorMessage`. The CLI helper now re-exports from here so the\n * historic CLI import path keeps working while kernel + BFF callers can\n * consume it directly without crossing the layering boundary.\n *\n * Kept deliberately tiny, same shape as the original CLI helper. The\n * surface grows (e.g. a `--verbose` stack mode, JSON envelope) only\n * when a concrete need surfaces.\n */\n\n/**\n * Compact error → string conversion.\n *\n * - `Error` → `err.message` verbatim. Callers wrap with their own\n * verb-specific context line via `tx(*_TEXTS.x, { message })` so\n * error catalogues stay greppable.\n * - Anything else → `String(value)`. Catches the rare throw-a-string\n * / throw-an-object path without exploding on `null`\n * (`String(null)` = `'null'`).\n */\nexport function formatErrorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n","/**\n * Kernel-side helpers that compose the layered-config file paths from\n * the canonical `SKILL_MAP_DIR` literal.\n *\n * `SKILL_MAP_DIR` is exported once from `core/paths/db-path.ts` and\n * re-exported here as `KERNEL_SKILL_MAP_DIR` so kernel-side callers\n * keep their historic name without the literal living in two files\n * (audit m3, one literal home, no `grep \"'\\.skill-map'\"` sweep\n * invariant to maintain across kernel + CLI).\n */\n\nimport { join } from 'node:path';\n\nimport { SKILL_MAP_DIR } from '../../core/paths/db-path.js';\n\n/**\n * Per-scope directory the kernel + CLI both store state under (DB file,\n * settings, plugins, etc.). Re-exported from `core/paths/db-path.ts`\n * the single canonical source for the literal.\n */\nexport const KERNEL_SKILL_MAP_DIR = SKILL_MAP_DIR;\n\nconst SETTINGS_FILENAME = 'settings.json';\nconst LOCAL_SETTINGS_FILENAME = 'settings.local.json';\n\n/**\n * `<scopeRoot>/.skill-map/settings.json`, the canonical layered-config\n * file. Used by `kernel/config/loader.ts` to compose its user / project\n * walk.\n */\nexport function kernelSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, KERNEL_SKILL_MAP_DIR, SETTINGS_FILENAME);\n}\n\n/**\n * `<scopeRoot>/.skill-map/settings.local.json`, the local-overrides\n * companion to `settings.json`. Used by the same loader walk.\n */\nexport function kernelLocalSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, KERNEL_SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);\n}\n","/**\n * Pure path helpers for the on-disk skill-map scope layout. Moved out\n * of `cli/util/db-path.ts` so the BFF (`src/server/`) can consume them\n * without reaching into the CLI layer. The CLI-only siblings\n * (`assertDbExists`, `requireDbOrExit`, they take a stderr stream and\n * an `ExitCode`) stay in `cli/util/db-path.ts` and re-export the\n * primitives from here.\n *\n * Spec global flags (per `spec/cli-contract.md` §Global flags):\n * -g / --global operate on `~/.skill-map/` instead of `./.skill-map/`\n * --db <path> escape hatch for explicit DB file\n */\n\nimport { join, resolve } from 'node:path';\n\nimport type { IRuntimeContext } from '../runtime/runtime-context.js';\n\n/**\n * Per-scope directory the CLI stores its state under (DB file, settings,\n * plugins, etc.). Same name in project (`<cwd>/.skill-map/`) and global\n * (`~/.skill-map/`) scopes; the difference is the parent. Exported so\n * write-side scaffolding (`sm init`) and other helpers can reuse the\n * convention without duplicating the literal.\n */\nexport const SKILL_MAP_DIR = '.skill-map';\n\nconst DB_FILENAME = 'skill-map.db';\nconst JOBS_DIRNAME = 'jobs';\nconst PLUGINS_DIRNAME = 'plugins';\nconst SETTINGS_FILENAME = 'settings.json';\nconst LOCAL_SETTINGS_FILENAME = 'settings.local.json';\nconst IGNORE_FILENAME = '.skillmapignore';\n\n/**\n * Single source of truth for the relative DB path inside a scope\n * directory (`.skill-map/skill-map.db`). Same string in project and\n * global scope; the difference is the parent directory the helper\n * resolves against.\n */\nconst DEFAULT_DB_REL = `${SKILL_MAP_DIR}/${DB_FILENAME}`;\n\n/**\n * Entries `sm init` appends to the project `.gitignore`. Centralised\n * here (instead of the verb file) so the literals live alongside their\n * filename constants and the verb consumes them as a frozen list.\n */\nexport const GITIGNORE_ENTRIES: readonly string[] = [\n `${SKILL_MAP_DIR}/${LOCAL_SETTINGS_FILENAME}`,\n `${SKILL_MAP_DIR}/${DB_FILENAME}`,\n];\n\n/**\n * Inputs for `resolveDbPath`. Extends `IRuntimeContext` so the helper\n * never reads `process.cwd()` / `homedir()` directly, every caller\n * threads the runtime context (mandatory) alongside the spec flags.\n * Pattern: `resolveDbPath({ global, db, ...defaultRuntimeContext() })`.\n */\nexport interface IDbLocationOptions extends IRuntimeContext {\n global: boolean;\n db: string | undefined;\n}\n\n/**\n * Resolve the DB file path from command-line options.\n *\n * Precedence: explicit `--db <path>` > `-g/--global` (~/.skill-map/) >\n * project default (cwd/.skill-map/).\n *\n * Always returns an absolute path. Does NOT verify existence, pair with\n * `assertDbExists` for read-side verbs.\n */\nexport function resolveDbPath(options: IDbLocationOptions): string {\n if (options.db) return resolve(options.db);\n if (options.global) return join(options.homedir, DEFAULT_DB_REL);\n return resolve(options.cwd, DEFAULT_DB_REL);\n}\n\n/**\n * Default project DB path (`<cwd>/.skill-map/skill-map.db`). Same effect\n * as `resolveDbPath({ global: false, db: undefined, ...ctx })`; this\n * helper is the cheaper and more explicit route for call sites that have\n * no `--global` / `--db` flags to honour (`sm scan`, `sm refresh`,\n * `sm watch`).\n */\nexport function defaultProjectDbPath(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, DEFAULT_DB_REL);\n}\n\n/**\n * Default project jobs directory (`<cwd>/.skill-map/jobs`). Used by the\n * `sm job prune` orphan-files pass and any other call site that needs\n * the project-scoped jobs spool.\n */\nexport function defaultProjectJobsDir(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, SKILL_MAP_DIR, JOBS_DIRNAME);\n}\n\n/**\n * Default project plugins directory (`<cwd>/.skill-map/plugins`).\n * Project + user plugin discovery composes this with the user-scoped\n * `<homedir>/.skill-map/plugins` peer.\n */\nexport function defaultProjectPluginsDir(ctx: IRuntimeContext): string {\n return resolve(ctx.cwd, SKILL_MAP_DIR, PLUGINS_DIRNAME);\n}\n\n/**\n * Default user (global) plugins directory (`<homedir>/.skill-map/plugins`).\n * Used alongside `defaultProjectPluginsDir` when discovery walks both\n * scopes.\n */\nexport function defaultUserPluginsDir(ctx: IRuntimeContext): string {\n return join(ctx.homedir, SKILL_MAP_DIR, PLUGINS_DIRNAME);\n}\n\n/**\n * Default DB path under an arbitrary scope root\n * (`<scopeRoot>/.skill-map/skill-map.db`). Companion to\n * `defaultProjectDbPath` for callers that already resolved the scope\n * root themselves (today: `sm init`, which switches between\n * `cwd`/`homedir` based on `--global`).\n */\nexport function defaultDbPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, DB_FILENAME);\n}\n\n/**\n * Default settings file (`<scopeRoot>/.skill-map/settings.json`).\n */\nexport function defaultSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, SETTINGS_FILENAME);\n}\n\n/**\n * Default local-overrides settings file\n * (`<scopeRoot>/.skill-map/settings.local.json`).\n */\nexport function defaultLocalSettingsPath(scopeRoot: string): string {\n return join(scopeRoot, SKILL_MAP_DIR, LOCAL_SETTINGS_FILENAME);\n}\n\n/**\n * Default `.skillmapignore` file path\n * (`<scopeRoot>/.skillmapignore`). Sits at the scope root, NOT inside\n * `.skill-map/`, `sm scan` reads it from the same level as `package.json`\n * etc. so authors can keep ignore rules visible in the project tree.\n */\nexport function defaultIgnoreFilePath(scopeRoot: string): string {\n return join(scopeRoot, IGNORE_FILENAME);\n}\n","/**\n * `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 * Strings emitted by the conformance runner (`conformance/index.ts`).\n * Same `tx(template, vars)` convention as every other `*.texts.ts` peer.\n *\n * Reasons surface in `IAssertionResult.reason`, visible to anyone\n * reading the runner output (CI logs, `sm conformance run --json`).\n * Keeping them in the catalog unblocks a future Transloco migration and\n * keeps the wording in one place.\n */\n\nexport const CONFORMANCE_RUNNER_TEXTS = {\n priorScanFailed:\n 'setup.priorScans step `{{fixture}}` failed with exit {{exit}}: {{stderr}}',\n\n pathMustBeRelative:\n 'conformance: {{label}} path \"{{path}}\" must be relative to its anchor ({{anchor}})',\n\n pathEscapesAnchor:\n 'conformance: {{label}} path \"{{path}}\" escapes its anchor ({{anchor}})',\n\n expectedExitCode:\n 'expected exit {{expected}}, got {{actual}}',\n\n fileNotFound:\n 'file not found: {{path}}',\n\n targetNotFound:\n 'target not found: {{path}}',\n\n targetMissingFixture:\n 'target does not contain fixture {{fixture}} verbatim',\n\n fileMatchesSchemaUnimplemented:\n 'file-matches-schema not yet implemented (requires ajv; lands with Step 2)',\n\n stderrDidNotMatch:\n 'stderr did not match /{{pattern}}/',\n\n stdoutNotJson:\n 'stdout is not valid JSON: {{message}}',\n\n unsupportedJsonPath:\n 'unsupported jsonpath: {{path}}',\n\n expectedArrayAtPath:\n 'expected array at {{path}}',\n\n cannotTraverseSegment:\n \"cannot traverse {{type}} at segment '{{segment}}'\",\n\n jsonPathEqualsMismatch:\n '{{path}} = {{actual}}, expected {{expected}}',\n\n jsonPathNotGreaterThan:\n '{{path}} not > {{value}}',\n\n jsonPathNotLessThan:\n '{{path}} not < {{value}}',\n\n jsonPathDidNotMatch:\n '{{path}} did not match /{{pattern}}/',\n\n jsonPathNoComparator:\n 'no comparator on json-path assertion',\n\n specRootMissingIndex:\n 'spec root missing index.json at {{specRoot}}',\n} as const;\n"],"mappings":"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","names":["join","resolve","join","join","resolve"]}
package/dist/index.js CHANGED
@@ -100,7 +100,7 @@ import cl100k_base from "js-tiktoken/ranks/cl100k_base";
100
100
  // package.json
101
101
  var package_default = {
102
102
  name: "@skill-map/cli",
103
- version: "0.22.0",
103
+ version: "0.23.1",
104
104
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
105
105
  license: "MIT",
106
106
  type: "module",
@@ -167,16 +167,16 @@ var package_default = {
167
167
  },
168
168
  dependencies: {
169
169
  "@hono/node-server": "2.0.1",
170
- "@skill-map/spec": "0.22.0",
170
+ "@skill-map/spec": "0.23.0",
171
171
  ajv: "8.18.0",
172
172
  "ajv-formats": "3.0.1",
173
173
  chokidar: "5.0.0",
174
174
  clipanion: "4.0.0-rc.4",
175
- hono: "4.12.16",
175
+ hono: "4.12.18",
176
176
  ignore: "7.0.5",
177
177
  "js-tiktoken": "1.0.21",
178
178
  "js-yaml": "4.1.1",
179
- kysely: "0.28.16",
179
+ kysely: "0.28.17",
180
180
  semver: "7.7.4",
181
181
  typanion: "3.14.0",
182
182
  ws: "8.20.0"
@@ -271,8 +271,8 @@ import { Ajv2020 as Ajv20202 } from "ajv/dist/2020.js";
271
271
 
272
272
  // kernel/i18n/plugin-store.texts.ts
273
273
  var PLUGIN_STORE_TEXTS = {
274
- kvValidationFailed: "plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema ({{schemaPath}}) \u2014 {{errors}}",
275
- dedicatedValidationFailed: "plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema ({{schemaPath}}) \u2014 {{errors}}"
274
+ kvValidationFailed: "plugin '{{pluginId}}' ctx.store.set('{{key}}', value): value violates declared schema ({{schemaPath}}): {{errors}}",
275
+ dedicatedValidationFailed: "plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema ({{schemaPath}}): {{errors}}"
276
276
  };
277
277
 
278
278
  // kernel/adapters/plugin-store.ts
@@ -360,6 +360,27 @@ import { readFileSync as readFileSync3 } from "fs";
360
360
  import { dirname, resolve as resolve4 } from "path";
361
361
  import { createRequire as createRequire2 } from "module";
362
362
  import { Ajv2020 as Ajv20203 } from "ajv/dist/2020.js";
363
+
364
+ // kernel/types/view-catalog.ts
365
+ var ALL_SLOT_NAMES = [
366
+ "card.title.right",
367
+ "card.subtitle.left",
368
+ "card.footer.left",
369
+ "card.footer.right",
370
+ "graph.node.alert",
371
+ "inspector.header.badge.counter",
372
+ "inspector.header.badge.tag",
373
+ "inspector.body.panel.breakdown",
374
+ "inspector.body.panel.records",
375
+ "inspector.body.panel.tree",
376
+ "inspector.body.panel.key-values",
377
+ "inspector.body.panel.link-list",
378
+ "inspector.body.panel.markdown",
379
+ "topbar.nav.start"
380
+ ];
381
+ var KNOWN_SLOT_NAMES = new Set(ALL_SLOT_NAMES);
382
+
383
+ // kernel/adapters/schema-validators.ts
363
384
  var SCHEMA_FILES = {
364
385
  node: "schemas/node.schema.json",
365
386
  link: "schemas/link.schema.json",
@@ -427,24 +448,8 @@ function buildSchemaValidators() {
427
448
  });
428
449
  const contributionValidators = /* @__PURE__ */ new Map();
429
450
  const VIEW_SLOTS_ID = "https://skill-map.dev/spec/v0/view-slots.schema.json";
430
- const KNOWN_SLOTS = /* @__PURE__ */ new Set([
431
- "card.title.right",
432
- "card.subtitle.left",
433
- "card.footer.left",
434
- "card.footer.right",
435
- "graph.node.alert",
436
- "inspector.header.badge.counter",
437
- "inspector.header.badge.tag",
438
- "inspector.body.panel.breakdown",
439
- "inspector.body.panel.records",
440
- "inspector.body.panel.tree",
441
- "inspector.body.panel.key-values",
442
- "inspector.body.panel.link-list",
443
- "inspector.body.panel.markdown",
444
- "topbar.nav.start"
445
- ]);
446
451
  function getContributionValidator(slot) {
447
- if (!KNOWN_SLOTS.has(slot)) return null;
452
+ if (!KNOWN_SLOT_NAMES.has(slot)) return null;
448
453
  const existing = contributionValidators.get(slot);
449
454
  if (existing) return existing;
450
455
  const ref = `${VIEW_SLOTS_ID}#/$defs/payloads/${slot}`;
@@ -542,7 +547,7 @@ function resolveSpecRoot() {
542
547
  return dirname(indexPath);
543
548
  } catch {
544
549
  throw new Error(
545
- "@skill-map/spec not resolvable \u2014 ensure the workspace is linked or the package is installed."
550
+ "@skill-map/spec not resolvable: ensure the workspace is linked or the package is installed."
546
551
  );
547
552
  }
548
553
  }
@@ -675,13 +680,14 @@ function buildHookContext(_hook, trigger, event) {
675
680
  // kernel/i18n/orchestrator.texts.ts
676
681
  var ORCHESTRATOR_TEXTS = {
677
682
  frontmatterInvalid: "Frontmatter for {{path}} ({{kind}}) failed schema validation: {{errors}}",
678
- frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only \u2014 the metadata block was silently lost. Move the `---` lines to the start of the line.",
683
+ frontmatterMalformedPasteWithIndent: "Frontmatter fence in {{path}} appears indented; YAML frontmatter MUST start with `---` at column 0. The file was scanned as body-only; the metadata block was silently lost. Move the `---` lines to the start of the line.",
679
684
  frontmatterMalformedByteOrderMark: "Frontmatter fence in {{path}} is preceded by a UTF-8 byte-order mark (BOM); the file was scanned as body-only. Re-save the file as UTF-8 without BOM. The metadata block was silently lost.",
680
- frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes \u2014 no matching `---` line at column 0 was found. The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
685
+ frontmatterMalformedMissingClose: "Frontmatter in {{path}} opens with `---` but never closes (no matching `---` line at column 0 was found). The file was scanned as body-only and every metadata field was silently lost. Add a closing `---` line below the metadata block.",
681
686
  extensionErrorLinkKindNotDeclared: 'Extractor "{{extractorId}}" emitted a link of kind "{{linkKind}}" outside its declared `emitsLinkKinds` set [{{declaredKinds}}]. Link dropped.',
682
687
  extensionErrorIssueInvalidSeverity: `Rule "{{analyzerId}}" emitted an issue with invalid severity {{severity}} (allowed: 'error' | 'warn' | 'info'). Issue dropped.`,
683
688
  extensionErrorContributionUnknownId: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}} but did not declare it in its `viewContributions` map. Contribution dropped.',
684
689
  extensionErrorContributionPayloadInvalid: 'Extractor "{{extractorId}}" emitted contribution "{{contributionId}}" on {{nodePath}}; payload failed the "{{slot}}" schema: {{errors}}. Contribution dropped.',
690
+ extensionErrorRecommendedActionMissing: 'Analyzer "{{analyzerId}}" declares recommendedAction "{{actionId}}" but no Action is registered under that qualified id. The analyzer stays registered; the recommendation will not surface in the inspector.',
685
691
  runScanRootEmptyArray: "runScan: roots must contain at least one path (spec requires minItems: 1)",
686
692
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
687
693
  };
@@ -870,10 +876,11 @@ function isExternalUrlLink(link) {
870
876
  }
871
877
 
872
878
  // kernel/orchestrator/analyzers.ts
873
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, emitter, hookDispatcher) {
879
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher) {
874
880
  const issues = [];
875
881
  const contributions = [];
876
882
  const validators = loadSchemaValidators();
883
+ validateRecommendedActions(analyzers, registeredActionIds, emitter);
877
884
  const analyzerOrphans = orphanSidecars.map((o) => ({
878
885
  relativePath: o.relativePath,
879
886
  expectedMdPath: o.expectedMdPath
@@ -945,6 +952,27 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
945
952
  }
946
953
  return { issues, contributions };
947
954
  }
955
+ function validateRecommendedActions(analyzers, registeredActionIds, emitter) {
956
+ for (const analyzer of analyzers) {
957
+ const refs = analyzer.recommendedActions;
958
+ if (refs === void 0 || refs.length === 0) continue;
959
+ const analyzerId = qualifiedExtensionId(analyzer.pluginId, analyzer.id);
960
+ for (const actionId of refs) {
961
+ if (registeredActionIds.has(actionId)) continue;
962
+ emitter.emit(
963
+ makeEvent("extension.error", {
964
+ kind: "recommended-action-missing",
965
+ extensionId: analyzerId,
966
+ actionId,
967
+ message: tx(ORCHESTRATOR_TEXTS.extensionErrorRecommendedActionMissing, {
968
+ analyzerId,
969
+ actionId
970
+ })
971
+ })
972
+ );
973
+ }
974
+ }
975
+ }
948
976
  function validateIssue(analyzer, issue, emitter) {
949
977
  const severity = issue.severity;
950
978
  if (severity !== "error" && severity !== "warn" && severity !== "info") {
@@ -994,9 +1022,20 @@ function indexPriorLinks(links, priorNodePaths, byOriginating) {
994
1022
  else byOriginating.set(key, [link]);
995
1023
  }
996
1024
  }
1025
+ var FRONTMATTER_ISSUE_ANALYZERS = /* @__PURE__ */ new Set([
1026
+ "frontmatter-invalid",
1027
+ "frontmatter-malformed",
1028
+ // Audit L1: parser parse-error is emitted by
1029
+ // `buildFreshNodeAndValidateFrontmatter` from `raw.parseIssues`. The
1030
+ // raw.parseIssues only flows through the non-cache path; a cached
1031
+ // node skips the rebuild, so the prior issue MUST survive the
1032
+ // incremental scan or the warning silently disappears on a clean
1033
+ // re-scan of an unchanged file.
1034
+ "frontmatter-parse-error"
1035
+ ]);
997
1036
  function indexPriorFrontmatterIssues(issues, byNode) {
998
1037
  for (const issue of issues) {
999
- if (issue.analyzerId !== "frontmatter-invalid" && issue.analyzerId !== "frontmatter-malformed") continue;
1038
+ if (!FRONTMATTER_ISSUE_ANALYZERS.has(issue.analyzerId)) continue;
1000
1039
  if (issue.nodeIds.length !== 1) continue;
1001
1040
  const path = issue.nodeIds[0];
1002
1041
  const list = byNode.get(path);
@@ -1195,7 +1234,7 @@ function flagAmbiguousRenames(opts) {
1195
1234
  analyzerId: "auto-rename-ambiguous",
1196
1235
  severity: "warn",
1197
1236
  nodeIds: [toPath],
1198
- message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters \u2014 pick one with \`sm orphans undo-rename ${toPath} --from <old.path>\`.`,
1237
+ message: `Auto-rename ambiguous: ${toPath} matches ${remaining.length} prior frontmatters; pick one with \`sm orphans undo-rename ${toPath} --from <old.path>\`.`,
1199
1238
  data: { to: toPath, candidates: remaining }
1200
1239
  });
1201
1240
  }
@@ -1252,7 +1291,7 @@ function detectRenamesAndOrphans(prior, current, issues) {
1252
1291
  }
1253
1292
 
1254
1293
  // kernel/scan/walk-content.ts
1255
- import { readFile, readdir, stat } from "fs/promises";
1294
+ import { readFile, readdir, lstat } from "fs/promises";
1256
1295
  import { join as join2, relative as relative2, sep } from "path";
1257
1296
 
1258
1297
  // kernel/scan/ignore.ts
@@ -1310,8 +1349,30 @@ function readDefaultsFromDisk() {
1310
1349
 
1311
1350
  // built-in-plugins/parsers/frontmatter-yaml/index.ts
1312
1351
  import yaml from "js-yaml";
1352
+
1353
+ // kernel/util/strip-prototype-pollution.ts
1354
+ var FORBIDDEN_KEYS = /* @__PURE__ */ new Set([
1355
+ "__proto__",
1356
+ "constructor",
1357
+ "prototype"
1358
+ ]);
1359
+ function stripPrototypePollution(value) {
1360
+ return strip(value);
1361
+ }
1362
+ function strip(value) {
1363
+ if (value === null || value === void 0) return value;
1364
+ if (typeof value !== "object") return value;
1365
+ if (Array.isArray(value)) return value.map(strip);
1366
+ const out = {};
1367
+ for (const [k, v] of Object.entries(value)) {
1368
+ if (FORBIDDEN_KEYS.has(k)) continue;
1369
+ out[k] = strip(v);
1370
+ }
1371
+ return out;
1372
+ }
1373
+
1374
+ // built-in-plugins/parsers/frontmatter-yaml/index.ts
1313
1375
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1314
- var FORBIDDEN_FRONTMATTER_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1315
1376
  var frontmatterYamlParser = {
1316
1377
  id: "frontmatter-yaml",
1317
1378
  parse(raw, _path) {
@@ -1319,20 +1380,30 @@ var frontmatterYamlParser = {
1319
1380
  if (!match) return { frontmatterRaw: "", frontmatter: {}, body: raw };
1320
1381
  const frontmatterRaw = match[1];
1321
1382
  const body = match[2];
1322
- const parsed = {};
1383
+ let parsed = {};
1384
+ const issues = [];
1323
1385
  try {
1324
1386
  const doc = yaml.load(frontmatterRaw, { schema: yaml.JSON_SCHEMA });
1325
1387
  if (doc && typeof doc === "object" && !Array.isArray(doc)) {
1326
- for (const [k, v] of Object.entries(doc)) {
1327
- if (FORBIDDEN_FRONTMATTER_KEYS.has(k)) continue;
1328
- parsed[k] = v;
1329
- }
1388
+ parsed = stripPrototypePollution(doc);
1330
1389
  }
1331
- } catch {
1390
+ } catch (err) {
1391
+ issues.push({
1392
+ code: "frontmatter-parse-error",
1393
+ message: sanitiseParseErrorMessage(err)
1394
+ });
1332
1395
  }
1333
- return { frontmatterRaw, frontmatter: parsed, body };
1396
+ const out = { frontmatterRaw, frontmatter: parsed, body };
1397
+ if (issues.length > 0) {
1398
+ return { ...out, issues };
1399
+ }
1400
+ return out;
1334
1401
  }
1335
1402
  };
1403
+ function sanitiseParseErrorMessage(err) {
1404
+ const raw = err instanceof Error ? err.message : String(err);
1405
+ return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
1406
+ }
1336
1407
 
1337
1408
  // built-in-plugins/parsers/plain/index.ts
1338
1409
  var plainParser = {
@@ -1378,7 +1449,12 @@ async function* walkContent(roots, options) {
1378
1449
  path: relPath,
1379
1450
  body: parsed.body,
1380
1451
  frontmatterRaw: parsed.frontmatterRaw,
1381
- frontmatter: parsed.frontmatter
1452
+ frontmatter: parsed.frontmatter,
1453
+ // Audit L1: forward parser diagnostics (e.g. malformed YAML)
1454
+ // through the IRawNode surface so the orchestrator can
1455
+ // convert them into warn-level kernel `Issue` rows. Omitted
1456
+ // when the parser reported no issues (happy path).
1457
+ ...parsed.issues && parsed.issues.length > 0 ? { parseIssues: parsed.issues } : {}
1382
1458
  };
1383
1459
  }
1384
1460
  }
@@ -1400,7 +1476,7 @@ async function* walkRoot(root, current, filter, extensions) {
1400
1476
  yield* walkRoot(root, full, filter, extensions);
1401
1477
  } else if (entry.isFile() && hasMatchingExtension(name, extensions)) {
1402
1478
  try {
1403
- const s = await stat(full);
1479
+ const s = await lstat(full);
1404
1480
  if (s.isFile()) yield full;
1405
1481
  } catch {
1406
1482
  }
@@ -1466,6 +1542,7 @@ function readSidecarFor(mdAbsolutePath) {
1466
1542
  issues: [{ message: `malformed YAML in ${sidecarPath}: ${err.message}` }]
1467
1543
  };
1468
1544
  }
1545
+ parsedYaml = stripPrototypePollution(parsedYaml);
1469
1546
  if (!isPlainObject(parsedYaml)) {
1470
1547
  return {
1471
1548
  parsed: null,
@@ -1531,7 +1608,7 @@ function resolveSpecRoot2() {
1531
1608
  return dirname3(indexPath);
1532
1609
  } catch {
1533
1610
  throw new Error(
1534
- "@skill-map/spec not resolvable \u2014 sidecar reader cannot load schemas."
1611
+ "@skill-map/spec not resolvable: sidecar reader cannot load schemas."
1535
1612
  );
1536
1613
  }
1537
1614
  }
@@ -1588,17 +1665,32 @@ function safeIsFile(path) {
1588
1665
  }
1589
1666
 
1590
1667
  // kernel/sidecar/store.ts
1591
- import { existsSync as existsSync7, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "fs";
1668
+ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
1592
1669
  import { dirname as dirname5, resolve as resolve9 } from "path";
1593
1670
  import { createRequire as createRequire4 } from "module";
1594
1671
  import { Ajv2020 as Ajv20205 } from "ajv/dist/2020.js";
1595
1672
  import yaml3 from "js-yaml";
1596
1673
 
1674
+ // core/config/atomic-write.ts
1675
+ import {
1676
+ closeSync,
1677
+ constants as fsConstants,
1678
+ existsSync as existsSync5,
1679
+ mkdirSync,
1680
+ openSync,
1681
+ readFileSync as readFileSync6,
1682
+ renameSync,
1683
+ unlinkSync,
1684
+ writeSync
1685
+ } from "fs";
1686
+ import { randomBytes } from "crypto";
1687
+ import { dirname as dirname4 } from "path";
1688
+
1597
1689
  // core/config/helper.ts
1598
1690
  import { isAbsolute as isAbsolute2, resolve as resolve8 } from "path";
1599
1691
 
1600
1692
  // kernel/config/loader.ts
1601
- import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
1693
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
1602
1694
 
1603
1695
  // kernel/util/skill-map-paths.ts
1604
1696
  import { join as join5 } from "path";
@@ -1614,17 +1706,6 @@ var GITIGNORE_ENTRIES = [
1614
1706
  `${SKILL_MAP_DIR}/${DB_FILENAME}`
1615
1707
  ];
1616
1708
 
1617
- // core/config/atomic-write.ts
1618
- import {
1619
- existsSync as existsSync6,
1620
- mkdirSync,
1621
- readFileSync as readFileSync7,
1622
- renameSync,
1623
- unlinkSync,
1624
- writeFileSync
1625
- } from "fs";
1626
- import { dirname as dirname4 } from "path";
1627
-
1628
1709
  // kernel/orchestrator/node-build.ts
1629
1710
  import { createHash } from "crypto";
1630
1711
  import { existsSync as existsSync8 } from "fs";
@@ -1773,11 +1854,11 @@ function resolveSidecarOverlay(relativePath, nodePathForIssue, roots, liveBodyHa
1773
1854
  liveFrontmatterHash
1774
1855
  });
1775
1856
  return {
1776
- // R15 closure (2026-05-07) surface the full parsed root on the
1857
+ // R15 closure (2026-05-07), surface the full parsed root on the
1777
1858
  // overlay so BFF consumers (UI inspector audit / plugin-contributions
1778
1859
  // / debug panels) can read `for.*`, `audit.*`, `settings.*`, and
1779
1860
  // plugin-namespaced sub-keys without re-reading the file. The
1780
- // `annotations` field above stays it duplicates `root.annotations`
1861
+ // `annotations` field above stays, it duplicates `root.annotations`
1781
1862
  // by design so existing consumers keep working unchanged.
1782
1863
  overlay: {
1783
1864
  present: true,
@@ -1821,6 +1902,16 @@ function buildFreshNodeAndValidateFrontmatter(opts) {
1821
1902
  encoder: opts.encoder
1822
1903
  });
1823
1904
  const frontmatterIssues = [];
1905
+ if (opts.raw.parseIssues && opts.raw.parseIssues.length > 0) {
1906
+ for (const pi of opts.raw.parseIssues) {
1907
+ frontmatterIssues.push({
1908
+ analyzerId: pi.code,
1909
+ severity: opts.strict ? "error" : "warn",
1910
+ nodeIds: [opts.raw.path],
1911
+ message: pi.message
1912
+ });
1913
+ }
1914
+ }
1824
1915
  if (opts.raw.frontmatterRaw.length > 0) {
1825
1916
  const fmIssue = validateFrontmatter(
1826
1917
  opts.providerFrontmatter,
@@ -1847,10 +1938,9 @@ function mergeNodeWithEnrichments(node, enrichments, opts = {}) {
1847
1938
  }
1848
1939
  return base;
1849
1940
  }
1850
- var FORBIDDEN_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1851
1941
  function assignSafe(target, source) {
1852
- for (const [k, v] of Object.entries(source)) {
1853
- if (FORBIDDEN_MERGE_KEYS.has(k)) continue;
1942
+ const safe = stripPrototypePollution(source);
1943
+ for (const [k, v] of Object.entries(safe)) {
1854
1944
  target[k] = v;
1855
1945
  }
1856
1946
  }
@@ -2129,6 +2219,9 @@ async function runScanInternal(_kernel, options) {
2129
2219
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
2130
2220
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
2131
2221
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
2222
+ const registeredActionIds = new Set(
2223
+ _kernel.registry.all("action").map((a) => qualifiedExtensionId(a.pluginId, a.id))
2224
+ );
2132
2225
  const analyzerResult = await runAnalyzers(
2133
2226
  exts.analyzers,
2134
2227
  walked.nodes,
@@ -2140,6 +2233,7 @@ async function runScanInternal(_kernel, options) {
2140
2233
  options.orphanJobFiles ?? [],
2141
2234
  options.referenceablePaths,
2142
2235
  options.cwd,
2236
+ registeredActionIds,
2143
2237
  emitter,
2144
2238
  hookDispatcher
2145
2239
  );
@@ -2261,7 +2355,8 @@ function createChokidarWatcher(opts) {
2261
2355
  const watcher = chokidar.watch(absRoots, {
2262
2356
  ignoreInitial: true,
2263
2357
  persistent: true,
2264
- ...ignored ? { ignored } : {}
2358
+ ...ignored ? { ignored } : {},
2359
+ ...opts.depth !== void 0 ? { depth: opts.depth } : {}
2265
2360
  });
2266
2361
  let pending = [];
2267
2362
  let timer = null;