@skill-map/cli 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/sm.js +3 -3
- package/dist/cli/tutorial/sm-tutorial.md +22 -4
- package/dist/cli.js +1450 -686
- package/dist/cli.js.map +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.js.map +1 -1
- package/dist/index.js +155 -60
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +383 -247
- package/dist/kernel/index.js +155 -60
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/{chunk-GXRWH2VL.js → chunk-2TPMJJYQ.js} +1 -1
- package/dist/ui/chunk-4BVLXZO3.js +61 -0
- package/dist/ui/{chunk-MPMBTIUR.js → chunk-BMAKIDAV.js} +30 -30
- package/dist/ui/{chunk-VVOEPDQD.js → chunk-GJJZ5QH6.js} +1 -1
- package/dist/ui/chunk-I7EELB7M.js +1 -0
- package/dist/ui/chunk-K47RR2IO.js +251 -0
- package/dist/ui/{chunk-OPPQMCMQ.js → chunk-KRNW54CI.js} +1 -1
- package/dist/ui/chunk-NJ4PSNK3.js +965 -0
- package/dist/ui/{chunk-W2EFGI3J.js → chunk-OU26UMVW.js} +1 -1
- package/dist/ui/chunk-QGRY6MDS.js +123 -0
- package/dist/ui/chunk-SCSYN7U2.js +1 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/main-CVCJMGY5.js +2 -0
- package/dist/ui/{styles-M2FETVAG.css → styles-ALBMEXCF.css} +1 -1
- package/package.json +4 -4
- package/dist/ui/chunk-25AWRVIC.js +0 -965
- package/dist/ui/chunk-GETTEQ3S.js +0 -123
- package/dist/ui/chunk-HC6PNQMW.js +0 -251
- package/dist/ui/chunk-HJHWJTFH.js +0 -1
- package/dist/ui/chunk-MF2M6GYF.js +0 -1
- package/dist/ui/chunk-V3SZQETX.js +0 -61
- package/dist/ui/main-Q2WC254P.js +0 -2
|
@@ -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.
|
|
103
|
+
version: "0.23.0",
|
|
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.
|
|
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.
|
|
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.
|
|
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}})
|
|
275
|
-
dedicatedValidationFailed: "plugin '{{pluginId}}' ctx.store.write('{{table}}', row): row violates declared schema ({{schemaPath}})
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
1853
|
-
|
|
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;
|