@narrativetrace/cli 0.1.3 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/doctor/doc-urls.ts","../src/doctor/finding.ts","../src/doctor/checks/approval-traces.ts","../src/doctor/checks/llms-before-you-start.ts","../src/semver-lite.ts","../src/doctor/checks/node-engine.ts","../src/doctor/checks/output-env.ts","../src/doctor/checks/parameter-arg0.ts","../src/doctor/checks/redaction-proof.ts","../src/doctor/checks/reporter-subpath.ts","../src/doctor/checks/sibling-packages.ts","../src/doctor/checks/silent-sink.ts","../src/doctor/checks/trace-object-keys.ts","../src/doctor/checks/vitest-peer.ts","../src/doctor/doctor.ts","../src/doctor/environment.ts","../src/doctor/render.ts"],"sourcesContent":["// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nexport { DOCTOR_CHECKS, runDoctor } from \"./doctor/doctor.js\";\nexport { buildSnapshot } from \"./doctor/environment.js\";\nexport { renderHuman, renderJson } from \"./doctor/render.js\";\nexport type {\n DoctorCheck,\n DoctorReport,\n DoctorSnapshot,\n Env,\n Finding,\n FindingStatus,\n PackageJsonLike,\n} from \"./doctor/types.js\";\nexport type { Version } from \"./semver-lite.js\";\nexport { compareVersions, parseVersion, satisfiesRange } from \"./semver-lite.js\";\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\n/**\n * Public doc anchors every finding points at. The TypeScript runtime's guides are not yet\n * published on narrativetrace.ai (only the Java docs are, as of 2026-09 — see the doctor P1 run's\n * report for the amendment note); this repository is public on GitHub\n * (`publishConfig.access: \"public\"` on every package), so a stable blob link into `documentation/`\n * on `main` is the honest \"public docs URL\" today. Swap the base once the TS guides land on the\n * site — the anchors (GitHub's own heading slugs) do not change.\n */\nconst BASE = \"https://github.com/narrativetrace/narrativetrace-typescript/blob/main/documentation/\";\n\nexport const DOC = {\n installationPrerequisites: `${BASE}installation-guide.md#prerequisites`,\n installationDependencies: `${BASE}installation-guide.md#1-add-dependencies`,\n vitestConfiguration: `${BASE}configuration-guide.md#2-vitest-configuration`,\n whereSettingsComeFrom: `${BASE}configuration-guide.md#2b-where-settings-come-from`,\n proxyOptions: `${BASE}configuration-guide.md#6-proxy-options`,\n eventPipelineBuffering: `${BASE}configuration-guide.md#8-event-pipeline-buffering-bufferedeventconsumer`,\n noTraceFilesWritten: `${BASE}troubleshooting.md#no-trace-files-are-written`,\n manualParameterNames: `${BASE}troubleshooting.md#parameters-show-as-arg0-arg1`,\n redactionSurfaceBySurface: `${BASE}privacy-and-redaction.md#redaction-surface-by-surface`,\n approvalTracesEndToEnd: `${BASE}structural-trace-format.md#approval-traces-end-to-end`,\n sixtySecondsNewProject: `${BASE}sixty-seconds.md#1-new-project-add-the-packages`,\n} as const;\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport type { Finding, FindingStatus } from \"./types.js\";\n\n/** A passing finding: no fix needed, by construction (see {@link Finding.fix}). */\nexport function pass(id: string, message: string, docUrl: string): Finding {\n return { id, status: \"pass\" as FindingStatus, message, fix: \"\", docUrl };\n}\n\n/** A failing finding: `fix` is mandatory — a fail with nothing to do about it is a wording bug. */\nexport function fail(id: string, message: string, fix: string, docUrl: string): Finding {\n return { id, status: \"fail\" as FindingStatus, message, fix, docUrl };\n}\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"trap.approval-traces\";\nconst FIX =\n \"Review each .received.nt diff against its .approved.nt baseline, then run pnpm run approve-narratives (or narrativetrace-approve) to promote it, or delete it if the change was wrong — never commit a .received.nt file.\";\n\n/**\n * Approval traces (`.approved.nt` reviewed baselines, `.received.nt` written on a mismatch) landed\n * in the runtime's structural-trace layer. A `.received.nt` sitting in the approved directory is a\n * reviewed-but-not-yet-resolved diff — stale ones are exactly what `what-to-commit.md` warns never\n * to commit, and exactly what a doctor run should surface before someone else does.\n */\nexport const checkApprovalTraces: DoctorCheck = (snapshot) => {\n const paths = [...snapshot.approvedDirFiles.keys()];\n if (paths.length === 0) {\n return pass(\n ID,\n \"no approval traces configured yet — nothing to check\",\n DOC.approvalTracesEndToEnd,\n );\n }\n const received = paths.filter((p) => p.endsWith(\".received.nt\"));\n if (received.length > 0) {\n const message = `${received.length} stale received trace(s) found: ${received.join(\", \")}`;\n return fail(ID, message, FIX, DOC.approvalTracesEndToEnd);\n }\n const approved = paths.filter((p) => p.endsWith(\".approved.nt\"));\n const message = `${approved.length} approved trace(s) found, no pending received diffs`;\n return pass(ID, message, DOC.approvalTracesEndToEnd);\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"trap.llms-before-you-start\";\nconst PLAIN_JS = /\\.js$/;\nconst ESM_IMPORT = /^\\s*import\\s/m;\nconst FIX =\n \"Run `npm pkg set type=module` before `npm add` — without it, Node throws SyntaxError: Cannot use import statement outside a module (pnpm/yarn default to it and never hit this).\";\n\n/**\n * `llms.txt`'s first \"before you start\" trap: a plain `.js` file using `import` syntax with no\n * `\"type\": \"module\"` in `package.json` throws `SyntaxError: Cannot use import statement outside a\n * module` the moment Node loads it — npm's default (unlike pnpm/yarn) does not set this for you.\n */\nexport const checkLlmsBeforeYouStart: DoctorCheck = (snapshot) => {\n if (snapshot.rootPackageJson?.type === \"module\") {\n return pass(ID, 'package.json declares \"type\": \"module\"', DOC.sixtySecondsNewProject);\n }\n const offender = [...snapshot.sourceFiles].find(\n ([path, content]) => PLAIN_JS.test(path) && ESM_IMPORT.test(content),\n );\n if (!offender) {\n const message = 'no plain .js file uses ESM import syntax without \"type\": \"module\"';\n return pass(ID, message, DOC.sixtySecondsNewProject);\n }\n const [path] = offender;\n const message = `${path} uses ESM import syntax but package.json has no \"type\": \"module\"`;\n return fail(ID, message, FIX, DOC.sixtySecondsNewProject);\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\n/**\n * Minimal semver range matcher covering exactly the range shapes this repository's own\n * `package.json` files use (`>=20`, `^1.0.0 || ^2.0.0 || ^3.0.0`, `1.x`): the doctor never needs to\n * parse an arbitrary npm range, only the ones NarrativeTrace itself declares in `engines` and\n * `peerDependencies`. A real `semver` dependency would cover forms this codebase never emits.\n */\n\nexport interface Version {\n readonly major: number;\n readonly minor: number;\n readonly patch: number;\n}\n\n/** Parses `\"20.11.0\"`, `\"v20.11.0\"`, `\"20\"`, or `\"20.11\"` — missing parts default to zero. */\nexport function parseVersion(raw: string): Version | undefined {\n const cleaned = raw.trim().replace(/^v/, \"\");\n const match = /^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?/.exec(cleaned);\n if (!match) return undefined;\n return {\n major: Number(match[1]),\n minor: match[2] === undefined ? 0 : Number(match[2]),\n patch: match[3] === undefined ? 0 : Number(match[3]),\n };\n}\n\n/** Three-way compare, standard sign convention: negative if `left` < `right`, positive if greater. */\nexport function compareVersions(left: Version, right: Version): number {\n if (left.major !== right.major) return left.major - right.major;\n if (left.minor !== right.minor) return left.minor - right.minor;\n return left.patch - right.patch;\n}\n\nfunction satisfiesGte(version: Version, floor: Version): boolean {\n return compareVersions(version, floor) >= 0;\n}\n\n/** Standard caret semantics: `^1.2.3` := `>=1.2.3 <2.0.0`; `^0.2.3` := `>=0.2.3 <0.3.0`; `^0.0.3` := `>=0.0.3 <0.0.4`. */\nfunction satisfiesCaret(version: Version, base: Version): boolean {\n if (compareVersions(version, base) < 0) return false;\n if (base.major > 0) return version.major === base.major;\n if (base.minor > 0) return version.major === 0 && version.minor === base.minor;\n return version.major === 0 && version.minor === 0 && version.patch === base.patch;\n}\n\n/**\n * One `||`-separated clause: `^x.y.z`, `>=x.y.z`, `x.x`/`x.y.x` (caret-equivalent), or an exact\n * version. `trimmed` must already be whitespace-trimmed — {@link satisfiesRange} does that once\n * for every clause it splits out, so the prefix checks below (`startsWith(\"^\")`) see it without a\n * leading space.\n */\nfunction satisfiesClause(version: Version, trimmed: string): boolean {\n if (trimmed.startsWith(\"^\")) {\n const base = parseVersion(trimmed.slice(1));\n return base !== undefined && satisfiesCaret(version, base);\n }\n if (trimmed.startsWith(\">=\")) {\n const floor = parseVersion(trimmed.slice(2));\n return floor !== undefined && satisfiesGte(version, floor);\n }\n if (/\\.x\\b/.test(trimmed)) {\n const base = parseVersion(trimmed.replace(/\\.x/g, \".0\"));\n return base !== undefined && satisfiesCaret(version, base);\n }\n const exact = parseVersion(trimmed);\n return exact !== undefined && compareVersions(version, exact) === 0;\n}\n\n/**\n * Whether `versionRaw` satisfies `range` (`||`-separated clauses; a version satisfies the range if\n * it satisfies any one clause). Unparseable input on either side is a mismatch, never a throw — a\n * doctor check reports a finding, it does not crash the run.\n */\nexport function satisfiesRange(versionRaw: string, range: string): boolean {\n const version = parseVersion(versionRaw);\n if (!version) return false;\n // Stryker disable next-line MethodExpression: dropping .filter(Boolean) is equivalent here —\n // an empty clause (from \"||\" or a trailing/leading \"||\") only ever adds a `satisfiesClause(v,\n // \"\")` call to the .some() chain, and that always returns false (parseVersion(\"\") is\n // undefined, and \"\" matches none of the prefix checks either), so it can never flip the\n // overall result. The filter is documentation of intent, not a behavior the type checker or a\n // mutation test can observe.\n return range\n .split(\"||\")\n .map((clause) => clause.trim())\n .filter(Boolean)\n .some((clause) => satisfiesClause(version, clause));\n}\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { satisfiesRange } from \"../../semver-lite.js\";\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"toolchain.node-engine\";\nconst DEFAULT_ENGINE_RANGE = \">=20\";\n\n/** Node version required by whichever core NarrativeTrace package is installed, else the repo default. */\nfunction requiredRange(snapshot: Parameters<DoctorCheck>[0]): string {\n const core = snapshot.installedPackages.get(\"@narrativetrace/core\");\n const coreNode = snapshot.installedPackages.get(\"@narrativetrace/core-node\");\n return core?.engines?.node ?? coreNode?.engines?.node ?? DEFAULT_ENGINE_RANGE;\n}\n\n/** Node engines (Installation Guide §Prerequisites): the running Node must satisfy the installed package's `engines.node`. */\nexport const checkNodeEngine: DoctorCheck = (snapshot) => {\n const required = requiredRange(snapshot);\n if (satisfiesRange(snapshot.nodeVersion, required)) {\n return pass(\n ID,\n `Node ${snapshot.nodeVersion} satisfies the required ${required}`,\n DOC.installationPrerequisites,\n );\n }\n return fail(\n ID,\n `Node ${snapshot.nodeVersion} does not satisfy the required ${required}`,\n `Upgrade Node to a version satisfying ${required} (nvm, volta, asdf, or your CI image).`,\n DOC.installationPrerequisites,\n );\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"config.output-env\";\nconst VALID_VALUES = new Set([\"true\", \"false\"]);\n\n/** `NARRATIVETRACE_OUTPUT`, if set, must be exactly `\"true\"` or `\"false\"` (case-insensitive). */\nexport const checkOutputEnv: DoctorCheck = (snapshot) => {\n const raw = snapshot.env.NARRATIVETRACE_OUTPUT;\n if (raw === undefined) {\n return pass(\n ID,\n \"NARRATIVETRACE_OUTPUT is not set — output stays on its default (on)\",\n DOC.whereSettingsComeFrom,\n );\n }\n if (VALID_VALUES.has(raw.toLowerCase())) {\n return pass(ID, `NARRATIVETRACE_OUTPUT=${raw}`, DOC.whereSettingsComeFrom);\n }\n return fail(\n ID,\n `NARRATIVETRACE_OUTPUT is set to '${raw}', which is neither \"true\" nor \"false\"`,\n \"Set NARRATIVETRACE_OUTPUT=true or NARRATIVETRACE_OUTPUT=false (or unset it) — any other value is read as a truthy string, most likely turning output on when you meant to turn it off.\",\n DOC.whereSettingsComeFrom,\n );\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"trap.parameter-arg0\";\nconst ARG_PLACEHOLDER = /\\barg0\\b/;\nconst FIX =\n 'Pass parameter names explicitly: traceObject(target, context, { methodName: [\"paramA\", \"paramB\"] }) — required for classes you do not own, or when a build tool strips them.';\n\n/**\n * Parameter names of classes you don't own (or a build that strips them) are lost at compile\n * time and render as `arg0`, `arg1`, ... Doctor reads already-rendered output, if any exists, and\n * flags the tell-tale placeholder rather than guessing from source.\n */\nexport const checkParameterArg0: DoctorCheck = (snapshot) => {\n if (snapshot.outputFiles.size === 0) {\n const message = \"no rendered output found yet — run your tests or app once to check this\";\n return pass(ID, message, DOC.manualParameterNames);\n }\n const offender = [...snapshot.outputFiles].find(([, content]) => ARG_PLACEHOLDER.test(content));\n if (!offender) {\n const message = \"rendered output carries real parameter names — no arg0 placeholders found\";\n return pass(ID, message, DOC.manualParameterNames);\n }\n const [path] = offender;\n const message = `rendered output shows arg0-style placeholders (first seen in ${path}) — parameter names were not captured`;\n return fail(ID, message, FIX, DOC.manualParameterNames);\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"trap.redaction-proof\";\nconst TEST_FILE = /\\.(test|spec)\\.[cm]?[jt]sx?$/;\nconst REDACTED_ASSERTION = /\\[REDACTED\\]/;\n\n/**\n * Redaction is a security property; the trap named across every study is trusting it by\n * inspection rather than proving it — importing the redaction primitives and never asserting on\n * their output. This looks for a test that actually asserts the literal `[REDACTED]` marker.\n */\nexport const checkRedactionProof: DoctorCheck = (snapshot) => {\n const testFiles = [...snapshot.sourceFiles].filter(([path]) => TEST_FILE.test(path));\n const proven = testFiles.some(([, content]) => REDACTED_ASSERTION.test(content));\n if (proven) {\n return pass(\n ID,\n \"a test asserts [REDACTED] for a deny-listed parameter name\",\n DOC.redactionSurfaceBySurface,\n );\n }\n return fail(\n ID,\n \"no test asserts [REDACTED] — redaction is unproven\",\n 'Render a call with a deny-listed parameter name (e.g. \"password\", \"token\") in a test and assert the output contains \"[REDACTED]\" — and that a neighboring, non-sensitive value is still present, so an over-broad redaction also fails.',\n DOC.redactionSurfaceBySurface,\n );\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"config.reporter-subpath\";\nconst VITEST_CONFIG_NAME = /(^|\\/)vitest\\.config\\.[cm]?[jt]s$/;\nconst REPORTER_NAMES = /\\b(ClaritySuiteReporter|GlossarySuiteReporter|StructuralSuiteReporter)\\b/;\nconst ROOT_IMPORT = /from\\s+[\"']@narrativetrace\\/vitest[\"']/;\nconst SUBPATH_IMPORT = /from\\s+[\"']@narrativetrace\\/vitest\\/reporters[\"']/;\n\nfunction importsFromRoot(content: string): boolean {\n return REPORTER_NAMES.test(content) && ROOT_IMPORT.test(content) && !SUBPATH_IMPORT.test(content);\n}\n\n/**\n * `ClaritySuiteReporter`/`GlossarySuiteReporter`/`StructuralSuiteReporter` must be imported from\n * the `/reporters` subpath — importing them from the package root also loads `vitest` itself,\n * which crashes config loading on every version (llms.txt \"Before you start\").\n */\nexport const checkReporterSubpath: DoctorCheck = (snapshot) => {\n const configs = [...snapshot.sourceFiles].filter(([path]) => VITEST_CONFIG_NAME.test(path));\n const offender = configs.find(([, content]) => importsFromRoot(content));\n if (offender) {\n const [path] = offender;\n return fail(\n ID,\n `${path} imports a NarrativeTrace vitest reporter from the package root, not the /reporters subpath`,\n 'Import reporters from \"@narrativetrace/vitest/reporters\" — importing from the package root also loads vitest itself and crashes config loading.',\n DOC.vitestConfiguration,\n );\n }\n const message =\n configs.length === 0\n ? \"no vitest.config found — nothing to check\"\n : \"every registered reporter is imported from the /reporters subpath\";\n return pass(ID, message, DOC.vitestConfiguration);\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"toolchain.sibling-packages\";\n\n/**\n * `@narrativetrace/vitest` depends on five sibling `@narrativetrace/*` packages (clarity,\n * core-node, diagrams, glossary, proxy — not peers, regular dependencies). Under pnpm's default\n * strict layout those resolve fine from inside `@narrativetrace/vitest`'s own `node_modules`, but a\n * consumer that also imports one of them directly needs it resolvable from ITS OWN root too, or\n * pnpm's non-hoisting layout hands the app a second, unrelated copy. This check reads the sibling\n * list from the installed package itself (never a hardcoded list — it tracks the dependency as it\n * grows) and confirms each one resolves from the consumer.\n */\nexport const checkSiblingPackages: DoctorCheck = (snapshot) => {\n const ntVitest = snapshot.installedPackages.get(\"@narrativetrace/vitest\");\n if (!ntVitest) {\n const message = \"@narrativetrace/vitest is not installed — nothing to check\";\n return pass(ID, message, DOC.installationDependencies);\n }\n const siblings = Object.keys(ntVitest.dependencies ?? {}).filter((n) =>\n n.startsWith(\"@narrativetrace/\"),\n );\n const unresolved = siblings.filter((name) => !snapshot.installedPackages.has(name));\n if (unresolved.length === 0) {\n const message = `all ${siblings.length} sibling package(s) of @narrativetrace/vitest resolve from the consumer`;\n return pass(ID, message, DOC.installationDependencies);\n }\n const message = `${unresolved.length} sibling package(s) of @narrativetrace/vitest do not resolve from the consumer: ${unresolved.join(\", \")}`;\n const fix = `Add ${unresolved.join(\", \")} as explicit direct dependencies — pnpm's strict layout does not hoist a dependency's own transitive dependencies to your project root.`;\n return fail(ID, message, fix, DOC.installationDependencies);\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"trap.silent-sink\";\nconst TRACE_OBJECT_CALL = /\\btraceObject\\s*\\(/;\nconst SINK_SIGNAL =\n /\\b(BufferedEventConsumer|captureTrace|registerConsumer|createNarrativeTest|narrativeTest)\\b|@narrativetrace\\/(vitest|pino|winston|opentelemetry|observability)\\b/;\n\n/**\n * The silent-sink trap: `traceObject()` proxies calls into events, but nothing narrates unless a\n * consumer/sink is attached (`BufferedEventConsumer`, `captureTrace()`, a log/OTel bridge, or the\n * vitest integration, which is its own sink). Wrapping without one of these is a project that looks\n * instrumented and narrates to nowhere.\n */\nexport const checkSilentSink: DoctorCheck = (snapshot) => {\n const contents = [...snapshot.sourceFiles.values()];\n if (!contents.some((content) => TRACE_OBJECT_CALL.test(content))) {\n return pass(ID, \"traceObject() is not used — nothing to check\", DOC.noTraceFilesWritten);\n }\n if (contents.some((content) => SINK_SIGNAL.test(content))) {\n return pass(\n ID,\n \"traceObject() is used and a consumer/sink is attached\",\n DOC.noTraceFilesWritten,\n );\n }\n return fail(\n ID,\n \"traceObject() is used but no consumer or sink (BufferedEventConsumer, captureTrace(), a log/OTel bridge, or @narrativetrace/vitest) was found\",\n \"Attach a sink: pass a BufferedEventConsumer to your pipeline, call captureTrace() and do something with the tree, or wire a log/OTel bridge — otherwise every traced call narrates to nowhere.\",\n DOC.noTraceFilesWritten,\n );\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck } from \"../types.js\";\n\nconst ID = \"config.trace-object-keys\";\n\n// The valid nested shape is `methods: { methodName: { params: [...], ... } }` — a per-method\n// config OBJECT. The trap is skipping the method-config level and putting the param array\n// straight under a method name (`methods: { methodName: [...] }`), which now throws naming the\n// accepted per-method keys rather than the silent no-op it used to be. `[^{}]*` stops the match at\n// the first nested `{`, so the legitimate form (an object, not an array, under the method name)\n// never matches here.\nconst OLD_SHAPE = /\\bmethods\\s*:\\s*\\{[^{}]*:\\s*\\[/;\n\n/**\n * `traceObject`'s options used to silently no-op when a per-method entry skipped straight to an\n * array instead of the `{ params: [...] }` object (now throws, naming the accepted keys) — a\n * source file still shaped that way needs the method-config object added back, not just an\n * upgrade.\n */\nexport const checkTraceObjectKeys: DoctorCheck = (snapshot) => {\n const offender = [...snapshot.sourceFiles].find(([, content]) => OLD_SHAPE.test(content));\n if (offender) {\n const [path] = offender;\n return fail(\n ID,\n `${path} calls traceObject(...) with a method entry that skips the per-method config object`,\n \"Nest the parameter names under params: traceObject(target, context, { methods: { methodName: { params: [...] } } }) — a bare array under the method name throws.\",\n DOC.proxyOptions,\n );\n }\n return pass(\n ID,\n \"no traceObject(...) call uses the old { methods: { ... } } option shape\",\n DOC.proxyOptions,\n );\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { satisfiesRange } from \"../../semver-lite.js\";\nimport { DOC } from \"../doc-urls.js\";\nimport { fail, pass } from \"../finding.js\";\nimport type { DoctorCheck, Finding, PackageJsonLike } from \"../types.js\";\n\nconst ID = \"toolchain.vitest-peer\";\n\nfunction noVitestInstalled(range: string): Finding {\n const message = `@narrativetrace/vitest requires a peer vitest@${range}, but no vitest install was found`;\n const fix = `Install a vitest version satisfying ${range} (npm add -D vitest, then npm ci for a clean lockfile install).`;\n return fail(ID, message, fix, DOC.vitestConfiguration);\n}\n\nfunction mismatchedVitest(range: string, installed: string): Finding {\n const message = `vitest@${installed} does not satisfy @narrativetrace/vitest's declared peer range ${range}`;\n const fix = `Install a vitest version satisfying ${range}, then reinstall clean (rm -rf node_modules && npm ci) — a mismatched peer here is the one failure mode that breaks the library's own build.`;\n return fail(ID, message, fix, DOC.vitestConfiguration);\n}\n\nfunction evaluateInstalledVitest(range: string, vitest: PackageJsonLike | undefined): Finding {\n if (!vitest?.version) return noVitestInstalled(range);\n if (satisfiesRange(vitest.version, range)) {\n const message = `vitest@${vitest.version} satisfies the declared peer range ${range}`;\n return pass(ID, message, DOC.vitestConfiguration);\n }\n return mismatchedVitest(range, vitest.version);\n}\n\n/** The installed `vitest` version must satisfy `@narrativetrace/vitest`'s declared peer range. */\nexport const checkVitestPeer: DoctorCheck = (snapshot) => {\n const ntVitest = snapshot.installedPackages.get(\"@narrativetrace/vitest\");\n const range = ntVitest?.peerDependencies?.vitest;\n if (!ntVitest || !range) {\n return pass(\n ID,\n \"@narrativetrace/vitest is not installed — nothing to check\",\n DOC.vitestConfiguration,\n );\n }\n return evaluateInstalledVitest(range, snapshot.installedPackages.get(\"vitest\"));\n};\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { checkApprovalTraces } from \"./checks/approval-traces.js\";\nimport { checkLlmsBeforeYouStart } from \"./checks/llms-before-you-start.js\";\nimport { checkNodeEngine } from \"./checks/node-engine.js\";\nimport { checkOutputEnv } from \"./checks/output-env.js\";\nimport { checkParameterArg0 } from \"./checks/parameter-arg0.js\";\nimport { checkRedactionProof } from \"./checks/redaction-proof.js\";\nimport { checkReporterSubpath } from \"./checks/reporter-subpath.js\";\nimport { checkSiblingPackages } from \"./checks/sibling-packages.js\";\nimport { checkSilentSink } from \"./checks/silent-sink.js\";\nimport { checkTraceObjectKeys } from \"./checks/trace-object-keys.js\";\nimport { checkVitestPeer } from \"./checks/vitest-peer.js\";\nimport type { DoctorCheck, DoctorReport, DoctorSnapshot } from \"./types.js\";\n\n/**\n * Every check `narrativetrace doctor` runs, in stable, documented order. Adding a check means\n * appending here — the id is what stays stable across releases, not the position.\n */\nexport const DOCTOR_CHECKS: readonly DoctorCheck[] = [\n checkNodeEngine,\n checkVitestPeer,\n checkSiblingPackages,\n checkOutputEnv,\n checkReporterSubpath,\n checkTraceObjectKeys,\n checkSilentSink,\n checkParameterArg0,\n checkRedactionProof,\n checkApprovalTraces,\n checkLlmsBeforeYouStart,\n];\n\n/** Runs every check over `snapshot` and derives the process exit code. Read-only: mutates nothing. */\nexport function runDoctor(snapshot: DoctorSnapshot): DoctorReport {\n const findings = DOCTOR_CHECKS.map((check) => check(snapshot));\n const exitCode = findings.some((f) => f.status === \"fail\") ? 1 : 0;\n return { findings, exitCode };\n}\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport { readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { join, relative } from \"node:path\";\nimport type { DoctorSnapshot, Env, PackageJsonLike } from \"./types.js\";\n\nconst EXCLUDED_DIRS = new Set([\n // Stryker disable next-line StringLiteral: equivalent — every dot-prefixed entry name is\n // already excluded earlier, in visitEntry's leading-dot check (the only exception there is\n // \".env\", which isn't a directory this set would ever mention), so \".git\" never actually\n // reaches this set's `.has()` check.\n \".git\",\n // Stryker disable next-line StringLiteral: same equivalence as \".git\" above.\n \".turbo\",\n // Stryker disable next-line StringLiteral: same equivalence as \".git\" above.\n \".stryker-tmp\",\n // Not dot-prefixed, so NOT equivalent — reaching this set's `.has()` check is the only thing\n // that excludes each of the four names below; each is covered by a real test.\n \"node_modules\",\n \"dist\",\n \"build\",\n \"coverage\",\n]);\n\nconst SOURCE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\", \".mjs\", \".cjs\", \".mts\", \".cts\"];\n\n/** Well-known package names the checks resolve directly; siblings of @narrativetrace/vitest are added dynamically. */\nconst BASE_PACKAGES = [\n \"vitest\",\n \"@narrativetrace/core\",\n \"@narrativetrace/core-node\",\n \"@narrativetrace/vitest\",\n];\n\nconst MAX_FILES = 20_000;\n\ntype Bucket = \"output\" | \"approved\" | \"source\" | \"skip\";\n\ninterface WalkState {\n readonly sourceFiles: Map<string, string>;\n readonly outputFiles: Map<string, string>;\n readonly approvedDirFiles: Map<string, string>;\n visited: number;\n}\n\n// Stryker disable BlockStatement: the three functions below whose catch block is just `return\n// undefined;` are equivalent under mutation — an emptied `catch {}` falls off the end of the\n// function and implicitly returns `undefined` too. No test can observe a difference between the\n// explicit and implicit forms. Restored below, before safeRead/listDirectory, whose catch blocks\n// return \"\" / [] respectively and are NOT equivalent (a real behavior change is observable there).\n\nfunction readJson(path: string): PackageJsonLike | undefined {\n try {\n return JSON.parse(readFileSync(path, \"utf8\")) as PackageJsonLike;\n } catch {\n return undefined;\n }\n}\n\nfunction resolvePackageJson(name: string, cwd: string): PackageJsonLike | undefined {\n try {\n const require = createRequire(join(cwd, \"package.json\"));\n return readJson(require.resolve(`${name}/package.json`));\n } catch {\n return undefined;\n }\n}\n\n/** `undefined` when `path` cannot be stat'd at all (a dangling symlink, a permission error, a race). */\nfunction isDirectorySafe(path: string): boolean | undefined {\n try {\n return statSync(path).isDirectory();\n } catch {\n return undefined;\n }\n}\n\n// Stryker restore BlockStatement\n\nfunction safeRead(path: string): string {\n try {\n return readFileSync(path, \"utf8\");\n } catch {\n return \"\";\n }\n}\n\nfunction listDirectory(dir: string): string[] {\n try {\n return readdirSync(dir);\n } catch {\n return [];\n }\n}\n\nfunction classify(rel: string, outputDirName: string, approvedDirName: string): Bucket {\n const topSegment = rel.split(\"/\")[0];\n if (topSegment === outputDirName) return \"output\";\n if (topSegment === approvedDirName) return \"approved\";\n // Stryker disable next-line StringLiteral: the \"source\" branch string is equivalent — bucketFor\n // below routes anything that isn't \"output\" or \"approved\" into sourceFiles by default, so\n // whether this literal reads \"source\" or something else never changes which map a file lands\n // in. The \"skip\" branch is NOT equivalent (a real behavior change is observable there) and is\n // exercised by a real test.\n return SOURCE_EXTENSIONS.some((ext) => rel.endsWith(ext)) ? \"source\" : \"skip\";\n}\n\nfunction bucketFor(state: WalkState, kind: Bucket): Map<string, string> {\n return kind === \"output\"\n ? state.outputFiles\n : kind === \"approved\"\n ? state.approvedDirFiles\n : state.sourceFiles;\n}\n\ninterface WalkContext {\n readonly state: WalkState;\n readonly root: string;\n readonly output: string;\n readonly approved: string;\n readonly queue: string[];\n}\n\n/** Visits one directory entry: enqueues a subdirectory, or files it into the right bucket. */\nfunction visitEntry(ctx: WalkContext, dir: string, entry: string): void {\n if (entry.startsWith(\".\") && entry !== \".env\") return;\n const full = join(dir, entry);\n const isDir = isDirectorySafe(full);\n if (isDir === undefined) return;\n if (isDir) {\n if (!EXCLUDED_DIRS.has(entry)) ctx.queue.push(full);\n return;\n }\n ctx.state.visited++;\n const rel = relative(ctx.root, full);\n const kind = classify(rel, ctx.output, ctx.approved);\n if (kind === \"skip\") return;\n bucketFor(ctx.state, kind).set(rel, safeRead(full));\n}\n\n/**\n * Walks `root` breadth-first, bucketing files into source (extension-filtered), the output\n * directory, and the approved-trace directory — excluding `node_modules`/build/coverage noise.\n * Bounded by {@link MAX_FILES} so a doctor run in a huge repo degrades to a partial scan rather\n * than hanging. The outer loop's own `state.visited < MAX_FILES` half of that bound is equivalent\n * under mutation — the inner loop's `if (state.visited >= MAX_FILES) break;` enforces the exact\n * same cap on its own, before any further entry is ever visited, so weakening the outer guard only\n * costs a few extra, immediately aborted `listDirectory` calls on already-queued directories; no\n * test can observe a different `WalkState`.\n */\nfunction walk(root: string, output: string, approved: string): WalkState {\n const state: WalkState = {\n sourceFiles: new Map(),\n outputFiles: new Map(),\n approvedDirFiles: new Map(),\n visited: 0,\n };\n const ctx: WalkContext = { state, root, output, approved, queue: [root] };\n // Stryker disable next-line ConditionalExpression,EqualityOperator: see the doc comment above.\n while (ctx.queue.length > 0 && state.visited < MAX_FILES) {\n const dir = ctx.queue.shift() as string;\n for (const entry of listDirectory(dir)) {\n if (state.visited >= MAX_FILES) break;\n visitEntry(ctx, dir, entry);\n }\n }\n return state;\n}\n\nfunction resolveBasePackages(cwd: string): Map<string, PackageJsonLike> {\n const installed = new Map<string, PackageJsonLike>();\n for (const name of BASE_PACKAGES) {\n const pkg = resolvePackageJson(name, cwd);\n if (pkg) installed.set(name, pkg);\n }\n return installed;\n}\n\n/** Resolves `@narrativetrace/vitest`'s own `@narrativetrace/*` dependencies — the sibling check's data. */\nfunction resolveVitestSiblings(cwd: string, installed: Map<string, PackageJsonLike>): void {\n const ntVitest = installed.get(\"@narrativetrace/vitest\");\n for (const name of Object.keys(ntVitest?.dependencies ?? {})) {\n if (!name.startsWith(\"@narrativetrace/\") || installed.has(name)) continue;\n const pkg = resolvePackageJson(name, cwd);\n if (pkg) installed.set(name, pkg);\n }\n}\n\nfunction resolveInstalledPackages(cwd: string): Map<string, PackageJsonLike> {\n const installed = resolveBasePackages(cwd);\n resolveVitestSiblings(cwd, installed);\n return installed;\n}\n\n/** Builds a {@link DoctorSnapshot} from the real filesystem rooted at `cwd`. The one impure module. */\nexport function buildSnapshot(cwd: string, env: Env): DoctorSnapshot {\n const output = env.NARRATIVETRACE_OUTPUT_DIR ?? \"narrativetrace-output\";\n const approved = env.NARRATIVETRACE_APPROVED_DIR ?? \"narratives\";\n const { sourceFiles, outputFiles, approvedDirFiles } = walk(cwd, output, approved);\n return {\n cwd,\n nodeVersion: process.version.replace(/^v/, \"\"),\n env,\n rootPackageJson: readJson(join(cwd, \"package.json\")),\n sourceFiles,\n outputFiles,\n approvedDirFiles,\n installedPackages: resolveInstalledPackages(cwd),\n };\n}\n","// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nimport type { DoctorReport, Finding } from \"./types.js\";\n\nfunction renderFinding(finding: Finding): string[] {\n const tag = finding.status === \"fail\" ? \"FAIL\" : \"PASS\";\n const lines = [`[${tag}] ${finding.id} — ${finding.message}`];\n if (finding.status === \"fail\") {\n lines.push(` fix: ${finding.fix}`, ` docs: ${finding.docUrl}`);\n }\n lines.push(\"\");\n return lines;\n}\n\n/** Human-readable default output: one block per finding, worst-first (failures before passes). */\nexport function renderHuman(report: DoctorReport): string {\n const failing = report.findings.filter((f) => f.status === \"fail\");\n const passing = report.findings.filter((f) => f.status === \"pass\");\n const header = `narrativetrace doctor — ${report.findings.length} check(s), ${failing.length} finding(s)`;\n const summary =\n failing.length === 0\n ? \"All checks passed.\"\n : `${failing.length} finding(s). Exit code ${report.exitCode}.`;\n const body = [...failing, ...passing].flatMap(renderFinding);\n return [header, \"\", ...body, summary].join(\"\\n\");\n}\n\n/** Machine-readable `--json` output: the report verbatim, stable field names. */\nexport function renderJson(report: DoctorReport): string {\n return JSON.stringify(report, null, 2);\n}\n"],"mappings":"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","names":["message","ID","FIX","path","message","ID","ID","ID","FIX","message","ID","ID","ID","message","ID","ID","ID","require"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nexport {\n buildSnapshot,\n compareVersions,\n DOCTOR_CHECKS,\n type DoctorCheck,\n type DoctorReport,\n type DoctorSnapshot,\n type Env,\n type Finding,\n type FindingStatus,\n type PackageJsonLike,\n parseVersion,\n renderHuman,\n renderJson,\n runDoctor,\n satisfiesRange,\n type Version,\n} from \"@narrativetrace/tooling\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,qBAiBO;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,91 +1 @@
1
- /**
2
- * The doctor's world-view, decoupled from the real filesystem: every check is a pure function
3
- * over a {@link DoctorSnapshot}, which is why every check has both a passing and a failing unit
4
- * test with no disk I/O at all. {@link buildSnapshot} (environment.ts) is the one impure module —
5
- * it walks the real project and builds this shape once per run.
6
- */
7
- /** The process environment, as `process.env` hands it over. */
8
- type Env = Readonly<Record<string, string | undefined>>;
9
- /** The subset of a `package.json` a check ever needs to read. */
10
- interface PackageJsonLike {
11
- readonly name?: string;
12
- readonly version?: string;
13
- readonly type?: string;
14
- readonly engines?: Readonly<Record<string, string>>;
15
- readonly dependencies?: Readonly<Record<string, string>>;
16
- readonly peerDependencies?: Readonly<Record<string, string>>;
17
- }
18
- interface DoctorSnapshot {
19
- /** Absolute path to the project being checked (informational — checks never touch disk). */
20
- readonly cwd: string;
21
- /** `process.version` with the leading `v` stripped, e.g. `"20.11.0"`. */
22
- readonly nodeVersion: string;
23
- readonly env: Env;
24
- readonly rootPackageJson: PackageJsonLike | undefined;
25
- /** Relative path → content, for source/config/test files under the project (bounded walk). */
26
- readonly sourceFiles: ReadonlyMap<string, string>;
27
- /** Relative path → content, for everything under the configured output directory. */
28
- readonly outputFiles: ReadonlyMap<string, string>;
29
- /** Relative path → content, for everything under the configured approved-trace directory. */
30
- readonly approvedDirFiles: ReadonlyMap<string, string>;
31
- /** Package name → its resolved `package.json`, for packages the checks care about. */
32
- readonly installedPackages: ReadonlyMap<string, PackageJsonLike>;
33
- }
34
- type FindingStatus = "pass" | "fail";
35
- interface Finding {
36
- /** Stable, dotted id (`"toolchain.node-engine"`) — never renamed once shipped; agents and CI grep it. */
37
- readonly id: string;
38
- readonly status: FindingStatus;
39
- /** One line: what the check found, true on pass or fail. */
40
- readonly message: string;
41
- /** What to do about it. Empty string on a pass — there is nothing to fix. */
42
- readonly fix: string;
43
- /** Public doc URL the finding points at (a GitHub blob link into this repo's `documentation/`). */
44
- readonly docUrl: string;
45
- }
46
- type DoctorCheck = (snapshot: DoctorSnapshot) => Finding;
47
- interface DoctorReport {
48
- readonly findings: readonly Finding[];
49
- /** 0 — every check passed. 1 — at least one finding failed. */
50
- readonly exitCode: 0 | 1;
51
- }
52
-
53
- /**
54
- * Every check `narrativetrace doctor` runs, in stable, documented order. Adding a check means
55
- * appending here — the id is what stays stable across releases, not the position.
56
- */
57
- declare const DOCTOR_CHECKS: readonly DoctorCheck[];
58
- /** Runs every check over `snapshot` and derives the process exit code. Read-only: mutates nothing. */
59
- declare function runDoctor(snapshot: DoctorSnapshot): DoctorReport;
60
-
61
- /** Builds a {@link DoctorSnapshot} from the real filesystem rooted at `cwd`. The one impure module. */
62
- declare function buildSnapshot(cwd: string, env: Env): DoctorSnapshot;
63
-
64
- /** Human-readable default output: one block per finding, worst-first (failures before passes). */
65
- declare function renderHuman(report: DoctorReport): string;
66
- /** Machine-readable `--json` output: the report verbatim, stable field names. */
67
- declare function renderJson(report: DoctorReport): string;
68
-
69
- /**
70
- * Minimal semver range matcher covering exactly the range shapes this repository's own
71
- * `package.json` files use (`>=20`, `^1.0.0 || ^2.0.0 || ^3.0.0`, `1.x`): the doctor never needs to
72
- * parse an arbitrary npm range, only the ones NarrativeTrace itself declares in `engines` and
73
- * `peerDependencies`. A real `semver` dependency would cover forms this codebase never emits.
74
- */
75
- interface Version {
76
- readonly major: number;
77
- readonly minor: number;
78
- readonly patch: number;
79
- }
80
- /** Parses `"20.11.0"`, `"v20.11.0"`, `"20"`, or `"20.11"` — missing parts default to zero. */
81
- declare function parseVersion(raw: string): Version | undefined;
82
- /** Three-way compare, standard sign convention: negative if `left` < `right`, positive if greater. */
83
- declare function compareVersions(left: Version, right: Version): number;
84
- /**
85
- * Whether `versionRaw` satisfies `range` (`||`-separated clauses; a version satisfies the range if
86
- * it satisfies any one clause). Unparseable input on either side is a mismatch, never a throw — a
87
- * doctor check reports a finding, it does not crash the run.
88
- */
89
- declare function satisfiesRange(versionRaw: string, range: string): boolean;
90
-
91
- export { DOCTOR_CHECKS, type DoctorCheck, type DoctorReport, type DoctorSnapshot, type Env, type Finding, type FindingStatus, type PackageJsonLike, type Version, buildSnapshot, compareVersions, parseVersion, renderHuman, renderJson, runDoctor, satisfiesRange };
1
+ export { DOCTOR_CHECKS, DoctorCheck, DoctorReport, DoctorSnapshot, Env, Finding, FindingStatus, PackageJsonLike, Version, buildSnapshot, compareVersions, parseVersion, renderHuman, renderJson, runDoctor, satisfiesRange } from '@narrativetrace/tooling';
package/dist/index.d.ts CHANGED
@@ -1,91 +1 @@
1
- /**
2
- * The doctor's world-view, decoupled from the real filesystem: every check is a pure function
3
- * over a {@link DoctorSnapshot}, which is why every check has both a passing and a failing unit
4
- * test with no disk I/O at all. {@link buildSnapshot} (environment.ts) is the one impure module —
5
- * it walks the real project and builds this shape once per run.
6
- */
7
- /** The process environment, as `process.env` hands it over. */
8
- type Env = Readonly<Record<string, string | undefined>>;
9
- /** The subset of a `package.json` a check ever needs to read. */
10
- interface PackageJsonLike {
11
- readonly name?: string;
12
- readonly version?: string;
13
- readonly type?: string;
14
- readonly engines?: Readonly<Record<string, string>>;
15
- readonly dependencies?: Readonly<Record<string, string>>;
16
- readonly peerDependencies?: Readonly<Record<string, string>>;
17
- }
18
- interface DoctorSnapshot {
19
- /** Absolute path to the project being checked (informational — checks never touch disk). */
20
- readonly cwd: string;
21
- /** `process.version` with the leading `v` stripped, e.g. `"20.11.0"`. */
22
- readonly nodeVersion: string;
23
- readonly env: Env;
24
- readonly rootPackageJson: PackageJsonLike | undefined;
25
- /** Relative path → content, for source/config/test files under the project (bounded walk). */
26
- readonly sourceFiles: ReadonlyMap<string, string>;
27
- /** Relative path → content, for everything under the configured output directory. */
28
- readonly outputFiles: ReadonlyMap<string, string>;
29
- /** Relative path → content, for everything under the configured approved-trace directory. */
30
- readonly approvedDirFiles: ReadonlyMap<string, string>;
31
- /** Package name → its resolved `package.json`, for packages the checks care about. */
32
- readonly installedPackages: ReadonlyMap<string, PackageJsonLike>;
33
- }
34
- type FindingStatus = "pass" | "fail";
35
- interface Finding {
36
- /** Stable, dotted id (`"toolchain.node-engine"`) — never renamed once shipped; agents and CI grep it. */
37
- readonly id: string;
38
- readonly status: FindingStatus;
39
- /** One line: what the check found, true on pass or fail. */
40
- readonly message: string;
41
- /** What to do about it. Empty string on a pass — there is nothing to fix. */
42
- readonly fix: string;
43
- /** Public doc URL the finding points at (a GitHub blob link into this repo's `documentation/`). */
44
- readonly docUrl: string;
45
- }
46
- type DoctorCheck = (snapshot: DoctorSnapshot) => Finding;
47
- interface DoctorReport {
48
- readonly findings: readonly Finding[];
49
- /** 0 — every check passed. 1 — at least one finding failed. */
50
- readonly exitCode: 0 | 1;
51
- }
52
-
53
- /**
54
- * Every check `narrativetrace doctor` runs, in stable, documented order. Adding a check means
55
- * appending here — the id is what stays stable across releases, not the position.
56
- */
57
- declare const DOCTOR_CHECKS: readonly DoctorCheck[];
58
- /** Runs every check over `snapshot` and derives the process exit code. Read-only: mutates nothing. */
59
- declare function runDoctor(snapshot: DoctorSnapshot): DoctorReport;
60
-
61
- /** Builds a {@link DoctorSnapshot} from the real filesystem rooted at `cwd`. The one impure module. */
62
- declare function buildSnapshot(cwd: string, env: Env): DoctorSnapshot;
63
-
64
- /** Human-readable default output: one block per finding, worst-first (failures before passes). */
65
- declare function renderHuman(report: DoctorReport): string;
66
- /** Machine-readable `--json` output: the report verbatim, stable field names. */
67
- declare function renderJson(report: DoctorReport): string;
68
-
69
- /**
70
- * Minimal semver range matcher covering exactly the range shapes this repository's own
71
- * `package.json` files use (`>=20`, `^1.0.0 || ^2.0.0 || ^3.0.0`, `1.x`): the doctor never needs to
72
- * parse an arbitrary npm range, only the ones NarrativeTrace itself declares in `engines` and
73
- * `peerDependencies`. A real `semver` dependency would cover forms this codebase never emits.
74
- */
75
- interface Version {
76
- readonly major: number;
77
- readonly minor: number;
78
- readonly patch: number;
79
- }
80
- /** Parses `"20.11.0"`, `"v20.11.0"`, `"20"`, or `"20.11"` — missing parts default to zero. */
81
- declare function parseVersion(raw: string): Version | undefined;
82
- /** Three-way compare, standard sign convention: negative if `left` < `right`, positive if greater. */
83
- declare function compareVersions(left: Version, right: Version): number;
84
- /**
85
- * Whether `versionRaw` satisfies `range` (`||`-separated clauses; a version satisfies the range if
86
- * it satisfies any one clause). Unparseable input on either side is a mismatch, never a throw — a
87
- * doctor check reports a finding, it does not crash the run.
88
- */
89
- declare function satisfiesRange(versionRaw: string, range: string): boolean;
90
-
91
- export { DOCTOR_CHECKS, type DoctorCheck, type DoctorReport, type DoctorSnapshot, type Env, type Finding, type FindingStatus, type PackageJsonLike, type Version, buildSnapshot, compareVersions, parseVersion, renderHuman, renderJson, runDoctor, satisfiesRange };
1
+ export { DOCTOR_CHECKS, DoctorCheck, DoctorReport, DoctorSnapshot, Env, Finding, FindingStatus, PackageJsonLike, Version, buildSnapshot, compareVersions, parseVersion, renderHuman, renderJson, runDoctor, satisfiesRange } from '@narrativetrace/tooling';
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
+ // src/index.ts
1
2
  import {
2
- DOCTOR_CHECKS,
3
3
  buildSnapshot,
4
4
  compareVersions,
5
+ DOCTOR_CHECKS,
5
6
  parseVersion,
6
7
  renderHuman,
7
8
  renderJson,
8
9
  runDoctor,
9
10
  satisfiesRange
10
- } from "./chunk-WUK4GWFW.js";
11
+ } from "@narrativetrace/tooling";
11
12
  export {
12
13
  DOCTOR_CHECKS,
13
14
  buildSnapshot,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// SPDX-License-Identifier: BUSL-1.1\n// Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four years from publication; Change License: Apache-2.0\n// Copyright (c) 2026 Empower Agile\nexport {\n buildSnapshot,\n compareVersions,\n DOCTOR_CHECKS,\n type DoctorCheck,\n type DoctorReport,\n type DoctorSnapshot,\n type Env,\n type Finding,\n type FindingStatus,\n type PackageJsonLike,\n parseVersion,\n renderHuman,\n renderJson,\n runDoctor,\n satisfiesRange,\n type Version,\n} from \"@narrativetrace/tooling\";\n"],"mappings":";AAGA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAQA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narrativetrace/cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "npx @narrativetrace/cli — one CLI over NarrativeTrace's open artifact formats (doctor today; view/validate/diff planned)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,9 +32,13 @@
32
32
  "files": [
33
33
  "bin",
34
34
  "dist",
35
+ "skills",
35
36
  "README.md",
36
37
  "LICENSE"
37
38
  ],
39
+ "dependencies": {
40
+ "@narrativetrace/tooling": "0.2.0"
41
+ },
38
42
  "devDependencies": {
39
43
  "@types/node": "^25.0.0"
40
44
  },
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: add-narrative-tracing
3
+ description: "Installs NarrativeTrace into a TypeScript project and gets it to a first trace. Use when NarrativeTrace is not yet installed, a project needs its very first traced call, or traces need to reach a real logger instead of a bare console.log. Installs @narrativetrace/core-node and @narrativetrace/proxy with the project's real package manager, wraps a class with traceObject, renders and runs the first trace, then wires a pino/winston/OpenTelemetry-style consumer so traces reach your logger. Runs narrativetrace doctor to confirm the install is correctly wired — narrativetrace-doctor owns diagnosis from there — then previews installing the NarrativeTrace agent skills for next time (`narrativetrace init --dry-run`); applying that preview is left to the reader. Say 'add narrative tracing to my service', 'install narrativetrace', 'get a trace in 60 seconds', 'wrap this class so I can see a trace', or 'send my traces to my logger' to invoke it."
4
+ ---
5
+
6
+ # add-narrative-tracing
7
+
8
+ ## 1. Install with the real toolchain
9
+
10
+ ```bash
11
+ pnpm install --frozen-lockfile
12
+ ```
13
+
14
+ **verify:** `npx @narrativetrace/cli doctor --json | node -e "const r=JSON.parse(require('fs').readFileSync(0,'utf8')); const bad=r.findings.filter(f=>f.id.startsWith('toolchain.')&&f.status!=='pass'); if(bad.length>0){console.error(JSON.stringify(bad)); process.exit(1);}"`
15
+
16
+ **failure:** vitest peer mismatch breaks the library's own build from a clean install — an installed vitest version outside @narrativetrace/vitest's declared peer range. Fix: run the narrativetrace-doctor skill's toolchain.vitest-peer check, then install a version satisfying the printed range
17
+
18
+ ## 2. First trace: wrap, call, render, run
19
+
20
+ <!-- snippet: examples/sixty-seconds/index.js -->
21
+ ```js
22
+ // index.js
23
+ import { NarrativeTraceConfig, SyncNarrativeContext, renderMarkdownBody } from "@narrativetrace/core-node";
24
+ import { traceObject } from "@narrativetrace/proxy";
25
+
26
+ class OrderService {
27
+ placeOrder(customerId, productId, quantity) {
28
+ return `ORD-${customerId}-${productId}-${quantity}`;
29
+ }
30
+ }
31
+
32
+ const context = new SyncNarrativeContext(new NarrativeTraceConfig());
33
+ const service = traceObject(new OrderService(), context, {
34
+ placeOrder: ["customerId", "productId", "quantity"],
35
+ });
36
+
37
+ service.placeOrder("C1", "P1", 2);
38
+ console.log(renderMarkdownBody(context.captureTrace()));
39
+ ```
40
+ <!-- /snippet -->
41
+
42
+ **verify:** `node index.js`
43
+
44
+ **failure:** parameters render as arg0, arg1, ... — parameter names of a class you don't own (or a build that strips them) are lost at compile time. Fix: pass them explicitly: traceObject(target, context, { methodName: ["paramA", "paramB"] })
45
+
46
+ ## 3. Send it to your logger
47
+
48
+ <!-- snippet: examples/sixty-seconds/index-with-logger.js -->
49
+ ```js
50
+ // index-with-logger.js
51
+ import {
52
+ NarrativeTraceConfig,
53
+ SyncNarrativeContext,
54
+ DualPathPipeline, // fans events out to two consumers: the pino bridge and the in-memory buffer
55
+ BufferedEventConsumer, // keeps captureTrace() working alongside the logger
56
+ parseTraceparent, // turns a traceparent header into the trace id fixed below
57
+ renderMarkdownBody,
58
+ } from "@narrativetrace/core-node";
59
+ import { traceObject } from "@narrativetrace/proxy";
60
+ import { createPinoEventConsumer } from "@narrativetrace/pino"; // bridges trace events into Pino
61
+ import pino from "pino";
62
+
63
+ class OrderService {
64
+ placeOrder(customerId, productId, quantity) {
65
+ return `ORD-${customerId}-${productId}-${quantity}`;
66
+ }
67
+ }
68
+
69
+ // A documented constant for THIS example only — never the library default, which always
70
+ // generates a random trace id — so nt.traceName/trace_id below stay the same phrase every time
71
+ // this page's output is regenerated.
72
+ const FIXED_TRACEPARENT = "00-a1b2c3d4a1b2c3d4a1b2c3d4a1b2c3d4-a1b2c3d4a1b2c3d4-01";
73
+ const fixedTraceId = parseTraceparent(FIXED_TRACEPARENT);
74
+
75
+ // Any pino instance works. `sync: true` is this script's own need, not the library's: pino's
76
+ // default destination writes on a later tick, so a script that logs and then prints can have the
77
+ // two land in either order — and a short-lived process can exit before the last line is flushed.
78
+ const logger = pino(pino.destination({ sync: true }));
79
+ const pinoConsumer = createPinoEventConsumer(logger, { levels: { enter: "info", return: "info" } });
80
+ const pipeline = new DualPathPipeline(pinoConsumer, new BufferedEventConsumer());
81
+ const context = new SyncNarrativeContext(
82
+ new NarrativeTraceConfig(),
83
+ undefined, // parentResolver — a plain script has no ambient parent span to resolve
84
+ pipeline, // routes events to both the logger above and the buffer captureTrace() reads
85
+ null, // rootParentOverride — no inbound parent span for this root call
86
+ undefined, // serviceIdentity — not needed for this example
87
+ fixedTraceId, // seeds the trace id an inbound traceparent header would carry on a real request
88
+ );
89
+ const service = traceObject(new OrderService(), context, {
90
+ placeOrder: ["customerId", "productId", "quantity"],
91
+ });
92
+
93
+ service.placeOrder("C1", "P1", 2);
94
+ console.log(renderMarkdownBody(context.captureTrace()));
95
+ ```
96
+ <!-- /snippet -->
97
+
98
+ **verify:** `node index-with-logger.js`
99
+
100
+ ## 4. Run the doctor and resolve its findings
101
+
102
+ ```bash
103
+ npx @narrativetrace/cli doctor || true
104
+ ```
105
+
106
+ **verify:** `npx @narrativetrace/cli doctor --json | node -e "const r=JSON.parse(require('fs').readFileSync(0,'utf8')); if(!Array.isArray(r.findings)||r.findings.length!==12) process.exit(1);"`
107
+
108
+ ## 5. Install the skills for next time
109
+
110
+ ```bash
111
+ npx @narrativetrace/cli init --dry-run
112
+ ```
113
+
114
+ **verify:** `npx @narrativetrace/cli init --dry-run --json | node -e "const r=JSON.parse(require('fs').readFileSync(0,'utf8')); if(!Array.isArray(r.actions)||r.actions.length===0||r.exitCode!==0) process.exit(1);"`
115
+
116
+ ## Always
117
+
118
+ - Reinstall clean (a frozen-lockfile install) rather than trusting whatever is already in node_modules. (a mismatched peer or stale lockfile is the single most common install failure, and it only surfaces on a clean install)
119
+
120
+ ## Never
121
+
122
+ - Never assume a step worked without running its verify. (self-reported success overstates reality — a build claimed green that does not reproduce from clean is not evidence)
123
+ - Never skip the final narrativetrace doctor call. (it is the seam that catches anything these four steps did not — narrativetrace-doctor owns diagnosis from here)
124
+ - Never run `narrativetrace init` without `--dry-run` from this skill. (the prompt's own step 3 human gate applies the plan only after a person has seen the diff — this skill only shows it)
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: narrativetrace-doctor
3
+ description: "Diagnoses a NarrativeTrace TypeScript install and configuration. Use when nothing is being traced, traces aren't showing up, the vitest config crashes on load, parameter names render as arg0/arg1, or you are not sure NarrativeTrace is wired up correctly. Checks Node/vitest-peer/sibling-package versions, the /reporters subpath, traceObject option shapes, NARRATIVETRACE_OUTPUT, whether any consumer is attached to a traced proxy, whether redaction is proven in a test, stale approval-trace diffs, and whether the NarrativeTrace agent skills are installed and current. Read-only — makes no changes. Say 'check my narrativetrace setup', 'is narrativetrace broken', or 'why isn't anything being traced' to invoke it."
4
+ ---
5
+
6
+ # narrativetrace-doctor
7
+
8
+ ## 1. Run the doctor and read its report
9
+
10
+ ```bash
11
+ npx @narrativetrace/cli doctor || true
12
+ ```
13
+
14
+ **verify:** `npx @narrativetrace/cli doctor --json | node -e "const r=JSON.parse(require('fs').readFileSync(0,'utf8')); if(!Array.isArray(r.findings)||r.findings.length!==12) process.exit(1);"`
15
+
16
+ **failure:** the CLI's JSON output does not parse, or is missing findings — the CLI crashed instead of reporting a finding. Fix: re-run `npx @narrativetrace/cli doctor --json` directly and read the raw output — a crash here is a doctor bug, never a project finding
17
+
18
+ ## 2. Prove redaction in a test
19
+
20
+ ```bash
21
+ node -e "console.log('Render a call with a deny-listed parameter name (e.g. password or token) in a test and assert the output contains [REDACTED], and that a neighboring non-sensitive value is still present.')"
22
+ ```
23
+
24
+ **verify:** `npx @narrativetrace/cli doctor --json | node -e "const r=JSON.parse(require('fs').readFileSync(0,'utf8')); const f=r.findings.find(x=>x.id==='trap.redaction-proof'); if(!f||(f.status!=='pass'&&f.status!=='fail')) process.exit(1);"`
25
+
26
+ **failure:** a redaction primitive is imported but never asserted on — trusting redaction by inspection instead of proving it in a test. Fix: render a call with a deny-listed parameter name and assert the output contains "[REDACTED]"
27
+
28
+ ## 3. Read the rendered trace before asserting
29
+
30
+ ```bash
31
+ node -e "const fs=require('fs'),path=require('path');function walk(d){return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>{const p=path.join(d,e.name);return e.isDirectory()?walk(p):[p];});}const files=walk('narrativetrace-output').filter(f=>f.endsWith('.md'));if(!files.length){console.error('no rendered .md file found under narrativetrace-output');process.exit(1);}const newest=files.map(f=>[f,fs.statSync(f).mtimeMs]).sort((a,b)=>b[1]-a[1])[0][0];console.log(newest);console.log(fs.readFileSync(newest,'utf8'));"
32
+ ```
33
+
34
+ ## 4. Approval flow: diff the structural trace, not just values
35
+
36
+ **Flagged:** unstudied — eval cell pending
37
+
38
+ ```bash
39
+ node -e "const fs=require('fs'),path=require('path');function walk(d){if(!fs.existsSync(d))return[];return fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>{const p=path.join(d,e.name);return e.isDirectory()?walk(p):[p];});}const dir='narrativetrace-output';const received=walk(dir).filter(f=>f.endsWith('.received.nt'));if(received.length){const newest=received.map(f=>[f,fs.statSync(f).mtimeMs]).sort((a,b)=>b[1]-a[1])[0][0];const approved=newest.slice(0,-'.received.nt'.length)+'.approved.nt';console.log('received: '+newest);if(fs.existsSync(approved)){console.log('approved: '+approved);console.log(fs.readFileSync(approved,'utf8'));console.log('--- vs received ---');console.log(fs.readFileSync(newest,'utf8'));}else{console.log('no .approved.nt yet - first approval, review then promote');console.log(fs.readFileSync(newest,'utf8'));}}else{const nt=walk(dir).filter(f=>f.endsWith('.nt'));if(!nt.length){console.error('no structural .nt file found under narrativetrace-output');process.exit(1);}const newest=nt.map(f=>[f,fs.statSync(f).mtimeMs]).sort((a,b)=>b[1]-a[1])[0][0];console.log('no .received.nt pending; newest structural trace: '+newest);console.log(fs.readFileSync(newest,'utf8'));}"
40
+ ```
41
+
42
+ ## Always
43
+
44
+ - Run the doctor CLI and read its report before making any change. (the tested tooling already computed the finding — re-deriving it by hand risks disagreeing with what ships)
45
+
46
+ ## Never
47
+
48
+ - Never have this skill edit, generate, or delete a file. (narrativetrace-doctor is scoped read-only by design — generation of the redaction-proof test itself is a separate, later skill)
49
+ - Never claim a finding passed without having run the doctor CLI in this session. (self-reported success overstates reality — a build claimed green that does not reproduce from clean is not evidence; verify is never 'ask the agent')
@@ -0,0 +1,17 @@
1
+ {
2
+ "runtime": "typescript",
3
+ "skills": [
4
+ {
5
+ "name": "narrativetrace-doctor",
6
+ "description": "Diagnoses a NarrativeTrace TypeScript install and configuration. Use when nothing is being traced, traces aren't showing up, the vitest config crashes on load, parameter names render as arg0/arg1, or you are not sure NarrativeTrace is wired up correctly. Checks Node/vitest-peer/sibling-package versions, the /reporters subpath, traceObject option shapes, NARRATIVETRACE_OUTPUT, whether any consumer is attached to a traced proxy, whether redaction is proven in a test, stale approval-trace diffs, and whether the NarrativeTrace agent skills are installed and current. Read-only — makes no changes. Say 'check my narrativetrace setup', 'is narrativetrace broken', or 'why isn't anything being traced' to invoke it.",
7
+ "agents": "agents/narrativetrace-doctor/SKILL.md",
8
+ "claude": "claude/narrativetrace-doctor/SKILL.md"
9
+ },
10
+ {
11
+ "name": "add-narrative-tracing",
12
+ "description": "Installs NarrativeTrace into a TypeScript project and gets it to a first trace. Use when NarrativeTrace is not yet installed, a project needs its very first traced call, or traces need to reach a real logger instead of a bare console.log. Installs @narrativetrace/core-node and @narrativetrace/proxy with the project's real package manager, wraps a class with traceObject, renders and runs the first trace, then wires a pino/winston/OpenTelemetry-style consumer so traces reach your logger. Runs narrativetrace doctor to confirm the install is correctly wired — narrativetrace-doctor owns diagnosis from there — then previews installing the NarrativeTrace agent skills for next time (`narrativetrace init --dry-run`); applying that preview is left to the reader. Say 'add narrative tracing to my service', 'install narrativetrace', 'get a trace in 60 seconds', 'wrap this class so I can see a trace', or 'send my traces to my logger' to invoke it.",
13
+ "agents": "agents/add-narrative-tracing/SKILL.md",
14
+ "claude": "claude/add-narrative-tracing/SKILL.md"
15
+ }
16
+ ]
17
+ }