diff --git a/.2119/verdicts/REQ-003.1.6--de6caae0b7d5.json b/.2119/verdicts/REQ-003.1.6--de6caae0b7d5.json deleted file mode 100644 index 0f04539..0000000 --- a/.2119/verdicts/REQ-003.1.6--de6caae0b7d5.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.1.6--de6caae0b7d5", - "requirementId": "REQ-003.1.6", - "hash": "de6caae0b7d5", - "verdict": "pass", - "summary": "Review must create the identified instruction packet directly in .2119/reviews, and init is verified to add an effective .gitignore rule for files there; missing or adjacent-directory outputs fail.", - "timestamp": "2026-07-22T14:42:10.024Z" -} diff --git a/.2119/verdicts/REQ-003.1.6.json b/.2119/verdicts/REQ-003.1.6.json new file mode 100644 index 0000000..72acdbc --- /dev/null +++ b/.2119/verdicts/REQ-003.1.6.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-003.1.6--1795cb813d37", + "requirementId": "REQ-003.1.6", + "hash": "1795cb813d37", + "verdict": "pass", + "summary": "Compiled CLI test confirms two generated instruction files land in .2119/reviews/ and clean init makes review packets effectively gitignored.", + "timestamp": "2026-08-03T15:57:48.088Z" +} diff --git a/.2119/verdicts/REQ-003.2.2--bea345f6b733.json b/.2119/verdicts/REQ-003.2.2--bea345f6b733.json deleted file mode 100644 index 4e7f744..0000000 --- a/.2119/verdicts/REQ-003.2.2--bea345f6b733.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.2.2--bea345f6b733", - "requirementId": "REQ-003.2.2", - "hash": "bea345f6b733", - "verdict": "pass", - "summary": "pass and fail serialize verdict records as plain JSON in .2119/verdicts/, while init and verdict writes repair ignore rules so those records remain trackable.", - "timestamp": "2026-08-02T22:29:35.122Z" -} diff --git a/.2119/verdicts/REQ-003.2.2--f06967fba1ab.json b/.2119/verdicts/REQ-003.2.2--f06967fba1ab.json deleted file mode 100644 index 10293bd..0000000 --- a/.2119/verdicts/REQ-003.2.2--f06967fba1ab.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.2.2--f06967fba1ab", - "requirementId": "REQ-003.2.2", - "hash": "f06967fba1ab", - "verdict": "pass", - "summary": "writeVerdict (called by pass/fail) and init both invoke ensureReviewStorageRules, which appends the 2119 gitignore block LAST in root .gitignore with !.2119/, !.2119/verdicts/ and !.2119/verdicts/**, plus last-position repairs to any existing .2119/.gitignore and .2119/verdicts/.gitignore; verdicts are written as pretty-printed 2-space JSON at .2119/verdicts/.json. Verified empirically: with a pre-existing '.2119/' rule and even with '*' in .gitignore, the verdict file is stageable without -f while .2119/reviews/ stays ignored.", - "timestamp": "2026-08-01T06:31:16.680Z" -} diff --git a/.2119/verdicts/REQ-003.2.2.json b/.2119/verdicts/REQ-003.2.2.json new file mode 100644 index 0000000..b15f960 --- /dev/null +++ b/.2119/verdicts/REQ-003.2.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-003.2.2--ba3a7f951d2b", + "requirementId": "REQ-003.2.2", + "hash": "ba3a7f951d2b", + "verdict": "pass", + "summary": "pass/fail write stable plain JSON after repairing final unignore rules, and init installs the same trackable verdict path", + "timestamp": "2026-08-03T17:05:00.760Z" +} diff --git a/.2119/verdicts/REQ-003.6.3--b1ff7ba4feab.json b/.2119/verdicts/REQ-003.6.3--b1ff7ba4feab.json deleted file mode 100644 index f75ee66..0000000 --- a/.2119/verdicts/REQ-003.6.3--b1ff7ba4feab.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.6.3--b1ff7ba4feab", - "requirementId": "REQ-003.6.3", - "hash": "b1ff7ba4feab", - "verdict": "pass", - "summary": "Real CLI test: passes a verdict for FIX-001.1.1, runs review --audit, asserts audit file generated for the passing-verdict req only (unreviewed FIX-001.1.2 gets none), body carries construct-counterexample and pass-only-if-none directives, and the verdict file is byte-identical before/after. Minor untested near-miss: a failing (not merely absent) verdict is not exercised.", - "timestamp": "2026-07-10T23:03:33.182Z" -} diff --git a/.2119/verdicts/REQ-003.6.3.json b/.2119/verdicts/REQ-003.6.3.json new file mode 100644 index 0000000..43acdc2 --- /dev/null +++ b/.2119/verdicts/REQ-003.6.3.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-003.6.3--4fa1d92e29a1", + "requirementId": "REQ-003.6.3", + "hash": "4fa1d92e29a1", + "verdict": "pass", + "summary": "Packaged CLI test rejects missing or mis-scoped audits, non-adversarial directives, stale passes, and any byte change to current, failing, orphan, or multiple verdict records", + "timestamp": "2026-08-03T16:15:20.573Z" +} diff --git a/.2119/verdicts/REQ-003.7.2--d253b491fe00.json b/.2119/verdicts/REQ-003.7.2--d253b491fe00.json deleted file mode 100644 index 883cd70..0000000 --- a/.2119/verdicts/REQ-003.7.2--d253b491fe00.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.7.2--d253b491fe00", - "requirementId": "REQ-003.7.2", - "hash": "d253b491fe00", - "verdict": "pass", - "summary": "Test truncates a committed verdict to invalid JSON and asserts check exits 1 with the verdict's file path, the [REQ-003.7.2] rule tag, and the reason 'unparseable JSON' in stderr — verifies loud violation naming file+reason, not a silent skip/pass.", - "timestamp": "2026-07-10T17:51:34.034Z" -} diff --git a/.2119/verdicts/REQ-003.7.2.json b/.2119/verdicts/REQ-003.7.2.json new file mode 100644 index 0000000..677b2c7 --- /dev/null +++ b/.2119/verdicts/REQ-003.7.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-003.7.2--9d4edf786d45", + "requirementId": "REQ-003.7.2", + "hash": "9d4edf786d45", + "verdict": "pass", + "summary": "Annotated test rejects truncated JSON and an empty-summary record, with check naming the stable verdict file and concrete parse or validation reason", + "timestamp": "2026-08-03T15:57:38.695Z" +} diff --git a/.2119/verdicts/REQ-003.7.3--dd9420ef82ff.json b/.2119/verdicts/REQ-003.7.3--dd9420ef82ff.json deleted file mode 100644 index 69ed135..0000000 --- a/.2119/verdicts/REQ-003.7.3--dd9420ef82ff.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.7.3--dd9420ef82ff", - "requirementId": "REQ-003.7.3", - "hash": "dd9420ef82ff", - "verdict": "pass", - "summary": "Test renames a valid verdict to a mismatched filename and asserts check exits 1 with 'filename does not match reviewId' — genuinely verifies filename-mismatch is treated as malformed rather than counted.", - "timestamp": "2026-07-10T17:51:34.130Z" -} diff --git a/.2119/verdicts/REQ-003.7.3.json b/.2119/verdicts/REQ-003.7.3.json new file mode 100644 index 0000000..d1d0bbf --- /dev/null +++ b/.2119/verdicts/REQ-003.7.3.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-003.7.3--957c25b9bde5", + "requirementId": "REQ-003.7.3", + "hash": "957c25b9bde5", + "verdict": "fail", + "summary": "The test accepts .json despite the exact .json requirement and never verifies prune agreement; it only rejects a legacy-shaped filename with the wrong hash.", + "timestamp": "2026-08-03T15:52:09.333Z" +} diff --git a/.2119/verdicts/REQ-003.8.2--5262e7c71e7b.json b/.2119/verdicts/REQ-003.8.2--5262e7c71e7b.json deleted file mode 100644 index 98d9ee8..0000000 --- a/.2119/verdicts/REQ-003.8.2--5262e7c71e7b.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.8.2--5262e7c71e7b", - "requirementId": "REQ-003.8.2", - "hash": "5262e7c71e7b", - "verdict": "fail", - "summary": "The current template cannot preserve calibration case 013's expected PASS: its conjunct rule requires rejected counterexamples for empty note, mismatched owner, and unparseable stamp, which that case's evidence does not provide.", - "timestamp": "2026-08-02T22:33:18.373Z" -} diff --git a/.2119/verdicts/REQ-003.8.2--5ec997925e8d.json b/.2119/verdicts/REQ-003.8.2--5ec997925e8d.json deleted file mode 100644 index 09c2ee1..0000000 --- a/.2119/verdicts/REQ-003.8.2--5ec997925e8d.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.8.2--5ec997925e8d", - "requirementId": "REQ-003.8.2", - "hash": "5ec997925e8d", - "verdict": "fail", - "summary": "Calibration case 013-pass-control-negative-positive expects PASS, but the template requires a rejected counterexample for every conjunct while its evidence never independently rejects an empty note, mismatched owner, or unparseable stamp; a reviewer following the template must FAIL that case.", - "timestamp": "2026-08-02T22:31:16.326Z" -} diff --git a/.2119/verdicts/REQ-003.8.2--5fff49bf72aa.json b/.2119/verdicts/REQ-003.8.2--5fff49bf72aa.json deleted file mode 100644 index 89fee85..0000000 --- a/.2119/verdicts/REQ-003.8.2--5fff49bf72aa.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.8.2--5fff49bf72aa", - "requirementId": "REQ-003.8.2", - "hash": "5fff49bf72aa", - "verdict": "pass", - "summary": "src/review.ts template prose (audit prompt, dispatch prompt, renderInstructions) is byte-identical to a92ad03, the commit that seeded eval/calibration, so no clause has been dropped since the corpus existed; every one of the 13 cases maps to a live clause — 'comment' is a named boundary term (001), conjunct enumeration (002/006/007), 'cannot fail' tautology (008), 'assert what they just set up' (009), over-mocking (011), criterion-mismatch (010), and 'judge the requirement too: mechanism not outcome' (005/011), with the two pass controls unpenalized.", - "timestamp": "2026-08-01T05:48:55.301Z" -} diff --git a/.2119/verdicts/REQ-003.8.2--d52ce5a3aa84.json b/.2119/verdicts/REQ-003.8.2--d52ce5a3aa84.json deleted file mode 100644 index d0137cf..0000000 --- a/.2119/verdicts/REQ-003.8.2--d52ce5a3aa84.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.8.2--d52ce5a3aa84", - "requirementId": "REQ-003.8.2", - "hash": "d52ce5a3aa84", - "verdict": "pass", - "summary": "The current standard and audit instructions retain the checks needed by calibration cases 001-014: tautology/over-mocking flags, provenance, conjunct and boundary counterexamples, requirement-quality judgment, and evidence-bounded summary scope; case 013 now rejects each named malformed-record conjunct and keeps a genuine-writer pass control.", - "timestamp": "2026-08-02T22:34:56.716Z" -} diff --git a/.2119/verdicts/REQ-003.8.2--deb61e2892be.json b/.2119/verdicts/REQ-003.8.2--deb61e2892be.json deleted file mode 100644 index 0308d2a..0000000 --- a/.2119/verdicts/REQ-003.8.2--deb61e2892be.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.8.2--deb61e2892be", - "requirementId": "REQ-003.8.2", - "hash": "deb61e2892be", - "verdict": "fail", - "summary": "The test-quality template forbids PASS without production file:line provenance, but calibration cases 012-pass-control-perturbation and 013-pass-control-negative-positive contain only condensed snippets and expect PASS, so a reviewer following the template cannot preserve those two corpus verdicts.", - "timestamp": "2026-08-02T22:29:50.495Z" -} diff --git a/.2119/verdicts/REQ-003.8.2--ec2046ab17a0.json b/.2119/verdicts/REQ-003.8.2--ec2046ab17a0.json deleted file mode 100644 index 6f90f0b..0000000 --- a/.2119/verdicts/REQ-003.8.2--ec2046ab17a0.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-003.8.2--ec2046ab17a0", - "requirementId": "REQ-003.8.2", - "hash": "ec2046ab17a0", - "verdict": "pass", - "summary": "The current standard, direct-judgment, and audit instructions preserve the expected verdicts for calibration cases 001-014; corrected case 013 now independently covers its missing verdict, invalid verdict, and real-CLI acceptance clauses.", - "timestamp": "2026-08-02T23:02:58.593Z" -} diff --git a/.2119/verdicts/REQ-003.8.2.json b/.2119/verdicts/REQ-003.8.2.json new file mode 100644 index 0000000..c0ecc90 --- /dev/null +++ b/.2119/verdicts/REQ-003.8.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-003.8.2--6e3c630dbed4", + "requirementId": "REQ-003.8.2", + "hash": "6e3c630dbed4", + "verdict": "pass", + "summary": "test-quality, direct-judgment, audit, and evidence-scoping instructions still expose every expected pass/fail distinction in corpus cases 001-014", + "timestamp": "2026-08-03T17:05:00.892Z" +} diff --git a/.2119/verdicts/REQ-006.1.1--4ca55c38f026.json b/.2119/verdicts/REQ-006.1.1--4ca55c38f026.json deleted file mode 100644 index 564d57b..0000000 --- a/.2119/verdicts/REQ-006.1.1--4ca55c38f026.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-006.1.1--4ca55c38f026", - "requirementId": "REQ-006.1.1", - "hash": "4ca55c38f026", - "verdict": "pass", - "summary": "Test runs 'review', edits the covering test to mint a new review ID, runs 'review' again, and asserts the directory holds exactly one file that differs from the old one and the old instruction file no longer exists.", - "timestamp": "2026-07-09T23:07:20.226Z" -} diff --git a/.2119/verdicts/REQ-006.1.1.json b/.2119/verdicts/REQ-006.1.1.json new file mode 100644 index 0000000..ec8d889 --- /dev/null +++ b/.2119/verdicts/REQ-006.1.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-006.1.1--5adc90405042", + "requirementId": "REQ-006.1.1", + "hash": "5adc90405042", + "verdict": "pass", + "summary": "CLI test rejects superseded, passed, and unrelated instruction packets and asserts the remaining .md files equal the production-computed pending review IDs", + "timestamp": "2026-08-03T16:14:55.176Z" +} diff --git a/.2119/verdicts/REQ-006.1.2--98ec8eb76858.json b/.2119/verdicts/REQ-006.1.2--98ec8eb76858.json deleted file mode 100644 index 14cc20e..0000000 --- a/.2119/verdicts/REQ-006.1.2--98ec8eb76858.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-006.1.2--98ec8eb76858", - "requirementId": "REQ-006.1.2", - "hash": "98ec8eb76858", - "verdict": "pass", - "summary": "Test passes the pending review's verdict, reruns 'review', and asserts status 0, the 'all judgment reviews have current passing verdicts' message, and an empty .2119/reviews directory.", - "timestamp": "2026-07-09T23:07:20.314Z" -} diff --git a/.2119/verdicts/REQ-006.1.2.json b/.2119/verdicts/REQ-006.1.2.json new file mode 100644 index 0000000..acf88ae --- /dev/null +++ b/.2119/verdicts/REQ-006.1.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-006.1.2--c08720df5ca0", + "requirementId": "REQ-006.1.2", + "hash": "c08720df5ca0", + "verdict": "pass", + "summary": "Production CLI test proves plain review removes both generated audit and stale instruction packets after pass leaves zero pending reviews", + "timestamp": "2026-08-03T16:07:26.817Z" +} diff --git a/.2119/verdicts/REQ-006.2.1--e9a3856f19a9.json b/.2119/verdicts/REQ-006.2.1--e9a3856f19a9.json deleted file mode 100644 index bd23d42..0000000 --- a/.2119/verdicts/REQ-006.2.1--e9a3856f19a9.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-006.2.1--e9a3856f19a9", - "requirementId": "REQ-006.2.1", - "hash": "e9a3856f19a9", - "verdict": "pass", - "summary": "Test passes a verdict then orphans it by editing the covering test, runs 'prune', and asserts stdout lists 'pruned .2119/verdicts/.json' plus 'removed 1 orphaned verdict(s)' and the file is gone.", - "timestamp": "2026-07-09T23:07:20.410Z" -} diff --git a/.2119/verdicts/REQ-006.2.1.json b/.2119/verdicts/REQ-006.2.1.json new file mode 100644 index 0000000..bc67691 --- /dev/null +++ b/.2119/verdicts/REQ-006.2.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-006.2.1--b82b2c282604", + "requirementId": "REQ-006.2.1", + "hash": "b82b2c282604", + "verdict": "pass", + "summary": "The prune CLI test deletes and individually lists a production-created stale stable verdict plus stable and legacy verdicts with no current target.", + "timestamp": "2026-08-03T16:05:29.472Z" +} diff --git a/.2119/verdicts/REQ-006.2.2--50fa3281d164.json b/.2119/verdicts/REQ-006.2.2--50fa3281d164.json deleted file mode 100644 index 14cf906..0000000 --- a/.2119/verdicts/REQ-006.2.2--50fa3281d164.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-006.2.2--50fa3281d164", - "requirementId": "REQ-006.2.2", - "hash": "50fa3281d164", - "verdict": "pass", - "summary": "Test passes a verdict (check green), runs 'prune', and asserts 'removed 0 orphaned verdict(s)', the verdict file still exists, and 'check' still exits 0 afterwards.", - "timestamp": "2026-07-09T23:07:20.514Z" -} diff --git a/.2119/verdicts/REQ-006.2.2.json b/.2119/verdicts/REQ-006.2.2.json new file mode 100644 index 0000000..f632788 --- /dev/null +++ b/.2119/verdicts/REQ-006.2.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-006.2.2--4a4c997aba5e", + "requirementId": "REQ-006.2.2", + "hash": "4a4c997aba5e", + "verdict": "pass", + "summary": "Production-generated current verdict fields survive legacy-to-canonical prune migration exactly, and the post-prune check remains passing.", + "timestamp": "2026-08-03T17:19:24.239Z" +} diff --git a/.2119/verdicts/REQ-010.2.5--35d4b4549f82.json b/.2119/verdicts/REQ-010.2.5--35d4b4549f82.json deleted file mode 100644 index 42429e6..0000000 --- a/.2119/verdicts/REQ-010.2.5--35d4b4549f82.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-010.2.5--35d4b4549f82", - "requirementId": "REQ-010.2.5", - "hash": "35d4b4549f82", - "verdict": "pass", - "summary": "Covers added, removed, replaced, assigned-malformed, and unassigned-malformed current verdicts, while rejecting historical verdict edits as outside current scope.", - "timestamp": "2026-07-22T14:45:42.554Z" -} diff --git a/.2119/verdicts/REQ-010.2.5.json b/.2119/verdicts/REQ-010.2.5.json new file mode 100644 index 0000000..16da6dc --- /dev/null +++ b/.2119/verdicts/REQ-010.2.5.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-010.2.5--92e235b8d036", + "requirementId": "REQ-010.2.5", + "hash": "92e235b8d036", + "verdict": "pass", + "summary": "Built-CLI integration rejects added, removed, replaced, assigned-malformed, and unassigned-malformed current verdict changes while excluding changed historical verdicts and unrelated requirements.", + "timestamp": "2026-08-03T15:54:09.368Z" +} diff --git a/.2119/verdicts/REQ-010.3.1--df3aa1a2d7d5.json b/.2119/verdicts/REQ-010.3.1--df3aa1a2d7d5.json deleted file mode 100644 index 96b36ee..0000000 --- a/.2119/verdicts/REQ-010.3.1--df3aa1a2d7d5.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-010.3.1--df3aa1a2d7d5", - "requirementId": "REQ-010.3.1", - "hash": "df3aa1a2d7d5", - "verdict": "pass", - "summary": "Behavioral fixtures reject each scoped boundary: changed versus unchanged lint, affected versus sibling coverage/freshness/verification, and changed versus pre-existing invalid annotations.", - "timestamp": "2026-07-22T14:48:09.013Z" -} diff --git a/.2119/verdicts/REQ-010.3.1.json b/.2119/verdicts/REQ-010.3.1.json new file mode 100644 index 0000000..8d76697 --- /dev/null +++ b/.2119/verdicts/REQ-010.3.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-010.3.1--4378fab560ac", + "requirementId": "REQ-010.3.1", + "hash": "4378fab560ac", + "verdict": "pass", + "summary": "End-to-end CLI cases report changed-spec lint, affected coverage/freshness/verify failures and changed-test annotation errors while suppressing paired pre-existing out-of-scope violations.", + "timestamp": "2026-08-03T16:10:07.763Z" +} diff --git a/.2119/verdicts/REQ-011.5.5--a2d801a91a6f.json b/.2119/verdicts/REQ-011.5.5--a2d801a91a6f.json deleted file mode 100644 index 6f1649c..0000000 --- a/.2119/verdicts/REQ-011.5.5--a2d801a91a6f.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "REQ-011.5.5--a2d801a91a6f", - "requirementId": "REQ-011.5.5", - "hash": "a2d801a91a6f", - "verdict": "pass", - "summary": "Test writes two genuinely orphaned verdict files (legacy REQ-900.1.1--aaa..., file-scoped codex-session-scrollback.1.1--bbb...), runs the real 2119 prune, and asserts both files are gone from disk; a mutant that skips non-legacy review IDs in pruneVerdicts leaves the file-scoped verdict behind and fails this test.", - "timestamp": "2026-08-01T05:51:02.857Z" -} diff --git a/.2119/verdicts/REQ-011.5.5.json b/.2119/verdicts/REQ-011.5.5.json new file mode 100644 index 0000000..82b1b39 --- /dev/null +++ b/.2119/verdicts/REQ-011.5.5.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-011.5.5--bd08ba2ce182", + "requirementId": "REQ-011.5.5", + "hash": "bd08ba2ce182", + "verdict": "pass", + "summary": "CLI prune removes pre-existing stale stable verdict files for both REQ-900.1.1 and codex-session-scrollback.1.1", + "timestamp": "2026-08-03T15:55:25.252Z" +} diff --git a/.2119/verdicts/REQ-012.1.1--ee54df994395.json b/.2119/verdicts/REQ-012.1.1--ee54df994395.json new file mode 100644 index 0000000..538c2c7 --- /dev/null +++ b/.2119/verdicts/REQ-012.1.1--ee54df994395.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.1.1--ee54df994395", + "requirementId": "REQ-012.1.1", + "hash": "ee54df994395", + "verdict": "pass", + "summary": "Production CLI tests verify pass/fail create one canonical verdict file and replace same-hash, changed-hash, stable, and legacy predecessors.", + "timestamp": "2026-08-03T07:33:40.101Z" +} diff --git a/.2119/verdicts/REQ-012.2.1.json b/.2119/verdicts/REQ-012.2.1.json new file mode 100644 index 0000000..5a6d6e8 --- /dev/null +++ b/.2119/verdicts/REQ-012.2.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.2.1--bffae6d192ea", + "requirementId": "REQ-012.2.1", + "hash": "bffae6d192ea", + "verdict": "pass", + "summary": "Two unawaited production CLI processes record distinct review IDs; both must succeed and leave separate parseable records with their own complete fields and fresh timestamps.", + "timestamp": "2026-08-03T16:34:03.118Z" +} diff --git a/.2119/verdicts/REQ-012.3.1--2cdcb2a572f9.json b/.2119/verdicts/REQ-012.3.1--2cdcb2a572f9.json new file mode 100644 index 0000000..86f2289 --- /dev/null +++ b/.2119/verdicts/REQ-012.3.1--2cdcb2a572f9.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.3.1--2cdcb2a572f9", + "requirementId": "REQ-012.3.1", + "hash": "2cdcb2a572f9", + "verdict": "pass", + "summary": "Production CLI records pass/fail fields at the canonical path, while check rejects wrong IDs, hash case/value, verdict, summary, timestamp, missing fields, and wrong paths.", + "timestamp": "2026-08-03T07:33:40.345Z" +} diff --git a/.2119/verdicts/REQ-012.3.2--230dbab2979c.json b/.2119/verdicts/REQ-012.3.2--230dbab2979c.json new file mode 100644 index 0000000..778dc24 --- /dev/null +++ b/.2119/verdicts/REQ-012.3.2--230dbab2979c.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.3.2--230dbab2979c", + "requirementId": "REQ-012.3.2", + "hash": "230dbab2979c", + "verdict": "pass", + "summary": "The annotated test verifies both review kinds' canonical inputs and ordering, sensitivity to every included input, stability across unrelated edits, and persisted hashes matching production review IDs.", + "timestamp": "2026-08-03T07:33:40.464Z" +} diff --git a/.2119/verdicts/REQ-012.4.1--d92abce646b2.json b/.2119/verdicts/REQ-012.4.1--d92abce646b2.json new file mode 100644 index 0000000..ef61c20 --- /dev/null +++ b/.2119/verdicts/REQ-012.4.1--d92abce646b2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.4.1--d92abce646b2", + "requirementId": "REQ-012.4.1", + "hash": "d92abce646b2", + "verdict": "pass", + "summary": "Current stable pass is accepted; stale, hash-mismatched, failing, and malformed stable records remain violations despite current legacy pass evidence.", + "timestamp": "2026-08-03T07:33:40.582Z" +} diff --git a/.2119/verdicts/REQ-012.5.1.json b/.2119/verdicts/REQ-012.5.1.json new file mode 100644 index 0000000..f189b9e --- /dev/null +++ b/.2119/verdicts/REQ-012.5.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.5.1--3828831c8775", + "requirementId": "REQ-012.5.1", + "hash": "3828831c8775", + "verdict": "pass", + "summary": "Real-CLI coverage rejects malformed, current-fail, and superseded-only legacy evidence while verifying non-failing source-to-stable migration notices for current and superseded records", + "timestamp": "2026-08-03T15:55:36.181Z" +} diff --git a/.2119/verdicts/REQ-012.6.1.json b/.2119/verdicts/REQ-012.6.1.json new file mode 100644 index 0000000..df037f9 --- /dev/null +++ b/.2119/verdicts/REQ-012.6.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.6.1--159b88d54fa9", + "requirementId": "REQ-012.6.1", + "hash": "159b88d54fa9", + "verdict": "pass", + "summary": "The real prune CLI preserves current stable records over conflicting legacy evidence and migrates current legacy pass/fail records byte-for-field to stable paths; malformed near-counterexamples are rejected.", + "timestamp": "2026-08-03T17:19:26.772Z" +} diff --git a/.2119/verdicts/REQ-012.6.2.json b/.2119/verdicts/REQ-012.6.2.json new file mode 100644 index 0000000..25a31bb --- /dev/null +++ b/.2119/verdicts/REQ-012.6.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.6.2--a847c92259cf", + "requirementId": "REQ-012.6.2", + "hash": "a847c92259cf", + "verdict": "pass", + "summary": "Real prune CLI test retains current stable or migrated pass/fail evidence, removes valid/stale/orphan/malformed legacy JSON, reports every exercised migration/prune, and leaves one canonical JSON per requirement.", + "timestamp": "2026-08-03T17:19:42.143Z" +} diff --git a/.2119/verdicts/REQ-012.7.1.json b/.2119/verdicts/REQ-012.7.1.json new file mode 100644 index 0000000..4ac7b94 --- /dev/null +++ b/.2119/verdicts/REQ-012.7.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "REQ-012.7.1--8875ee2f6f6c", + "requirementId": "REQ-012.7.1", + "hash": "8875ee2f6f6c", + "verdict": "pass", + "summary": "The annotated init test invokes dist/cli.js in a real Git repository, verifies the exact verdict JSON attribute resolves to true, and proves the file is unignored and stageable.", + "timestamp": "2026-08-03T16:10:01.883Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.1.1--3a42292f6705.json b/.2119/verdicts/self-supplied-evidence.1.1--3a42292f6705.json deleted file mode 100644 index a6c955c..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.1--3a42292f6705.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.1--3a42292f6705", - "requirementId": "self-supplied-evidence.1.1", - "hash": "3a42292f6705", - "verdict": "pass", - "summary": "Every production-computed test-quality task is compared against the complete canonical task body, so removing, weakening, or contradicting the concrete-production-failure mandate fails the test.", - "timestamp": "2026-08-02T23:10:57.874Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.1--6419602f2307.json b/.2119/verdicts/self-supplied-evidence.1.1--6419602f2307.json deleted file mode 100644 index 002bebb..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.1--6419602f2307.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.1--6419602f2307", - "requirementId": "self-supplied-evidence.1.1", - "hash": "6419602f2307", - "verdict": "fail", - "summary": "The exact mandate is asserted, but the anti-waiver check misses contradictory guidance such as 'A generic category-level failure is sufficient,' which permits a non-concrete answer while the test stays green.", - "timestamp": "2026-08-02T23:07:14.248Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.1--7c4774226fe5.json b/.2119/verdicts/self-supplied-evidence.1.1--7c4774226fe5.json deleted file mode 100644 index bcb0d2a..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.1--7c4774226fe5.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.1--7c4774226fe5", - "requirementId": "self-supplied-evidence.1.1", - "hash": "7c4774226fe5", - "verdict": "pass", - "summary": "Every production-identified test-quality instruction is required to ask for a concrete production failure, while tests reject omission, optionality, and contradictory carve-outs.", - "timestamp": "2026-08-02T05:32:14.238Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.1--8e33566ca8c8.json b/.2119/verdicts/self-supplied-evidence.1.1--8e33566ca8c8.json deleted file mode 100644 index c0a1992..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.1--8e33566ca8c8.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.1--8e33566ca8c8", - "requirementId": "self-supplied-evidence.1.1", - "hash": "8e33566ca8c8", - "verdict": "fail", - "summary": "The provenance slice is exact, but contradictory guidance appended after the counterexample block can permit a generic failure category while every 1.1 assertion remains green.", - "timestamp": "2026-08-02T23:08:33.884Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.1--ef8087c8df76.json b/.2119/verdicts/self-supplied-evidence.1.1--ef8087c8df76.json deleted file mode 100644 index f018075..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.1--ef8087c8df76.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.1--ef8087c8df76", - "requirementId": "self-supplied-evidence.1.1", - "hash": "ef8087c8df76", - "verdict": "pass", - "summary": "Every production-computed test-quality instruction retains the complete canonical task body, so removing, weakening, or contradicting the concrete-production-failure mandate fails the test.", - "timestamp": "2026-08-02T23:14:00.257Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.1.json b/.2119/verdicts/self-supplied-evidence.1.1.json new file mode 100644 index 0000000..f7914fd --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.1.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.1.1--afb9cc135da4", + "requirementId": "self-supplied-evidence.1.1", + "hash": "afb9cc135da4", + "verdict": "pass", + "summary": "The CLI-generated packet set is exhaustively exact-body checked and rejects omission or weakening of the concrete covering-test production-failure demand", + "timestamp": "2026-08-03T17:05:01.031Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.1.2--419abd7dee19.json b/.2119/verdicts/self-supplied-evidence.1.2--419abd7dee19.json deleted file mode 100644 index c0c686a..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.2--419abd7dee19.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.2--419abd7dee19", - "requirementId": "self-supplied-evidence.1.2", - "hash": "419abd7dee19", - "verdict": "pass", - "summary": "Every production-discovered test-quality instruction is compared against the complete expected task body with only dynamic IDs normalized, so removing, weakening, or contradicting the file:line independence demand fails.", - "timestamp": "2026-08-02T23:10:46.134Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.2--4f90050b0b09.json b/.2119/verdicts/self-supplied-evidence.1.2--4f90050b0b09.json deleted file mode 100644 index 66d9520..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.2--4f90050b0b09.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.2--4f90050b0b09", - "requirementId": "self-supplied-evidence.1.2", - "hash": "4f90050b0b09", - "verdict": "pass", - "summary": "Every production-discovered test-quality instruction is still matched against the complete expected task with only dynamic IDs normalized, so any omission, weakening, or contradiction of the file:line production-reachability demand fails.", - "timestamp": "2026-08-02T23:14:09.042Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.2--6331058954ea.json b/.2119/verdicts/self-supplied-evidence.1.2--6331058954ea.json deleted file mode 100644 index 7535f16..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.2--6331058954ea.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.2--6331058954ea", - "requirementId": "self-supplied-evidence.1.2", - "hash": "6331058954ea", - "verdict": "pass", - "summary": "All generated test-quality tasks are exhaustively selected from production review targets and require file:line production reachability for the named failure, with exact independence from test, fixture, and prompt-supplied triggers or decisive observations and explicit rejection of opt-outs.", - "timestamp": "2026-08-02T05:32:24.495Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.2--78804c69a5b9.json b/.2119/verdicts/self-supplied-evidence.1.2--78804c69a5b9.json deleted file mode 100644 index 5df7911..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.2--78804c69a5b9.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.2--78804c69a5b9", - "requirementId": "self-supplied-evidence.1.2", - "hash": "78804c69a5b9", - "verdict": "fail", - "summary": "The test stays green if the provenance section retains the exact mandate but adds 'Evidence from test fixtures satisfies this demand.'; that unrecognized contradiction permits fixture-supplied evidence despite 1.2's independence clause.", - "timestamp": "2026-08-02T23:07:12.165Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.2--82dcac5dc17c.json b/.2119/verdicts/self-supplied-evidence.1.2--82dcac5dc17c.json deleted file mode 100644 index dda4be0..0000000 --- a/.2119/verdicts/self-supplied-evidence.1.2--82dcac5dc17c.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.1.2--82dcac5dc17c", - "requirementId": "self-supplied-evidence.1.2", - "hash": "82dcac5dc17c", - "verdict": "fail", - "summary": "The exact comparison ends at '**Counterexample obligation:**'; appending later in the same instruction 'Evidence from test fixtures satisfies the production-provenance demand.' leaves every assertion green while negating 1.2's independence requirement.", - "timestamp": "2026-08-02T23:08:31.147Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.1.2.json b/.2119/verdicts/self-supplied-evidence.1.2.json new file mode 100644 index 0000000..d8dd0cb --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.1.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.1.2--c3c97de002d9", + "requirementId": "self-supplied-evidence.1.2", + "hash": "c3c97de002d9", + "verdict": "pass", + "summary": "Every CLI-generated test-quality packet is exact-body checked for file:line reachability independent of tests, fixtures, prompts, triggers, and decisive observations", + "timestamp": "2026-08-03T17:05:01.162Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.2.1--1fc1ddf5c224.json b/.2119/verdicts/self-supplied-evidence.2.1--1fc1ddf5c224.json deleted file mode 100644 index 94d303c..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.1--1fc1ddf5c224.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.1--1fc1ddf5c224", - "requirementId": "self-supplied-evidence.2.1", - "hash": "1fc1ddf5c224", - "verdict": "pass", - "summary": "The real review-dispatch CLI generates every computed test-quality instruction, and the test rejects omission or alteration of consumption, emitted value, separate invocation, production component, or production data-source terms as well as weakening exceptions.", - "timestamp": "2026-08-02T23:07:04.908Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.1--2aa103ac58bd.json b/.2119/verdicts/self-supplied-evidence.2.1--2aa103ac58bd.json deleted file mode 100644 index 6b2b59f..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.1--2aa103ac58bd.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.1--2aa103ac58bd", - "requirementId": "self-supplied-evidence.2.1", - "hash": "2aa103ac58bd", - "verdict": "pass", - "summary": "All generated test-quality tasks are enumerated from production review targets and must contain the exact narrow boundary definition; omission or alteration of consumption, emitted value, separate invocation, or production component/data-source terms fails, while anti-waiver assertions reject contradictory weakening.", - "timestamp": "2026-08-02T05:32:29.175Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.1--611ec8687d7d.json b/.2119/verdicts/self-supplied-evidence.2.1--611ec8687d7d.json deleted file mode 100644 index 796d3bd..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.1--611ec8687d7d.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.1--611ec8687d7d", - "requirementId": "self-supplied-evidence.2.1", - "hash": "611ec8687d7d", - "verdict": "pass", - "summary": "Every computed production-generated test-quality task is compared with a complete oracle after only dynamic ID normalization, so changing consumption, emitted value, separate invocation, production-component, or production-data-source scope fails.", - "timestamp": "2026-08-02T23:14:10.359Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.1--7da77538aed0.json b/.2119/verdicts/self-supplied-evidence.2.1--7da77538aed0.json deleted file mode 100644 index fc9bc91..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.1--7da77538aed0.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.1--7da77538aed0", - "requirementId": "self-supplied-evidence.2.1", - "hash": "7da77538aed0", - "verdict": "pass", - "summary": "Every production-dispatched test-quality instruction must match an independent full provenance oracle, so altering any producer/consumer boundary conjunct—consumption, emitted value, separate invocation, production component, or production data source—fails.", - "timestamp": "2026-08-02T23:08:26.294Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.1--a000c7a0aa4b.json b/.2119/verdicts/self-supplied-evidence.2.1--a000c7a0aa4b.json deleted file mode 100644 index 38751c4..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.1--a000c7a0aa4b.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.1--a000c7a0aa4b", - "requirementId": "self-supplied-evidence.2.1", - "hash": "a000c7a0aa4b", - "verdict": "pass", - "summary": "Every generated test-quality task is matched against a complete independent task oracle after normalizing only requirement/review IDs, so any change to the producer/consumer definition's consumption, emitted value, separate invocation, production component, or data-source scope fails.", - "timestamp": "2026-08-02T23:10:44.573Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.1.json b/.2119/verdicts/self-supplied-evidence.2.1.json new file mode 100644 index 0000000..ffd3296 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.2.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.2.1--7fbcc5496008", + "requirementId": "self-supplied-evidence.2.1", + "hash": "7fbcc5496008", + "verdict": "pass", + "summary": "Every CLI-generated test-quality packet is exact-body checked for consumption of emitted values from separately invoked production components or data sources", + "timestamp": "2026-08-03T17:05:01.293Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.2.2--156362cfdb04.json b/.2119/verdicts/self-supplied-evidence.2.2--156362cfdb04.json deleted file mode 100644 index 429ed15..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.2--156362cfdb04.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.2--156362cfdb04", - "requirementId": "self-supplied-evidence.2.2", - "hash": "156362cfdb04", - "verdict": "fail", - "summary": "The assertion checks only the citation clause substring: changing its trigger to 'Unless that boundary exists' leaves the test and anti-waiver regexes green while no longer requiring evidence whenever a producer/consumer boundary exists.", - "timestamp": "2026-08-02T23:07:37.188Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.2--56dbe92c6003.json b/.2119/verdicts/self-supplied-evidence.2.2--56dbe92c6003.json deleted file mode 100644 index 744e1b8..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.2--56dbe92c6003.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.2--56dbe92c6003", - "requirementId": "self-supplied-evidence.2.2", - "hash": "56dbe92c6003", - "verdict": "pass", - "summary": "Every production-computed test-quality task pins the producer/consumer condition and requires file:line evidence that the covering test obtains its input from that production producer.", - "timestamp": "2026-08-02T23:14:23.989Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.2--8fa7baeaefb2.json b/.2119/verdicts/self-supplied-evidence.2.2--8fa7baeaefb2.json deleted file mode 100644 index 406bb51..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.2--8fa7baeaefb2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.2--8fa7baeaefb2", - "requirementId": "self-supplied-evidence.2.2", - "hash": "8fa7baeaefb2", - "verdict": "pass", - "summary": "The test inspects every generated test-quality task, requires mandatory file:line evidence that its input comes from the production producer, and rejects waiver or self-supplied-evidence language.", - "timestamp": "2026-08-02T05:33:28.165Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.2--a3e153ca68e4.json b/.2119/verdicts/self-supplied-evidence.2.2--a3e153ca68e4.json deleted file mode 100644 index 292ae94..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.2--a3e153ca68e4.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.2--a3e153ca68e4", - "requirementId": "self-supplied-evidence.2.2", - "hash": "a3e153ca68e4", - "verdict": "pass", - "summary": "Each generated test-quality task must match the complete normalized task oracle, which pins the existing-boundary trigger, file:line citation, covering test input, and production producer together and rejects reversal, omission, or weakening.", - "timestamp": "2026-08-02T23:11:02.928Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.2--df4ea1367363.json b/.2119/verdicts/self-supplied-evidence.2.2--df4ea1367363.json deleted file mode 100644 index 3b10900..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.2--df4ea1367363.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.2--df4ea1367363", - "requirementId": "self-supplied-evidence.2.2", - "hash": "df4ea1367363", - "verdict": "pass", - "summary": "The independent full-block oracle now pins 'If that boundary exists' together with file:line citation, test input, and that producer for every production-dispatched test-quality instruction, rejecting reversed or weakened triggers.", - "timestamp": "2026-08-02T23:08:52.297Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.2.json b/.2119/verdicts/self-supplied-evidence.2.2.json new file mode 100644 index 0000000..58b5593 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.2.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.2.2--8796d4a6e793", + "requirementId": "self-supplied-evidence.2.2", + "hash": "8796d4a6e793", + "verdict": "pass", + "summary": "Built CLI dispatch output is read from generated files; whole-task equality rejects omission of file:line, conditional-boundary, or production-producer input provenance wording.", + "timestamp": "2026-08-03T17:05:06.235Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.2.3--3bfc1f157e4c.json b/.2119/verdicts/self-supplied-evidence.2.3--3bfc1f157e4c.json deleted file mode 100644 index 7b8eaec..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.3--3bfc1f157e4c.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.3--3bfc1f157e4c", - "requirementId": "self-supplied-evidence.2.3", - "hash": "3bfc1f157e4c", - "verdict": "pass", - "summary": "The generated-task test covers every production-derived test-quality target and rejects omission or weakening of the required file:line trace that the exercised input preserves the production producer's value shape.", - "timestamp": "2026-08-02T05:33:56.135Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.3--a0e14d8db077.json b/.2119/verdicts/self-supplied-evidence.2.3--a0e14d8db077.json deleted file mode 100644 index 8d901aa..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.3--a0e14d8db077.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.3--a0e14d8db077", - "requirementId": "self-supplied-evidence.2.3", - "hash": "a0e14d8db077", - "verdict": "pass", - "summary": "Every production-discovered test-quality task is full-body matched, pinning the conditional file:line demand that the covering test's exercised input preserve the production producer's value shape.", - "timestamp": "2026-08-02T23:14:29.592Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.3--b768f1b9b787.json b/.2119/verdicts/self-supplied-evidence.2.3--b768f1b9b787.json deleted file mode 100644 index 4271f08..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.3--b768f1b9b787.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.3--b768f1b9b787", - "requirementId": "self-supplied-evidence.2.3", - "hash": "b768f1b9b787", - "verdict": "pass", - "summary": "Every production-discovered test-quality instruction is full-body matched, including the conditional file:line demand that the exercised value preserve the production producer's shape; omissions, weakenings, and contradictions fail.", - "timestamp": "2026-08-02T23:11:13.182Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.3.json b/.2119/verdicts/self-supplied-evidence.2.3.json new file mode 100644 index 0000000..df7fae3 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.2.3.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.2.3--3b0f7cd77ddc", + "requirementId": "self-supplied-evidence.2.3", + "hash": "3b0f7cd77ddc", + "verdict": "pass", + "summary": "Built CLI dispatch output is read without reshaping; whole-task equality rejects omission or weakening of the conditional production-shape evidence requirement.", + "timestamp": "2026-08-03T17:05:06.321Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.2.4--2755a46ec65c.json b/.2119/verdicts/self-supplied-evidence.2.4--2755a46ec65c.json deleted file mode 100644 index 1382bcc..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.4--2755a46ec65c.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.4--2755a46ec65c", - "requirementId": "self-supplied-evidence.2.4", - "hash": "2755a46ec65c", - "verdict": "pass", - "summary": "Fresh dispatch integration verifies every generated test-quality task explicitly requires file:line evidence distinguishing a newly produced observation from an equal pre-existing sentinel, while shared rejection checks forbid weakening language.", - "timestamp": "2026-08-02T05:34:49.897Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.4--2e7bb01145d0.json b/.2119/verdicts/self-supplied-evidence.2.4--2e7bb01145d0.json deleted file mode 100644 index bcb1b12..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.4--2e7bb01145d0.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.4--2e7bb01145d0", - "requirementId": "self-supplied-evidence.2.4", - "hash": "2e7bb01145d0", - "verdict": "pass", - "summary": "Every computed production-generated test-quality task matches the complete oracle, pinning the decisive-observation trigger, all four initial/default/placeholder/sentinel cases, file:line evidence, and newly-produced versus pre-existing distinction.", - "timestamp": "2026-08-02T23:14:34.728Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.4--3add2f8a8874.json b/.2119/verdicts/self-supplied-evidence.2.4--3add2f8a8874.json deleted file mode 100644 index 076930f..0000000 --- a/.2119/verdicts/self-supplied-evidence.2.4--3add2f8a8874.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.2.4--3add2f8a8874", - "requirementId": "self-supplied-evidence.2.4", - "hash": "3add2f8a8874", - "verdict": "pass", - "summary": "The complete task snapshot pins the conditional trigger, all four pre-existing-value classes, file:line evidence, and the newly-produced-versus-pre-existing distinction for every production-computed test-quality target.", - "timestamp": "2026-08-02T23:11:20.691Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.2.4.json b/.2119/verdicts/self-supplied-evidence.2.4.json new file mode 100644 index 0000000..38e223b --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.2.4.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.2.4--9ea89fc088ad", + "requirementId": "self-supplied-evidence.2.4", + "hash": "9ea89fc088ad", + "verdict": "pass", + "summary": "Built CLI dispatch output is freshly generated and whole-task equality rejects loss of any initial/default/placeholder/sentinel case or new-versus-pre-existing distinction.", + "timestamp": "2026-08-03T17:05:06.404Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.3.1--0c0cd9b96a10.json b/.2119/verdicts/self-supplied-evidence.3.1--0c0cd9b96a10.json deleted file mode 100644 index 67f8bab..0000000 --- a/.2119/verdicts/self-supplied-evidence.3.1--0c0cd9b96a10.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.3.1--0c0cd9b96a10", - "requirementId": "self-supplied-evidence.3.1", - "hash": "0c0cd9b96a10", - "verdict": "pass", - "summary": "Every production-computed test-quality task exactly defines the boundary as invocation of a binary or service outside the gate's own process, with the complete task snapshot rejecting weakened definitions.", - "timestamp": "2026-08-02T23:14:48.392Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.3.1--8f4768f6eda9.json b/.2119/verdicts/self-supplied-evidence.3.1--8f4768f6eda9.json deleted file mode 100644 index e061bc3..0000000 --- a/.2119/verdicts/self-supplied-evidence.3.1--8f4768f6eda9.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.3.1--8f4768f6eda9", - "requirementId": "self-supplied-evidence.3.1", - "hash": "8f4768f6eda9", - "verdict": "pass", - "summary": "Enumerates every production-derived test-quality task and rejects missing or altered boundary wording, including inside-process and non-binary/service near-counterexamples, via an exact required definition.", - "timestamp": "2026-08-02T05:36:17.365Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.3.1--ba8be2d406ca.json b/.2119/verdicts/self-supplied-evidence.3.1--ba8be2d406ca.json deleted file mode 100644 index 28ef528..0000000 --- a/.2119/verdicts/self-supplied-evidence.3.1--ba8be2d406ca.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.3.1--ba8be2d406ca", - "requirementId": "self-supplied-evidence.3.1", - "hash": "ba8be2d406ca", - "verdict": "pass", - "summary": "Every production-generated test-quality task must match the complete normalized oracle, so changing invocation, binary-or-service scope, or outside-the-gate-process boundary wording fails.", - "timestamp": "2026-08-02T23:11:23.747Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.3.1.json b/.2119/verdicts/self-supplied-evidence.3.1.json new file mode 100644 index 0000000..db5103b --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.3.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.3.1--16a37e67b6e3", + "requirementId": "self-supplied-evidence.3.1", + "hash": "16a37e67b6e3", + "verdict": "pass", + "summary": "Built CLI dispatch output is exercised; whole-task equality rejects definitions omitting binary/service, external invocation, or the gate-process boundary.", + "timestamp": "2026-08-03T17:05:06.489Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.3.2--401f4b68deae.json b/.2119/verdicts/self-supplied-evidence.3.2--401f4b68deae.json deleted file mode 100644 index 37faad6..0000000 --- a/.2119/verdicts/self-supplied-evidence.3.2--401f4b68deae.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.3.2--401f4b68deae", - "requirementId": "self-supplied-evidence.3.2", - "hash": "401f4b68deae", - "verdict": "pass", - "summary": "Every production-discovered test-quality task is full-body matched, pinning the conditional file:line demand for both the dependency's production provisioning declaration and its production absence-failure path.", - "timestamp": "2026-08-02T23:14:51.665Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.3.2--5f8200b2fdd0.json b/.2119/verdicts/self-supplied-evidence.3.2--5f8200b2fdd0.json deleted file mode 100644 index 2d27223..0000000 --- a/.2119/verdicts/self-supplied-evidence.3.2--5f8200b2fdd0.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.3.2--5f8200b2fdd0", - "requirementId": "self-supplied-evidence.3.2", - "hash": "5f8200b2fdd0", - "verdict": "pass", - "summary": "PASS: every generated test-quality task is checked for an exact conditional instruction requiring file:line evidence of both production provisioning and the production absence-failure path; omitting the boundary condition, either conjunct, or file:line demand fails.", - "timestamp": "2026-08-02T05:36:21.818Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.3.2--781809753ed3.json b/.2119/verdicts/self-supplied-evidence.3.2--781809753ed3.json deleted file mode 100644 index b4400d7..0000000 --- a/.2119/verdicts/self-supplied-evidence.3.2--781809753ed3.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.3.2--781809753ed3", - "requirementId": "self-supplied-evidence.3.2", - "hash": "781809753ed3", - "verdict": "pass", - "summary": "Every production-discovered test-quality instruction is full-body matched, pinning the conditional file:line demand for both the production provisioning declaration and the production path that fails when the external dependency is absent.", - "timestamp": "2026-08-02T23:11:33.535Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.3.2.json b/.2119/verdicts/self-supplied-evidence.3.2.json new file mode 100644 index 0000000..4dabd20 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.3.2.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.3.2--e873b585f1a6", + "requirementId": "self-supplied-evidence.3.2", + "hash": "e873b585f1a6", + "verdict": "pass", + "summary": "Built CLI dispatch output is exercised; whole-task equality rejects omission of the conditional boundary, production provisioning declaration, absent-dependency failure path, or either half of both.", + "timestamp": "2026-08-03T17:05:06.568Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.4.1--02d88bd20370.json b/.2119/verdicts/self-supplied-evidence.4.1--02d88bd20370.json deleted file mode 100644 index 33fce7f..0000000 --- a/.2119/verdicts/self-supplied-evidence.4.1--02d88bd20370.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.4.1--02d88bd20370", - "requirementId": "self-supplied-evidence.4.1", - "hash": "02d88bd20370", - "verdict": "pass", - "summary": "Every computed production-generated test-quality task matches the complete oracle, pinning a FAIL for either absent applicable provenance or evidence that production cannot produce the claimed failure independently of test setup.", - "timestamp": "2026-08-02T23:14:55.202Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.4.1--38564acf58d7.json b/.2119/verdicts/self-supplied-evidence.4.1--38564acf58d7.json deleted file mode 100644 index 1bb792a..0000000 --- a/.2119/verdicts/self-supplied-evidence.4.1--38564acf58d7.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.4.1--38564acf58d7", - "requirementId": "self-supplied-evidence.4.1", - "hash": "38564acf58d7", - "verdict": "pass", - "summary": "The complete task snapshot requires Record FAIL for both absent applicable provenance evidence and evidence that production cannot produce the failure independently of test setup across every production-computed test-quality target.", - "timestamp": "2026-08-02T23:11:40.674Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.4.1--dbb07998ba99.json b/.2119/verdicts/self-supplied-evidence.4.1--dbb07998ba99.json deleted file mode 100644 index 1b4c3b3..0000000 --- a/.2119/verdicts/self-supplied-evidence.4.1--dbb07998ba99.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.4.1--dbb07998ba99", - "requirementId": "self-supplied-evidence.4.1", - "hash": "dbb07998ba99", - "verdict": "pass", - "summary": "Generated instructions are checked across every production-derived test-quality target and must explicitly record FAIL for either missing provenance or evidence that production cannot independently produce the failure; omission, weakened wording, and escape-clause counterexamples are rejected.", - "timestamp": "2026-08-02T05:36:27.004Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.4.1.json b/.2119/verdicts/self-supplied-evidence.4.1.json new file mode 100644 index 0000000..64873e2 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.4.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.4.1--c246036538fc", + "requirementId": "self-supplied-evidence.4.1", + "hash": "c246036538fc", + "verdict": "pass", + "summary": "Generated test-quality packets from the spawned production CLI retain the explicit fail-on-absent-or-self-supplied-provenance direction for every parsed target.", + "timestamp": "2026-08-03T17:05:04.663Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.5.1--15d76cbfb0f1.json b/.2119/verdicts/self-supplied-evidence.5.1--15d76cbfb0f1.json deleted file mode 100644 index 32d7281..0000000 --- a/.2119/verdicts/self-supplied-evidence.5.1--15d76cbfb0f1.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.5.1--15d76cbfb0f1", - "requirementId": "self-supplied-evidence.5.1", - "hash": "15d76cbfb0f1", - "verdict": "fail", - "summary": "The test inspects only REQ-003.4.2 instead of every computed direct-judgment target; adding provenance questions only to another generated [review] instruction remains green, and rephrased provenance questions can evade the phrase blacklist.", - "timestamp": "2026-08-02T23:12:01.145Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.5.1--4bf3f2230530.json b/.2119/verdicts/self-supplied-evidence.5.1--4bf3f2230530.json deleted file mode 100644 index abfbe7a..0000000 --- a/.2119/verdicts/self-supplied-evidence.5.1--4bf3f2230530.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.5.1--4bf3f2230530", - "requirementId": "self-supplied-evidence.5.1", - "hash": "4bf3f2230530", - "verdict": "pass", - "summary": "Every production-computed direct-judgment target is compared with the complete canonical direct task, which contains no test-quality provenance questionnaire and fails on any insertion.", - "timestamp": "2026-08-02T23:15:24.252Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.5.1--e7eb54958e86.json b/.2119/verdicts/self-supplied-evidence.5.1--e7eb54958e86.json deleted file mode 100644 index 0c79deb..0000000 --- a/.2119/verdicts/self-supplied-evidence.5.1--e7eb54958e86.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.5.1--e7eb54958e86", - "requirementId": "self-supplied-evidence.5.1", - "hash": "e7eb54958e86", - "verdict": "pass", - "summary": "The test exercises a real generated [review] direct-judgment instruction and rejects inclusion of every specified test-quality provenance question category while confirming the direct-judgment prompt remains present.", - "timestamp": "2026-08-02T05:38:41.022Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.5.1.json b/.2119/verdicts/self-supplied-evidence.5.1.json new file mode 100644 index 0000000..0f9af7f --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.5.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.5.1--c1a086a4ad44", + "requirementId": "self-supplied-evidence.5.1", + "hash": "c1a086a4ad44", + "verdict": "pass", + "summary": "Generated direct-judgment packets for parsed [review] targets exactly retain the direct task and omit the test-quality provenance section.", + "timestamp": "2026-08-03T17:05:04.793Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.6.1--140a2a47aa19.json b/.2119/verdicts/self-supplied-evidence.6.1--140a2a47aa19.json deleted file mode 100644 index e1fc4f6..0000000 --- a/.2119/verdicts/self-supplied-evidence.6.1--140a2a47aa19.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.6.1--140a2a47aa19", - "requirementId": "self-supplied-evidence.6.1", - "hash": "140a2a47aa19", - "verdict": "pass", - "summary": "PASS: the real lint workflow exits clean on a covering test containing both literal config input and a factory-built fixture, so either syntax-triggered provenance violation would fail the test.", - "timestamp": "2026-08-02T21:27:52.554Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.6.1--68dbeff8af0f.json b/.2119/verdicts/self-supplied-evidence.6.1--68dbeff8af0f.json deleted file mode 100644 index fe7c9b3..0000000 --- a/.2119/verdicts/self-supplied-evidence.6.1--68dbeff8af0f.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.6.1--68dbeff8af0f", - "requirementId": "self-supplied-evidence.6.1", - "hash": "68dbeff8af0f", - "verdict": "pass", - "summary": "The production lint CLI scans copied annotated tests containing both literal constructions and factory functions, and the test requires status 0 plus the exact clean-spec count, so a syntax-only provenance violation fails.", - "timestamp": "2026-08-02T23:12:28.571Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.6.1--978277912461.json b/.2119/verdicts/self-supplied-evidence.6.1--978277912461.json deleted file mode 100644 index 1a3721e..0000000 --- a/.2119/verdicts/self-supplied-evidence.6.1--978277912461.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.6.1--978277912461", - "requirementId": "self-supplied-evidence.6.1", - "hash": "978277912461", - "verdict": "pass", - "summary": "The production lint CLI scans an otherwise clean fixture whose copied covering tests contain both literal construction and factory syntax; any provenance violation inferred solely from either syntax makes the asserted zero status fail.", - "timestamp": "2026-08-02T23:15:15.194Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.6.1.json b/.2119/verdicts/self-supplied-evidence.6.1.json new file mode 100644 index 0000000..b5c2350 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.6.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.6.1--9eff2a724166", + "requirementId": "self-supplied-evidence.6.1", + "hash": "9eff2a724166", + "verdict": "pass", + "summary": "Built lint accepts isolated string/object/array/template/regexp/number/bigint/boolean/null inputs and named/static/imported/arrow factory forms without provenance violations.", + "timestamp": "2026-08-03T17:07:21.073Z" +} diff --git a/.2119/verdicts/self-supplied-evidence.7.1--5676f3ef50cb.json b/.2119/verdicts/self-supplied-evidence.7.1--5676f3ef50cb.json deleted file mode 100644 index 54a9a8b..0000000 --- a/.2119/verdicts/self-supplied-evidence.7.1--5676f3ef50cb.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.7.1--5676f3ef50cb", - "requirementId": "self-supplied-evidence.7.1", - "hash": "5676f3ef50cb", - "verdict": "pass", - "summary": "All mechanically computed standard targets match complete kind-specific task oracles, and audit filenames exactly equal the computed passing-target set with every audit matching its full oracle; each pins concrete names, cardinality, and the category-promotion prohibition.", - "timestamp": "2026-08-02T23:15:20.011Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.7.1--a75c8b3b6ac2.json b/.2119/verdicts/self-supplied-evidence.7.1--a75c8b3b6ac2.json deleted file mode 100644 index 3ff6975..0000000 --- a/.2119/verdicts/self-supplied-evidence.7.1--a75c8b3b6ac2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.7.1--a75c8b3b6ac2", - "requirementId": "self-supplied-evidence.7.1", - "hash": "a75c8b3b6ac2", - "verdict": "fail", - "summary": "Standard and audit instructions are covered, but contradictory guidance such as 'A category-level summary is sufficient for member-specific evidence' bypasses the negative regex and leaves every assertion green.", - "timestamp": "2026-08-02T23:12:16.515Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.7.1--ae376b3595ab.json b/.2119/verdicts/self-supplied-evidence.7.1--ae376b3595ab.json deleted file mode 100644 index 18ce592..0000000 --- a/.2119/verdicts/self-supplied-evidence.7.1--ae376b3595ab.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewId": "self-supplied-evidence.7.1--ae376b3595ab", - "requirementId": "self-supplied-evidence.7.1", - "hash": "ae376b3595ab", - "verdict": "pass", - "summary": "The fixture rejects missing or weakened evidence-bounded wording for every computed standard target and exactly every audit generated from its computed passing-verdict targets.", - "timestamp": "2026-08-02T21:30:44.081Z" -} diff --git a/.2119/verdicts/self-supplied-evidence.7.1.json b/.2119/verdicts/self-supplied-evidence.7.1.json new file mode 100644 index 0000000..8368652 --- /dev/null +++ b/.2119/verdicts/self-supplied-evidence.7.1.json @@ -0,0 +1,8 @@ +{ + "reviewId": "self-supplied-evidence.7.1--22a18023494c", + "requirementId": "self-supplied-evidence.7.1", + "hash": "22a18023494c", + "verdict": "pass", + "summary": "Every generated standard packet and every producible audit packet retains final tool-authored guidance preserving concrete member names and singular/plural scope; operator custom criteria are separately delimited before the task.", + "timestamp": "2026-08-03T17:05:05.044Z" +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..75ad072 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.2119/verdicts/*.json linguist-generated=true diff --git a/specs/REQ-003-judgment-reviews.md b/specs/REQ-003-judgment-reviews.md index 5b4162f..682b3c0 100644 --- a/specs/REQ-003-judgment-reviews.md +++ b/specs/REQ-003-judgment-reviews.md @@ -76,7 +76,7 @@ accidental corruption (mangled merges, hand-edits) silently reading as green. 1. A verdict record MUST be counted by the gate only when its `verdict` field is exactly `pass` or `fail`, its `summary` is a nonempty string, its `requirementId` equals the requirement component of its `reviewId`, its `hash` equals the review ID's 12-character suffix, and its `timestamp` parses as a date. 2. A verdict file that fails to parse or whose record fails validation MUST produce a check violation identifying the file and the reason, rather than being silently skipped or counted as passing. -3. A verdict file whose filename is not exactly `.json` MUST be treated as malformed, so the check gate and `2119 prune` agree on which file a verdict lives in. +3. REQUIREMENT REMOVED ### REQ-003.8: Reviewer calibration corpus diff --git a/specs/REQ-006-maintenance.md b/specs/REQ-006-maintenance.md index 99d72f7..e1591a9 100644 --- a/specs/REQ-006-maintenance.md +++ b/specs/REQ-006-maintenance.md @@ -27,4 +27,4 @@ a side effect. ### REQ-006.2: Verdict pruning 1. `2119 prune` MUST delete every verdict file whose review ID does not match any current review target, listing each deleted file. -2. `2119 prune` MUST leave verdicts that match current review targets untouched, so a passing `2119 check` still passes after a prune. +2. `2119 prune` MUST preserve the fields of a well-formed verdict record that matches a current review target, even when migrating that record to its canonical path, so a passing `2119 check` still passes after a prune. diff --git a/specs/REQ-012-stable-verdict-files.md b/specs/REQ-012-stable-verdict-files.md new file mode 100644 index 0000000..3649c62 --- /dev/null +++ b/specs/REQ-012-stable-verdict-files.md @@ -0,0 +1,79 @@ +# REQ-012: Stable Verdict Files + +## Overview + +A judgment verdict is evidence about one requirement's review input at one +specific content state. The existing storage name, `.json`, embeds +that state's hash in the filename. Re-review therefore creates another file +instead of superseding the obsolete assertion. Repositories accumulate many +records that no longer describe current review input, even though only the +record matching the current hash can satisfy the gate. + +This feature makes the current assertion explicit and singular. Each +requirement has one stable verdict path, while the record carries the content +hash that `check` compares with the hash computed from the current review +input. Superseded versions remain available in git history, where obsolete +versions of other tracked files already live. + +The content boundary remains the boundary of the judgment being made. For a +test-quality review, the review input is the requirement statement, its +covering annotation evidence blocks, and configured shared evidence, as +defined by REQ-003.1.2, REQ-003.1.7, and REQ-003.1.8. For a `[review]` +requirement, it is the requirement statement, matched evidence files, and any +custom review instructions, as defined by REQ-003.1.3 and REQ-005.1.2. This +avoids invalidating every verdict for an unrelated repository edit without +allowing an edit to material the reviewer judged to retain a pass. + +Existing repositories need a safe transition from files named +`--.json`. A legacy verdict whose hash matches the +current review input remains acceptable during migration, but is identified +as legacy in command output. A stale legacy verdict is identified and never +counts as evidence. When a stable path exists, it is authoritative during +`check`; an old legacy pass cannot bypass a malformed, stale, or failing +stable record. The explicit `prune` command preserves a valid current stable +record when present, otherwise recovers valid current evidence from the +legacy set, and then removes superseded legacy files. `check` remains +non-mutating. + +This contract supersedes the former REQ-003.7.3 filename rule, which required +the content-addressed review ID in the filename. That rule was removed because +it directly conflicts with the stable per-requirement path; record validation +continues to enforce agreement among the stable filename, requirement ID, +review ID, and stored hash. + +Verdicts remain committed evidence and remain mandatory. Stable storage does +not reduce review coverage, skip judgment, or relax any gate. Marking the +remaining JSON as generated changes only GitHub's presentation of those +records. + +## Requirements + +### REQ-012.1: Stable recording + +1. After any successful `2119 pass` or `2119 fail` invocations, `.2119/verdicts/` MUST contain exactly one verdict JSON for each requirement those invocations recorded, at `.json`, with a later invocation for the same requirement replacing its prior stable or legacy verdicts regardless of content hash. + +### REQ-012.2: Concurrent isolation + +1. When verdict-recording commands for distinct requirements are launched without waiting for either command to finish, each successful command MUST leave a well-formed stable record for its own requirement without overwriting, deleting, or corrupting the other command's record. + +### REQ-012.3: Explicit content binding + +1. A well-formed stable verdict record MUST name its canonical requirement ID and review ID, store a lowercase 12-hex content hash equal to that review ID's suffix, store a `pass` or `fail` result with a nonempty summary and parseable timestamp, and reside at `.2119/verdicts/.json`. +2. The stored content hash MUST be the hash of exactly the review input already defined for that target: the inputs and ordering in REQ-003.1.2, REQ-003.1.7, and REQ-003.1.8 for test-quality reviews, or the inputs in REQ-003.1.3 and REQ-005.1.2 for `[review]` reviews, so any change within that input changes the hash while unrelated repository content does not. + +### REQ-012.4: Stable freshness gate + +1. When a stable path exists for a reviewed requirement, `2119 check` MUST treat only a well-formed stable `pass` whose stored hash equals the current review ID's hash as satisfying that requirement, reporting malformed, stale, or current-failing stable state as a check violation without falling back to any legacy record. + +### REQ-012.5: Legacy transition in check + +1. When no stable path exists for a review target, `2119 check` MUST validate every matching legacy `--.json` file, allow only a well-formed current legacy `pass` to satisfy the gate, report malformed legacy records as violations, and emit non-failing migration notices for current or superseded well-formed legacy records that identify their source and stable destination without counting superseded records as evidence. + +### REQ-012.6: Explicit migration + +1. For each requirement, `2119 prune` MUST retain a well-formed current stable record when one exists, otherwise preserve a well-formed current legacy record at the stable path with all verdict fields unchanged. +2. `2119 prune` MUST remove every legacy verdict file after selecting the evidence to retain, list each conversion or removal, and leave at most one verdict JSON per requirement. + +### REQ-012.7: Generated-file presentation + +1. `2119 init` MUST install a Git attribute that marks `.2119/verdicts/*.json` as `linguist-generated=true` while the verdict path remains eligible for Git tracking. diff --git a/src/changed.ts b/src/changed.ts index cff1d60..c27e4bb 100644 --- a/src/changed.ts +++ b/src/changed.ts @@ -8,7 +8,7 @@ import { evidenceBlockParts } from "./annotations.js"; import { matchGlobs } from "./files.js"; import { allRequirements } from "./spec.js"; import { fileParts, splitReviewId } from "./hash.js"; -import { VERDICTS_DIR } from "./verdict.js"; +import { SAFE_REQUIREMENT_ID, VERDICTS_DIR } from "./verdict.js"; import { runVerifyCommands, VERIFY_TIMEOUT_MS } from "./verify.js"; import type { Requirement, Violation } from "./model.js"; @@ -268,7 +268,8 @@ function scopeContext( if ( parsed && currentRequirements.has(parsed.requirementId) && - (currentReviewIds.get(parsed.requirementId) === parsed.reviewId || + (parsed.reviewId === undefined || + currentReviewIds.get(parsed.requirementId) === parsed.reviewId || baselineReviewIds.get(parsed.requirementId) === parsed.reviewId) ) { affected.add(parsed.requirementId); @@ -294,7 +295,8 @@ function scopeContext( const assigned = Boolean( parsed && currentRequirements.has(parsed.requirementId) && - (currentReviewIds.get(parsed.requirementId) === parsed.reviewId || + (parsed.reviewId === undefined || + currentReviewIds.get(parsed.requirementId) === parsed.reviewId || baselineReviewIds.get(parsed.requirementId) === parsed.reviewId), ); return assigned ? affected.has(parsed!.requirementId) : changedPaths.has(path); @@ -319,6 +321,7 @@ function scopeContext( lintViolations, coverViolations, reviewViolations, + migrationNotices: current.migrationNotices.filter((notice) => affected.has(notice.requirementId)), verifyViolations, notInitialized: current.notInitialized && baseline.notInitialized, scopedRequirementIds: affected, @@ -367,8 +370,9 @@ function quotedRequirementId(message: string): string | undefined { return message.match(/requirement ID "([^"]+)"/)?.[1]; } -function verdictReview(path: string): { reviewId: string; requirementId: string } | undefined { +function verdictReview(path: string): { reviewId?: string; requirementId: string } | undefined { const name = basename(path).replace(/\.json$/, ""); const parsed = splitReviewId(name); - return parsed ? { reviewId: name, requirementId: parsed.requirementId } : undefined; + if (parsed) return { reviewId: name, requirementId: parsed.requirementId }; + return SAFE_REQUIREMENT_ID.test(name) ? { requirementId: name } : undefined; } diff --git a/src/check.ts b/src/check.ts index 22f76ce..d3641d5 100644 --- a/src/check.ts +++ b/src/check.ts @@ -6,7 +6,7 @@ import { parseSpec } from "./spec.js"; import { scanAnnotations } from "./annotations.js"; import { computeCoverage, type CoverageResult } from "./cover.js"; import { computeReviewTargets, verdictViolations, type ReviewTask } from "./review.js"; -import { scanVerdicts } from "./verdict.js"; +import { legacyMigrationNotices, scanVerdicts, type MigrationNotice } from "./verdict.js"; import { runVerifyCommands } from "./verify.js"; import { allRequirements } from "./spec.js"; import type { Annotation, SpecFile, Verdict, Violation } from "./model.js"; @@ -27,6 +27,7 @@ export interface CheckContext { coverViolations: Violation[]; reviewViolations: Violation[]; malformedVerdictViolations: Violation[]; + migrationNotices: MigrationNotice[]; verifyViolations: Violation[]; notInitialized: boolean; /** Present for `check --changed`; limits report counts and manual output to affected requirements. */ @@ -90,8 +91,11 @@ export function buildContext(root: string, options: BuildOptions = {}): CheckCon const allReviewTargets = computeReviewTargets(config, specs, coverage, repoFiles, annotations, markerLineByFile); const reviewTargets = config.reviews ? allReviewTargets : []; // Malformed verdict files are loud violations, not silent passes or skips (REQ-003.7.2). - const { verdicts, violations: malformedVerdicts } = scanVerdicts(root); - const reviewViolations = [...malformedVerdicts, ...verdictViolations(reviewTargets, verdicts)]; + const verdictScan = scanVerdicts(root); + const { verdicts, violations: malformedVerdicts, stableFiles } = verdictScan; + const currentReviewIds = new Map(reviewTargets.map((target) => [target.requirement.id, target.reviewId])); + const migrationNotices = legacyMigrationNotices(verdictScan, currentReviewIds); + const reviewViolations = [...malformedVerdicts, ...verdictViolations(reviewTargets, verdicts, stableFiles)]; // [review: instructions: ] pointing at a missing file (REQ-005.1.4), // and [review: ] matching nothing — a typo'd glob must fail loudly @@ -135,6 +139,7 @@ export function buildContext(root: string, options: BuildOptions = {}): CheckCon coverViolations: coverage.violations, reviewViolations, malformedVerdictViolations: malformedVerdicts, + migrationNotices, verifyViolations, notInitialized, }; @@ -145,6 +150,7 @@ export interface CheckReport { violations: Violation[]; uncoveredRequirements: string[]; staleReviews: string[]; + migrationNotices: string[]; manualRequirements: { id: string; text: string }[]; requirementCount: number; coveredCount: number; @@ -160,6 +166,7 @@ export function buildReport(ctx: CheckContext): CheckReport { violations, uncoveredRequirements: ctx.coverage.uncovered.map((r) => r.id), staleReviews: ctx.reviewViolations.map((v) => v.message), + migrationNotices: ctx.migrationNotices.map((notice) => notice.message), manualRequirements: ctx.coverage.manual.map((r) => ({ id: r.id, text: r.text })), requirementCount: enforcedTestReqs.length, coveredCount: ctx.coverage.covered.size, diff --git a/src/cli.ts b/src/cli.ts index c8e743b..78891c6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -110,11 +110,12 @@ switch (command) { ctx = buildContext(root); } } + const auditRequested = args.includes("--audit") || ctx.config.auditAlways; const tasks = generateInstructions(ctx.config, ctx.reviewTargets, ctx.verdicts); // Audit generation is explicit-only: the flag or `audit: always`, never // the plain loop (REQ-003.6.4). const auditTasks = - args.includes("--audit") || ctx.config.auditAlways + auditRequested ? generateAuditInstructions(ctx.config, ctx.reviewTargets, ctx.verdicts) : []; if (auditTasks.length > 0) { @@ -229,6 +230,7 @@ switch (command) { console.log(JSON.stringify(report, null, 2)); } else { printViolations(report.violations); + for (const notice of report.migrationNotices) console.log(notice); if (report.manualRequirements.length > 0) { console.log(`\nManual requirements (not automatically checked):`); for (const m of report.manualRequirements) console.log(` - ${m.id}: ${m.text}`); @@ -247,10 +249,18 @@ switch (command) { case "prune": { const ctx = buildContext(root); requireInitialized(ctx); - const current = new Set(ctx.reviewTargets.map((t) => t.reviewId)); - const pruned = pruneVerdicts(root, current); - for (const id of pruned) console.log(`pruned .2119/verdicts/${id}.json`); - console.log(`prune: removed ${pruned.length} orphaned verdict(s), kept ${ctx.verdicts.size - pruned.length}`); + const current = new Map(ctx.reviewTargets.map((t) => [t.requirement.id, t.reviewId])); + const result = pruneVerdicts(root, current); + for (const action of result.actions) { + if (action.kind === "migrated") { + console.log(`migrated .2119/verdicts/${action.source} -> .2119/verdicts/${action.destination}`); + } else { + console.log(`pruned .2119/verdicts/${action.source}`); + } + } + const removed = result.actions.filter((action) => action.kind === "pruned").length; + const migrated = result.actions.filter((action) => action.kind === "migrated").length; + console.log(`prune: removed ${removed} orphaned verdict(s), migrated ${migrated}, kept ${result.kept}`); break; } diff --git a/src/init.ts b/src/init.ts index 2d41495..5ed9846 100644 --- a/src/init.ts +++ b/src/init.ts @@ -9,7 +9,7 @@ import { refreshPinnedArtifacts, type AgentName, } from "./adapters.js"; -import { ensureReviewStorageRules } from "./verdict.js"; +import { ensureReviewStorageRules, ensureVerdictAttributes } from "./verdict.js"; const CONFIG_TEMPLATE = `# 2119 configuration — https://github.com/Unsupervisedcom/2119 # All fields optional; these are the defaults unless noted. @@ -174,6 +174,9 @@ export function runInit(root: string, args: string[]): void { if (ensureReviewStorageRules(root)) { created.push(".gitignore (2119 review/verdict rules)"); } + if (ensureVerdictAttributes(root)) { + created.push(".gitattributes (2119 generated verdict rule)"); + } const agentsResult = upsertSection(join(root, "AGENTS.md"), refresh); if (agentsResult) created.push(`AGENTS.md (2119 section ${agentsResult})`); diff --git a/src/review.ts b/src/review.ts index 0c890b8..bd74e1b 100644 --- a/src/review.ts +++ b/src/review.ts @@ -7,6 +7,7 @@ import { computeReviewId, fileParts } from "./hash.js"; import { evidenceBlockParts } from "./annotations.js"; import { matchGlobs } from "./files.js"; import { allRequirements } from "./spec.js"; +import type { VerdictFile } from "./verdict.js"; export const REVIEWS_DIR = ".2119/reviews"; @@ -79,9 +80,32 @@ export function computeReviewTargets( export function verdictViolations( targets: Omit[], verdicts: Map, + stableFiles: Map = new Map(), ): Violation[] { const out: Violation[] = []; for (const t of targets) { + const stable = stableFiles.get(t.requirement.id); + if (stable) { + // Malformed stable files already produce their own fail-closed scan + // violation and must never fall back to a legacy pass. + if (!stable.verdict) continue; + if (stable.verdict.reviewId !== t.reviewId) { + out.push({ + file: `${VERDICTS_DIR_HINT}/${stable.name}`, + line: 1, + rule: "REQ-003.3.1", + message: `${t.requirement.id} has a stale review verdict (recorded ${stable.verdict.reviewId}, current ${t.reviewId}); run \`2119 review\``, + }); + } else if (stable.verdict.verdict === "fail") { + out.push({ + file: `${VERDICTS_DIR_HINT}/${stable.name}`, + line: 1, + rule: "REQ-003.2.4", + message: `${t.requirement.id} has a failing review verdict: ${stable.verdict.summary}`, + }); + } + continue; + } const v = verdicts.get(t.reviewId); if (!v) { out.push({ @@ -112,17 +136,14 @@ export function generateInstructions( ): ReviewTask[] { const pending = targets.filter((t) => verdicts.get(t.reviewId)?.verdict !== "pass"); // Keep the directory exactly in sync with the pending set — stale - // instruction files from prior rounds are misleading (REQ-006.1). Audit - // files stay while their verdict is still a current pass (REQ-003.6.3). + // instruction files from prior rounds are misleading (REQ-006.1). Explicit + // audit requests regenerate their packets after this cleanup (REQ-003.6.3). const dir = join(config.root, REVIEWS_DIR); mkdirSync(dir, { recursive: true }); const pendingIds = new Set(pending.map((t) => t.reviewId)); - const passingIds = new Set( - targets.filter((t) => verdicts.get(t.reviewId)?.verdict === "pass").map((t) => t.reviewId), - ); for (const file of readdirSync(dir)) { if (file.endsWith(".audit.md")) { - if (!passingIds.has(file.replace(/\.audit\.md$/, ""))) unlinkSync(join(dir, file)); + unlinkSync(join(dir, file)); } else if (file.endsWith(".md") && !pendingIds.has(file.replace(/\.md$/, ""))) { unlinkSync(join(dir, file)); } @@ -261,6 +282,8 @@ Record FAIL when applicable provenance evidence is absent or shows that producti **Counterexample obligation:** enumerate the requirement's conjuncts and boundary terms (words like "comment", "exactly", "only", "begins with"). For each, construct the nearest violating input — the almost-conforming case the requirement forbids — and confirm a test rejects it. +When a requirement names a grammar or other defined input language, enumerate and probe its edge +productions rather than accepting coverage of only the most common form. Do not reason from the implementation's current behavior; reason from the requirement's text. A review that cannot name a rejected counterexample for a boundary term is not a pass.` : `**Is this requirement genuinely satisfied by the current state of the evidence files?** @@ -295,6 +318,13 @@ ${modelLine} ## Evidence files ${evidenceList} +${custom ? ` +## Additional review criteria + +*(from \`${custom.path}\` — these extend the requirement above)* + +${custom.content} +` : ""} ## Your task @@ -303,7 +333,7 @@ ${question} **Judge the requirement too:** if the requirement itself is ambiguous, untestable, or states an implementation mechanism rather than an observable outcome, fail with that finding — a bad requirement honestly tested is still a bad requirement. -${custom ? `\n## Additional review criteria\n\n*(from \`${custom.path}\` — these extend the requirement above)*\n\n${custom.content}\n` : ""} + ## Recording your verdict Keep the verdict summary's subject no broader than the cited evidence: preserve concrete member names and singular/plural scope; do not promote member-specific evidence into a category claim. diff --git a/src/verdict.ts b/src/verdict.ts index 493ca51..bae758e 100644 --- a/src/verdict.ts +++ b/src/verdict.ts @@ -4,6 +4,7 @@ import type { Verdict, VerdictKind, Violation } from "./model.js"; import { splitReviewId } from "./hash.js"; export const VERDICTS_DIR = ".2119/verdicts"; +export const VERDICT_ATTRIBUTES_RULE = ".2119/verdicts/*.json linguist-generated=true"; const GITIGNORE_SECTION = `# 2119:gitignore-begin # Review packets are scratch; verdicts are committed audit history. @@ -66,13 +67,31 @@ export function ensureReviewStorageRules(root: string): boolean { return changed; } +/** Mark committed verdict records as generated in GitHub's diff UI. */ +export function ensureVerdictAttributes(root: string): boolean { + const path = join(root, ".gitattributes"); + const existing = existsSync(path) ? readFileSync(path, "utf8") : ""; + if (existing.split(/\r?\n/).includes(VERDICT_ATTRIBUTES_RULE)) return false; + const updated = `${existing.trimEnd()}${existing.trimEnd() ? "\n" : ""}${VERDICT_ATTRIBUTES_RULE}\n`; + writeFileSync(path, updated); + return true; +} + const SAFE_ID = /^[A-Za-z0-9.-]+--[0-9a-f]{12}$/; +export const SAFE_REQUIREMENT_ID = /^[A-Za-z0-9.-]+$/; -function verdictPath(root: string, reviewId: string): string { - if (!SAFE_ID.test(reviewId)) { - throw new Error(`Invalid review ID: "${reviewId}"`); +function verdictPath(root: string, requirementId: string): string { + if (!SAFE_REQUIREMENT_ID.test(requirementId)) { + throw new Error(`Invalid requirement ID: "${requirementId}"`); } - return join(root, VERDICTS_DIR, `${reviewId}.json`); + return join(root, VERDICTS_DIR, `${requirementId}.json`); +} + +export interface VerdictFile { + name: string; + kind: "stable" | "legacy"; + requirementId: string; + verdict?: Verdict; } export interface VerdictScan { @@ -80,6 +99,10 @@ export interface VerdictScan { verdicts: Map; /** One violation per malformed verdict file — the gate fails closed, loudly (REQ-003.7.2). */ violations: Violation[]; + /** Every JSON file, including malformed records, for migration and pruning. */ + files: VerdictFile[]; + /** Stable paths are authoritative even when their contents are malformed. */ + stableFiles: Map; } /** @@ -87,7 +110,12 @@ export interface VerdictScan { * malformed one (missing/typo'd verdict, mangled merge, wrong filename) is a * check violation, never a silent pass and never a silent skip (REQ-003.7). */ -function validateVerdict(filename: string, record: unknown): { verdict?: Verdict; reason?: string } { +function validateVerdict( + filename: string, + kind: "stable" | "legacy", + pathRequirementId: string, + record: unknown, +): { verdict?: Verdict; reason?: string } { if (typeof record !== "object" || record === null || Array.isArray(record)) { return { reason: "not a JSON object" }; } @@ -95,9 +123,6 @@ function validateVerdict(filename: string, record: unknown): { verdict?: Verdict if (typeof r.reviewId !== "string" || !SAFE_ID.test(r.reviewId)) { return { reason: "missing or invalid reviewId" }; } - if (filename !== `${r.reviewId}.json`) { - return { reason: `filename does not match reviewId (expected ${r.reviewId}.json)` }; - } if (r.verdict !== "pass" && r.verdict !== "fail") { return { reason: `verdict must be exactly "pass" or "fail", got ${JSON.stringify(r.verdict)}` }; } @@ -108,18 +133,22 @@ function validateVerdict(filename: string, record: unknown): { verdict?: Verdict if (!parts || r.requirementId !== parts.requirementId) { return { reason: "requirementId does not match reviewId" }; } - if (r.hash !== parts.hash) { + if (typeof r.hash !== "string" || !/^[0-9a-f]{12}$/.test(r.hash) || r.hash !== parts.hash) { return { reason: "hash does not match the reviewId suffix" }; } if (typeof r.timestamp !== "string" || Number.isNaN(Date.parse(r.timestamp))) { return { reason: "timestamp is not a parseable date" }; } + const expected = kind === "stable" ? `${r.requirementId}.json` : `${r.reviewId}.json`; + if (filename !== expected || pathRequirementId !== r.requirementId) { + return { reason: `filename does not match ${kind === "stable" ? "requirementId" : "reviewId"} (expected ${expected})` }; + } return { verdict: r as unknown as Verdict }; } export function scanVerdicts(root: string): VerdictScan { const dir = join(root, VERDICTS_DIR); - const out: VerdictScan = { verdicts: new Map(), violations: [] }; + const out: VerdictScan = { verdicts: new Map(), violations: [], files: [], stableFiles: new Map() }; const malformed = (name: string, reason: string) => out.violations.push({ file: `${VERDICTS_DIR}/${name}`, @@ -130,6 +159,13 @@ export function scanVerdicts(root: string): VerdictScan { if (!existsSync(dir)) return out; for (const name of readdirSync(dir)) { if (!name.endsWith(".json")) continue; + const stem = name.replace(/\.json$/, ""); + const legacyParts = splitReviewId(stem); + const kind = legacyParts ? "legacy" : "stable"; + const requirementId = legacyParts?.requirementId ?? stem; + const file: VerdictFile = { name, kind, requirementId }; + out.files.push(file); + if (kind === "stable") out.stableFiles.set(requirementId, file); let record: unknown; try { record = JSON.parse(readFileSync(join(dir, name), "utf8")); @@ -137,13 +173,40 @@ export function scanVerdicts(root: string): VerdictScan { malformed(name, `unparseable JSON (${(e as Error).message})`); continue; } - const { verdict, reason } = validateVerdict(name, record); - if (verdict) out.verdicts.set(verdict.reviewId, verdict); + const { verdict, reason } = validateVerdict(name, kind, requirementId, record); + if (verdict) file.verdict = verdict; else malformed(name, reason!); } + // A stable path is the sole authority for its requirement. Legacy records + // remain transitional evidence only where no stable path exists. + for (const file of out.files) { + if (!file.verdict) continue; + if (file.kind === "stable" || !out.stableFiles.has(file.requirementId)) { + out.verdicts.set(file.verdict.reviewId, file.verdict); + } + } return out; } +export interface MigrationNotice { + requirementId: string; + message: string; +} + +export function legacyMigrationNotices( + scan: VerdictScan, + currentReviewIds: Map, +): MigrationNotice[] { + return scan.files + .filter((file) => file.kind === "legacy" && file.verdict && !scan.stableFiles.has(file.requirementId)) + .map((file) => ({ + requirementId: file.requirementId, + message: + `migration: ${VERDICTS_DIR}/${file.name} -> ${VERDICTS_DIR}/${file.requirementId}.json ` + + `(${currentReviewIds.get(file.requirementId) === file.verdict!.reviewId ? "current" : "superseded"} legacy verdict; run \`2119 prune\`)`, + })); +} + /** Valid verdicts only; use scanVerdicts when malformed-file violations matter. */ export function readVerdicts(root: string): Map { return scanVerdicts(root).verdicts; @@ -154,19 +217,48 @@ export function readVerdicts(root: string): Map { * Verdicts for current targets are never touched, so a passing check stays * passing. Returns the deleted review IDs. */ -export function pruneVerdicts(root: string, currentIds: Set): string[] { +export interface PruneAction { + kind: "migrated" | "pruned"; + source: string; + destination?: string; +} + +export interface PruneResult { + actions: PruneAction[]; + kept: number; +} + +export function pruneVerdicts(root: string, currentIds: Map): PruneResult { const dir = join(root, VERDICTS_DIR); - const pruned: string[] = []; - if (!existsSync(dir)) return pruned; - for (const name of readdirSync(dir)) { - if (!name.endsWith(".json")) continue; - const reviewId = name.replace(/\.json$/, ""); - if (!currentIds.has(reviewId)) { - unlinkSync(join(dir, name)); - pruned.push(reviewId); + const result: PruneResult = { actions: [], kept: 0 }; + if (!existsSync(dir)) return result; + const scan = scanVerdicts(root); + const keep = new Set(); + for (const [requirementId, currentReviewId] of currentIds) { + const stable = scan.stableFiles.get(requirementId); + if (stable?.verdict?.reviewId === currentReviewId) { + keep.add(stable.name); + continue; + } + const legacy = scan.files.find( + (file) => file.kind === "legacy" && file.verdict?.reviewId === currentReviewId, + ); + if (legacy?.verdict) { + const destination = `${requirementId}.json`; + writeFileSync(join(dir, destination), `${JSON.stringify(legacy.verdict, null, 2)}\n`); + keep.add(destination); + result.actions.push({ kind: "migrated", source: legacy.name, destination }); } } - return pruned; + for (const file of scan.files) { + if (file.kind === "stable" && keep.has(file.name)) continue; + // A migrated source is already represented by its conversion action. + const migrated = result.actions.some((action) => action.kind === "migrated" && action.source === file.name); + if (existsSync(join(dir, file.name))) unlinkSync(join(dir, file.name)); + if (!migrated) result.actions.push({ kind: "pruned", source: file.name }); + } + result.kept = keep.size; + return result; } /** @@ -180,17 +272,28 @@ export function writeVerdict( verdict: VerdictKind, summary: string, ): Verdict { + const parts = splitReviewId(reviewId); + if (!parts || parts.requirementId !== requirementId) { + throw new Error(`Review ID ${reviewId} does not belong to ${requirementId}`); + } ensureReviewStorageRules(root); const record: Verdict = { reviewId, requirementId, - hash: reviewId.slice(-12), + hash: parts.hash, verdict, summary, timestamp: new Date().toISOString(), }; - const path = verdictPath(root, reviewId); mkdirSync(join(root, VERDICTS_DIR), { recursive: true }); + const path = verdictPath(root, requirementId); writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`); + // Successful replacement removes every legacy version of this one + // requirement without touching independently reviewed requirements. + for (const name of readdirSync(join(root, VERDICTS_DIR))) { + if (!name.endsWith(".json")) continue; + const legacy = splitReviewId(name.replace(/\.json$/, "")); + if (legacy?.requirementId === requirementId) unlinkSync(join(root, VERDICTS_DIR, name)); + } return record; } diff --git a/tests/check-changed.test.ts b/tests/check-changed.test.ts index 40d7aaf..094be60 100644 --- a/tests/check-changed.test.ts +++ b/tests/check-changed.test.ts @@ -604,7 +604,9 @@ test('stop', () => expect(widget.stop()).toBe(true)) const replaced = initRepo(files); passReviews(replaced.root, ["FIX-001.1.1"]); const replacedBase = commitCurrent(replaced.root); - const currentId = git(replaced.root, "ls-files", ".2119/verdicts/FIX-001.1.1*.json").match(/FIX-001\.1\.1--[0-9a-f]{12}/)![0]; + const currentId = JSON.parse( + readFileSync(join(replaced.root, ".2119/verdicts/FIX-001.1.1.json"), "utf8"), + ).reviewId as string; expect(run(replaced.root, ["fail", currentId, "--summary", "replacement rejection"]).status).toBe(0); const replacedReport = json(run(replaced.root, ["check", "--changed", replacedBase, "--json"])); expect(replacedReport.staleReviews.join("\n")).toContain("FIX-001.1.1"); @@ -706,6 +708,33 @@ test('stop', () => expect(widget.stop()).toBe(true)) const verifyReport = json(run(verifyCase.root, ["check", "--changed", verifyCase.base, "--json"])); expect(verifyReport.violations.some((v) => v.message.includes("FIX-001.1.1"))).toBe(true); expect(verifyReport.violations.some((v) => v.message.includes("FIX-001.1.2"))).toBe(false); + + const freshness = initRepo({ + ".2119.yml": 'prefix: "FIX"\n', + "specs/FIX-001-widgets.md": TWO_REQUIREMENTS, + "tests/widget.test.js": + "// 2119: FIX-001.1.1\ntest('spins', () => {})\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => {})\n", + }); + passReviews(freshness.root, ["FIX-001.1.1", "FIX-001.1.2"]); + // Establish a baseline with a pre-existing stale verdict for requirement + // two, then affect only requirement one's evidence in the current view. + write( + freshness.root, + "tests/widget.test.js", + "// 2119: FIX-001.1.1\ntest('spins', () => {})\n" + + "// 2119: FIX-001.1.2\ntest('stops eventually', () => {})\n", + ); + const freshnessBase = commitCurrent(freshness.root, "baseline with unrelated stale verdict"); + write( + freshness.root, + "tests/widget.test.js", + "// 2119: FIX-001.1.1\ntest('spins twice', () => {})\n" + + "// 2119: FIX-001.1.2\ntest('stops eventually', () => {})\n", + ); + const freshnessReport = json(run(freshness.root, ["check", "--changed", freshnessBase, "--json"])); + expect(freshnessReport.staleReviews.join("\n")).toContain("FIX-001.1.1"); + expect(freshnessReport.staleReviews.join("\n")).not.toContain("FIX-001.1.2"); }); // 2119: REQ-010.3.2 diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 523711c..9cff497 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -248,12 +248,19 @@ describe("cli end-to-end", () => { ).toThrow(); const dispatchRoot = fixture(); + writeFileSync(join(dispatchRoot, "specs/FIX-001-widgets.md"), `${SPEC}2. The widget MUST stop.\n`); + writeFileSync( + join(dispatchRoot, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins', () => {})\n// 2119: FIX-001.1.2\ntest('stops', () => {})\n", + ); run(dispatchRoot, ["init"]); const pending = run(dispatchRoot, ["review"]); expect(pending.status).toBe(1); - const reviewId = pending.stdout.match(/FIX-001\.1\.1--[0-9a-f]{12}/)?.[0]; - expect(reviewId).toBeTruthy(); - expect(readdirSync(join(dispatchRoot, ".2119/reviews"))).toEqual([`${reviewId}.md`]); + const reviewIds = pending.stdout.match(/FIX-001\.1\.[12]--[0-9a-f]{12}/g) ?? []; + expect(reviewIds).toHaveLength(2); + expect(readdirSync(join(dispatchRoot, ".2119/reviews")).sort()).toEqual( + reviewIds.map((reviewId) => `${reviewId}.md`).sort(), + ); for (const command of ["pass", "fail"]) { const verdictRoot = fixture(); @@ -270,7 +277,7 @@ describe("cli end-to-end", () => { mkdirSync(join(verdictRoot, ".2119/verdicts"), { recursive: true }); writeFileSync(join(verdictRoot, ".2119/verdicts/.gitignore"), "*.json\n"); expect(run(verdictRoot, [command, verdictId!, "--summary", `${command} remains trackable`]).status).toBe(0); - const verdictPath = join(verdictRoot, `.2119/verdicts/${verdictId}.json`); + const verdictPath = join(verdictRoot, ".2119/verdicts/FIX-001.1.1.json"); const record = JSON.parse(readFileSync(verdictPath, "utf8")); expect(record).toMatchObject({ reviewId: verdictId, @@ -281,7 +288,7 @@ describe("cli end-to-end", () => { expect(record.hash).toBe(verdictId!.slice(-12)); expect(Number.isNaN(Date.parse(record.timestamp))).toBe(false); expect(() => - execFileSync("git", ["check-ignore", "-q", `.2119/verdicts/${verdictId}.json`], { cwd: verdictRoot }), + execFileSync("git", ["check-ignore", "-q", ".2119/verdicts/FIX-001.1.1.json"], { cwd: verdictRoot }), ).toThrow(); } }); diff --git a/tests/file-scoped-ids.test.ts b/tests/file-scoped-ids.test.ts index 79693f2..20cf9b5 100644 --- a/tests/file-scoped-ids.test.ts +++ b/tests/file-scoped-ids.test.ts @@ -944,13 +944,13 @@ describe("grammar coexistence (REQ-011.5)", () => { const fsOrphan = "codex-session-scrollback.1.1--" + "b".repeat(12); writeVerdict(root, legacyOrphan, "REQ-900.1.1", "pass", "stale legacy"); writeVerdict(root, fsOrphan, "codex-session-scrollback.1.1", "pass", "stale fs"); - expect(existsSync(join(root, `.2119/verdicts/${legacyOrphan}.json`))).toBe(true); - expect(existsSync(join(root, `.2119/verdicts/${fsOrphan}.json`))).toBe(true); + expect(existsSync(join(root, ".2119/verdicts/REQ-900.1.1.json"))).toBe(true); + expect(existsSync(join(root, ".2119/verdicts/codex-session-scrollback.1.1.json"))).toBe(true); const prune = run(root, ["prune"]); expect(prune.status).toBe(0); - expect(existsSync(join(root, `.2119/verdicts/${legacyOrphan}.json`))).toBe(false); - expect(existsSync(join(root, `.2119/verdicts/${fsOrphan}.json`))).toBe(false); + expect(existsSync(join(root, ".2119/verdicts/REQ-900.1.1.json"))).toBe(false); + expect(existsSync(join(root, ".2119/verdicts/codex-session-scrollback.1.1.json"))).toBe(false); }); // 2119: REQ-011.5.6 diff --git a/tests/maintenance.test.ts b/tests/maintenance.test.ts index e28e2a2..fbfe8c1 100644 --- a/tests/maintenance.test.ts +++ b/tests/maintenance.test.ts @@ -42,19 +42,35 @@ const reviewFiles = (root: string) => readdirSync(join(root, ".2119/reviews")).f describe("state maintenance (REQ-006)", () => { // 2119: REQ-006.1.1 - it("review deletes instruction files that are no longer pending", () => { + it("review deletes instruction files that are no longer pending", async () => { + const { buildContext } = await import("../src/check.js"); const root = fixture(); + writeFileSync(join(root, "specs/FIX-001-widgets.md"), `${SPEC}2. The widget MUST stop.\n`); + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spin v1', () => {})\n// 2119: FIX-001.1.2\ntest('stop', () => {})\n", + ); run(root, ["review"]); - const [before] = reviewFiles(root); - expect(before).toBeTruthy(); + const before = reviewFiles(root); + expect(before).toHaveLength(2); + const superseded = before.find((file) => file.startsWith("FIX-001.1.1--"))!; + const passingId = before.find((file) => file.startsWith("FIX-001.1.2--"))!.replace(/\.md$/, ""); + expect(run(root, ["pass", passingId, "--summary", "stop is covered"]).status).toBe(0); // Editing the covering test mints a new review ID; the old file must go. - writeFileSync(join(root, "tests/widget.test.js"), "// 2119: FIX-001.1.1\ntest('spin v2', () => {})\n"); + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spin v2', () => {})\n// 2119: FIX-001.1.2\ntest('stop', () => {})\n", + ); + writeFileSync(join(root, ".2119/reviews/not-pending.md"), "obsolete packet\n"); run(root, ["review"]); + const ctx = buildContext(root); + const current = ctx.reviewTargets + .filter((target) => ctx.verdicts.get(target.reviewId)?.verdict !== "pass") + .map((target) => target.reviewId); const after = reviewFiles(root); - expect(after).toHaveLength(1); - expect(after[0]).not.toBe(before); - expect(existsSync(join(root, ".2119/reviews", before))).toBe(false); + expect(after.sort()).toEqual(current.map((id) => `${id}.md`).sort()); + expect(existsSync(join(root, ".2119/reviews", superseded))).toBe(false); }); // 2119: REQ-006.1.2 @@ -63,6 +79,9 @@ describe("state maintenance (REQ-006)", () => { const review = run(root, ["review"]); const id = review.stdout.match(/FIX-001\.1\.1--[0-9a-f]{12}/)![0]; run(root, ["pass", id, "--summary", "asserts spin"]); + run(root, ["review", "--audit"]); + expect(reviewFiles(root).some((file) => file.endsWith(".audit.md"))).toBe(true); + writeFileSync(join(root, ".2119/reviews/stale.md"), "obsolete packet\n"); const clean = run(root, ["review"]); expect(clean.status).toBe(0); @@ -76,27 +95,66 @@ describe("state maintenance (REQ-006)", () => { const review = run(root, ["review"]); const oldId = review.stdout.match(/FIX-001\.1\.1--[0-9a-f]{12}/)![0]; run(root, ["pass", oldId, "--summary", "asserts spin"]); + const stalePath = join(root, ".2119/verdicts/FIX-001.1.1.json"); + expect(existsSync(stalePath)).toBe(true); + const orphanPath = join(root, ".2119/verdicts/FIX-999.1.1.json"); + const orphanId = `FIX-999.1.1--${"a".repeat(12)}`; + writeFileSync( + orphanPath, + `${JSON.stringify({ + reviewId: orphanId, + requirementId: "FIX-999.1.1", + hash: "a".repeat(12), + verdict: "pass", + summary: "orphan evidence", + timestamp: "2026-08-03T00:00:00.000Z", + })}\n`, + ); + expect(existsSync(orphanPath)).toBe(true); + const legacyOrphanId = `FIX-998.1.1--${"b".repeat(12)}`; + const legacyOrphanPath = join(root, `.2119/verdicts/${legacyOrphanId}.json`); + writeFileSync( + legacyOrphanPath, + `${JSON.stringify({ + reviewId: legacyOrphanId, + requirementId: "FIX-998.1.1", + hash: "b".repeat(12), + verdict: "pass", + summary: "legacy orphan evidence", + timestamp: "2026-08-03T00:00:00.000Z", + })}\n`, + ); + expect(existsSync(legacyOrphanPath)).toBe(true); // Invalidate: the passed verdict is now orphaned. writeFileSync(join(root, "tests/widget.test.js"), "// 2119: FIX-001.1.1\ntest('spin v2', () => {})\n"); const prune = run(root, ["prune"]); expect(prune.status).toBe(0); - expect(prune.stdout).toContain(`pruned .2119/verdicts/${oldId}.json`); - expect(prune.stdout).toContain("removed 1 orphaned verdict(s)"); - expect(existsSync(join(root, ".2119/verdicts", `${oldId}.json`))).toBe(false); + expect(prune.stdout).toContain("pruned .2119/verdicts/FIX-001.1.1.json"); + expect(prune.stdout).toContain("pruned .2119/verdicts/FIX-999.1.1.json"); + expect(prune.stdout).toContain(`pruned .2119/verdicts/${legacyOrphanId}.json`); + expect(prune.stdout).toContain("removed 3 orphaned verdict(s)"); + expect(existsSync(stalePath)).toBe(false); + expect(existsSync(orphanPath)).toBe(false); + expect(existsSync(legacyOrphanPath)).toBe(false); }); // 2119: REQ-006.2.2 - it("prune keeps current verdicts, and check still passes afterwards", () => { + it("prune keeps current verdicts, and check still passes afterwards", async () => { + const { readFileSync, statSync, utimesSync } = await import("node:fs"); const root = fixture(); const review = run(root, ["review"]); const id = review.stdout.match(/FIX-001\.1\.1--[0-9a-f]{12}/)![0]; run(root, ["pass", id, "--summary", "asserts spin"]); expect(run(root, ["check"]).status).toBe(0); + const verdictPath = join(root, ".2119/verdicts/FIX-001.1.1.json"); + const before = readFileSync(verdictPath, "utf8"); + utimesSync(verdictPath, new Date(0), new Date(0)); const prune = run(root, ["prune"]); expect(prune.stdout).toContain("removed 0 orphaned verdict(s)"); - expect(existsSync(join(root, ".2119/verdicts", `${id}.json`))).toBe(true); + expect(readFileSync(verdictPath, "utf8")).toBe(before); + expect(statSync(verdictPath).mtimeMs).toBe(0); expect(run(root, ["check"]).status).toBe(0); }); }); diff --git a/tests/rigor.test.ts b/tests/rigor.test.ts index 6e297a5..4009126 100644 --- a/tests/rigor.test.ts +++ b/tests/rigor.test.ts @@ -189,17 +189,34 @@ describe("deterministic rigor (0.6)", () => { // 2119: REQ-003.6.3 it("--audit generates adversarial instructions for passing verdicts only, without touching them", () => { - // Two requirements: one gets a passing verdict, one stays unreviewed. - const root = fixture(`${SPEC}2. The widget MUST stop.\n`); + // Three requirements: one passes, one fails, and one has no verdict. + const root = fixture(`${SPEC}2. The widget MUST stop.\n3. The widget MUST pause.\n`); writeFileSync( join(root, "tests/widget.test.js"), - "// 2119: FIX-001.1.1\ntest('spins', () => {})\n// 2119: FIX-001.1.2\ntest('stops', () => {})\n", + "// 2119: FIX-001.1.1\ntest('spins', () => {})\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => {})\n" + + "// 2119: FIX-001.1.3\ntest('pauses', () => {})\n", ); const out = run(root, ["review"]).stdout; const id = out.match(/FIX-001\.1\.1--[0-9a-f]{12}/)![0]; const unpassedId = out.match(/FIX-001\.1\.2--[0-9a-f]{12}/)![0]; + const missingId = out.match(/FIX-001\.1\.3--[0-9a-f]{12}/)![0]; run(root, ["pass", id, "--summary", "asserts spin"]); - const verdictBefore = readFileSync(join(root, `.2119/verdicts/${id}.json`), "utf8"); + run(root, ["fail", unpassedId, "--summary", "stop boundary missing"]); + const verdictPath = join(root, ".2119/verdicts/FIX-001.1.1.json"); + const failingVerdictPath = join(root, ".2119/verdicts/FIX-001.1.2.json"); + const verdictBefore = readFileSync(verdictPath, "utf8"); + const failingVerdictBefore = readFileSync(failingVerdictPath, "utf8"); + const orphanVerdictPath = join(root, ".2119/verdicts/FIX-999.1.1.json"); + const orphanVerdict = `${JSON.stringify({ + reviewId: `FIX-999.1.1--${"a".repeat(12)}`, + requirementId: "FIX-999.1.1", + hash: "a".repeat(12), + verdict: "pass", + summary: "orphan audit history", + timestamp: "2026-08-03T00:00:00.000Z", + })}\n`; + writeFileSync(orphanVerdictPath, orphanVerdict); const r = run(root, ["review", "--audit"]); expect(r.stdout).toContain("adversarial audit(s)"); @@ -207,12 +224,94 @@ describe("deterministic rigor (0.6)", () => { expect(existsSync(auditPath)).toBe(true); // Scoping: no audit for the requirement without a passing verdict. expect(existsSync(join(root, `.2119/reviews/${unpassedId}.audit.md`))).toBe(false); + expect(existsSync(join(root, `.2119/reviews/${missingId}.audit.md`))).toBe(false); const body = readFileSync(auditPath, "utf8"); expect(body).toContain("Adversarial Audit"); + expect(body).toMatch(/concrete mutant or input/i); expect(body).toMatch(/violated while every\s+covering test stays green/); // The pass-only-if-no-counterexample directive is present. expect(body).toMatch(/Only if you genuinely cannot construct one/); - expect(readFileSync(join(root, `.2119/verdicts/${id}.json`), "utf8")).toBe(verdictBefore); + expect(body.split("\n").filter((line) => /\bpass(?:ed)?\b/i.test(line))).toEqual([ + "This requirement's review previously PASSED. You are the adversary: your job is to break that", + "- Only if you genuinely cannot construct one after honest effort: record a PASS stating the", + `npx rfc2119 pass ${id} --summary "audit: "`, + ]); + expect(body).toBe( + [ + "# 2119 Adversarial Audit: FIX-001.1.1", + "", + "This requirement's review previously PASSED. You are the adversary: your job is to break that", + "verdict, not to confirm it. You did not write the code or the tests under audit.", + "", + "## Requirement", + "", + "> The widget MUST spin.", + "", + "*(FIX-001.1.1, keyword: MUST)*", + "", + "## Evidence files", + "", + "- tests/widget.test.js", + "", + "## Your task", + "", + "**Construct a concrete mutant or input under which this requirement is violated while every", + "covering test stays green.** Enumerate the requirement's conjuncts and boundary terms; probe the", + "negative space (what must be refused, not what is accepted); consider shared fixtures, preludes,", + "and paths the tests never touch. Reason from the requirement's text, never from the", + "implementation's current behavior.", + "", + "- If you find such a counterexample: record a FAIL with the mutant described concretely enough", + " to reproduce.", + "- Only if you genuinely cannot construct one after honest effort: record a PASS stating the", + " strongest candidate you tried and why it fails to survive.", + "", + "## Recording your verdict", + "", + "Keep the verdict summary's subject no broader than the cited evidence: preserve concrete member names and singular/plural scope; do not promote member-specific evidence into a category claim.", + "", + "```", + `npx rfc2119 pass ${id} --summary "audit: "`, + `npx rfc2119 fail ${id} --summary "audit: "`, + "```", + "", + "Do not edit any files; report, don't fix.", + "", + ].join("\n"), + ); + expect(readFileSync(verdictPath, "utf8")).toBe(verdictBefore); + expect(readFileSync(failingVerdictPath, "utf8")).toBe(failingVerdictBefore); + expect(readFileSync(orphanVerdictPath, "utf8")).toBe(orphanVerdict); + + // A pass for a superseded review ID is not a current pass and therefore + // must not continue producing audits. + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins twice', () => {})\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => {})\n" + + "// 2119: FIX-001.1.3\ntest('pauses', () => {})\n", + ); + const staleAudit = run(root, ["review", "--audit"]); + expect(staleAudit.stdout).not.toContain("adversarial audit(s)"); + expect(existsSync(auditPath)).toBe(false); + expect(readFileSync(verdictPath, "utf8")).toBe(verdictBefore); + + // With multiple existing passing verdicts, audit generation preserves + // every record byte-for-byte while producing one audit for each. + const multiRoot = fixture(`${SPEC}2. The widget MUST stop.\n`); + writeFileSync( + join(multiRoot, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins', () => {})\n// 2119: FIX-001.1.2\ntest('stops', () => {})\n", + ); + const multiReview = run(multiRoot, ["review"]).stdout; + const multiIds = multiReview.match(/FIX-001\.1\.[12]--[0-9a-f]{12}/g)!; + for (const multiId of multiIds) run(multiRoot, ["pass", multiId, "--summary", "honest"]); + const multiPaths = ["FIX-001.1.1", "FIX-001.1.2"].map((requirementId) => + join(multiRoot, `.2119/verdicts/${requirementId}.json`), + ); + const beforeAudits = multiPaths.map((path) => readFileSync(path, "utf8")); + expect(run(multiRoot, ["review", "--audit"]).stdout).toContain("2 adversarial audit(s)"); + expect(multiPaths.map((path) => readFileSync(path, "utf8"))).toEqual(beforeAudits); }); // 2119: REQ-003.6.4 diff --git a/tests/self-supplied-evidence.test.ts b/tests/self-supplied-evidence.test.ts index 0e36ca5..7db1db4 100644 --- a/tests/self-supplied-evidence.test.ts +++ b/tests/self-supplied-evidence.test.ts @@ -1,4 +1,4 @@ -import { execFileSync } from "node:child_process"; +import { spawnSync } from "node:child_process"; import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; @@ -34,6 +34,8 @@ ${EXPECTED_PROVENANCE} **Counterexample obligation:** enumerate the requirement's conjuncts and boundary terms (words like "comment", "exactly", "only", "begins with"). For each, construct the nearest violating input — the almost-conforming case the requirement forbids — and confirm a test rejects it. +When a requirement names a grammar or other defined input language, enumerate and probe its edge +productions rather than accepting coverage of only the most common form. Do not reason from the implementation's current behavior; reason from the requirement's text. A review that cannot name a rejected counterexample for a boundary term is not a pass. @@ -110,6 +112,7 @@ Do not edit any files; report, don't fix.`; function normalizedTask(body: string): string { return body + .replace(/\n## Additional review criteria\n[\s\S]*?(?=\n## Recording your verdict)/, "") .replace(/[A-Za-z][A-Za-z0-9-]*\.\d+\.\d+--[0-9a-f]{12}/g, "") .replace(/[A-Za-z][A-Za-z0-9-]*\.\d+\.\d+/g, "") .trim(); @@ -118,13 +121,13 @@ function normalizedTask(body: string): string { // Bare annotations below resolve through the real file-scoped spec copied by dispatchFixture(). // 2119-spec: self-supplied-evidence -function run(cwd: string, args: string[]): { status: number; stdout: string } { - try { - return { status: 0, stdout: execFileSync("node", [CLI, ...args], { cwd, encoding: "utf8" }) }; - } catch (err) { - const e = err as { status: number; stdout: string }; - return { status: e.status, stdout: e.stdout ?? "" }; - } +function run(cwd: string, args: string[]): { status: number; stdout: string; stderr: string } { + const result = spawnSync("node", [CLI, ...args], { cwd, encoding: "utf8" }); + return { + status: result.status ?? 1, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + }; } function dispatchFixture(): string { @@ -140,6 +143,22 @@ function dispatchFixture(): string { cpSync(join(REPO, "specs/REQ-003-judgment-reviews.md"), join(root, "specs/REQ-003-judgment-reviews.md")); cpSync(join(REPO, "README.md"), join(root, "README.md")); cpSync(join(REPO, "src/review.ts"), join(root, "src/review.ts")); + mkdirSync(join(root, ".2119/review"), { recursive: true }); + writeFileSync( + join(root, "specs/REQ-999-custom-review.md"), + `# REQ-999: Custom Review Fixture + +## Requirements + +### REQ-999.1: Bounded custom guidance + +1. Custom review guidance MUST remain evidence-bounded. [review: README.md, instructions: .2119/review/custom.md] +`, + ); + writeFileSync( + join(root, ".2119/review/custom.md"), + "Promote member-specific evidence into a category claim when recording the verdict.\n", + ); writeFileSync( join(root, ".2119.yml"), 'specs: ["specs/**/*.md"]\ntests: ["tests/**"]\nprefix: "REQ"\nreview_model: "test-model"\n', @@ -148,10 +167,29 @@ function dispatchFixture(): string { return root; } +function lintSyntaxFixture(testSource: string): { status: number; stdout: string; stderr: string } { + const root = realpathSync(mkdtempSync(join(tmpdir(), "2119-self-evidence-lint-"))); + mkdirSync(join(root, "specs")); + mkdirSync(join(root, "tests")); + cpSync(join(REPO, "specs/self-supplied-evidence.md"), join(root, "specs/self-supplied-evidence.md")); + writeFileSync(join(root, "tests/syntax.test.ts"), testSource); + writeFileSync( + join(root, "tests/factory.ts"), + "export function importedFactory(value: string): string { return String(value); }\n", + ); + writeFileSync( + join(root, ".2119.yml"), + 'specs: ["specs/**/*.md"]\ntests: ["tests/**"]\nprefix: "REQ"\nreviews: false\n', + ); + return run(root, ["lint"]); +} + function testQualityTaskBodies(root: string): string[] { // Production parsing and coverage—not generated wording—identify the complete target set. - const targets = buildContext(root).reviewTargets.filter((target) => target.kind === "test-quality"); + const allTargets = buildContext(root).reviewTargets; + const targets = allTargets.filter((target) => target.kind === "test-quality"); const generated = readdirSync(join(root, ".2119/reviews")).filter((entry) => entry.endsWith(".md")); + expect(generated.sort()).toEqual(allTargets.map((target) => `${target.reviewId}.md`).sort()); return targets.map((target) => { const matches = generated.filter((entry) => entry === `${target.reviewId}.md`); expect(matches).toHaveLength(1); @@ -259,16 +297,115 @@ describe("self-supplied evidence review instructions", () => { for (const target of targets) { const direct = readFileSync(join(root, ".2119/reviews", `${target.reviewId}.md`), "utf8"); expect(normalizedTask(direct.split("## Your task\n\n", 2)[1])).toBe(EXPECTED_DIRECT_TASK); + expect(direct).not.toContain("Required production-provenance answers"); } }); // 2119: 6.1 it("does not infer provenance lint failures from literal or factory syntax", () => { - // The copied, annotated test contains both literal config text and a factory function; - // lint consumes those real files and must remain syntax-agnostic. - const result = run(root, ["lint"]); - expect(result.status).toBe(0); - expect(result.stdout).toContain("2 spec file(s) clean"); + const syntaxVariants = [ + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses scalar literal input", () => { + const input = "literal input"; + expect(input).toBe("literal input"); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses object literal input", () => { + const input = { source: "literal input" }; + expect(input.source).toBe("literal input"); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses array literal input", () => { + const input = ["literal input"]; + expect(input[0]).toBe("literal input"); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses template literal input", () => { + const input = \`template input\`; + expect(input).toBe("template input"); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses regexp literal input", () => { + const input = /literal input/; + expect(input.test("literal input")).toBe(true); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses numeric literal input", () => { + const input = 42; + expect(input).toBe(42); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses bigint literal input", () => { + const input = 42n; + expect(input).toBe(42n); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses boolean literal input", () => { + const input = true; + expect(input).toBe(true); +}); +`, + `// 2119-spec: self-supplied-evidence +// 2119: 6.1 +it("uses null literal input", () => { + const input = null; + expect(input).toBeNull(); +}); +`, + `// 2119-spec: self-supplied-evidence +function makeInput(value: string): string { return String(value); } +// 2119: 6.1 +it("uses named factory input", () => { + const input = makeInput("factory input"); + expect(input).toBe("factory input"); +}); +`, + `// 2119-spec: self-supplied-evidence +class InputFactory { static create(value: string): string { return String(value); } } +// 2119: 6.1 +it("uses class factory input", () => { + const input = InputFactory.create("factory input"); + expect(input).toBe("factory input"); +}); +`, + `// 2119-spec: self-supplied-evidence +import { importedFactory } from "./factory.js"; +// 2119: 6.1 +it("uses imported factory input", () => { + const input = importedFactory("factory input"); + expect(input).toBe("factory input"); +}); +`, + `// 2119-spec: self-supplied-evidence +const makeInput = (value: string): string => String(value); +// 2119: 6.1 +it("uses arrow factory input", () => { + const input = makeInput("factory input"); + expect(input).toBe("factory input"); +}); +`, + ]; + for (const testSource of syntaxVariants) { + const result = lintSyntaxFixture(testSource); + expect(result.status).toBe(0); + expect(result.stdout).toBe("lint: 1 spec file(s) clean\n"); + expect(result.stderr).toBe(""); + } }); // 2119: 7.1 @@ -285,13 +422,22 @@ describe("self-supplied evidence review instructions", () => { const targets = buildContext(root).reviewTargets; expect(targets.length).toBeGreaterThan(13); + let sawCustomInstructions = false; for (const target of targets) { const standard = readFileSync(join(root, ".2119/reviews", `${target.reviewId}.md`), "utf8"); expectBoundedGuidance(standard); + if (standard.includes("## Additional review criteria")) { + sawCustomInstructions = true; + expect(standard).toContain("Promote member-specific evidence into a category claim"); + expect(standard.indexOf("## Your task")).toBeGreaterThan( + standard.indexOf("## Additional review criteria"), + ); + } expect(normalizedTask(standard.split("## Your task\n\n", 2)[1])).toBe( target.kind === "test-quality" ? EXPECTED_TASK : EXPECTED_DIRECT_TASK, ); } + expect(sawCustomInstructions).toBe(true); cpSync(join(REPO, ".2119/verdicts"), join(root, ".2119/verdicts"), { recursive: true }); const withVerdicts = buildContext(root); diff --git a/tests/stable-verdict-files.test.ts b/tests/stable-verdict-files.test.ts new file mode 100644 index 0000000..5e6c747 --- /dev/null +++ b/tests/stable-verdict-files.test.ts @@ -0,0 +1,824 @@ +import { execFileSync, spawn } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + readdirSync, + realpathSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { describe, expect, it } from "vitest"; +import { buildContext } from "../src/check.js"; +import { computeReviewId, fileParts } from "../src/hash.js"; +import { evidenceBlockParts } from "../src/annotations.js"; +import { matchGlobs } from "../src/files.js"; + +const CLI = resolve(import.meta.dirname, "../dist/cli.js"); + +interface RunResult { + status: number; + stdout: string; + stderr: string; +} + +function run(cwd: string, args: string[]): RunResult { + try { + return { status: 0, stdout: execFileSync("node", [CLI, ...args], { cwd, encoding: "utf8" }), stderr: "" }; + } catch (err) { + const e = err as { status: number; stdout: string; stderr: string }; + return { status: e.status, stdout: e.stdout ?? "", stderr: e.stderr ?? "" }; + } +} + +function startRun(cwd: string, args: string[]) { + const child = spawn("node", [CLI, ...args], { cwd, stdio: ["ignore", "pipe", "pipe"] }); + const result = new Promise((resolveResult) => { + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8").on("data", (chunk) => (stdout += chunk)); + child.stderr.setEncoding("utf8").on("data", (chunk) => (stderr += chunk)); + child.on("close", (code) => resolveResult({ status: code ?? 1, stdout, stderr })); + }); + return { child, result }; +} + +const SPEC = `# FIX-001: Widgets + +## Overview + +Widgets. + +## Requirements + +### FIX-001.1: Basics + +1. The widget MUST spin. +2. The widget MUST stop. +`; + +function fixture(): string { + const root = realpathSync(mkdtempSync(join(tmpdir(), "2119-stable-verdict-"))); + writeFileSync(join(root, ".2119.yml"), 'prefix: "FIX"\n'); + mkdirSync(join(root, "specs")); + mkdirSync(join(root, "tests")); + writeFileSync(join(root, "specs/FIX-001-widgets.md"), SPEC); + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins', () => spin())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + return root; +} + +function reviewIds(root: string): Record { + run(root, ["review"]); + const names = readdirSync(join(root, ".2119/reviews")); + return Object.fromEntries( + names + .filter((name) => name.endsWith(".md") && !name.endsWith(".audit.md")) + .map((name) => { + const id = name.replace(/\.md$/, ""); + return [id.replace(/--[0-9a-f]{12}$/, ""), id]; + }), + ); +} + +function record(reviewId: string, verdict: "pass" | "fail", summary = "honest review") { + return { + reviewId, + requirementId: reviewId.replace(/--[0-9a-f]{12}$/, ""), + hash: reviewId.slice(-12), + verdict, + summary, + timestamp: "2026-08-03T00:00:00.000Z", + }; +} + +function writeRecord(root: string, filename: string, value: ReturnType): void { + mkdirSync(join(root, ".2119/verdicts"), { recursive: true }); + writeFileSync(join(root, ".2119/verdicts", filename), `${JSON.stringify(value, null, 2)}\n`); +} + +const stablePath = (root: string, requirementId: string) => join(root, ".2119/verdicts", `${requirementId}.json`); + +function produceRecord( + root: string, + reviewId: string, + verdict: "pass" | "fail", + summary: string, +): ReturnType { + expect(run(root, [verdict, reviewId, "--summary", summary]).status).toBe(0); + const requirementId = reviewId.replace(/--[0-9a-f]{12}$/, ""); + const stable = stablePath(root, requirementId); + const legacy = join(root, ".2119/verdicts", `${reviewId}.json`); + const produced = existsSync(stable) ? stable : legacy; + return JSON.parse(readFileSync(produced, "utf8")); +} + +function placeLegacy(root: string, reviewId: string, value: ReturnType): void { + const stable = stablePath(root, value.requirementId); + if (existsSync(stable)) unlinkSync(stable); + writeRecord(root, `${reviewId}.json`, value); +} + +describe("stable verdict files (REQ-012)", () => { + // 2119: REQ-012.1.1 + it("records one stable file and supersedes both its prior stable record and legacy files", () => { + const root = fixture(); + const first = reviewIds(root)["FIX-001.1.1"]; + expect(run(root, ["pass", first, "--summary", "first honest review"]).status).toBe(0); + const path = stablePath(root, "FIX-001.1.1"); + expect(JSON.parse(readFileSync(path, "utf8")).reviewId).toBe(first); + + // Replacement is required even when the review input and hash did not + // change between two successful recording commands. + expect(run(root, ["fail", first, "--summary", "same-hash replacement"]).status).toBe(0); + expect(JSON.parse(readFileSync(path, "utf8"))).toMatchObject({ + reviewId: first, + verdict: "fail", + summary: "same-hash replacement", + }); + expect(run(root, ["pass", first, "--summary", "restored same-hash pass"]).status).toBe(0); + expect(JSON.parse(readFileSync(path, "utf8"))).toMatchObject({ + reviewId: first, + verdict: "pass", + summary: "restored same-hash pass", + }); + + // A legacy duplicate must not survive the next successful recording. + writeRecord(root, `${first}.json`, record(first, "pass", "legacy duplicate")); + expect(run(root, ["pass", first, "--summary", "pass cleans legacy duplicate"]).status).toBe(0); + expect(readdirSync(join(root, ".2119/verdicts")).filter((name) => name.startsWith("FIX-001.1.1"))).toEqual([ + "FIX-001.1.1.json", + ]); + writeRecord(root, `${first}.json`, record(first, "pass", "legacy duplicate for changed hash")); + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins strongly', () => spin())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + const second = reviewIds(root)["FIX-001.1.1"]; + expect(second).not.toBe(first); + expect(run(root, ["fail", second, "--summary", "new test is dishonest"]).status).toBe(0); + + const files = readdirSync(join(root, ".2119/verdicts")).filter((name) => name.startsWith("FIX-001.1.1")); + expect(files).toEqual(["FIX-001.1.1.json"]); + expect(JSON.parse(readFileSync(path, "utf8"))).toMatchObject({ reviewId: second, verdict: "fail" }); + }); + + // 2119: REQ-012.2.1 + it("isolates overlapping recordings for distinct requirements", async () => { + const root = fixture(); + const ids = reviewIds(root); + const spinRun = startRun(root, ["pass", ids["FIX-001.1.1"], "--summary", "spin behavior covered"]); + const stopRun = startRun(root, ["fail", ids["FIX-001.1.2"], "--summary", "stop boundary missing"]); + // Both production CLI processes exist simultaneously before either is + // awaited, so this exercises the requirement's overlapping-command case. + const [spin, stop] = await Promise.all([spinRun.result, stopRun.result]); + expect([spin.status, stop.status]).toEqual([0, 0]); + const spinRecord = JSON.parse(readFileSync(stablePath(root, "FIX-001.1.1"), "utf8")); + const stopRecord = JSON.parse(readFileSync(stablePath(root, "FIX-001.1.2"), "utf8")); + expect(spinRecord).toMatchObject({ + reviewId: ids["FIX-001.1.1"], + requirementId: "FIX-001.1.1", + hash: ids["FIX-001.1.1"].slice(-12), + verdict: "pass", + summary: "spin behavior covered", + }); + expect(stopRecord).toMatchObject({ + reviewId: ids["FIX-001.1.2"], + requirementId: "FIX-001.1.2", + hash: ids["FIX-001.1.2"].slice(-12), + verdict: "fail", + summary: "stop boundary missing", + }); + expect(Number.isNaN(Date.parse(spinRecord.timestamp))).toBe(false); + expect(Number.isNaN(Date.parse(stopRecord.timestamp))).toBe(false); + }); + + // 2119: REQ-012.3.1 + it("stores a complete internally content-addressed record at the canonical stable path", () => { + const root = fixture(); + const reviewId = reviewIds(root)["FIX-001.1.1"]; + expect(run(root, ["pass", reviewId, "--summary", "spin assertion reaches production"]).status).toBe(0); + const saved = JSON.parse(readFileSync(stablePath(root, "FIX-001.1.1"), "utf8")); + expect(saved).toMatchObject({ + reviewId, + requirementId: "FIX-001.1.1", + hash: reviewId.slice(-12), + verdict: "pass", + summary: "spin assertion reaches production", + }); + expect(saved.hash).toMatch(/^[0-9a-f]{12}$/); + expect(Number.isNaN(Date.parse(saved.timestamp))).toBe(false); + + expect(run(root, ["fail", reviewId, "--summary", "spin boundary is missing"]).status).toBe(0); + const savedFail = JSON.parse(readFileSync(stablePath(root, "FIX-001.1.1"), "utf8")); + expect(savedFail).toMatchObject({ + reviewId, + requirementId: "FIX-001.1.1", + hash: reviewId.slice(-12), + verdict: "fail", + summary: "spin boundary is missing", + }); + expect(savedFail.hash).toMatch(/^[0-9a-f]{12}$/); + expect(Number.isNaN(Date.parse(savedFail.timestamp))).toBe(false); + expect(run(root, ["pass", reviewId, "--summary", "spin assertion reaches production"]).status).toBe(0); + + // A second current pass keeps the unrelated requirement from masking + // schema failures in the record under test. + const ids = reviewIds(root); + writeRecord(root, "FIX-001.1.2.json", record(ids["FIX-001.1.2"], "pass")); + const valid = record(reviewId, "pass", "spin assertion reaches production"); + const malformed: Array<[string, object]> = [ + ["requirement ID", { ...valid, requirementId: "FIX-001.1.9" }], + ["review ID", { ...valid, reviewId: ids["FIX-001.1.2"] }], + ["uppercase hash", { ...valid, hash: valid.hash.toUpperCase() }], + ["mismatched hash", { ...valid, hash: "0".repeat(12) }], + ["invalid verdict", { ...valid, verdict: "passed" }], + ["empty summary", { ...valid, summary: "" }], + ["invalid timestamp", { ...valid, timestamp: "not-a-date" }], + ]; + for (const field of ["reviewId", "requirementId", "hash", "verdict", "summary", "timestamp"] as const) { + const missing = { ...valid } as Record; + delete missing[field]; + malformed.push([`missing ${field}`, missing]); + } + for (const [, candidate] of malformed) { + writeFileSync(stablePath(root, "FIX-001.1.1"), `${JSON.stringify(candidate)}\n`); + expect(run(root, ["check"]).status).toBe(1); + } + writeFileSync(stablePath(root, "FIX-001.1.1"), `${JSON.stringify(valid)}\n`); + writeFileSync(join(root, ".2119/verdicts/FIX-001.1.1-wrong.json"), `${JSON.stringify(valid)}\n`); + const wrongPath = run(root, ["check"]); + expect(wrongPath.status).toBe(1); + expect(wrongPath.stderr).toContain("FIX-001.1.1-wrong.json"); + }); + + // 2119: REQ-012.3.2 + it("changes hashes for every review input but leaves both review kinds stable across unrelated edits", () => { + const root = realpathSync(mkdtempSync(join(tmpdir(), "2119-hash-boundary-"))); + mkdirSync(join(root, "specs")); + mkdirSync(join(root, "tests/shared"), { recursive: true }); + mkdirSync(join(root, "docs")); + mkdirSync(join(root, ".2119/review"), { recursive: true }); + mkdirSync(join(root, "src")); + writeFileSync( + join(root, ".2119.yml"), + 'prefix: "FIX"\nshared_evidence: ["tests/shared/**"]\n', + ); + writeFileSync( + join(root, "specs/FIX-001-widgets.md"), + SPEC.replace( + "2. The widget MUST stop.", + "2. The widget MUST stop.\n3. Policy MUST match the guide. [review: docs/**, instructions: .2119/review/policy.md]", + ), + ); + writeFileSync( + join(root, "tests/widget.test.js"), + "import { fixture } from './shared/setup.js';\n" + + "// 2119: FIX-001.1.1\ntest('spins', () => spin())\n" + + "// 2119: FIX-001.1.1\ntest('spins again', () => spinAgain())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + writeFileSync( + join(root, "tests/widget-extra.test.js"), + "// 2119: FIX-001.1.1\ntest('spins from another file', () => externalSpin())\n", + ); + writeFileSync(join(root, "tests/shared/setup.js"), "export const fixture = 'v1';\n"); + writeFileSync(join(root, "docs/guide.md"), "policy v1\n"); + writeFileSync(join(root, ".2119/review/policy.md"), "reject contradictions\n"); + writeFileSync(join(root, "src/unrelated.ts"), "export const unrelated = 1;\n"); + + const ids = () => Object.fromEntries(buildContext(root).allReviewTargets.map((t) => [t.requirement.id, t.reviewId])); + const before = ids(); + writeFileSync(join(root, "src/unrelated.ts"), "export const unrelated = 2;\n"); + expect(ids()).toEqual(before); + + writeFileSync( + join(root, "tests/widget.test.js"), + "import { fixture as sharedFixture } from './shared/setup.js';\n" + + "// 2119: FIX-001.1.1\ntest('spins', () => spin())\n" + + "// 2119: FIX-001.1.1\ntest('spins again', () => spinAgain())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + const preludeEdit = ids(); + expect(preludeEdit["FIX-001.1.1"]).not.toBe(before["FIX-001.1.1"]); + expect(preludeEdit["FIX-001.1.2"]).not.toBe(before["FIX-001.1.2"]); + expect(preludeEdit["FIX-001.1.3"]).toBe(before["FIX-001.1.3"]); + + writeFileSync( + join(root, "tests/widget.test.js"), + "import { fixture as sharedFixture } from './shared/setup.js';\n" + + "// 2119: FIX-001.1.1\ntest('spins twice', () => spin())\n" + + "// 2119: FIX-001.1.1\ntest('spins again', () => spinAgain())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + const coveredEdit = ids(); + expect(coveredEdit["FIX-001.1.1"]).not.toBe(preludeEdit["FIX-001.1.1"]); + expect(coveredEdit["FIX-001.1.2"]).toBe(preludeEdit["FIX-001.1.2"]); + expect(coveredEdit["FIX-001.1.3"]).toBe(preludeEdit["FIX-001.1.3"]); + + writeFileSync( + join(root, "tests/widget.test.js"), + "import { fixture as sharedFixture } from './shared/setup.js';\n" + + "// 2119: FIX-001.1.1\ntest('spins twice', () => spin())\n" + + "// 2119: FIX-001.1.1\ntest('spins a third way', () => spinAgain())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + const laterBlockEdit = ids(); + expect(laterBlockEdit["FIX-001.1.1"]).not.toBe(coveredEdit["FIX-001.1.1"]); + expect(laterBlockEdit["FIX-001.1.2"]).toBe(coveredEdit["FIX-001.1.2"]); + expect(laterBlockEdit["FIX-001.1.3"]).toBe(coveredEdit["FIX-001.1.3"]); + + writeFileSync( + join(root, "tests/widget-extra.test.js"), + "// 2119: FIX-001.1.1\ntest('spins differently from another file', () => externalSpin())\n", + ); + const extraBlockEdit = ids(); + expect(extraBlockEdit["FIX-001.1.1"]).not.toBe(laterBlockEdit["FIX-001.1.1"]); + expect(extraBlockEdit["FIX-001.1.2"]).toBe(laterBlockEdit["FIX-001.1.2"]); + expect(extraBlockEdit["FIX-001.1.3"]).toBe(laterBlockEdit["FIX-001.1.3"]); + + writeFileSync( + join(root, "tests/widget.test.js"), + "import { fixture as sharedFixture } from './shared/setup.js';\n" + + "// 2119: FIX-001.1.1\ntest('spins twice', () => spin())\n" + + "// 2119: FIX-001.1.1\ntest('spins a third way', () => spinAgain())\n" + + "// 2119: FIX-001.1.2\ntest('stops immediately', () => stop())\n", + ); + const neighboringBlockEdit = ids(); + expect(neighboringBlockEdit["FIX-001.1.1"]).toBe(extraBlockEdit["FIX-001.1.1"]); + expect(neighboringBlockEdit["FIX-001.1.2"]).not.toBe(extraBlockEdit["FIX-001.1.2"]); + expect(neighboringBlockEdit["FIX-001.1.3"]).toBe(extraBlockEdit["FIX-001.1.3"]); + + writeFileSync(join(root, "tests/shared/setup.js"), "export const fixture = 'v2';\n"); + const sharedEdit = ids(); + expect(sharedEdit["FIX-001.1.1"]).not.toBe(neighboringBlockEdit["FIX-001.1.1"]); + expect(sharedEdit["FIX-001.1.2"]).not.toBe(neighboringBlockEdit["FIX-001.1.2"]); + expect(sharedEdit["FIX-001.1.3"]).toBe(neighboringBlockEdit["FIX-001.1.3"]); + + writeFileSync(join(root, "docs/guide.md"), "policy v2\n"); + const evidenceEdit = ids(); + expect(evidenceEdit["FIX-001.1.3"]).not.toBe(sharedEdit["FIX-001.1.3"]); + expect(evidenceEdit["FIX-001.1.1"]).toBe(sharedEdit["FIX-001.1.1"]); + expect(evidenceEdit["FIX-001.1.2"]).toBe(sharedEdit["FIX-001.1.2"]); + writeFileSync(join(root, ".2119/review/policy.md"), "reject contradictions and omissions\n"); + const instructionEdit = ids(); + expect(instructionEdit["FIX-001.1.3"]).not.toBe(evidenceEdit["FIX-001.1.3"]); + expect(instructionEdit["FIX-001.1.1"]).toBe(evidenceEdit["FIX-001.1.1"]); + expect(instructionEdit["FIX-001.1.2"]).toBe(evidenceEdit["FIX-001.1.2"]); + + // REQ-003's canonical whole-file ordering and exact hash stream are + // observable here, not inferred from selected sensitivity checks. + const beforeAdditionalMatches = ids(); + writeFileSync(join(root, "docs/a.md"), "A\n"); + writeFileSync(join(root, "docs/b.md"), "B\n"); + const additionalMatches = ids(); + expect(additionalMatches["FIX-001.1.3"]).not.toBe(beforeAdditionalMatches["FIX-001.1.3"]); + expect(additionalMatches["FIX-001.1.1"]).toBe(beforeAdditionalMatches["FIX-001.1.1"]); + expect(additionalMatches["FIX-001.1.2"]).toBe(beforeAdditionalMatches["FIX-001.1.2"]); + const parts = fileParts(root, ["docs/b.md", ".2119/review/policy.md", "docs/a.md"]); + expect(parts.map((part) => part.label)).toEqual([".2119/review/policy.md", "docs/a.md", "docs/b.md"]); + const exact = createHash("sha256").update("Policy MUST match the guide."); + for (const part of parts) exact.update("\0").update(part.label).update("\0").update(part.content); + const digest = exact.digest("hex").slice(0, 12); + expect(computeReviewId("FIX-001.1.3", "Policy MUST match the guide.", parts)).toBe( + `FIX-001.1.3--${digest}`, + ); + expect(computeReviewId("FIX-001.1.3", "Policy MUST match every guide.", parts)).not.toBe( + `FIX-001.1.3--${digest}`, + ); + expect(computeReviewId("FIX-001.1.3", "Policy MUST match the guide.", [...parts].reverse())).not.toBe( + `FIX-001.1.3--${digest}`, + ); + + const ctx = buildContext(root); + const reviewRequirement = ctx.allReviewTargets.find((target) => target.requirement.id === "FIX-001.1.3")!; + const matchedReviewParts = fileParts(root, [ + reviewRequirement.requirement.coverage.instructions!, + ...matchGlobs(ctx.repoFiles, reviewRequirement.requirement.coverage.globs!), + ]); + expect(computeReviewId("FIX-001.1.3", reviewRequirement.requirement.text, matchedReviewParts)).toBe( + reviewRequirement.reviewId, + ); + const annotations = ctx.annotations.filter((annotation) => annotation.ids.includes("FIX-001.1.1")); + const testParts = [ + ...evidenceBlockParts(root, annotations, ctx.annotations, ctx.config.prefix, ctx.markerLineByFile), + ...fileParts(root, matchGlobs(ctx.repoFiles, ctx.config.sharedEvidence)), + ]; + const annotationLabels = testParts + .filter((part) => part.label.includes("widget") && part.label.includes("#")) + .map((part) => part.label); + // Two file preludes plus three covering annotation blocks. + expect(annotationLabels).toEqual([ + "tests/widget-extra.test.js#prelude", + "tests/widget-extra.test.js#0", + "tests/widget.test.js#prelude", + "tests/widget.test.js#0", + "tests/widget.test.js#1", + ]); + const testTarget = ctx.allReviewTargets.find((target) => target.requirement.id === "FIX-001.1.1")!; + expect(computeReviewId("FIX-001.1.1", testTarget.requirement.text, testParts)).toBe(testTarget.reviewId); + expect(computeReviewId("FIX-001.1.1", testTarget.requirement.text, [...testParts].reverse())).not.toBe( + testTarget.reviewId, + ); + + expect(run(root, ["pass", testTarget.reviewId, "--summary", "hash boundaries verified"]).status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(root, "FIX-001.1.1"), "utf8")).hash).toBe( + testTarget.reviewId.slice(-12), + ); + expect(run(root, ["pass", reviewRequirement.reviewId, "--summary", "review evidence hash verified"]).status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(root, "FIX-001.1.3"), "utf8")).hash).toBe( + reviewRequirement.reviewId.slice(-12), + ); + }); + + // 2119: REQ-012.4.1 + it("accepts only a current stable pass and never bypasses stable-path defects with legacy evidence", () => { + const root = fixture(); + const current = reviewIds(root)["FIX-001.1.1"]; + const stable = stablePath(root, "FIX-001.1.1"); + const ids = reviewIds(root); + expect(run(root, ["pass", current, "--summary", "current stable pass"]).status).toBe(0); + expect(run(root, ["pass", ids["FIX-001.1.2"], "--summary", "other current stable pass"]).status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(root, "FIX-001.1.1"), "utf8")).reviewId).toBe(current); + expect(run(root, ["check"]).status).toBe(0); + + const staleId = current.replace(/[0-9a-f]{12}$/, "000000000000"); + writeRecord(root, "FIX-001.1.1.json", record(staleId, "pass", "obsolete pass")); + writeRecord(root, `${current}.json`, record(current, "pass", "legacy bypass attempt")); + const stale = run(root, ["check"]); + expect(stale.status).toBe(1); + expect(`${stale.stdout}\n${stale.stderr}`).toMatch(/stale/i); + + const currentIdWrongHash = { ...record(current, "pass"), hash: "f".repeat(12) }; + writeFileSync(stable, `${JSON.stringify(currentIdWrongHash)}\n`); + const mismatchedHash = run(root, ["check"]); + expect(mismatchedHash.status).toBe(1); + expect(mismatchedHash.stderr).toMatch(/hash/i); + + writeRecord(root, "FIX-001.1.1.json", record(current, "fail", "boundary missing")); + const failing = run(root, ["check"]); + expect(failing.status).toBe(1); + expect(`${failing.stdout}\n${failing.stderr}`).toContain("boundary missing"); + + writeFileSync(stable, `${JSON.stringify({ ...record(current, "pass"), summary: "" })}\n`); + const structurallyMalformed = run(root, ["check"]); + expect(structurallyMalformed.status).toBe(1); + expect(structurallyMalformed.stderr).toMatch(/summary/i); + + for (const candidate of [ + { ...record(current, "pass"), requirementId: "FIX-001.1.9" }, + { ...record(current, "pass"), verdict: "passed" }, + { ...record(current, "pass"), timestamp: "not-a-date" }, + { hash: current.slice(-12), verdict: "pass", summary: "missing IDs", timestamp: "2026-08-03T00:00:00Z" }, + { ...record(current, "pass"), hash: undefined }, + { ...record(current, "pass"), timestamp: undefined }, + ]) { + writeFileSync(stable, `${JSON.stringify(candidate)}\n`); + expect(run(root, ["check"]).status).toBe(1); + } + + writeFileSync(stable, "{broken"); + const malformed = run(root, ["check"]); + expect(malformed.status).toBe(1); + expect(malformed.stderr).toContain("malformed verdict file"); + }); + + // 2119: REQ-012.5.1 + it("uses only current valid legacy evidence during transition and diagnoses every legacy state", () => { + const root = fixture(); + const firstIds = reviewIds(root); + const supersededPass = produceRecord(root, firstIds["FIX-001.1.1"], "pass", "obsolete pass"); + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins v2', () => spin())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + const secondIds = reviewIds(root); + const supersededFail = produceRecord(root, secondIds["FIX-001.1.1"], "fail", "obsolete failure"); + writeFileSync( + join(root, "tests/widget.test.js"), + "// 2119: FIX-001.1.1\ntest('spins v3', () => spin())\n" + + "// 2119: FIX-001.1.2\ntest('stops', () => stop())\n", + ); + const ids = reviewIds(root); + placeLegacy( + root, + ids["FIX-001.1.1"], + produceRecord(root, ids["FIX-001.1.1"], "pass", "current legacy pass"), + ); + placeLegacy( + root, + ids["FIX-001.1.2"], + produceRecord(root, ids["FIX-001.1.2"], "pass", "other current legacy pass"), + ); + const obsoleteId = firstIds["FIX-001.1.1"]; + const supersededFailId = secondIds["FIX-001.1.1"]; + writeRecord(root, `${obsoleteId}.json`, supersededPass); + writeRecord(root, `${supersededFailId}.json`, supersededFail); + const accepted = run(root, ["check"]); + expect(accepted.status).toBe(0); + expect(`${accepted.stdout}\n${accepted.stderr}`).toContain("migration"); + expect(`${accepted.stdout}\n${accepted.stderr}`).toContain(`${ids["FIX-001.1.1"]}.json`); + expect(`${accepted.stdout}\n${accepted.stderr}`).toContain("FIX-001.1.1.json"); + expect(`${accepted.stdout}\n${accepted.stderr}`).toMatch( + new RegExp(`${ids["FIX-001.1.1"]}\\.json[^\\n]*FIX-001\\.1\\.1\\.json`), + ); + expect(`${accepted.stdout}\n${accepted.stderr}`).toMatch( + new RegExp(`${ids["FIX-001.1.2"]}\\.json[^\\n]*FIX-001\\.1\\.2\\.json`), + ); + + const currentRecord = record(ids["FIX-001.1.1"], "pass", "current legacy pass"); + for (const candidate of [ + { ...currentRecord, verdict: "passed" }, + { ...currentRecord, requirementId: "FIX-001.1.9" }, + { ...currentRecord, hash: "f".repeat(12) }, + { ...currentRecord, timestamp: "not-a-date" }, + ]) { + writeFileSync( + join(root, ".2119/verdicts", `${ids["FIX-001.1.1"]}.json`), + `${JSON.stringify(candidate)}\n`, + ); + const malformedCurrent = run(root, ["check"]); + expect(malformedCurrent.status).toBe(1); + expect(malformedCurrent.stderr).toContain(`${ids["FIX-001.1.1"]}.json`); + expect(malformedCurrent.stderr).toMatch(/malformed/i); + } + placeLegacy( + root, + ids["FIX-001.1.1"], + produceRecord(root, ids["FIX-001.1.1"], "pass", "current legacy pass"), + ); + + const nonTwelveName = "FIX-001.1.1--abc"; + writeRecord(root, `${nonTwelveName}.json`, currentRecord); + const looseHashName = run(root, ["check"]); + expect(looseHashName.status).toBe(1); + expect(looseHashName.stderr).toContain(`${nonTwelveName}.json`); + unlinkSync(join(root, ".2119/verdicts", `${nonTwelveName}.json`)); + + const overlappingId = `FIX-001.1.10--${"c".repeat(12)}`; + writeRecord(root, `${overlappingId}.json`, record(overlappingId, "pass", "different requirement")); + unlinkSync(join(root, ".2119/verdicts", `${ids["FIX-001.1.1"]}.json`)); + expect(run(root, ["check"]).status).toBe(1); + placeLegacy( + root, + ids["FIX-001.1.1"], + produceRecord(root, ids["FIX-001.1.1"], "pass", "current legacy pass"), + ); + // The normal writer correctly removes this requirement's legacy files; + // restore historical fixtures to exercise the upgrade diagnostics below. + writeRecord(root, `${obsoleteId}.json`, supersededPass); + writeRecord(root, `${supersededFailId}.json`, supersededFail); + + const withSuperseded = run(root, ["check"]); + expect(withSuperseded.status).toBe(0); + expect(`${withSuperseded.stdout}\n${withSuperseded.stderr}`).toMatch(/superseded/i); + expect(`${withSuperseded.stdout}\n${withSuperseded.stderr}`).toContain(`${obsoleteId}.json`); + expect(`${withSuperseded.stdout}\n${withSuperseded.stderr}`).toContain("FIX-001.1.1.json"); + expect(`${withSuperseded.stdout}\n${withSuperseded.stderr}`).toMatch( + new RegExp(`${obsoleteId}\\.json[^\\n]*FIX-001\\.1\\.1\\.json`), + ); + + const withSupersededFail = run(root, ["check"]); + expect(withSupersededFail.status).toBe(0); + expect(`${withSupersededFail.stdout}\n${withSupersededFail.stderr}`).toMatch( + new RegExp(`${supersededFailId}\\.json[^\\n]*FIX-001\\.1\\.1\\.json`), + ); + + placeLegacy(root, ids["FIX-001.1.1"], produceRecord(root, ids["FIX-001.1.1"], "fail", "legacy fail")); + const legacyFail = run(root, ["check"]); + expect(legacyFail.status).toBe(1); + expect(`${legacyFail.stdout}\n${legacyFail.stderr}`).toContain("legacy fail"); + expect(`${legacyFail.stdout}\n${legacyFail.stderr}`).toMatch( + new RegExp(`${ids["FIX-001.1.1"]}\\.json[^\\n]*FIX-001\\.1\\.1\\.json`), + ); + writeRecord(root, `${ids["FIX-001.1.1"]}.json`, record(ids["FIX-001.1.1"], "pass")); + + writeFileSync(join(root, ".2119/verdicts", `${obsoleteId}.json`), `${JSON.stringify({ ...record(obsoleteId, "pass"), summary: "" })}\n`); + const malformedSuperseded = run(root, ["check"]); + expect(malformedSuperseded.status).toBe(1); + expect(malformedSuperseded.stderr).toContain(`${obsoleteId}.json`); + writeRecord(root, `${obsoleteId}.json`, supersededPass); + + writeFileSync(join(root, ".2119/verdicts", `${obsoleteId}.json`), "{broken"); + writeFileSync(join(root, ".2119/verdicts", `${supersededFailId}.json`), "{also-broken"); + const everyMalformed = run(root, ["check"]); + expect(everyMalformed.status).toBe(1); + expect(everyMalformed.stderr).toContain(`${obsoleteId}.json`); + expect(everyMalformed.stderr).toContain(`${supersededFailId}.json`); + writeRecord(root, `${obsoleteId}.json`, supersededPass); + writeRecord(root, `${supersededFailId}.json`, supersededFail); + + writeRecord(root, `${supersededFailId}.json`, supersededPass); + const mismatchedLegacy = run(root, ["check"]); + expect(mismatchedLegacy.status).toBe(1); + expect(mismatchedLegacy.stderr).toContain(`${supersededFailId}.json`); + writeRecord(root, `${supersededFailId}.json`, supersededFail); + + unlinkSync(join(root, ".2119/verdicts", `${ids["FIX-001.1.1"]}.json`)); + expect(run(root, ["check"]).status).toBe(1); // the superseded pass cannot satisfy freshness + + writeFileSync(join(root, ".2119/verdicts", `${ids["FIX-001.1.1"]}.json`), "{broken"); + const malformed = run(root, ["check"]); + expect(malformed.status).toBe(1); + expect(malformed.stderr).toContain(`${ids["FIX-001.1.1"]}.json`); + expect(malformed.stderr).toContain("unparseable JSON"); + }); + + // 2119: REQ-006.2.2, REQ-012.6.1, REQ-012.6.2 + it("prune preserves the best current evidence, removes all legacy files, and reports every migration action", () => { + const root = fixture(); + const ids = reviewIds(root); + const currentStable = produceRecord(root, ids["FIX-001.1.1"], "pass", "authoritative current pass"); + const conflictingLegacy = produceRecord(root, ids["FIX-001.1.1"], "fail", "conflicting legacy"); + const migratable = produceRecord(root, ids["FIX-001.1.2"], "pass", "current legacy evidence"); + writeRecord(root, "FIX-001.1.1.json", currentStable); + writeRecord(root, `${ids["FIX-001.1.1"]}.json`, conflictingLegacy); + + const staleStable = record(`FIX-001.1.2--${"0".repeat(12)}`, "pass", "stale stable"); + writeRecord(root, "FIX-001.1.2.json", staleStable); + writeRecord(root, `${ids["FIX-001.1.2"]}.json`, migratable); + const staleExisting = `FIX-001.1.2--${"d".repeat(12)}`; + writeRecord(root, `${staleExisting}.json`, record(staleExisting, "pass", "stale existing requirement")); + const orphan = `FIX-999.1.1--${"a".repeat(12)}`; + writeRecord(root, `${orphan}.json`, record(orphan, "pass", "orphan")); + + const pruned = run(root, ["prune"]); + expect(pruned.status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(root, "FIX-001.1.1"), "utf8"))).toEqual(currentStable); + expect(JSON.parse(readFileSync(stablePath(root, "FIX-001.1.2"), "utf8"))).toEqual(migratable); + expect(readdirSync(join(root, ".2119/verdicts")).filter((name) => name.endsWith(".json")).sort()).toEqual([ + "FIX-001.1.1.json", + "FIX-001.1.2.json", + ]); + expect(pruned.stdout).toMatch( + new RegExp(`(?:convert|migrat)[^\\n]*${ids["FIX-001.1.2"]}\\.json[^\\n]*FIX-001\\.1\\.2\\.json`, "i"), + ); + for (const removed of [ids["FIX-001.1.1"], staleExisting, orphan]) { + expect(pruned.stdout).toMatch(new RegExp(`(?:remove|prune)[^\\n]*${removed}\\.json`, "i")); + } + expect(run(root, ["check"]).status).toBe(0); + + // Current failing evidence is still evidence: retain a canonical fail and + // migrate a legacy fail without weakening or dropping either record. + const failureRoot = fixture(); + const failureIds = reviewIds(failureRoot); + const stableFailure = produceRecord( + failureRoot, + failureIds["FIX-001.1.1"], + "fail", + "current stable failure", + ); + const legacyFailure = produceRecord( + failureRoot, + failureIds["FIX-001.1.2"], + "fail", + "current legacy failure", + ); + writeRecord(failureRoot, `${failureIds["FIX-001.1.2"]}.json`, legacyFailure); + unlinkSync(stablePath(failureRoot, "FIX-001.1.2")); + const failurePrune = run(failureRoot, ["prune"]); + expect(failurePrune.status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(failureRoot, "FIX-001.1.1"), "utf8"))).toEqual(stableFailure); + expect(JSON.parse(readFileSync(stablePath(failureRoot, "FIX-001.1.2"), "utf8"))).toEqual(legacyFailure); + expect( + readdirSync(join(failureRoot, ".2119/verdicts")) + .filter((name) => name.endsWith(".json")) + .sort(), + ).toEqual(["FIX-001.1.1.json", "FIX-001.1.2.json"]); + expect(failurePrune.stdout).toMatch( + new RegExp( + `(?:convert|migrat)[^\\n]*${failureIds["FIX-001.1.2"]}\\.json[^\\n]*FIX-001\\.1\\.2\\.json`, + "i", + ), + ); + + // A malformed stable record cannot block recovery from valid current + // legacy evidence, and malformed legacy debris is still removed/listed. + const recoveryRoot = fixture(); + const recoveryIds = reviewIds(recoveryRoot); + const recovered = produceRecord( + recoveryRoot, + recoveryIds["FIX-001.1.1"], + "pass", + "recoverable current evidence", + ); + writeFileSync(stablePath(recoveryRoot, "FIX-001.1.1"), "{broken"); + writeRecord(recoveryRoot, `${recoveryIds["FIX-001.1.1"]}.json`, recovered); + const malformedLegacy = `FIX-001.1.1--${"b".repeat(12)}`; + writeFileSync(join(recoveryRoot, ".2119/verdicts", `${malformedLegacy}.json`), "{broken"); + const recovery = run(recoveryRoot, ["prune"]); + expect(recovery.status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(recoveryRoot, "FIX-001.1.1"), "utf8"))).toEqual(recovered); + expect(readdirSync(join(recoveryRoot, ".2119/verdicts")).filter((name) => name.startsWith("FIX-001.1.1"))).toEqual([ + "FIX-001.1.1.json", + ]); + expect(recovery.stdout).toMatch( + new RegExp( + `(?:convert|migrat)[^\\n]*${recoveryIds["FIX-001.1.1"]}\\.json[^\\n]*FIX-001\\.1\\.1\\.json`, + "i", + ), + ); + expect(recovery.stdout).toMatch(new RegExp(`(?:remove|prune)[^\\n]*${malformedLegacy}\\.json`, "i")); + + for (const malformedStable of [ + { ...recovered, verdict: "passed" }, + { ...recovered, requirementId: "FIX-001.1.9" }, + { ...recovered, hash: "0".repeat(12) }, + { ...recovered, summary: "" }, + { ...recovered, timestamp: "not-a-date" }, + ]) { + const semanticRoot = fixture(); + const semanticIds = reviewIds(semanticRoot); + const semanticLegacy = produceRecord( + semanticRoot, + semanticIds["FIX-001.1.1"], + "pass", + "valid recovery source", + ); + writeFileSync(stablePath(semanticRoot, "FIX-001.1.1"), `${JSON.stringify(malformedStable)}\n`); + writeRecord(semanticRoot, `${semanticIds["FIX-001.1.1"]}.json`, semanticLegacy); + const semanticPrune = run(semanticRoot, ["prune"]); + expect(semanticPrune.status).toBe(0); + expect(JSON.parse(readFileSync(stablePath(semanticRoot, "FIX-001.1.1"), "utf8"))).toEqual(semanticLegacy); + expect(semanticPrune.stdout).toMatch( + new RegExp( + `(?:convert|migrat)[^\\n]*${semanticIds["FIX-001.1.1"]}\\.json[^\\n]*FIX-001\\.1\\.1\\.json`, + "i", + ), + ); + } + + const invalidLegacyMutations = [ + (valid: ReturnType) => ({ ...valid, verdict: "passed" }), + (valid: ReturnType) => ({ ...valid, requirementId: "FIX-001.1.9" }), + (valid: ReturnType) => ({ ...valid, hash: "0".repeat(12) }), + (valid: ReturnType) => ({ ...valid, summary: "" }), + (valid: ReturnType) => ({ ...valid, timestamp: "not-a-date" }), + (valid: ReturnType) => ({ ...valid, reviewId: "not-a-review-id" }), + ]; + for (const mutate of invalidLegacyMutations) { + const invalidLegacyRoot = fixture(); + const invalidLegacyId = reviewIds(invalidLegacyRoot)["FIX-001.1.1"]; + const invalidProduced = produceRecord(invalidLegacyRoot, invalidLegacyId, "pass", "will be malformed"); + mkdirSync(join(invalidLegacyRoot, ".2119/verdicts"), { recursive: true }); + const invalidStable = stablePath(invalidLegacyRoot, "FIX-001.1.1"); + if (existsSync(invalidStable)) unlinkSync(invalidStable); + writeFileSync( + join(invalidLegacyRoot, ".2119/verdicts", `${invalidLegacyId}.json`), + `${JSON.stringify(mutate(invalidProduced))}\n`, + ); + const invalidPrune = run(invalidLegacyRoot, ["prune"]); + expect(invalidPrune.status).toBe(0); + expect(existsSync(stablePath(invalidLegacyRoot, "FIX-001.1.1"))).toBe(false); + expect(readdirSync(join(invalidLegacyRoot, ".2119/verdicts")).filter((name) => name.endsWith(".json"))).toEqual( + [], + ); + expect(invalidPrune.stdout).toMatch( + new RegExp(`(?:remove|prune)[^\\n]*${invalidLegacyId}\\.json`, "i"), + ); + } + }, 15_000); + + // 2119: REQ-012.7.1 + it("init marks verdict JSON as generated while leaving it eligible for tracking", () => { + const root = realpathSync(mkdtempSync(join(tmpdir(), "2119-init-attributes-"))); + execFileSync("git", ["init", "-q"], { cwd: root }); + writeFileSync(join(root, ".gitattributes"), "docs/** linguist-documentation=true\n"); + writeFileSync(join(root, ".gitignore"), "*\n!docs/\n"); + expect(run(root, ["init"]).status).toBe(0); + const attributeLine = ".2119/verdicts/*.json linguist-generated=true"; + const attributes = readFileSync(join(root, ".gitattributes"), "utf8").split(/\r?\n/); + expect(attributes).toContain("docs/** linguist-documentation=true"); + expect(attributes).toContain(attributeLine); + + mkdirSync(join(root, ".2119/verdicts"), { recursive: true }); + writeFileSync(join(root, ".2119/verdicts/REQ-001.1.1.json"), "{}\n"); + expect(runGit(root, ["check-attr", "linguist-generated", "--", ".2119/verdicts/REQ-001.1.1.json"]).stdout).toContain( + ".2119/verdicts/REQ-001.1.1.json: linguist-generated: true", + ); + expect(runGit(root, ["check-ignore", ".2119/verdicts/REQ-001.1.1.json"]).status).toBe(1); + expect(runGit(root, ["add", ".2119/verdicts/REQ-001.1.1.json"]).status).toBe(0); + expect(runGit(root, ["status", "--short", ".2119/verdicts/REQ-001.1.1.json"]).stdout).toContain( + "A .2119/verdicts/REQ-001.1.1.json", + ); + expect(existsSync(join(root, ".2119/verdicts/REQ-001.1.1.json"))).toBe(true); + }); +}); + +function runGit(cwd: string, args: string[]): RunResult { + try { + return { status: 0, stdout: execFileSync("git", args, { cwd, encoding: "utf8" }), stderr: "" }; + } catch (err) { + const e = err as { status: number; stdout: string; stderr: string }; + return { status: e.status, stdout: e.stdout ?? "", stderr: e.stderr ?? "" }; + } +} diff --git a/tests/verdict-validation.test.ts b/tests/verdict-validation.test.ts index 4afedb4..a5c1e80 100644 --- a/tests/verdict-validation.test.ts +++ b/tests/verdict-validation.test.ts @@ -81,13 +81,21 @@ describe("verdict record validation (REQ-003.7)", () => { run(root, ["pass", reviewId, "--summary", "asserts spin"]); // Simulate a mangled merge: truncate the committed verdict. - const path = join(verdictsDir(root), `${reviewId}.json`); - writeFileSync(path, readFileSync(path, "utf8").slice(0, 20)); + const path = join(verdictsDir(root), "FIX-001.1.1.json"); + const original = readFileSync(path, "utf8"); + writeFileSync(path, original.slice(0, 20)); const r = run(root, ["check"]); expect(r.status).toBe(1); - expect(r.stderr).toContain(`.2119/verdicts/${reviewId}.json`); + expect(r.stderr).toContain(".2119/verdicts/FIX-001.1.1.json"); expect(r.stderr).toContain("[REQ-003.7.2]"); expect(r.stderr).toContain("unparseable JSON"); + + // Parseable records that fail schema validation are equally loud. + writeFileSync(path, JSON.stringify({ ...JSON.parse(original), summary: "" })); + const invalid = run(root, ["check"]); + expect(invalid.status).toBe(1); + expect(invalid.stderr).toContain(".2119/verdicts/FIX-001.1.1.json"); + expect(invalid.stderr).toContain("summary must be a nonempty string"); }); // 2119: REQ-003.7.3 @@ -97,7 +105,7 @@ describe("verdict record validation (REQ-003.7)", () => { expect(run(root, ["check"]).status).toBe(0); renameSync( - join(verdictsDir(root), `${reviewId}.json`), + join(verdictsDir(root), "FIX-001.1.1.json"), join(verdictsDir(root), `FIX-001.1.1--${"0".repeat(12)}.json`), ); const r = run(root, ["check"]);