paperlint 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +28 -41
- package/.github/workflows/dependabot-automerge.yml +1 -1
- package/CLAUDE.md +51 -56
- package/CONTRIBUTING.md +30 -18
- package/README.md +179 -180
- package/bin/paperlint.test.mjs +45 -0
- package/dist/adapters/node/files.io.d.ts +2 -0
- package/dist/adapters/node/files.io.d.ts.map +1 -1
- package/dist/adapters/node/files.io.js +3 -2
- package/dist/adapters/node/files.io.js.map +1 -1
- package/dist/adapters/pdfjs/fill.d.ts +15 -4
- package/dist/adapters/pdfjs/fill.d.ts.map +1 -1
- package/dist/adapters/pdfjs/fill.js +31 -21
- package/dist/adapters/pdfjs/fill.js.map +1 -1
- package/dist/adapters/references/index.d.ts.map +1 -1
- package/dist/adapters/references/index.js +14 -5
- package/dist/adapters/references/index.js.map +1 -1
- package/dist/adapters/references/pool.d.ts +8 -0
- package/dist/adapters/references/pool.d.ts.map +1 -0
- package/dist/adapters/references/pool.js +19 -0
- package/dist/adapters/references/pool.js.map +1 -0
- package/dist/build-engine.d.ts.map +1 -1
- package/dist/build-engine.js +6 -2
- package/dist/build-engine.js.map +1 -1
- package/dist/build.d.ts +10 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +29 -11
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +93 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +296 -80
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain/result.d.ts +0 -2
- package/dist/domain/result.d.ts.map +1 -1
- package/dist/domain/result.js +0 -4
- package/dist/domain/result.js.map +1 -1
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -1
- package/dist/hooks-settings.d.ts +8 -5
- package/dist/hooks-settings.d.ts.map +1 -1
- package/dist/hooks-settings.js +32 -32
- package/dist/hooks-settings.js.map +1 -1
- package/dist/init.d.ts +8 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -4
- package/dist/init.js.map +1 -1
- package/dist/latex-log.d.ts +0 -2
- package/dist/latex-log.d.ts.map +1 -1
- package/dist/latex-log.js +7 -13
- package/dist/latex-log.js.map +1 -1
- package/dist/link-skills.d.ts +15 -2
- package/dist/link-skills.d.ts.map +1 -1
- package/dist/link-skills.js +36 -20
- package/dist/link-skills.js.map +1 -1
- package/dist/new-paper.d.ts +8 -1
- package/dist/new-paper.d.ts.map +1 -1
- package/dist/new-paper.js +50 -15
- package/dist/new-paper.js.map +1 -1
- package/dist/paper-files.d.ts +30 -8
- package/dist/paper-files.d.ts.map +1 -1
- package/dist/paper-files.js +59 -30
- package/dist/paper-files.js.map +1 -1
- package/dist/pdf-facts.d.ts +11 -0
- package/dist/pdf-facts.d.ts.map +1 -1
- package/dist/pdf-facts.js +16 -18
- package/dist/pdf-facts.js.map +1 -1
- package/dist/pdf-geometry.js +1 -1
- package/dist/pdf-geometry.js.map +1 -1
- package/dist/presets.d.ts +2 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +3 -1
- package/dist/presets.js.map +1 -1
- package/dist/references.js +2 -2
- package/dist/references.js.map +1 -1
- package/dist/structure.js +1 -1
- package/dist/structure.js.map +1 -1
- package/dist/tex-requirements.d.ts +8 -0
- package/dist/tex-requirements.d.ts.map +1 -1
- package/dist/tex-requirements.js +6 -5
- package/dist/tex-requirements.js.map +1 -1
- package/dist/toolchain.d.ts +8 -3
- package/dist/toolchain.d.ts.map +1 -1
- package/dist/toolchain.js +26 -10
- package/dist/toolchain.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/venue-rules.d.ts.map +1 -1
- package/dist/venue-rules.js +12 -10
- package/dist/venue-rules.js.map +1 -1
- package/docs/configuration.md +48 -4
- package/docs/prior-art/README.md +1 -1
- package/docs/prior-art/test-tooling.md +7 -30
- package/docs/rules.md +29 -25
- package/docs/skills.md +80 -0
- package/eslint-rules/install-path-literals.harness.mjs +17 -1
- package/eslint-rules/latex-language.harness.mjs +3 -3
- package/eslint-rules/latex-language.mjs +12 -9
- package/eslint-rules/latex-language.test.mjs +200 -0
- package/eslint-rules/paper-research-question.harness.mjs +73 -7
- package/eslint-rules/paper-stages.harness.mjs +9 -14
- package/eslint-rules/paper-stages.test.mjs +135 -0
- package/eslint-rules/paper-typography.mjs +56 -9
- package/eslint-rules/paper-typography.test.mjs +186 -1
- package/eslint-rules/pdf-last-page-balance.harness.mjs +63 -7
- package/eslint-rules/review-frontmatter.mjs +2 -1
- package/eslint-rules/review-frontmatter.schema.json +1 -1
- package/eslint-rules/review-frontmatter.test.mjs +37 -1
- package/eslint-rules/temp-root-realpath.harness.mjs +49 -0
- package/eslint-rules/tex-build.mjs +13 -13
- package/eslint.config.mjs +6 -2
- package/fixtures/toolchain-mirror/catalog.txt +1 -0
- package/fixtures/toolchain-mirror/install-tl +4 -1
- package/fixtures/toolchain-mirror/stub-fmtutil-sys +4 -0
- package/fixtures/toolchain-mirror/stub-tlmgr +15 -0
- package/hooks/hooks.harness.mjs +6 -11
- package/hooks/paper-edit-guard.hook.mjs +2 -2
- package/hooks/paper-edit-guard.test.mjs +61 -0
- package/hooks/paper-skills-nudge.hook.mjs +2 -2
- package/hooks/paper-skills-nudge.test.mjs +38 -0
- package/hooks/paper-status-gates.test.mjs +42 -0
- package/lib/agent-cli-version.harness.mjs +7 -0
- package/lib/check.d.mts +17 -0
- package/lib/check.mjs +50 -0
- package/lib/check.test.ts +67 -0
- package/lib/markdown.mjs +16 -69
- package/lib/markdown.test.mjs +246 -0
- package/lib/paper-config.harness.mjs +28 -9
- package/lib/paper-config.mjs +7 -2
- package/lib/skill-checks.mjs +18 -46
- package/lib/skill-checks.test.mjs +297 -0
- package/lib/skill-corpus.mjs +11 -13
- package/lib/skill-corpus.test.mjs +54 -0
- package/lib/skill-trigger-cases.harness.mjs +57 -4
- package/package.json +6 -5
- package/scripts/check.harness.mjs +4 -10
- package/scripts/check.mjs +45 -42
- package/scripts/check.test.mjs +104 -0
- package/scripts/coverage-config.test.ts +406 -0
- package/scripts/eslint-report-guard.test.mjs +48 -0
- package/scripts/harness-api.frozen.json +0 -2
- package/scripts/layer-legacy-frozen.d.mts +9 -1
- package/scripts/layer-legacy-frozen.mjs +28 -11
- package/scripts/layer-legacy-frozen.test.ts +143 -1
- package/scripts/rules-are-content-only.harness.mjs +3 -11
- package/scripts/rules-are-content-only.mjs +29 -19
- package/scripts/rules-are-content-only.test.mjs +61 -0
- package/scripts/rules-see-files.mjs +11 -9
- package/scripts/rules-see-files.test.mjs +71 -0
- package/scripts/test.mjs +44 -0
- package/scripts/test.test.mjs +52 -0
- package/skills/grade-paper-writing/__snapshots__/prose-lint.test.mjs.snap +57 -0
- package/skills/grade-paper-writing/prose-lint.mjs +31 -12
- package/skills/grade-paper-writing/prose-lint.test.mjs +153 -0
- package/skills/paper-pipeline/grade-paper-writing-ablation.eval.mjs +3 -3
- package/skills/paper-pipeline/scripts/README.md +13 -132
- package/skills/paper-pipeline/scripts/announce.test.mjs +102 -0
- package/skills/paper-pipeline/scripts/artifact-coverage.test.mjs +86 -0
- package/skills/paper-pipeline/scripts/consumer.d.mts +8 -1
- package/skills/paper-pipeline/scripts/consumer.harness.mjs +3 -3
- package/skills/paper-pipeline/scripts/consumer.mjs +13 -25
- package/skills/paper-pipeline/scripts/consumer.test.mjs +113 -0
- package/skills/paper-pipeline/scripts/extract-ref-facts.mjs +9 -4
- package/skills/paper-pipeline/scripts/extract-ref-facts.test.mjs +380 -0
- package/skills/paper-pipeline/scripts/fixtures/scorecard.mjs +71 -0
- package/skills/paper-pipeline/scripts/generated-code.harness.mjs +4 -4
- package/skills/paper-pipeline/scripts/generated-code.mjs +3 -2
- package/skills/paper-pipeline/scripts/generated-code.test.mjs +62 -0
- package/skills/paper-pipeline/scripts/ledger.cli.test.mjs +302 -0
- package/skills/paper-pipeline/scripts/ledger.mjs +1 -1
- package/skills/paper-pipeline/scripts/{ledger.selftest.mjs → ledger.test.mjs} +44 -76
- package/skills/paper-pipeline/scripts/pipeline-check.cli.test.mjs +474 -0
- package/skills/paper-pipeline/scripts/pipeline-check.harness.mjs +2 -63
- package/skills/paper-pipeline/scripts/pipeline-check.mjs +37 -21
- package/skills/paper-pipeline/scripts/pipeline-edges.mjs +2 -2
- package/skills/paper-pipeline/scripts/pipeline-edges.test.mjs +73 -0
- package/skills/paper-pipeline/scripts/population-map.harness.mjs +4 -47
- package/skills/paper-pipeline/scripts/population-map.mjs +12 -2
- package/skills/paper-pipeline/scripts/population-map.test.mjs +154 -0
- package/skills/paper-pipeline/scripts/provenance.harness.mjs +26 -0
- package/skills/paper-pipeline/scripts/round-diff.harness.mjs +1 -2
- package/skills/paper-pipeline/scripts/round-diff.mjs +13 -6
- package/skills/paper-pipeline/scripts/round-diff.test.mjs +159 -0
- package/skills/paper-pipeline/scripts/run-mechanical.mjs +30 -6
- package/skills/paper-pipeline/scripts/run-mechanical.test.mjs +468 -0
- package/skills/paper-pipeline/scripts/status.mjs +2 -2
- package/skills/paper-pipeline/scripts/status.test.mjs +232 -0
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.harness.mjs +2 -2
- package/skills/render-paper/extract-pdf-facts.harness.mjs +56 -7
- package/skills/tighten-paper/__snapshots__/structure.test.mjs.snap +133 -0
- package/skills/tighten-paper/structure.test.mjs +116 -0
- package/skills/verify-citations/SKILL.md +5 -5
- package/skills/verify-citations/SKILL.md.spec.ts +4 -4
- package/skills/verify-citations/scripts/__snapshots__/verify-cites.net.test.mjs.snap +235 -0
- package/skills/verify-citations/scripts/bib-authors.check.test.mjs +274 -0
- package/skills/verify-citations/scripts/bib-authors.cli.test.mjs +178 -0
- package/skills/verify-citations/scripts/bib-authors.mjs +2 -1
- package/skills/verify-citations/scripts/verify-cites.cli.test.mjs +160 -0
- package/skills/verify-citations/scripts/verify-cites.mjs +96 -96
- package/skills/verify-citations/scripts/verify-cites.net.test.mjs +287 -0
- package/skills/verify-citations/scripts/verify-cites.test.mjs +149 -5
- package/src/adapters/banal/failure.test.ts +70 -1
- package/src/adapters/banal/index.test.ts +16 -0
- package/src/adapters/banal/output.test.ts +5 -0
- package/src/adapters/curl/download.test.ts +43 -1
- package/src/adapters/memory/index.test.ts +25 -0
- package/src/adapters/node/files.io.ts +3 -2
- package/src/adapters/node/files.test.ts +13 -1
- package/src/adapters/node/process.test.ts +38 -0
- package/src/adapters/pdfjs/fill.test.ts +34 -0
- package/src/adapters/pdfjs/fill.ts +44 -25
- package/src/adapters/references/index.test.ts +228 -0
- package/src/adapters/references/index.ts +21 -6
- package/src/adapters/references/pool.test.ts +31 -0
- package/src/adapters/references/pool.ts +24 -0
- package/src/build-engine.harness.mjs +3 -7
- package/src/build-engine.test.ts +137 -0
- package/src/build-engine.ts +6 -1
- package/src/build.harness.mjs +51 -11
- package/src/build.test.ts +362 -0
- package/src/build.ts +40 -14
- package/src/cli.harness.mjs +12 -19
- package/src/cli.test.ts +584 -0
- package/src/cli.ts +409 -94
- package/src/doctor.harness.mjs +3 -8
- package/src/doctor.test.ts +64 -0
- package/src/doctor.ts +12 -4
- package/src/domain/result.test.ts +4 -0
- package/src/domain/result.ts +0 -5
- package/src/engine.harness.mjs +77 -7
- package/src/engine.ts +35 -0
- package/src/facts-file.test.ts +110 -0
- package/src/hooks-settings.harness.mjs +43 -13
- package/src/hooks-settings.ts +41 -35
- package/src/init.test.ts +308 -5
- package/src/init.ts +4 -4
- package/src/latex-log.harness.mjs +32 -7
- package/src/latex-log.ts +6 -13
- package/src/latex-loop.harness.mjs +8 -7
- package/src/link-skills.harness.mjs +3 -8
- package/src/link-skills.test.ts +105 -0
- package/src/link-skills.ts +75 -21
- package/src/linted-set.test.ts +118 -0
- package/src/new-paper.harness.mjs +5 -7
- package/src/new-paper.test.ts +203 -2
- package/src/new-paper.ts +84 -17
- package/src/paper-files.test.ts +122 -5
- package/src/paper-files.ts +99 -39
- package/src/paper-settings-commands.test.ts +41 -5
- package/src/paper-settings.test.ts +20 -0
- package/src/parse-args.test.ts +48 -0
- package/src/pdf-facts.harness.mjs +3 -6
- package/src/pdf-facts.test.ts +144 -0
- package/src/pdf-facts.ts +17 -19
- package/src/pdf-geometry.harness.mjs +3 -7
- package/src/pdf-geometry.ts +1 -1
- package/src/presets.test.ts +2 -0
- package/src/presets.ts +7 -1
- package/src/references.test.ts +97 -0
- package/src/references.ts +2 -2
- package/src/rules-config.harness.mjs +10 -8
- package/src/rules-off.test.ts +111 -0
- package/src/structure.harness.mjs +5 -9
- package/src/structure.ts +1 -1
- package/src/tex-requirements.harness.mjs +34 -7
- package/src/tex-requirements.ts +12 -6
- package/src/toolchain.harness.mjs +44 -13
- package/src/toolchain.test.ts +280 -0
- package/src/toolchain.ts +37 -12
- package/src/types.ts +6 -0
- package/src/venue-rules.test.ts +63 -0
- package/src/venue-rules.ts +14 -12
- package/templates/paper/paperlint.json +1 -1
- package/action.mutations.mjs +0 -162
- package/eslint-rules/install-path-literals.mutations.mjs +0 -62
- package/eslint-rules/latex-language.mutations.mjs +0 -196
- package/eslint-rules/paper-research-question.mutations.mjs +0 -127
- package/eslint-rules/paper-stages.mutations.mjs +0 -81
- package/eslint-rules/papers.mutations.mjs +0 -186
- package/eslint-rules/temp-root-realpath.mutations.mjs +0 -99
- package/eslint-rules/tex-build.mutations.mjs +0 -258
- package/hooks/hooks.mutations.mjs +0 -337
- package/lib/agent-cli-version.mutations.mjs +0 -109
- package/lib/mutation-driver.harness.mjs +0 -227
- package/lib/mutation-driver.mjs +0 -397
- package/lib/mutation-driver.mutations.mjs +0 -68
- package/lib/paper-config.mutations.mjs +0 -143
- package/lib/skill-trigger-cases.mutations.mjs +0 -65
- package/scripts/check.mutations.mjs +0 -110
- package/scripts/exclusive.mjs +0 -138
- package/scripts/mutation-batteries-frozen.harness.mjs +0 -204
- package/scripts/mutation-batteries-frozen.mjs +0 -238
- package/scripts/mutation-batteries.frozen.json +0 -108
- package/scripts/rules-are-content-only.mutations.mjs +0 -81
- package/scripts/rules-see-files.mutations.mjs +0 -131
- package/scripts/run-mutations.mjs +0 -100
- package/skills/paper-pipeline/scripts/artifact-coverage.mutations.mjs +0 -218
- package/skills/paper-pipeline/scripts/consumer.mutations.mjs +0 -190
- package/skills/paper-pipeline/scripts/extract-ref-facts.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/generated-code.mutations.mjs +0 -254
- package/skills/paper-pipeline/scripts/pipeline-check.mutations.mjs +0 -54
- package/skills/paper-pipeline/scripts/population-map.mutations.mjs +0 -65
- package/skills/paper-pipeline/scripts/population-map.selftest.mjs +0 -122
- package/skills/paper-pipeline/scripts/provenance.mutations.mjs +0 -59
- package/skills/paper-pipeline/scripts/round-diff.mutations.mjs +0 -276
- package/skills/plan-paper-timeline/plan-paper-timeline.effects.mutations.mjs +0 -65
- package/skills/skill-contract.mutations.mjs +0 -179
- package/src/build.mutations.mjs +0 -196
- package/src/cli.mutations.mjs +0 -414
- package/src/doctor.mutations.mjs +0 -175
- package/src/hooks-settings.mutations.mjs +0 -99
- package/src/link-skills.mutations.mjs +0 -140
- package/src/new-paper.mutations.mjs +0 -79
- package/src/structure.mutations.mjs +0 -83
|
@@ -114,7 +114,7 @@ function fixture({ scripts = {}, allow = null, bundle = {}, at = "repro" }) {
|
|
|
114
114
|
"explicit",
|
|
115
115
|
"import random\nrandom.seed(7)\nrows = random.sample(all_rows, 30)\n",
|
|
116
116
|
],
|
|
117
|
-
// 🔴 THIS FIXTURE WAS VACUOUS IN ITS FIRST FORM
|
|
117
|
+
// 🔴 THIS FIXTURE WAS VACUOUS IN ITS FIRST FORM. It drew with
|
|
118
118
|
// `rnd.sample(...)` off a local `random.Random(...)`, which the DRAWS pattern does not recognise
|
|
119
119
|
// as a draw at all — so the case passed because NOTHING was detected, not because the seed was.
|
|
120
120
|
// Neutering the `--seed` alternation changed no verdict. It now draws with `np.random.choice`,
|
|
@@ -243,9 +243,9 @@ function fixture({ scripts = {}, allow = null, bundle = {}, at = "repro" }) {
|
|
|
243
243
|
// ever fails, one of the two checks should be deleted rather than both kept.
|
|
244
244
|
{
|
|
245
245
|
// Silence asserted through --flags-only rather than through the header's count, so that this case
|
|
246
|
-
// tests the EXCLUSION and not the count. Sharing the count assertion with case 11
|
|
247
|
-
//
|
|
248
|
-
// actually failed.
|
|
246
|
+
// tests the EXCLUSION and not the count. Sharing the count assertion with case 11 meant a defect
|
|
247
|
+
// in the header would be caught here instead, which reads as an off-target result and hides
|
|
248
|
+
// which property actually failed.
|
|
249
249
|
const r = run(
|
|
250
250
|
fixture({ bundle: { "leak.py": 'open("/home/example/secret.tsv")\n' } }),
|
|
251
251
|
false,
|
|
@@ -252,8 +252,9 @@ for (const f of files.sort()) {
|
|
|
252
252
|
|
|
253
253
|
// 🔴 `ABS.flags`, not a literal 'g'. The first version passed 'g' alone, which silently DROPPED
|
|
254
254
|
// the case-sensitivity that keeps an Express route `/users/export` from matching `/Users/` —
|
|
255
|
-
// so the property the harness asserts lived nowhere the harness could reach it. Found by
|
|
256
|
-
//
|
|
255
|
+
// so the property the harness asserts lived nowhere the harness could reach it. Found by
|
|
256
|
+
// testing directly: adding an `i` to ABS changed no verdict, i.e. the property was
|
|
257
|
+
// unreachable.
|
|
257
258
|
const abs = code.match(
|
|
258
259
|
new RegExp(ABS.source + "[^\\s'\"`)\\],]*", ABS.flags + "g"),
|
|
259
260
|
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* generated-code.mjs on the inputs its harness does not build: no argument, a directory that does
|
|
3
|
+
* not exist, and a repro/ holding a dangling link, dot/dependency directories, trees deeper than
|
|
4
|
+
* the scan's depth (more than eight, so the list is elided), and an allow row with no reason.
|
|
5
|
+
*/
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { symlinkSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { test } from "vitest";
|
|
11
|
+
import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
|
|
12
|
+
|
|
13
|
+
const SCRIPT = join(
|
|
14
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
15
|
+
"generated-code.mjs",
|
|
16
|
+
);
|
|
17
|
+
const root = useTempDir("generated-code-");
|
|
18
|
+
const run = (...args) => runNode(SCRIPT, args);
|
|
19
|
+
|
|
20
|
+
test("no argument prints the usage; a directory that does not exist scanned nothing, and says so", () => {
|
|
21
|
+
assert.deepEqual(run(), {
|
|
22
|
+
status: 0,
|
|
23
|
+
stdout: "",
|
|
24
|
+
stderr: "usage: node generated-code.mjs <paper-dir> [--flags-only]\n",
|
|
25
|
+
});
|
|
26
|
+
const r = run(join(root, "nowhere"));
|
|
27
|
+
assert.equal(r.status, 0);
|
|
28
|
+
assert.match(
|
|
29
|
+
r.stdout,
|
|
30
|
+
/NOTHING WAS SCANNED: no repro\/ here or one level below/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("repro/: a dangling link is passed over; skipped trees are named, more than eight elided; a reasonless allow row says so", () => {
|
|
35
|
+
const deep = Object.fromEntries(
|
|
36
|
+
Array.from({ length: 10 }, (_, i) => [
|
|
37
|
+
`repro/exp/d${String(i)}/deeper/x.py`,
|
|
38
|
+
"print(1)\n",
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
const dir = writeTree(join(root, "paper"), {
|
|
42
|
+
...deep,
|
|
43
|
+
"repro/.cache/x.py": "",
|
|
44
|
+
// eslint-disable-next-line port/js-install-path -- a fixture directory the scan must skip, not an install location
|
|
45
|
+
"repro/node_modules/x.js": "",
|
|
46
|
+
"repro/__pycache__/x.py": "",
|
|
47
|
+
// Draws randomness unseeded: a NO_SEED finding, which the reasonless row below waves through.
|
|
48
|
+
"repro/exp/run.py": "import random\nprint(random.random())\n",
|
|
49
|
+
"repro/generated-code-grandfathered.txt": "exp/run.py\t*\n",
|
|
50
|
+
});
|
|
51
|
+
symlinkSync(join(root, "gone"), join(dir, "repro", "dangling.py"));
|
|
52
|
+
const out = run(dir).stdout;
|
|
53
|
+
assert.match(
|
|
54
|
+
out,
|
|
55
|
+
/• 3 tree\(s\) not entered — dependency, VCS and build trees[^\n]*\n {7}\.cache, __pycache__, node_modules/,
|
|
56
|
+
);
|
|
57
|
+
assert.match(
|
|
58
|
+
out,
|
|
59
|
+
/• 10 tree\(s\) not entered — nested deeper than 3 levels[^\n]*\n {7}(?:exp\/d\d\/deeper, ){7}exp\/d\d\/deeper … and 2 more/,
|
|
60
|
+
);
|
|
61
|
+
assert.match(out, / {7}exp\/run\.py · NO_SEED {2}→ {2}\(NO REASON GIVEN\)/);
|
|
62
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ledger.mjs — the `record`/`status` CLI every wired skill ends with, the refusals `record` makes,
|
|
3
|
+
* the evidence contract on a FINDING, and how rows from the retired vocabulary and rows out of
|
|
4
|
+
* time order are read. The ledger and the consumer are temp paths, set before import.
|
|
5
|
+
*
|
|
6
|
+
* Refusal messages are several lines of explanation; where the wording is not the subject, the
|
|
7
|
+
* FIRST line (which names the defect) and the exit code are compared.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { cpSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { afterEach, test } from "vitest";
|
|
14
|
+
import { runNode, useTempDir, writeTree } from "../../../test/support.mjs";
|
|
15
|
+
|
|
16
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const SCRIPT = join(HERE, "ledger.mjs");
|
|
18
|
+
const root = useTempDir("ledger-cli-");
|
|
19
|
+
const consumer = join(root, "consumer");
|
|
20
|
+
const LEDGER = join(root, "runs.jsonl");
|
|
21
|
+
const paper = join(consumer, "papers", "p");
|
|
22
|
+
writeTree(paper, {
|
|
23
|
+
"paper.md": "# P\n\ntext\n",
|
|
24
|
+
"counted.md": "---\nfindings: 2\n---\n",
|
|
25
|
+
"uncounted.md": "no frontmatter count\n",
|
|
26
|
+
});
|
|
27
|
+
cpSync(
|
|
28
|
+
join(HERE, "..", "..", "cold-read-diff"),
|
|
29
|
+
join(consumer, ".claude", "skills", "cold-read-diff"),
|
|
30
|
+
{
|
|
31
|
+
recursive: true,
|
|
32
|
+
verbatimSymlinks: true,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
const env = { CLAUDE_PROJECT_DIR: consumer, PIPELINE_LEDGER: LEDGER };
|
|
36
|
+
Object.assign(process.env, env);
|
|
37
|
+
const { readLedger, record, status } = await import("./ledger.mjs");
|
|
38
|
+
afterEach(() => rmSync(LEDGER, { force: true }));
|
|
39
|
+
|
|
40
|
+
const cli = (...args) => runNode(SCRIPT, args, { env });
|
|
41
|
+
const firstLine = (s) => s.trim().split("\n")[0];
|
|
42
|
+
const kinds = () =>
|
|
43
|
+
readLedger().map((r) => `${r.kind} ${r.findings} ${r.blocking}`);
|
|
44
|
+
|
|
45
|
+
test("record FINDING with a report that states the same count: one row, printed, exit 0", () => {
|
|
46
|
+
const r = cli(
|
|
47
|
+
"record",
|
|
48
|
+
"cold-read-diff",
|
|
49
|
+
paper,
|
|
50
|
+
"FINDING",
|
|
51
|
+
"2",
|
|
52
|
+
"counted.md",
|
|
53
|
+
"--blocking",
|
|
54
|
+
"--check=sub",
|
|
55
|
+
);
|
|
56
|
+
const row = JSON.parse(r.stdout);
|
|
57
|
+
assert.deepEqual(
|
|
58
|
+
{ status: r.status, stderr: r.stderr, check: row.check, kinds: kinds() },
|
|
59
|
+
{ status: 0, stderr: "", check: "sub", kinds: ["FINDING 2 true"] },
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("the evidence contract: no path, a path to nothing, a disagreeing count — recorded, then exit 1", () => {
|
|
64
|
+
const cases = [
|
|
65
|
+
["FINDING", "2"],
|
|
66
|
+
["FINDING", "2", "missing.md"],
|
|
67
|
+
["FINDING", "3", "counted.md"],
|
|
68
|
+
].map((args) => {
|
|
69
|
+
const r = cli("record", "cold-read-diff", paper, ...args);
|
|
70
|
+
return [r.status, firstLine(r.stderr)];
|
|
71
|
+
});
|
|
72
|
+
assert.deepEqual(
|
|
73
|
+
{ cases, kinds: kinds() },
|
|
74
|
+
{
|
|
75
|
+
cases: [
|
|
76
|
+
[1, "🔴 cold-read-diff: recorded 2 finding(s) with NO report path."],
|
|
77
|
+
[
|
|
78
|
+
1,
|
|
79
|
+
'🔴 cold-read-diff: recorded report path "missing.md" — nothing is there.',
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
1,
|
|
83
|
+
"🔴 cold-read-diff: the ledger says 3 finding(s), counted.md says 2.",
|
|
84
|
+
],
|
|
85
|
+
],
|
|
86
|
+
kinds: ["FINDING 2 false", "FINDING 2 false", "FINDING 3 false"],
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a report that states no count is only a nudge: recorded, exit 0", () => {
|
|
92
|
+
const r = cli(
|
|
93
|
+
"record",
|
|
94
|
+
"cold-read-diff",
|
|
95
|
+
paper,
|
|
96
|
+
"FINDING",
|
|
97
|
+
"1",
|
|
98
|
+
"uncounted.md",
|
|
99
|
+
);
|
|
100
|
+
assert.deepEqual(
|
|
101
|
+
{ status: r.status, nudge: firstLine(r.stderr) },
|
|
102
|
+
{
|
|
103
|
+
status: 0,
|
|
104
|
+
nudge:
|
|
105
|
+
"⚠️ cold-read-diff: uncounted.md does not state its own finding count.",
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("refusals before writing: a retired kind, an unknown kind, a free-text reason, a finding of zero", () => {
|
|
111
|
+
const cases = [
|
|
112
|
+
["PASS"],
|
|
113
|
+
["probably fine"],
|
|
114
|
+
["ABSTAINED", "looked fine"],
|
|
115
|
+
["FINDING", "0", "counted.md"],
|
|
116
|
+
["ABSTAINED", "no-witness", "a note"],
|
|
117
|
+
].map((args) => {
|
|
118
|
+
const r = cli("record", "cold-read-diff", paper, ...args);
|
|
119
|
+
return [
|
|
120
|
+
r.status,
|
|
121
|
+
r.status === 0 ? JSON.parse(r.stdout).reason : firstLine(r.stderr),
|
|
122
|
+
];
|
|
123
|
+
});
|
|
124
|
+
assert.deepEqual(
|
|
125
|
+
{ cases, kinds: kinds() },
|
|
126
|
+
{
|
|
127
|
+
cases: [
|
|
128
|
+
[
|
|
129
|
+
2,
|
|
130
|
+
'🔴 "PASS" is not a verdict any more — deleted: it stored "nothing was wrong" as a value. Record ABSTAINED `no-witness`, and let the reader derive cleanliness from the absence of findings.',
|
|
131
|
+
],
|
|
132
|
+
[2, '🔴 kind must be one of FINDING|ABSTAINED, got "probably fine"'],
|
|
133
|
+
[
|
|
134
|
+
2,
|
|
135
|
+
'🔴 ABSTAINED needs a reason from started|no-witness|input-missing|blocked|crashed, got "looked fine".',
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
2,
|
|
139
|
+
"🔴 FINDING with findings=0. A finding of nothing is an abstention:",
|
|
140
|
+
],
|
|
141
|
+
[0, "no-witness"],
|
|
142
|
+
],
|
|
143
|
+
// Only the valid abstention was written.
|
|
144
|
+
kinds: ["ABSTAINED 0 false"],
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("status prints the derived rows; no command prints the usage and exits 2", () => {
|
|
150
|
+
cli("record", "cold-read-diff", paper, "ABSTAINED", "no-witness");
|
|
151
|
+
const s = cli("status", paper);
|
|
152
|
+
const u = cli();
|
|
153
|
+
assert.deepEqual(
|
|
154
|
+
{
|
|
155
|
+
status: s.status,
|
|
156
|
+
rows: JSON.parse(s.stdout).map((r) => [r.key, r.state, r.runs]),
|
|
157
|
+
usage: [u.status, firstLine(u.stderr)],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
status: 0,
|
|
161
|
+
rows: [["cold-read-diff", "FRESH", 1]],
|
|
162
|
+
usage: [
|
|
163
|
+
2,
|
|
164
|
+
"usage: ledger.mjs record <skill> <paperDir> FINDING <count> <report-path> [--check=<id>] [--blocking]",
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("status with no paper argument reads the current directory", () => {
|
|
171
|
+
const r = runNode(SCRIPT, ["status"], { env, cwd: paper });
|
|
172
|
+
assert.deepEqual([r.status, JSON.parse(r.stdout)], [0, []]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("retired rows are read as LEGACY, keep their word, and only FINDINGS/FAIL carry a count", () => {
|
|
176
|
+
const legacy = (verdict, extra = {}) =>
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
ts: "2026-08-01T00:00:00Z",
|
|
179
|
+
skill: "old-skill",
|
|
180
|
+
paper: "p",
|
|
181
|
+
verdict,
|
|
182
|
+
...extra,
|
|
183
|
+
});
|
|
184
|
+
writeFileSync(
|
|
185
|
+
LEDGER,
|
|
186
|
+
[
|
|
187
|
+
legacy("PASS"),
|
|
188
|
+
legacy("FINDINGS", { findings: "4" }),
|
|
189
|
+
legacy("FAIL"),
|
|
190
|
+
legacy(undefined),
|
|
191
|
+
"not json at all",
|
|
192
|
+
"",
|
|
193
|
+
].join("\n"),
|
|
194
|
+
);
|
|
195
|
+
assert.deepEqual(
|
|
196
|
+
readLedger().map((r) => [
|
|
197
|
+
r.kind,
|
|
198
|
+
r.check,
|
|
199
|
+
r.legacyVerdict,
|
|
200
|
+
r.findings,
|
|
201
|
+
r.blocking,
|
|
202
|
+
]),
|
|
203
|
+
[
|
|
204
|
+
["LEGACY", "old-skill", "PASS", 0, false],
|
|
205
|
+
["LEGACY", "old-skill", "FINDINGS", 4, false],
|
|
206
|
+
["LEGACY", "old-skill", "FAIL", 0, true],
|
|
207
|
+
["LEGACY", "old-skill", null, 0, false],
|
|
208
|
+
],
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("the latest run is chosen by timestamp; a stamped row beats unstamped ones; unstamped fall back to file order", () => {
|
|
213
|
+
const row = (ts, findings) =>
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
...(ts ? { ts } : {}),
|
|
216
|
+
skill: "cold-read-diff",
|
|
217
|
+
paper: "p",
|
|
218
|
+
kind: findings ? "FINDING" : "ABSTAINED",
|
|
219
|
+
findings,
|
|
220
|
+
reason: findings ? null : "no-witness",
|
|
221
|
+
report: findings ? "counted.md" : null,
|
|
222
|
+
});
|
|
223
|
+
const latest = (...lines) => {
|
|
224
|
+
writeFileSync(LEDGER, lines.join("\n") + "\n");
|
|
225
|
+
return status(paper)[0].findings;
|
|
226
|
+
};
|
|
227
|
+
assert.deepEqual(
|
|
228
|
+
[
|
|
229
|
+
latest(row("2026-09-02T00:00:00Z", 2), row("2026-09-01T00:00:00Z", 0)),
|
|
230
|
+
latest(row(null, 0), row("2026-09-01T00:00:00Z", 2), row(null, 0)),
|
|
231
|
+
latest(row(null, 2), row(null, 0)),
|
|
232
|
+
latest(row(null, 0), row(null, 2)),
|
|
233
|
+
],
|
|
234
|
+
[2, 2, 0, 2],
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("record straight from code returns the row it wrote", () => {
|
|
239
|
+
const row = record({
|
|
240
|
+
skill: "cold-read-diff",
|
|
241
|
+
paper,
|
|
242
|
+
kind: "ABSTAINED",
|
|
243
|
+
reason: "blocked",
|
|
244
|
+
});
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
[row.kind, row.reason, row.check, row.paper, readLedger().length],
|
|
247
|
+
["ABSTAINED", "blocked", "cold-read-diff", "p", 1],
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("status over hand-edited and retired rows: a bare FINDING, and a legacy FINDINGS that still counts", () => {
|
|
252
|
+
writeFileSync(
|
|
253
|
+
LEDGER,
|
|
254
|
+
[
|
|
255
|
+
{
|
|
256
|
+
ts: "2026-09-01T00:00:00Z",
|
|
257
|
+
skill: "hand",
|
|
258
|
+
paper: "p",
|
|
259
|
+
kind: "FINDING",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
ts: "2026-08-01T00:00:00Z",
|
|
263
|
+
skill: "old-skill",
|
|
264
|
+
paper: "p",
|
|
265
|
+
verdict: "FINDINGS",
|
|
266
|
+
findings: 4,
|
|
267
|
+
report: "r.md",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
ts: "2026-08-01T00:00:00Z",
|
|
271
|
+
skill: "failed-skill",
|
|
272
|
+
paper: "p",
|
|
273
|
+
verdict: "FAIL",
|
|
274
|
+
findings: 1,
|
|
275
|
+
},
|
|
276
|
+
]
|
|
277
|
+
.map((r) => JSON.stringify(r))
|
|
278
|
+
.join("\n") + "\n",
|
|
279
|
+
);
|
|
280
|
+
assert.deepEqual(
|
|
281
|
+
status(paper, { gates: ["hand", "old-skill", "failed-skill"] }).map((r) => [
|
|
282
|
+
r.key,
|
|
283
|
+
r.findings,
|
|
284
|
+
r.report,
|
|
285
|
+
r.legacy,
|
|
286
|
+
r.everFound,
|
|
287
|
+
]),
|
|
288
|
+
[
|
|
289
|
+
["hand", 0, null, null, true],
|
|
290
|
+
["old-skill", 4, "r.md", "FINDINGS", true],
|
|
291
|
+
["failed-skill", 1, null, "FAIL", true],
|
|
292
|
+
],
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("record FINDING with no count at all is a finding of zero, refused", () => {
|
|
297
|
+
const r = cli("record", "cold-read-diff", paper, "FINDING");
|
|
298
|
+
assert.deepEqual(
|
|
299
|
+
[r.status, firstLine(r.stderr)],
|
|
300
|
+
[2, "🔴 FINDING with findings=0. A finding of nothing is an abstention:"],
|
|
301
|
+
);
|
|
302
|
+
});
|
|
@@ -120,7 +120,7 @@ export const LEDGER = ledgerPath(HERE);
|
|
|
120
120
|
// `.claude/skills/paper-pipeline/scripts/` that resolves to `…/paper-pipeline/skills`, which does
|
|
121
121
|
// not exist. And the failure is SILENT in the worst direction — `skillHash()` over a missing
|
|
122
122
|
// directory is stable, so every recorded verdict reads FRESH forever and staleness detection is
|
|
123
|
-
// dead without a single error. Caught only because `ledger.
|
|
123
|
+
// dead without a single error. Caught only because the ledger self-test (now `ledger.test.mjs`) asserts the transition.
|
|
124
124
|
//
|
|
125
125
|
// 🔴 AND IT BROKE A SECOND TIME, THE SAME WAY, ON 2026-09-12 — which is why it is no longer a
|
|
126
126
|
// walk at all. The skills being hashed are the CONSUMER's: `skillHash()` answers "has the checker
|
|
@@ -1,54 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The ledger says NO when it should and YES when it should — both directions, for every property
|
|
3
|
+
* the design claims (formerly `ledger.selftest.mjs`, which nothing ran).
|
|
4
|
+
*
|
|
5
|
+
* Everything lives under one temp directory: the ledger file (PIPELINE_LEDGER) and a consumer root
|
|
6
|
+
* (CLAUDE_PROJECT_DIR) holding a copy of the one skill whose hash is watched. Both are set BEFORE
|
|
7
|
+
* `ledger.mjs` is imported, because it resolves them at import time.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
10
|
import {
|
|
11
|
+
appendFileSync,
|
|
12
|
+
cpSync,
|
|
13
|
+
mkdirSync,
|
|
11
14
|
mkdtempSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
rmSync,
|
|
14
15
|
readFileSync,
|
|
15
|
-
appendFileSync,
|
|
16
|
-
existsSync,
|
|
17
16
|
realpathSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync,
|
|
18
19
|
} from "node:fs";
|
|
19
|
-
import { consumerSkillsDir } from "./consumer.mjs";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
|
-
import { join } from "node:path";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { afterAll, test } from "vitest";
|
|
22
24
|
|
|
23
|
-
const
|
|
25
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const tmp = realpathSync(mkdtempSync(join(tmpdir(), "ledger-test-")));
|
|
24
27
|
const paper = join(tmp, "a-paper");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
const consumer = join(tmp, "consumer");
|
|
29
|
+
const gate = "cold-read-diff"; // a real skill, so skillHash resolves
|
|
30
|
+
mkdirSync(paper, { recursive: true });
|
|
31
|
+
mkdirSync(join(consumer, ".claude", "skills"), { recursive: true });
|
|
32
|
+
cpSync(
|
|
33
|
+
join(HERE, "..", "..", gate),
|
|
34
|
+
join(consumer, ".claude", "skills", gate),
|
|
35
|
+
{
|
|
36
|
+
recursive: true,
|
|
37
|
+
verbatimSymlinks: true,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
29
40
|
process.env.PIPELINE_LEDGER = join(tmp, "runs.jsonl");
|
|
41
|
+
process.env.CLAUDE_PROJECT_DIR = consumer;
|
|
42
|
+
const { record, status } = await import("./ledger.mjs");
|
|
43
|
+
afterAll(() => rmSync(tmp, { recursive: true, force: true }));
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (!ok) failures++;
|
|
35
|
-
console.log(
|
|
36
|
-
` ${ok ? "ok " : "FAIL"} ${name}${ok ? "" : ` — expected ${expected}, got ${actual}`}`,
|
|
37
|
-
);
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
// `skillHash` was dropped from the destructuring on 2026-08-28 — no check below was reading it.
|
|
42
|
-
const { record, status } = await import("./ledger.mjs");
|
|
43
|
-
const mk = (t) => {
|
|
44
|
-
writeFileSync(join(paper, "paper.md"), t);
|
|
45
|
-
};
|
|
46
|
-
const { mkdirSync } = await import("node:fs");
|
|
47
|
-
mkdirSync(paper, { recursive: true });
|
|
48
|
-
|
|
49
|
-
const gate = "cold-read-diff"; // a real skill, so skillHash resolves
|
|
50
|
-
const st = () => status(paper, { gates: [gate] })[0];
|
|
45
|
+
const check = (name, actual, expected) => assert.equal(actual, expected, name);
|
|
46
|
+
const mk = (t) => writeFileSync(join(paper, "paper.md"), t);
|
|
47
|
+
const st = () => status(paper, { gates: [gate] })[0];
|
|
51
48
|
|
|
49
|
+
test("the ledger catches every planted error, in order", () => {
|
|
52
50
|
// ── 1. NEVER-RUN is a state, not an absence ──────────────────────────────────────────────
|
|
53
51
|
mk("version one");
|
|
54
52
|
check("a gate nobody has run reads NEVER-RUN", st().state, "NEVER-RUN");
|
|
@@ -76,30 +74,11 @@ try {
|
|
|
76
74
|
ran();
|
|
77
75
|
check("back on the original bytes it is FRESH again", st().state, "FRESH");
|
|
78
76
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
// the new location and the old relative segment.
|
|
85
|
-
//
|
|
86
|
-
// 🔴 AND A SEVENTH FORM, 2026-09-12: the segment was right and the ANCHOR moved. Once the
|
|
87
|
-
// mechanism ships as a package, `import.meta.url` is inside `node_modules`, so the same two `..`
|
|
88
|
-
// now name a skill directory of the PACKAGE — which holds no consumer skill. The subject here
|
|
89
|
-
// was never "a file near me"; it is "the SKILL.md whose bytes `skillHash(gate)` folds into the
|
|
90
|
-
// key", and that file belongs to the repository under test. So it is resolved the way
|
|
91
|
-
// `ledger.mjs` resolves `SKILLS`, from the consumer root, and the two cannot drift apart.
|
|
92
|
-
const skillFile = join(consumerSkillsDir(), gate, "SKILL.md");
|
|
93
|
-
// Loud, not skipped. This assertion is the only one covering property 2 of the design ("the
|
|
94
|
-
// checker's own source is part of the key"); skipping it when the file is absent would leave the
|
|
95
|
-
// selftest printing success while proving one property fewer — the green-over-emptiness this
|
|
96
|
-
// whole file exists to forbid.
|
|
97
|
-
if (!existsSync(skillFile))
|
|
98
|
-
throw new Error(
|
|
99
|
-
`ledger.selftest: ${skillFile} does not exist.\n` +
|
|
100
|
-
`This selftest runs against a CONSUMER of the pipeline — a repository that has the ` +
|
|
101
|
-
`pipeline's skills installed under .claude/skills/ — not against this package on its own.`,
|
|
102
|
-
);
|
|
77
|
+
// The SKILL.md whose bytes `skillHash(gate)` folds into the key is the CONSUMER's, resolved from
|
|
78
|
+
// CLAUDE_PROJECT_DIR — here a temp copy, so the edit below never touches a tracked file. (The
|
|
79
|
+
// selftest this replaced appended to the real skills/cold-read-diff/SKILL.md and restored it,
|
|
80
|
+
// i.e. it edited the working tree in place while other tests could be reading it.)
|
|
81
|
+
const skillFile = join(consumer, ".claude", "skills", gate, "SKILL.md");
|
|
103
82
|
const skillBefore = readFileSync(skillFile);
|
|
104
83
|
try {
|
|
105
84
|
appendFileSync(
|
|
@@ -272,15 +251,4 @@ try {
|
|
|
272
251
|
0,
|
|
273
252
|
);
|
|
274
253
|
}
|
|
275
|
-
}
|
|
276
|
-
// The ledger lives inside `tmp`, so removing the fixture removes it too. Nothing to restore,
|
|
277
|
-
// which is the point — there is no window in which real history is at risk.
|
|
278
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
console.log(
|
|
282
|
-
failures === 0
|
|
283
|
-
? "\n all planted errors were caught\n"
|
|
284
|
-
: `\n 🔴 ${failures} assertion(s) failed — the ledger is not doing what it claims\n`,
|
|
285
|
-
);
|
|
286
|
-
process.exit(failures === 0 ? 0 : 1);
|
|
254
|
+
});
|