@dzhechkov/p-replicator 1.5.18 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.dz-manifest.json +126 -46
  2. package/CHANGELOG.md +135 -0
  3. package/MULTIPLATFORM_ROADMAP.md +1 -1
  4. package/README/eng/01_quickstart.md +2 -2
  5. package/README/eng/02_user_guide.md +1 -1
  6. package/README/eng/03_admin_guide.md +2 -2
  7. package/README/eng/05_architecture.md +6 -2
  8. package/README/eng/README.md +1 -1
  9. package/README/ru/01_quickstart.md +2 -2
  10. package/README/ru/02_user_guide.md +1 -1
  11. package/README/ru/03_admin_guide.md +2 -2
  12. package/README/ru/05_architecture.md +1 -1
  13. package/README/ru/README.md +1 -1
  14. package/README/ru/html/index.html +7 -7
  15. package/README.md +36 -15
  16. package/bin/cli.js +0 -0
  17. package/package.json +11 -10
  18. package/sbom.json +245 -45
  19. package/src/utils.js +2 -0
  20. package/templates/.claude/agents/doc-validator.md +2 -1
  21. package/templates/.claude/agents/product-discoverer.md +1 -1
  22. package/templates/.claude/commands/next.md +16 -0
  23. package/templates/.claude/commands/replicate.md +126 -8
  24. package/templates/.claude/commands/start.md +19 -1
  25. package/templates/.claude/hooks/autocommit-insights.cjs +95 -10
  26. package/templates/.claude/hooks/autocommit-plans.cjs +95 -10
  27. package/templates/.claude/hooks/autocommit-roadmap.cjs +96 -13
  28. package/templates/.claude/hooks/check-ports.cjs +232 -0
  29. package/templates/.claude/hooks/session-insights.cjs +13 -1
  30. package/templates/.claude/hooks/state-update.cjs +13 -1
  31. package/templates/.claude/hooks/statusline.cjs +145 -18
  32. package/templates/.claude/rules/docker-ports.md +123 -0
  33. package/templates/.claude/rules/replicate-pipeline.md +5 -2
  34. package/templates/.claude/settings.json +5 -5
  35. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md +57 -14
  36. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/02-analyze-map.md +9 -7
  37. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md +6 -4
  38. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md +2 -2
  39. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md +2 -2
  40. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md +1 -1
  41. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md +2 -2
  42. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-suggestions.md +11 -2
  43. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +1 -1
  44. package/templates/.claude/skills/requirements-validator/SKILL.md +7 -0
  45. package/templates/.claude/skills/requirements-validator/references/scoring-system.md +28 -0
  46. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +143 -1
  47. package/tests/e2e/lifecycle.test.js +21 -10
  48. package/tests/snapshot/baseline.json +29 -27
  49. package/tests/snapshot/update-baseline.js +2 -1
  50. package/tests/unit/adr-decision-coverage.test.js +137 -0
  51. package/tests/unit/adr-scanner-contract.test.js +108 -0
  52. package/tests/unit/autocommit-deletion.test.js +242 -0
  53. package/tests/unit/check-ports.test.js +184 -0
  54. package/tests/unit/db-port-rule.test.js +216 -0
  55. package/tests/unit/detect-parse-anchor.test.js +109 -0
  56. package/tests/unit/external-dependency-check.test.js +209 -0
  57. package/tests/unit/growth-module-b2b-gate.test.js +104 -0
  58. package/tests/unit/hooks-project-anchored.test.js +223 -0
  59. package/tests/unit/hooks-report-failures.test.js +207 -0
  60. package/tests/unit/pipeline-file-ownership.test.js +95 -0
  61. package/tests/unit/roadmap-one-schema.test.js +179 -0
  62. package/tests/unit/sparc-reconciliation.test.js +117 -0
  63. package/tests/unit/spec-pseudocode-traceability.test.js +146 -0
  64. package/tests/unit/statusline-honest-labels.test.js +178 -0
  65. package/tests/unit/statusline-two-roots.test.js +237 -0
  66. package/tests/unit/sync-templates-guard.test.js +209 -0
  67. package/tests/unit/utils.test.js +2 -2
  68. package/tests/unit/validation-gate-teeth.test.js +158 -0
  69. package/LICENSE +0 -21
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+
3
+ // scripts/sync-templates.js copies <found root>/.claude/{skills,commands,agents,rules,hooks} over
4
+ // templates/.claude/ — the directory that becomes the npm tarball. It finds its source by walking up
5
+ // to five parents and taking the FIRST one containing .claude/skills, which inside a monorepo is the
6
+ // MONOREPO, whose .claude/skills holds a whole different toolkit.
7
+ //
8
+ // Nothing had fired only because package.json's prepublishOnly points at the publish gate, not here.
9
+ // "Safe because dead" is not a safety property: a dead script can be revived by anyone who does not
10
+ // know what it does — and two places used to invite exactly that, a test helper printing
11
+ // "Run prepublishOnly first: node scripts/sync-templates.js" and an architecture doc calling it the
12
+ // prepublishOnly hook.
13
+ //
14
+ // These tests run the REAL script as a real subprocess against real fixtures and compare the tree
15
+ // before and after, because a script that exits 0 having copied two hundred files and one that exits
16
+ // 0 having copied nothing look identical from the outside.
17
+
18
+ const { test, describe } = require('node:test');
19
+ const assert = require('node:assert/strict');
20
+ const { execFileSync } = require('node:child_process');
21
+ const crypto = require('node:crypto');
22
+ const fs = require('node:fs');
23
+ const os = require('node:os');
24
+ const path = require('node:path');
25
+
26
+ const PKG_DIR = path.resolve(__dirname, '..', '..');
27
+ const SCRIPT_SRC = path.join(PKG_DIR, 'scripts', 'sync-templates.js');
28
+ const MARKER = '.p-replicator-sync-source';
29
+ const DECLARATION = 'p-replicator-sync-source: v1';
30
+
31
+ /** A fixture shaped like the real geometry: <root>/.claude/skills, and the package one level down
32
+ * with its own templates/.claude tree. `marker` decides whether the root claims to be the source. */
33
+ function fixture(opts) {
34
+ const o = opts || {};
35
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-sync-')));
36
+ if (o.rootHasClaude !== false) {
37
+ fs.mkdirSync(path.join(root, '.claude', 'skills', 'intruder'), { recursive: true });
38
+ fs.writeFileSync(path.join(root, '.claude', 'skills', 'intruder', 'SKILL.md'),
39
+ '# a skill that belongs to the monorepo, not to this package\n');
40
+ }
41
+ // A VALID marker is a regular file carrying the declaration. Cross-family QE: the first fixture
42
+ // wrote an EMPTY file, which institutionalised exactly the weakest form of the authorization.
43
+ if (o.marker === true) fs.writeFileSync(path.join(root, MARKER), DECLARATION + '\n');
44
+ if (o.marker === 'empty') fs.writeFileSync(path.join(root, MARKER), '');
45
+ if (o.marker === 'dir') fs.mkdirSync(path.join(root, MARKER));
46
+ if (o.marker === 'symlink') {
47
+ const real = path.join(root, 'elsewhere');
48
+ fs.writeFileSync(real, DECLARATION + '\n');
49
+ fs.symlinkSync(real, path.join(root, MARKER));
50
+ }
51
+ if (o.marker === 'wrong') fs.writeFileSync(path.join(root, MARKER), 'yes please\n');
52
+
53
+ const pkg = path.join(root, 'packages', 'p-replicator');
54
+ fs.mkdirSync(path.join(pkg, 'scripts'), { recursive: true });
55
+ fs.copyFileSync(SCRIPT_SRC, path.join(pkg, 'scripts', 'sync-templates.js'));
56
+ fs.mkdirSync(path.join(pkg, 'templates', '.claude', 'skills', 'shipped'), { recursive: true });
57
+ fs.writeFileSync(path.join(pkg, 'templates', '.claude', 'skills', 'shipped', 'SKILL.md'),
58
+ '# the package\'s own shipped skill\n');
59
+ return { root, pkg };
60
+ }
61
+
62
+ /** SHA-256 of every file under a directory, keyed by relative path. */
63
+ function treeHash(dir) {
64
+ const out = {};
65
+ const walk = (d, base) => {
66
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
67
+ const full = path.join(d, e.name);
68
+ const rel = path.join(base, e.name);
69
+ if (e.isDirectory()) walk(full, rel);
70
+ else out[rel] = crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex');
71
+ }
72
+ };
73
+ if (fs.existsSync(dir)) walk(dir, '');
74
+ return out;
75
+ }
76
+
77
+ function runScript(pkgDir) {
78
+ try {
79
+ const stdout = execFileSync(process.execPath, [path.join(pkgDir, 'scripts', 'sync-templates.js')],
80
+ { cwd: pkgDir, encoding: 'utf8', stdio: 'pipe' });
81
+ return { code: 0, out: stdout || '' };
82
+ } catch (err) {
83
+ return {
84
+ code: err.status == null ? 1 : err.status,
85
+ out: (err.stdout ? err.stdout.toString() : '') + (err.stderr ? err.stderr.toString() : ''),
86
+ };
87
+ }
88
+ }
89
+
90
+ const cleanup = (d) => fs.rmSync(d, { recursive: true, force: true });
91
+ const readPkg = (rel) => fs.readFileSync(path.join(PKG_DIR, rel), 'utf-8');
92
+
93
+ describe('a publish-time copy that was safe only because nobody called it', () => {
94
+ test('P1 — an unmarked root is REFUSED, and not one byte of templates/ changes', () => {
95
+ const f = fixture({});
96
+ try {
97
+ const templates = path.join(f.pkg, 'templates');
98
+ const before = treeHash(templates);
99
+ const r = runScript(f.pkg);
100
+ const after = treeHash(templates);
101
+ assert.notEqual(r.code, 0,
102
+ 'a refusal must exit non-zero; exit 0 is how a guard becomes invisible to a caller');
103
+ assert.deepEqual(after, before,
104
+ 'the refusal path must touch nothing — the check has to precede every write, not follow it');
105
+ assert.ok(!fs.existsSync(path.join(templates, '.claude', 'skills', 'intruder')),
106
+ 'the monorepo skill must not have arrived in what becomes the tarball');
107
+ } finally { cleanup(f.root); }
108
+ });
109
+
110
+ test('P2 — the refusal says what it found and why, not just that it stopped', () => {
111
+ const f = fixture({});
112
+ try {
113
+ const out = runScript(f.pkg).out;
114
+ assert.match(out, /REFUSING to sync/, 'it must say it refused');
115
+ assert.ok(out.includes(f.root),
116
+ 'and name the root it found, or the reader cannot tell which directory it objected to');
117
+ assert.match(out, /becomes the npm tarball/,
118
+ 'and say what was at stake, or the refusal reads as a configuration nag');
119
+ assert.ok(out.includes(DECLARATION),
120
+ 'and say how to opt in — quoting the exact declaration, or someone will delete the check');
121
+ assert.match(out, /a directory, a symlink, or an empty file is NOT a declaration/,
122
+ 'and say which near-misses do not count, since those are the accidental ones');
123
+ } finally { cleanup(f.root); }
124
+ });
125
+
126
+ test('P3 — a MARKED root still syncs: the guard must not break the legitimate path', () => {
127
+ // A guard that also blocks the intended use is a deletion wearing a disguise.
128
+ const f = fixture({ marker: true });
129
+ try {
130
+ const r = runScript(f.pkg);
131
+ assert.equal(r.code, 0, 'an opted-in root must proceed: ' + r.out);
132
+ assert.ok(fs.existsSync(path.join(f.pkg, 'templates', '.claude', 'skills', 'intruder', 'SKILL.md')),
133
+ 'and the source files must actually arrive');
134
+ assert.ok(fs.existsSync(path.join(f.pkg, 'templates', '.claude', 'skills', 'shipped', 'SKILL.md')),
135
+ 'while MERGE mode preserves what the package already shipped');
136
+ } finally { cleanup(f.root); }
137
+ });
138
+
139
+ test('P8 — a near-miss marker does NOT authorize: empty, directory, symlink, wrong text', () => {
140
+ // Cross-family QE: any node satisfies existsSync, so an empty file committed by accident, or a
141
+ // stray directory, would have permanently authorized overwriting the tarball. lstat keeps a
142
+ // symlink from vouching for a tree this script cannot see, and the declaration is content nobody
143
+ // types by accident.
144
+ for (const kind of ['empty', 'dir', 'symlink', 'wrong']) {
145
+ const f = fixture({ marker: kind });
146
+ try {
147
+ const templates = path.join(f.pkg, 'templates');
148
+ const before = treeHash(templates);
149
+ const r = runScript(f.pkg);
150
+ assert.notEqual(r.code, 0, 'a ' + kind + ' marker must not authorize the copy');
151
+ assert.deepEqual(treeHash(templates), before,
152
+ 'and must touch nothing: ' + kind);
153
+ } finally { cleanup(f.root); }
154
+ }
155
+ });
156
+
157
+ test('P9 — the script header does not claim to be the prepublishOnly hook', () => {
158
+ // The same false claim lived in three places. Fixing the doc and the helper while leaving the
159
+ // script's own docblock saying it "runs automatically before npm publish" would have left the
160
+ // lie in the most authoritative one.
161
+ const src = readPkg(path.join('scripts', 'sync-templates.js'));
162
+ const header = src.slice(0, src.indexOf('const fs ='));
163
+ assert.ok(!/prepublishOnly hook/.test(header),
164
+ 'the header must not call it the prepublishOnly hook');
165
+ assert.ok(!/Run automatically before/.test(header),
166
+ 'nor say it runs automatically — it does not, and that belief is what gets it run');
167
+ assert.match(header, /OPT-IN, and NOT part of publishing/,
168
+ 'it must say what it actually is, at the top where a reader looks first');
169
+ });
170
+
171
+ test('P4 — no candidate root at all: the pre-existing skip is unchanged', () => {
172
+ const f = fixture({ rootHasClaude: false });
173
+ try {
174
+ const r = runScript(f.pkg);
175
+ assert.equal(r.code, 0, 'this path was always benign and must stay exit 0: ' + r.out);
176
+ assert.match(r.out, /Not in repo context — skipping sync/,
177
+ 'and keep its existing message — this feature adds a guard, it does not rewrite the script');
178
+ } finally { cleanup(f.root); }
179
+ });
180
+
181
+ test('P5 — no helper tells a reader to run it', () => {
182
+ // The helper printed "Run prepublishOnly first: node scripts/sync-templates.js" when templates/
183
+ // was missing: a live invitation to the hazardous command, in the one situation where a reader
184
+ // is already confused.
185
+ assert.ok(!readPkg(path.join('tests', 'snapshot', 'update-baseline.js')).includes('sync-templates'),
186
+ 'update-baseline.js must not name the script as a recovery step');
187
+ });
188
+
189
+ test('P6 — the architecture doc no longer calls it the prepublishOnly hook', () => {
190
+ // Being false in THIS direction is the worst one: it makes the script sound sanctioned and
191
+ // current, which is exactly the belief that gets it run.
192
+ const doc = readPkg(path.join('README', 'eng', '05_architecture.md'));
193
+ assert.ok(!/`scripts\/sync-templates\.js` — runs as `prepublishOnly` hook/.test(doc),
194
+ 'the doc must not claim it runs as prepublishOnly — it does not');
195
+ assert.match(doc, /refuses to run unless the root it finds carries a `\.p-replicator-sync-source` marker/,
196
+ 'and must record what it actually is now');
197
+ });
198
+
199
+ test('P7 — prepublishOnly does not point at it', () => {
200
+ // The regression guard on the fact that kept it dormant. If someone wires it into publishing,
201
+ // this goes red — and after this feature the guard would stop it anyway, which is the point:
202
+ // reviving it is now safe rather than catastrophic.
203
+ const pkg = JSON.parse(readPkg('package.json'));
204
+ const hook = (pkg.scripts && pkg.scripts.prepublishOnly) || '';
205
+ assert.ok(!hook.includes('sync-templates'),
206
+ 'publishing must not run the template overwrite: ' + hook);
207
+ assert.match(hook, /prepublish-gate\.mjs/, 'publishing runs the gate');
208
+ });
209
+ });
@@ -341,11 +341,11 @@ describe('COMPONENTS.items SSOT', () => {
341
341
  }
342
342
  });
343
343
 
344
- test('item counts: 10 skills, 11 commands, 4 agents, 5 rules, 1 settings', () => {
344
+ test('item counts: 10 skills, 11 commands, 4 agents, 6 rules, 1 settings', () => {
345
345
  assert.equal(Object.keys(utils.COMPONENTS.skills.items).length, 10, 'skills count');
346
346
  assert.equal(Object.keys(utils.COMPONENTS.commands.items).length, 11, 'commands count (post v1.4: + 9 generic commands)');
347
347
  assert.equal(Object.keys(utils.COMPONENTS.agents.items).length, 4, 'agents count');
348
- assert.equal(Object.keys(utils.COMPONENTS.rules.items).length, 5, 'rules count (post v1.4: + 3 generic rules)');
348
+ assert.equal(Object.keys(utils.COMPONENTS.rules.items).length, 6, 'rules count (post v1.4: + 3 generic rules; + docker-ports)');
349
349
  assert.ok(utils.COMPONENTS.settings, 'settings component group added in v1.4');
350
350
  assert.equal(Object.keys(utils.COMPONENTS.settings.items).length, 1, 'settings.json count');
351
351
  });
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ // The Phase-2 quality gate existed but could not block on the thing it exists to measure.
4
+ //
5
+ // MEASURED from the weight tables in scoring-system.md: a user story with NO acceptance criteria and
6
+ // NO test links loses exactly Testable (8), Completeness (10) and Traceability (10), keeps every
7
+ // other criterion legitimately full, and totals 72/100 — above the 70 READY line, filed by the
8
+ // interpretation table under "fix minor issues, then proceed". The skill whose stated job is "Block
9
+ // untestable requirements" passed it.
10
+ //
11
+ // The field report named a different pair — Measurable (8) + Traceability (10) = 82. Same arithmetic
12
+ // hole, wrong pair: Measurable scores 0 for ANY requirement without a number, and plenty of correct
13
+ // functional requirements have none, so a floor there converts a false PASS into a false BLOCK. The
14
+ // floor is closed at Testable/Completeness, and P2 below is what keeps it closed.
15
+ //
16
+ // Second half: replicate.md said "Never skip validation" and nothing checked it. Phase 3 now carries
17
+ // a precondition naming the file, both admissible verdicts, and the action on failure.
18
+ //
19
+ // These are PROMPT files a model executes, so the deterministic layer available is their content.
20
+ // Every assertion below is DISCRIMINATING: remove what it names and it goes red.
21
+
22
+ const { test, describe } = require('node:test');
23
+ const assert = require('node:assert/strict');
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const T = path.join(__dirname, '..', '..', 'templates', '.claude');
28
+ const read = (rel) => fs.readFileSync(path.join(T, rel), 'utf-8');
29
+
30
+ const SCORING = 'skills/requirements-validator/references/scoring-system.md';
31
+ const VALIDATOR = 'skills/requirements-validator/SKILL.md';
32
+ const REPLICATE = 'commands/replicate.md';
33
+
34
+ /** A named section of a markdown file: the heading and everything up to the next same-level one.
35
+ * Cross-family QE: an assertion that searches a WHOLE file passes on explanatory prose elsewhere,
36
+ * which is exactly the "a mention is not a rule" failure this suite exists to prevent. */
37
+ function section(src, heading, nextHeading) {
38
+ const start = src.indexOf(heading);
39
+ const end = src.indexOf(nextHeading, start + 1);
40
+ assert.ok(start >= 0, 'missing section: ' + heading);
41
+ assert.ok(end > start, 'missing section terminator: ' + nextHeading);
42
+ return src.slice(start, end);
43
+ }
44
+
45
+ /** Phase 3 of replicate.md — placement is the property: a precondition stated in a later phase
46
+ * guards nothing, because the toolkit is already generated by then. */
47
+ function phase3(src) {
48
+ const start = src.indexOf('### Phase 3: TOOLKIT GENERATION');
49
+ const end = src.indexOf('### Phase 4');
50
+ assert.ok(start > 0, 'replicate.md must have a Phase 3');
51
+ assert.ok(end > start, 'replicate.md must have a Phase 4 after it');
52
+ return src.slice(start, end);
53
+ }
54
+
55
+ describe('the Phase-2 gate must be able to block (PR-009 + PR-004)', () => {
56
+ test('P1 — the blocking floor exists and names BOTH vetoing criteria', () => {
57
+ const src = read(SCORING);
58
+ assert.match(src, /Blocking floor/, 'the floor must be stated where the arithmetic lives');
59
+ assert.match(src, /weakest link decides, never the average/,
60
+ 'the family phrase — goap-research-ed25519/SKILL.md already states this rule in these words, '
61
+ + 'so the two skills read as one doctrine rather than two');
62
+ assert.match(src, /`Testable`[^\n]*\|[^\n]*no acceptance criteria/,
63
+ 'Testable = 0 must veto: it is the criterion that asks whether AC exist at all');
64
+ assert.match(src, /`Completeness`[^\n]*\|[^\n]*No AC/,
65
+ 'Completeness = 0 must veto too — the rubric\'s own "No AC" row');
66
+ assert.match(src, /72\/100/,
67
+ 'the worked case must carry its number, or a later reader cannot check the claim');
68
+ // Cross-family QE, the load-bearing finding: the SAME agent assigns these scores and is bound by
69
+ // the floor, so the floor read a number the floor's subject had written. Reporting Testable = 4
70
+ // ("vague AC") on a story with no AC evades the veto without contradicting the rubric. The floor
71
+ // must therefore key on the artifact — a non-zero score has to quote the AC it is scoring.
72
+ assert.match(src, /REQUIRES quoting the acceptance criteria/,
73
+ 'a non-zero Testable/Completeness must be evidenced by the AC text, not asserted');
74
+ assert.match(src, /No quote ⇒ the score is 0/,
75
+ 'and the consequence of having no quote must be stated, or the requirement is decorative');
76
+ });
77
+
78
+ test('P2 — the floor is CLOSED, and says so with its reason', () => {
79
+ // Without this, the next editor "strengthens" the gate by adding Measurable, and every
80
+ // non-numeric functional requirement starts failing. The refusal has to be as visible as the rule.
81
+ const src = read(SCORING);
82
+ assert.match(src, /floor is CLOSED/, 'the list must declare itself closed');
83
+ assert.match(src, /`Measurable`[\s\S]{0,80}deliberately NOT/,
84
+ 'Measurable must be named as excluded — naming it is what makes the exclusion survive an edit');
85
+ assert.match(src, /false pass into a false block/,
86
+ 'the REASON must be recorded, not just the exclusion');
87
+ });
88
+
89
+ test('P3 — the skill\'s own verdict table carries the floor too', () => {
90
+ // One document fixed and the other not is worse than neither: a reader who stops at SKILL.md
91
+ // gets a verdict table that still says 72 is a minor fix.
92
+ // Scoped to the section that CARRIES the verdict table. Searching the whole file would pass on
93
+ // any prose that happens to mention the floor — the finding cross-family QE raised here.
94
+ const block = section(read(VALIDATOR), '## Scoring System', '## Output Format');
95
+ assert.match(block, /Blocking floor/, 'the floor must sit beside the score table it overrides');
96
+ assert.match(block, /Testable = 0/, 'and name the first vetoing criterion');
97
+ assert.match(block, /Completeness = 0/, 'and the second');
98
+ assert.match(block, /REQUIRES quoting/, 'and carry the evidence rule, not only the veto');
99
+ });
100
+
101
+ test('P4 — Phase 3 states a checkable precondition: file, verdicts, action', () => {
102
+ const block = phase3(read(REPLICATE));
103
+ assert.match(block, /docs\/validation-report\.md/,
104
+ 'the precondition must name the FILE — "validated docs" is not checkable');
105
+ assert.match(block, /must EXIST/, 'existence is the first check');
106
+ // Cross-family QE: naming the three verdict emoji ANYWHERE in the phase is satisfied by prose.
107
+ // The property is a single anchored contract — WHICH line carries the verdict, and that a
108
+ // verdict word found elsewhere does not count.
109
+ assert.match(block, /\*\*first line\*\* must be `\*\*Verdict:\*\*` followed by 🟢 READY or 🟡 CAVEATS/,
110
+ 'the admissible verdicts must be bound to the ONE authoritative line, not merely mentioned');
111
+ assert.match(block, /is NOT the verdict/,
112
+ 'a verdict word elsewhere in the report must be explicitly rejected');
113
+ assert.match(block, /more than one line starting\s+with `\*\*Verdict:\*\*`/,
114
+ 'uniqueness must be required, or two verdicts leave the reader to choose');
115
+ assert.match(block, /do NOT generate anything/,
116
+ 'a precondition with no stated consequence is advice; the action is the rule');
117
+ assert.match(block, /return to Phase 2/, 'and where to go instead');
118
+ });
119
+
120
+ test('P5 — the Phase-2 exit table carries the same floor, so both ends agree', () => {
121
+ const src = read(REPLICATE);
122
+ const start = src.indexOf('### Phase 2: VALIDATION');
123
+ const end = src.indexOf('### Phase 3');
124
+ const block = src.slice(start, end);
125
+ assert.ok(start > 0 && end > start, 'replicate.md must have a Phase 2 before Phase 3');
126
+ assert.match(block, /blocking floor/,
127
+ 'the phase that DECIDES the verdict must state the floor, not only the phase that consumes it');
128
+ assert.match(block, /`Testable = 0`/, 'named in the NEEDS WORK row');
129
+ assert.match(block, /`Completeness = 0`/, 'both of them');
130
+ });
131
+
132
+ test('P7 — Phase 2 EMITS the anchored verdict line Phase 3 reads', () => {
133
+ // A reader-side contract with no writer-side obligation is half a contract: Phase 3 would demand
134
+ // a first-line **Verdict:** that Phase 2 was never told to write, and every run would bounce.
135
+ const src = read(REPLICATE);
136
+ const block = src.slice(src.indexOf('### Phase 2: VALIDATION'), src.indexOf('### Phase 3'));
137
+ assert.match(block, /\*\*first line\*\* must be exactly one of/,
138
+ 'Phase 2 must be told the exact form Phase 3 will read');
139
+ assert.match(block, /\*\*Verdict:\*\* 🟢 READY/, 'and shown it verbatim');
140
+ assert.match(block, /no other line in the file may begin with `\*\*Verdict:\*\*`/,
141
+ 'and told to keep it unique, which is what makes Phase 3\'s uniqueness check satisfiable');
142
+ });
143
+
144
+ test('P6 — the precondition is imperative and file-naming, not advisory', () => {
145
+ const block = phase3(read(REPLICATE));
146
+ assert.ok(!/make sure|try to|ideally|should probably/i.test(block),
147
+ 'advisory wording would pass a reviewer and instruct nothing');
148
+ // "Never skip validation" was TRUE, imperative, and unenforceable — it named no file to read.
149
+ // The NEVER line must now point at the check rather than stand alone.
150
+ // Cross-family QE was right that the first version accepted a self-attestation: a line reading
151
+ // "this is CHECKED, not assumed" is a CLAIM about the rule, and a claim is what the old advisory
152
+ // line already was. What makes the NEVER line non-advisory is that it POINTS at a named,
153
+ // readable artifact — so that is what this asserts.
154
+ const src = read(REPLICATE);
155
+ assert.match(src, /Never skip validation[^\n]*\n?[^\n]*`\*\*Verdict:\*\*` line of `docs\/validation-report\.md`/,
156
+ 'the NEVER line must point at the exact line of the exact file that enforces it');
157
+ });
158
+ });
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dzhechko
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.