@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.
- package/README.md +77 -6
- package/dist/cli-bin.cjs +234 -513
- package/dist/cli-bin.cjs.map +1 -1
- package/dist/cli-bin.d.cts +1 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +249 -13
- package/dist/cli-bin.js.map +1 -1
- package/dist/index.cjs +9 -520
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -91
- package/dist/index.d.ts +1 -91
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/agents/add-narrative-tracing/SKILL.md +124 -0
- package/skills/agents/narrativetrace-doctor/SKILL.md +49 -0
- package/skills/catalogue.json +17 -0
- package/skills/claude/add-narrative-tracing/SKILL.md +126 -0
- package/skills/claude/narrativetrace-doctor/SKILL.md +51 -0
- package/dist/chunk-WUK4GWFW.js +0 -523
- package/dist/chunk-WUK4GWFW.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -20,528 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
DOCTOR_CHECKS: () => DOCTOR_CHECKS,
|
|
24
|
-
buildSnapshot: () => buildSnapshot,
|
|
25
|
-
compareVersions: () => compareVersions,
|
|
26
|
-
parseVersion: () => parseVersion,
|
|
27
|
-
renderHuman: () => renderHuman,
|
|
28
|
-
renderJson: () => renderJson,
|
|
29
|
-
runDoctor: () => runDoctor,
|
|
30
|
-
satisfiesRange: () => satisfiesRange
|
|
23
|
+
DOCTOR_CHECKS: () => import_tooling.DOCTOR_CHECKS,
|
|
24
|
+
buildSnapshot: () => import_tooling.buildSnapshot,
|
|
25
|
+
compareVersions: () => import_tooling.compareVersions,
|
|
26
|
+
parseVersion: () => import_tooling.parseVersion,
|
|
27
|
+
renderHuman: () => import_tooling.renderHuman,
|
|
28
|
+
renderJson: () => import_tooling.renderJson,
|
|
29
|
+
runDoctor: () => import_tooling.runDoctor,
|
|
30
|
+
satisfiesRange: () => import_tooling.satisfiesRange
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(index_exports);
|
|
33
|
-
|
|
34
|
-
// src/doctor/doc-urls.ts
|
|
35
|
-
var BASE = "https://github.com/narrativetrace/narrativetrace-typescript/blob/main/documentation/";
|
|
36
|
-
var DOC = {
|
|
37
|
-
installationPrerequisites: `${BASE}installation-guide.md#prerequisites`,
|
|
38
|
-
installationDependencies: `${BASE}installation-guide.md#1-add-dependencies`,
|
|
39
|
-
vitestConfiguration: `${BASE}configuration-guide.md#2-vitest-configuration`,
|
|
40
|
-
whereSettingsComeFrom: `${BASE}configuration-guide.md#2b-where-settings-come-from`,
|
|
41
|
-
proxyOptions: `${BASE}configuration-guide.md#6-proxy-options`,
|
|
42
|
-
eventPipelineBuffering: `${BASE}configuration-guide.md#8-event-pipeline-buffering-bufferedeventconsumer`,
|
|
43
|
-
noTraceFilesWritten: `${BASE}troubleshooting.md#no-trace-files-are-written`,
|
|
44
|
-
manualParameterNames: `${BASE}troubleshooting.md#parameters-show-as-arg0-arg1`,
|
|
45
|
-
redactionSurfaceBySurface: `${BASE}privacy-and-redaction.md#redaction-surface-by-surface`,
|
|
46
|
-
approvalTracesEndToEnd: `${BASE}structural-trace-format.md#approval-traces-end-to-end`,
|
|
47
|
-
sixtySecondsNewProject: `${BASE}sixty-seconds.md#1-new-project-add-the-packages`
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// src/doctor/finding.ts
|
|
51
|
-
function pass(id, message, docUrl) {
|
|
52
|
-
return { id, status: "pass", message, fix: "", docUrl };
|
|
53
|
-
}
|
|
54
|
-
function fail(id, message, fix, docUrl) {
|
|
55
|
-
return { id, status: "fail", message, fix, docUrl };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/doctor/checks/approval-traces.ts
|
|
59
|
-
var ID = "trap.approval-traces";
|
|
60
|
-
var FIX = "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 \u2014 never commit a .received.nt file.";
|
|
61
|
-
var checkApprovalTraces = (snapshot) => {
|
|
62
|
-
const paths = [...snapshot.approvedDirFiles.keys()];
|
|
63
|
-
if (paths.length === 0) {
|
|
64
|
-
return pass(
|
|
65
|
-
ID,
|
|
66
|
-
"no approval traces configured yet \u2014 nothing to check",
|
|
67
|
-
DOC.approvalTracesEndToEnd
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
const received = paths.filter((p) => p.endsWith(".received.nt"));
|
|
71
|
-
if (received.length > 0) {
|
|
72
|
-
const message2 = `${received.length} stale received trace(s) found: ${received.join(", ")}`;
|
|
73
|
-
return fail(ID, message2, FIX, DOC.approvalTracesEndToEnd);
|
|
74
|
-
}
|
|
75
|
-
const approved = paths.filter((p) => p.endsWith(".approved.nt"));
|
|
76
|
-
const message = `${approved.length} approved trace(s) found, no pending received diffs`;
|
|
77
|
-
return pass(ID, message, DOC.approvalTracesEndToEnd);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// src/doctor/checks/llms-before-you-start.ts
|
|
81
|
-
var ID2 = "trap.llms-before-you-start";
|
|
82
|
-
var PLAIN_JS = /\.js$/;
|
|
83
|
-
var ESM_IMPORT = /^\s*import\s/m;
|
|
84
|
-
var FIX2 = "Run `npm pkg set type=module` before `npm add` \u2014 without it, Node throws SyntaxError: Cannot use import statement outside a module (pnpm/yarn default to it and never hit this).";
|
|
85
|
-
var checkLlmsBeforeYouStart = (snapshot) => {
|
|
86
|
-
if (snapshot.rootPackageJson?.type === "module") {
|
|
87
|
-
return pass(ID2, 'package.json declares "type": "module"', DOC.sixtySecondsNewProject);
|
|
88
|
-
}
|
|
89
|
-
const offender = [...snapshot.sourceFiles].find(
|
|
90
|
-
([path2, content]) => PLAIN_JS.test(path2) && ESM_IMPORT.test(content)
|
|
91
|
-
);
|
|
92
|
-
if (!offender) {
|
|
93
|
-
const message2 = 'no plain .js file uses ESM import syntax without "type": "module"';
|
|
94
|
-
return pass(ID2, message2, DOC.sixtySecondsNewProject);
|
|
95
|
-
}
|
|
96
|
-
const [path] = offender;
|
|
97
|
-
const message = `${path} uses ESM import syntax but package.json has no "type": "module"`;
|
|
98
|
-
return fail(ID2, message, FIX2, DOC.sixtySecondsNewProject);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
// src/semver-lite.ts
|
|
102
|
-
function parseVersion(raw) {
|
|
103
|
-
const cleaned = raw.trim().replace(/^v/, "");
|
|
104
|
-
const match = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(cleaned);
|
|
105
|
-
if (!match) return void 0;
|
|
106
|
-
return {
|
|
107
|
-
major: Number(match[1]),
|
|
108
|
-
minor: match[2] === void 0 ? 0 : Number(match[2]),
|
|
109
|
-
patch: match[3] === void 0 ? 0 : Number(match[3])
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function compareVersions(left, right) {
|
|
113
|
-
if (left.major !== right.major) return left.major - right.major;
|
|
114
|
-
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
115
|
-
return left.patch - right.patch;
|
|
116
|
-
}
|
|
117
|
-
function satisfiesGte(version, floor) {
|
|
118
|
-
return compareVersions(version, floor) >= 0;
|
|
119
|
-
}
|
|
120
|
-
function satisfiesCaret(version, base) {
|
|
121
|
-
if (compareVersions(version, base) < 0) return false;
|
|
122
|
-
if (base.major > 0) return version.major === base.major;
|
|
123
|
-
if (base.minor > 0) return version.major === 0 && version.minor === base.minor;
|
|
124
|
-
return version.major === 0 && version.minor === 0 && version.patch === base.patch;
|
|
125
|
-
}
|
|
126
|
-
function satisfiesClause(version, trimmed) {
|
|
127
|
-
if (trimmed.startsWith("^")) {
|
|
128
|
-
const base = parseVersion(trimmed.slice(1));
|
|
129
|
-
return base !== void 0 && satisfiesCaret(version, base);
|
|
130
|
-
}
|
|
131
|
-
if (trimmed.startsWith(">=")) {
|
|
132
|
-
const floor = parseVersion(trimmed.slice(2));
|
|
133
|
-
return floor !== void 0 && satisfiesGte(version, floor);
|
|
134
|
-
}
|
|
135
|
-
if (/\.x\b/.test(trimmed)) {
|
|
136
|
-
const base = parseVersion(trimmed.replace(/\.x/g, ".0"));
|
|
137
|
-
return base !== void 0 && satisfiesCaret(version, base);
|
|
138
|
-
}
|
|
139
|
-
const exact = parseVersion(trimmed);
|
|
140
|
-
return exact !== void 0 && compareVersions(version, exact) === 0;
|
|
141
|
-
}
|
|
142
|
-
function satisfiesRange(versionRaw, range) {
|
|
143
|
-
const version = parseVersion(versionRaw);
|
|
144
|
-
if (!version) return false;
|
|
145
|
-
return range.split("||").map((clause) => clause.trim()).filter(Boolean).some((clause) => satisfiesClause(version, clause));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// src/doctor/checks/node-engine.ts
|
|
149
|
-
var ID3 = "toolchain.node-engine";
|
|
150
|
-
var DEFAULT_ENGINE_RANGE = ">=20";
|
|
151
|
-
function requiredRange(snapshot) {
|
|
152
|
-
const core = snapshot.installedPackages.get("@narrativetrace/core");
|
|
153
|
-
const coreNode = snapshot.installedPackages.get("@narrativetrace/core-node");
|
|
154
|
-
return core?.engines?.node ?? coreNode?.engines?.node ?? DEFAULT_ENGINE_RANGE;
|
|
155
|
-
}
|
|
156
|
-
var checkNodeEngine = (snapshot) => {
|
|
157
|
-
const required = requiredRange(snapshot);
|
|
158
|
-
if (satisfiesRange(snapshot.nodeVersion, required)) {
|
|
159
|
-
return pass(
|
|
160
|
-
ID3,
|
|
161
|
-
`Node ${snapshot.nodeVersion} satisfies the required ${required}`,
|
|
162
|
-
DOC.installationPrerequisites
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
return fail(
|
|
166
|
-
ID3,
|
|
167
|
-
`Node ${snapshot.nodeVersion} does not satisfy the required ${required}`,
|
|
168
|
-
`Upgrade Node to a version satisfying ${required} (nvm, volta, asdf, or your CI image).`,
|
|
169
|
-
DOC.installationPrerequisites
|
|
170
|
-
);
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
// src/doctor/checks/output-env.ts
|
|
174
|
-
var ID4 = "config.output-env";
|
|
175
|
-
var VALID_VALUES = /* @__PURE__ */ new Set(["true", "false"]);
|
|
176
|
-
var checkOutputEnv = (snapshot) => {
|
|
177
|
-
const raw = snapshot.env.NARRATIVETRACE_OUTPUT;
|
|
178
|
-
if (raw === void 0) {
|
|
179
|
-
return pass(
|
|
180
|
-
ID4,
|
|
181
|
-
"NARRATIVETRACE_OUTPUT is not set \u2014 output stays on its default (on)",
|
|
182
|
-
DOC.whereSettingsComeFrom
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (VALID_VALUES.has(raw.toLowerCase())) {
|
|
186
|
-
return pass(ID4, `NARRATIVETRACE_OUTPUT=${raw}`, DOC.whereSettingsComeFrom);
|
|
187
|
-
}
|
|
188
|
-
return fail(
|
|
189
|
-
ID4,
|
|
190
|
-
`NARRATIVETRACE_OUTPUT is set to '${raw}', which is neither "true" nor "false"`,
|
|
191
|
-
"Set NARRATIVETRACE_OUTPUT=true or NARRATIVETRACE_OUTPUT=false (or unset it) \u2014 any other value is read as a truthy string, most likely turning output on when you meant to turn it off.",
|
|
192
|
-
DOC.whereSettingsComeFrom
|
|
193
|
-
);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// src/doctor/checks/parameter-arg0.ts
|
|
197
|
-
var ID5 = "trap.parameter-arg0";
|
|
198
|
-
var ARG_PLACEHOLDER = /\barg0\b/;
|
|
199
|
-
var FIX3 = 'Pass parameter names explicitly: traceObject(target, context, { methodName: ["paramA", "paramB"] }) \u2014 required for classes you do not own, or when a build tool strips them.';
|
|
200
|
-
var checkParameterArg0 = (snapshot) => {
|
|
201
|
-
if (snapshot.outputFiles.size === 0) {
|
|
202
|
-
const message2 = "no rendered output found yet \u2014 run your tests or app once to check this";
|
|
203
|
-
return pass(ID5, message2, DOC.manualParameterNames);
|
|
204
|
-
}
|
|
205
|
-
const offender = [...snapshot.outputFiles].find(([, content]) => ARG_PLACEHOLDER.test(content));
|
|
206
|
-
if (!offender) {
|
|
207
|
-
const message2 = "rendered output carries real parameter names \u2014 no arg0 placeholders found";
|
|
208
|
-
return pass(ID5, message2, DOC.manualParameterNames);
|
|
209
|
-
}
|
|
210
|
-
const [path] = offender;
|
|
211
|
-
const message = `rendered output shows arg0-style placeholders (first seen in ${path}) \u2014 parameter names were not captured`;
|
|
212
|
-
return fail(ID5, message, FIX3, DOC.manualParameterNames);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// src/doctor/checks/redaction-proof.ts
|
|
216
|
-
var ID6 = "trap.redaction-proof";
|
|
217
|
-
var TEST_FILE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
218
|
-
var REDACTED_ASSERTION = /\[REDACTED\]/;
|
|
219
|
-
var checkRedactionProof = (snapshot) => {
|
|
220
|
-
const testFiles = [...snapshot.sourceFiles].filter(([path]) => TEST_FILE.test(path));
|
|
221
|
-
const proven = testFiles.some(([, content]) => REDACTED_ASSERTION.test(content));
|
|
222
|
-
if (proven) {
|
|
223
|
-
return pass(
|
|
224
|
-
ID6,
|
|
225
|
-
"a test asserts [REDACTED] for a deny-listed parameter name",
|
|
226
|
-
DOC.redactionSurfaceBySurface
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
return fail(
|
|
230
|
-
ID6,
|
|
231
|
-
"no test asserts [REDACTED] \u2014 redaction is unproven",
|
|
232
|
-
'Render a call with a deny-listed parameter name (e.g. "password", "token") in a test and assert the output contains "[REDACTED]" \u2014 and that a neighboring, non-sensitive value is still present, so an over-broad redaction also fails.',
|
|
233
|
-
DOC.redactionSurfaceBySurface
|
|
234
|
-
);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// src/doctor/checks/reporter-subpath.ts
|
|
238
|
-
var ID7 = "config.reporter-subpath";
|
|
239
|
-
var VITEST_CONFIG_NAME = /(^|\/)vitest\.config\.[cm]?[jt]s$/;
|
|
240
|
-
var REPORTER_NAMES = /\b(ClaritySuiteReporter|GlossarySuiteReporter|StructuralSuiteReporter)\b/;
|
|
241
|
-
var ROOT_IMPORT = /from\s+["']@narrativetrace\/vitest["']/;
|
|
242
|
-
var SUBPATH_IMPORT = /from\s+["']@narrativetrace\/vitest\/reporters["']/;
|
|
243
|
-
function importsFromRoot(content) {
|
|
244
|
-
return REPORTER_NAMES.test(content) && ROOT_IMPORT.test(content) && !SUBPATH_IMPORT.test(content);
|
|
245
|
-
}
|
|
246
|
-
var checkReporterSubpath = (snapshot) => {
|
|
247
|
-
const configs = [...snapshot.sourceFiles].filter(([path]) => VITEST_CONFIG_NAME.test(path));
|
|
248
|
-
const offender = configs.find(([, content]) => importsFromRoot(content));
|
|
249
|
-
if (offender) {
|
|
250
|
-
const [path] = offender;
|
|
251
|
-
return fail(
|
|
252
|
-
ID7,
|
|
253
|
-
`${path} imports a NarrativeTrace vitest reporter from the package root, not the /reporters subpath`,
|
|
254
|
-
'Import reporters from "@narrativetrace/vitest/reporters" \u2014 importing from the package root also loads vitest itself and crashes config loading.',
|
|
255
|
-
DOC.vitestConfiguration
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
const message = configs.length === 0 ? "no vitest.config found \u2014 nothing to check" : "every registered reporter is imported from the /reporters subpath";
|
|
259
|
-
return pass(ID7, message, DOC.vitestConfiguration);
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
// src/doctor/checks/sibling-packages.ts
|
|
263
|
-
var ID8 = "toolchain.sibling-packages";
|
|
264
|
-
var checkSiblingPackages = (snapshot) => {
|
|
265
|
-
const ntVitest = snapshot.installedPackages.get("@narrativetrace/vitest");
|
|
266
|
-
if (!ntVitest) {
|
|
267
|
-
const message2 = "@narrativetrace/vitest is not installed \u2014 nothing to check";
|
|
268
|
-
return pass(ID8, message2, DOC.installationDependencies);
|
|
269
|
-
}
|
|
270
|
-
const siblings = Object.keys(ntVitest.dependencies ?? {}).filter(
|
|
271
|
-
(n) => n.startsWith("@narrativetrace/")
|
|
272
|
-
);
|
|
273
|
-
const unresolved = siblings.filter((name) => !snapshot.installedPackages.has(name));
|
|
274
|
-
if (unresolved.length === 0) {
|
|
275
|
-
const message2 = `all ${siblings.length} sibling package(s) of @narrativetrace/vitest resolve from the consumer`;
|
|
276
|
-
return pass(ID8, message2, DOC.installationDependencies);
|
|
277
|
-
}
|
|
278
|
-
const message = `${unresolved.length} sibling package(s) of @narrativetrace/vitest do not resolve from the consumer: ${unresolved.join(", ")}`;
|
|
279
|
-
const fix = `Add ${unresolved.join(", ")} as explicit direct dependencies \u2014 pnpm's strict layout does not hoist a dependency's own transitive dependencies to your project root.`;
|
|
280
|
-
return fail(ID8, message, fix, DOC.installationDependencies);
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
// src/doctor/checks/silent-sink.ts
|
|
284
|
-
var ID9 = "trap.silent-sink";
|
|
285
|
-
var TRACE_OBJECT_CALL = /\btraceObject\s*\(/;
|
|
286
|
-
var SINK_SIGNAL = /\b(BufferedEventConsumer|captureTrace|registerConsumer|createNarrativeTest|narrativeTest)\b|@narrativetrace\/(vitest|pino|winston|opentelemetry|observability)\b/;
|
|
287
|
-
var checkSilentSink = (snapshot) => {
|
|
288
|
-
const contents = [...snapshot.sourceFiles.values()];
|
|
289
|
-
if (!contents.some((content) => TRACE_OBJECT_CALL.test(content))) {
|
|
290
|
-
return pass(ID9, "traceObject() is not used \u2014 nothing to check", DOC.noTraceFilesWritten);
|
|
291
|
-
}
|
|
292
|
-
if (contents.some((content) => SINK_SIGNAL.test(content))) {
|
|
293
|
-
return pass(
|
|
294
|
-
ID9,
|
|
295
|
-
"traceObject() is used and a consumer/sink is attached",
|
|
296
|
-
DOC.noTraceFilesWritten
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
return fail(
|
|
300
|
-
ID9,
|
|
301
|
-
"traceObject() is used but no consumer or sink (BufferedEventConsumer, captureTrace(), a log/OTel bridge, or @narrativetrace/vitest) was found",
|
|
302
|
-
"Attach a sink: pass a BufferedEventConsumer to your pipeline, call captureTrace() and do something with the tree, or wire a log/OTel bridge \u2014 otherwise every traced call narrates to nowhere.",
|
|
303
|
-
DOC.noTraceFilesWritten
|
|
304
|
-
);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// src/doctor/checks/trace-object-keys.ts
|
|
308
|
-
var ID10 = "config.trace-object-keys";
|
|
309
|
-
var OLD_SHAPE = /\bmethods\s*:\s*\{[^{}]*:\s*\[/;
|
|
310
|
-
var checkTraceObjectKeys = (snapshot) => {
|
|
311
|
-
const offender = [...snapshot.sourceFiles].find(([, content]) => OLD_SHAPE.test(content));
|
|
312
|
-
if (offender) {
|
|
313
|
-
const [path] = offender;
|
|
314
|
-
return fail(
|
|
315
|
-
ID10,
|
|
316
|
-
`${path} calls traceObject(...) with a method entry that skips the per-method config object`,
|
|
317
|
-
"Nest the parameter names under params: traceObject(target, context, { methods: { methodName: { params: [...] } } }) \u2014 a bare array under the method name throws.",
|
|
318
|
-
DOC.proxyOptions
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
return pass(
|
|
322
|
-
ID10,
|
|
323
|
-
"no traceObject(...) call uses the old { methods: { ... } } option shape",
|
|
324
|
-
DOC.proxyOptions
|
|
325
|
-
);
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// src/doctor/checks/vitest-peer.ts
|
|
329
|
-
var ID11 = "toolchain.vitest-peer";
|
|
330
|
-
function noVitestInstalled(range) {
|
|
331
|
-
const message = `@narrativetrace/vitest requires a peer vitest@${range}, but no vitest install was found`;
|
|
332
|
-
const fix = `Install a vitest version satisfying ${range} (npm add -D vitest, then npm ci for a clean lockfile install).`;
|
|
333
|
-
return fail(ID11, message, fix, DOC.vitestConfiguration);
|
|
334
|
-
}
|
|
335
|
-
function mismatchedVitest(range, installed) {
|
|
336
|
-
const message = `vitest@${installed} does not satisfy @narrativetrace/vitest's declared peer range ${range}`;
|
|
337
|
-
const fix = `Install a vitest version satisfying ${range}, then reinstall clean (rm -rf node_modules && npm ci) \u2014 a mismatched peer here is the one failure mode that breaks the library's own build.`;
|
|
338
|
-
return fail(ID11, message, fix, DOC.vitestConfiguration);
|
|
339
|
-
}
|
|
340
|
-
function evaluateInstalledVitest(range, vitest) {
|
|
341
|
-
if (!vitest?.version) return noVitestInstalled(range);
|
|
342
|
-
if (satisfiesRange(vitest.version, range)) {
|
|
343
|
-
const message = `vitest@${vitest.version} satisfies the declared peer range ${range}`;
|
|
344
|
-
return pass(ID11, message, DOC.vitestConfiguration);
|
|
345
|
-
}
|
|
346
|
-
return mismatchedVitest(range, vitest.version);
|
|
347
|
-
}
|
|
348
|
-
var checkVitestPeer = (snapshot) => {
|
|
349
|
-
const ntVitest = snapshot.installedPackages.get("@narrativetrace/vitest");
|
|
350
|
-
const range = ntVitest?.peerDependencies?.vitest;
|
|
351
|
-
if (!ntVitest || !range) {
|
|
352
|
-
return pass(
|
|
353
|
-
ID11,
|
|
354
|
-
"@narrativetrace/vitest is not installed \u2014 nothing to check",
|
|
355
|
-
DOC.vitestConfiguration
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
return evaluateInstalledVitest(range, snapshot.installedPackages.get("vitest"));
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
// src/doctor/doctor.ts
|
|
362
|
-
var DOCTOR_CHECKS = [
|
|
363
|
-
checkNodeEngine,
|
|
364
|
-
checkVitestPeer,
|
|
365
|
-
checkSiblingPackages,
|
|
366
|
-
checkOutputEnv,
|
|
367
|
-
checkReporterSubpath,
|
|
368
|
-
checkTraceObjectKeys,
|
|
369
|
-
checkSilentSink,
|
|
370
|
-
checkParameterArg0,
|
|
371
|
-
checkRedactionProof,
|
|
372
|
-
checkApprovalTraces,
|
|
373
|
-
checkLlmsBeforeYouStart
|
|
374
|
-
];
|
|
375
|
-
function runDoctor(snapshot) {
|
|
376
|
-
const findings = DOCTOR_CHECKS.map((check) => check(snapshot));
|
|
377
|
-
const exitCode = findings.some((f) => f.status === "fail") ? 1 : 0;
|
|
378
|
-
return { findings, exitCode };
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// src/doctor/environment.ts
|
|
382
|
-
var import_node_fs = require("fs");
|
|
383
|
-
var import_node_module = require("module");
|
|
384
|
-
var import_node_path = require("path");
|
|
385
|
-
var EXCLUDED_DIRS = /* @__PURE__ */ new Set([
|
|
386
|
-
// Stryker disable next-line StringLiteral: equivalent — every dot-prefixed entry name is
|
|
387
|
-
// already excluded earlier, in visitEntry's leading-dot check (the only exception there is
|
|
388
|
-
// ".env", which isn't a directory this set would ever mention), so ".git" never actually
|
|
389
|
-
// reaches this set's `.has()` check.
|
|
390
|
-
".git",
|
|
391
|
-
// Stryker disable next-line StringLiteral: same equivalence as ".git" above.
|
|
392
|
-
".turbo",
|
|
393
|
-
// Stryker disable next-line StringLiteral: same equivalence as ".git" above.
|
|
394
|
-
".stryker-tmp",
|
|
395
|
-
// Not dot-prefixed, so NOT equivalent — reaching this set's `.has()` check is the only thing
|
|
396
|
-
// that excludes each of the four names below; each is covered by a real test.
|
|
397
|
-
"node_modules",
|
|
398
|
-
"dist",
|
|
399
|
-
"build",
|
|
400
|
-
"coverage"
|
|
401
|
-
]);
|
|
402
|
-
var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
|
|
403
|
-
var BASE_PACKAGES = [
|
|
404
|
-
"vitest",
|
|
405
|
-
"@narrativetrace/core",
|
|
406
|
-
"@narrativetrace/core-node",
|
|
407
|
-
"@narrativetrace/vitest"
|
|
408
|
-
];
|
|
409
|
-
var MAX_FILES = 2e4;
|
|
410
|
-
function readJson(path) {
|
|
411
|
-
try {
|
|
412
|
-
return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
413
|
-
} catch {
|
|
414
|
-
return void 0;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function resolvePackageJson(name, cwd) {
|
|
418
|
-
try {
|
|
419
|
-
const require2 = (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json"));
|
|
420
|
-
return readJson(require2.resolve(`${name}/package.json`));
|
|
421
|
-
} catch {
|
|
422
|
-
return void 0;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
function isDirectorySafe(path) {
|
|
426
|
-
try {
|
|
427
|
-
return (0, import_node_fs.statSync)(path).isDirectory();
|
|
428
|
-
} catch {
|
|
429
|
-
return void 0;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
function safeRead(path) {
|
|
433
|
-
try {
|
|
434
|
-
return (0, import_node_fs.readFileSync)(path, "utf8");
|
|
435
|
-
} catch {
|
|
436
|
-
return "";
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
function listDirectory(dir) {
|
|
440
|
-
try {
|
|
441
|
-
return (0, import_node_fs.readdirSync)(dir);
|
|
442
|
-
} catch {
|
|
443
|
-
return [];
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
function classify(rel, outputDirName, approvedDirName) {
|
|
447
|
-
const topSegment = rel.split("/")[0];
|
|
448
|
-
if (topSegment === outputDirName) return "output";
|
|
449
|
-
if (topSegment === approvedDirName) return "approved";
|
|
450
|
-
return SOURCE_EXTENSIONS.some((ext) => rel.endsWith(ext)) ? "source" : "skip";
|
|
451
|
-
}
|
|
452
|
-
function bucketFor(state, kind) {
|
|
453
|
-
return kind === "output" ? state.outputFiles : kind === "approved" ? state.approvedDirFiles : state.sourceFiles;
|
|
454
|
-
}
|
|
455
|
-
function visitEntry(ctx, dir, entry) {
|
|
456
|
-
if (entry.startsWith(".") && entry !== ".env") return;
|
|
457
|
-
const full = (0, import_node_path.join)(dir, entry);
|
|
458
|
-
const isDir = isDirectorySafe(full);
|
|
459
|
-
if (isDir === void 0) return;
|
|
460
|
-
if (isDir) {
|
|
461
|
-
if (!EXCLUDED_DIRS.has(entry)) ctx.queue.push(full);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
ctx.state.visited++;
|
|
465
|
-
const rel = (0, import_node_path.relative)(ctx.root, full);
|
|
466
|
-
const kind = classify(rel, ctx.output, ctx.approved);
|
|
467
|
-
if (kind === "skip") return;
|
|
468
|
-
bucketFor(ctx.state, kind).set(rel, safeRead(full));
|
|
469
|
-
}
|
|
470
|
-
function walk(root, output, approved) {
|
|
471
|
-
const state = {
|
|
472
|
-
sourceFiles: /* @__PURE__ */ new Map(),
|
|
473
|
-
outputFiles: /* @__PURE__ */ new Map(),
|
|
474
|
-
approvedDirFiles: /* @__PURE__ */ new Map(),
|
|
475
|
-
visited: 0
|
|
476
|
-
};
|
|
477
|
-
const ctx = { state, root, output, approved, queue: [root] };
|
|
478
|
-
while (ctx.queue.length > 0 && state.visited < MAX_FILES) {
|
|
479
|
-
const dir = ctx.queue.shift();
|
|
480
|
-
for (const entry of listDirectory(dir)) {
|
|
481
|
-
if (state.visited >= MAX_FILES) break;
|
|
482
|
-
visitEntry(ctx, dir, entry);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
return state;
|
|
486
|
-
}
|
|
487
|
-
function resolveBasePackages(cwd) {
|
|
488
|
-
const installed = /* @__PURE__ */ new Map();
|
|
489
|
-
for (const name of BASE_PACKAGES) {
|
|
490
|
-
const pkg = resolvePackageJson(name, cwd);
|
|
491
|
-
if (pkg) installed.set(name, pkg);
|
|
492
|
-
}
|
|
493
|
-
return installed;
|
|
494
|
-
}
|
|
495
|
-
function resolveVitestSiblings(cwd, installed) {
|
|
496
|
-
const ntVitest = installed.get("@narrativetrace/vitest");
|
|
497
|
-
for (const name of Object.keys(ntVitest?.dependencies ?? {})) {
|
|
498
|
-
if (!name.startsWith("@narrativetrace/") || installed.has(name)) continue;
|
|
499
|
-
const pkg = resolvePackageJson(name, cwd);
|
|
500
|
-
if (pkg) installed.set(name, pkg);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
function resolveInstalledPackages(cwd) {
|
|
504
|
-
const installed = resolveBasePackages(cwd);
|
|
505
|
-
resolveVitestSiblings(cwd, installed);
|
|
506
|
-
return installed;
|
|
507
|
-
}
|
|
508
|
-
function buildSnapshot(cwd, env) {
|
|
509
|
-
const output = env.NARRATIVETRACE_OUTPUT_DIR ?? "narrativetrace-output";
|
|
510
|
-
const approved = env.NARRATIVETRACE_APPROVED_DIR ?? "narratives";
|
|
511
|
-
const { sourceFiles, outputFiles, approvedDirFiles } = walk(cwd, output, approved);
|
|
512
|
-
return {
|
|
513
|
-
cwd,
|
|
514
|
-
nodeVersion: process.version.replace(/^v/, ""),
|
|
515
|
-
env,
|
|
516
|
-
rootPackageJson: readJson((0, import_node_path.join)(cwd, "package.json")),
|
|
517
|
-
sourceFiles,
|
|
518
|
-
outputFiles,
|
|
519
|
-
approvedDirFiles,
|
|
520
|
-
installedPackages: resolveInstalledPackages(cwd)
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// src/doctor/render.ts
|
|
525
|
-
function renderFinding(finding) {
|
|
526
|
-
const tag = finding.status === "fail" ? "FAIL" : "PASS";
|
|
527
|
-
const lines = [`[${tag}] ${finding.id} \u2014 ${finding.message}`];
|
|
528
|
-
if (finding.status === "fail") {
|
|
529
|
-
lines.push(` fix: ${finding.fix}`, ` docs: ${finding.docUrl}`);
|
|
530
|
-
}
|
|
531
|
-
lines.push("");
|
|
532
|
-
return lines;
|
|
533
|
-
}
|
|
534
|
-
function renderHuman(report) {
|
|
535
|
-
const failing = report.findings.filter((f) => f.status === "fail");
|
|
536
|
-
const passing = report.findings.filter((f) => f.status === "pass");
|
|
537
|
-
const header = `narrativetrace doctor \u2014 ${report.findings.length} check(s), ${failing.length} finding(s)`;
|
|
538
|
-
const summary = failing.length === 0 ? "All checks passed." : `${failing.length} finding(s). Exit code ${report.exitCode}.`;
|
|
539
|
-
const body = [...failing, ...passing].flatMap(renderFinding);
|
|
540
|
-
return [header, "", ...body, summary].join("\n");
|
|
541
|
-
}
|
|
542
|
-
function renderJson(report) {
|
|
543
|
-
return JSON.stringify(report, null, 2);
|
|
544
|
-
}
|
|
33
|
+
var import_tooling = require("@narrativetrace/tooling");
|
|
545
34
|
// Annotate the CommonJS export names for ESM import in node:
|
|
546
35
|
0 && (module.exports = {
|
|
547
36
|
DOCTOR_CHECKS,
|