@dzhechkov/p-replicator 1.5.18 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +173 -57
- package/CHANGELOG.md +262 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +2 -2
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/05_architecture.md +6 -2
- package/README/eng/README.md +1 -1
- package/README/ru/01_quickstart.md +2 -2
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +1 -1
- package/README/ru/html/index.html +7 -7
- package/README.md +139 -16
- package/bin/cli.js +0 -0
- package/package.json +11 -10
- package/sbom.json +346 -56
- package/src/utils.js +4 -0
- package/templates/.claude/agents/doc-validator.md +2 -1
- package/templates/.claude/agents/product-discoverer.md +1 -1
- package/templates/.claude/commands/next.md +16 -0
- package/templates/.claude/commands/replicate.md +183 -9
- package/templates/.claude/commands/start.md +19 -1
- package/templates/.claude/hooks/autocommit-insights.cjs +95 -10
- package/templates/.claude/hooks/autocommit-plans.cjs +95 -10
- package/templates/.claude/hooks/autocommit-roadmap.cjs +96 -13
- package/templates/.claude/hooks/check-docs-complete.cjs +174 -0
- package/templates/.claude/hooks/check-growth-trace.cjs +191 -0
- package/templates/.claude/hooks/check-ports.cjs +232 -0
- package/templates/.claude/hooks/session-insights.cjs +13 -1
- package/templates/.claude/hooks/state-update.cjs +13 -1
- package/templates/.claude/hooks/statusline.cjs +145 -18
- package/templates/.claude/rules/docker-ports.md +123 -0
- package/templates/.claude/rules/replicate-pipeline.md +19 -6
- package/templates/.claude/rules/skill-interface-protocol.md +9 -0
- package/templates/.claude/settings.json +5 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md +57 -14
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/02-analyze-map.md +9 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +6 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/08-skill-composition.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/enhanced-recommendations.md +6 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md +1 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-suggestions.md +11 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +1 -1
- package/templates/.claude/skills/requirements-validator/SKILL.md +59 -0
- package/templates/.claude/skills/requirements-validator/references/scoring-system.md +28 -0
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/01-intelligence.md +4 -4
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/02-product-customers.md +2 -2
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +9 -1
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md +3 -3
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/04-business-finance.md +3 -3
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/05-growth-engine.md +132 -12
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/06-playbook-synthesis.md +1 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +152 -10
- package/tests/e2e/lifecycle.test.js +21 -10
- package/tests/snapshot/baseline.json +42 -38
- package/tests/snapshot/update-baseline.js +2 -1
- package/tests/unit/adr-decision-coverage.test.js +137 -0
- package/tests/unit/adr-scanner-contract.test.js +108 -0
- package/tests/unit/autocommit-deletion.test.js +242 -0
- package/tests/unit/check-docs-complete.test.js +249 -0
- package/tests/unit/check-growth-trace.test.js +188 -0
- package/tests/unit/check-ports.test.js +184 -0
- package/tests/unit/db-port-rule.test.js +216 -0
- package/tests/unit/detect-parse-anchor.test.js +109 -0
- package/tests/unit/external-dependency-check.test.js +209 -0
- package/tests/unit/growth-axes-and-compliance.test.js +169 -0
- package/tests/unit/growth-gate-conditional.test.js +122 -0
- package/tests/unit/growth-module-b2b-gate.test.js +122 -0
- package/tests/unit/growth-requirements-bridge.test.js +127 -0
- package/tests/unit/hooks-project-anchored.test.js +223 -0
- package/tests/unit/hooks-report-failures.test.js +207 -0
- package/tests/unit/module-copy-identity.test.js +76 -0
- package/tests/unit/pipeline-file-ownership.test.js +95 -0
- package/tests/unit/roadmap-one-schema.test.js +179 -0
- package/tests/unit/skill-paths-prebaked.test.js +174 -0
- package/tests/unit/sparc-reconciliation.test.js +117 -0
- package/tests/unit/spec-pseudocode-traceability.test.js +146 -0
- package/tests/unit/statusline-honest-labels.test.js +178 -0
- package/tests/unit/statusline-two-roots.test.js +237 -0
- package/tests/unit/sync-templates-guard.test.js +209 -0
- package/tests/unit/utils.test.js +2 -2
- package/tests/unit/validation-gate-teeth.test.js +158 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The project-type scanner probed seven paths from one parameter, and the paths disagreed about what
|
|
4
|
+
// that parameter was. MEASURED before the fix:
|
|
5
|
+
//
|
|
6
|
+
// {docs_path}/docs/ddd/ · /.ai-context/ · /docs/tests/*.feature · /docs/adr/*.md · /docs/ADR.md
|
|
7
|
+
// → all five imply docs_path is the PROJECT ROOT
|
|
8
|
+
// {docs_path}/Architecture.md · {docs_path}/Solution_Strategy.md
|
|
9
|
+
// → these two imply docs_path is `docs/`
|
|
10
|
+
//
|
|
11
|
+
// And the pipeline writes those two files to docs/ (commands/replicate.md:234,237). So the SPARC
|
|
12
|
+
// probes looked exactly where the pipeline does not write: the scanner could not recognise its own
|
|
13
|
+
// output as SPARC-shaped.
|
|
14
|
+
//
|
|
15
|
+
// The parameter description was the root of it — one sentence naming two directories: "In Claude
|
|
16
|
+
// Code / replicate context this is `docs/` (project root)." A reader implementing a probe picked
|
|
17
|
+
// whichever half they read last. Nobody passes the parameter explicitly (a sweep of SKILL.md and
|
|
18
|
+
// commands/replicate.md for `docs_path` returns nothing), so that sentence IS the specification.
|
|
19
|
+
//
|
|
20
|
+
// Both the field report and the reviewing swarm got this wrong in opposite directions: the report
|
|
21
|
+
// said all six probes were wrong, the swarm said the table was wrong and the probes right. Five
|
|
22
|
+
// probes are right, two are wrong, and the table contradicts itself.
|
|
23
|
+
|
|
24
|
+
const { test, describe } = require('node:test');
|
|
25
|
+
const assert = require('node:assert/strict');
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
const MODULE = path.join(__dirname, '..', '..', 'templates', '.claude', 'skills',
|
|
30
|
+
'cc-toolkit-generator-enhanced', 'modules', '01-detect-parse.md');
|
|
31
|
+
const read = () => fs.readFileSync(MODULE, 'utf-8');
|
|
32
|
+
|
|
33
|
+
/** Every path this module probes, as written. */
|
|
34
|
+
function probes() {
|
|
35
|
+
const src = read();
|
|
36
|
+
return [...src.matchAll(/\{docs_path\}\/([^"')\s]+)/g)].map((m) => m[1]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('every probe resolves from ONE anchor, and the anchor is named once', () => {
|
|
40
|
+
test('P1 — no probe implies an anchor other than the project root', () => {
|
|
41
|
+
// The property is AGREEMENT across all of them, not the correctness of any single path. A
|
|
42
|
+
// top-level SPARC artifact would mean docs_path is `docs/` for that probe and the root for the
|
|
43
|
+
// others — which is the state this feature removes.
|
|
44
|
+
// DERIVED, not typed. Cross-family QE: the first version hard-coded seven basenames, so a probe
|
|
45
|
+
// for Research_Findings.md — a real pipeline document I had not listed — would have escaped it.
|
|
46
|
+
// The authoritative list is the pipeline's own: replicate.md writes every SPARC document to docs/.
|
|
47
|
+
const replicate = fs.readFileSync(path.join(__dirname, '..', '..', 'templates', '.claude',
|
|
48
|
+
'commands', 'replicate.md'), 'utf-8');
|
|
49
|
+
const written = new Set([...replicate.matchAll(/^- `docs\/([A-Z][A-Za-z_]*\.md)`/gm)].map((m) => m[1]));
|
|
50
|
+
assert.ok(written.size >= 9,
|
|
51
|
+
'the pipeline document list must be readable from replicate.md: ' + [...written]);
|
|
52
|
+
|
|
53
|
+
const disagreeing = probes().filter((p) => written.has(p));
|
|
54
|
+
assert.deepEqual(disagreeing, [],
|
|
55
|
+
'these probe a document the pipeline writes to docs/, as if it were at the project root — '
|
|
56
|
+
+ 'which only makes sense if docs_path is `docs/`, while every other probe treats it as the '
|
|
57
|
+
+ 'root: ' + JSON.stringify(disagreeing));
|
|
58
|
+
|
|
59
|
+
// And the prose must not re-introduce the ambiguity the probes just lost. Five sites carried it.
|
|
60
|
+
const src = read();
|
|
61
|
+
assert.ok(!/top-level or docs\//.test(src),
|
|
62
|
+
'the scan catalog still preserves the "top-level or docs/" reading this feature removes');
|
|
63
|
+
for (const doc of ['Architecture.md', 'Solution_Strategy.md', 'PRD.md', 'Specification.md']) {
|
|
64
|
+
const bare = new RegExp('(?<![/\\w])`' + doc.replace('.', '\\.') + '`');
|
|
65
|
+
assert.ok(!bare.test(src),
|
|
66
|
+
'a pipeline document is named without its docs/ prefix, which reads as project-root: ' + doc);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('P2 — the two SPARC probes look under docs/, where the pipeline writes', () => {
|
|
71
|
+
const src = read();
|
|
72
|
+
assert.match(src, /has_sparc_arch = exists\(f"\{docs_path\}\/docs\/Architecture\.md"\)/,
|
|
73
|
+
'commands/replicate.md:237 writes docs/Architecture.md');
|
|
74
|
+
assert.match(src, /has_sparc_sol\s+= exists\(f"\{docs_path\}\/docs\/Solution_Strategy\.md"\)/,
|
|
75
|
+
'commands/replicate.md:234 writes docs/Solution_Strategy.md');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('P3 — the root-level probe stays at the root', () => {
|
|
79
|
+
// .ai-context/ sits beside .claude/, not under docs/. Moving it "for consistency" would break a
|
|
80
|
+
// probe that was correct all along — the failure mode the field report actually proposed.
|
|
81
|
+
assert.ok(probes().includes('.ai-context/'),
|
|
82
|
+
'the .ai-context probe must remain anchored at the project root: ' + JSON.stringify(probes()));
|
|
83
|
+
assert.ok(!probes().includes('docs/.ai-context/'),
|
|
84
|
+
'.ai-context/ is a root-level directory; probing it under docs/ finds nothing');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('P4 — the parameter names ONE directory', () => {
|
|
88
|
+
const src = read();
|
|
89
|
+
const row = src.split('\n').find((l) => l.startsWith('| `docs_path` |'));
|
|
90
|
+
assert.ok(row, 'the parameter table row must exist');
|
|
91
|
+
assert.ok(!/this is `docs\/` \(project root\)/.test(row),
|
|
92
|
+
'the row still names two different directories in one breath — that sentence is what split '
|
|
93
|
+
+ 'the probes in the first place: ' + row);
|
|
94
|
+
assert.match(row, /the PROJECT ROOT/,
|
|
95
|
+
'it must name the anchor unambiguously');
|
|
96
|
+
assert.match(row, /not `docs\/`/,
|
|
97
|
+
'and rule out the other reading explicitly, since that reading is what produced the split');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('P5 — both ADR shapes are still collected', () => {
|
|
101
|
+
// This logic shipped in an earlier feature (one decision per file, and /replicate's single
|
|
102
|
+
// docs/ADR.md). Re-anchoring must not disturb it.
|
|
103
|
+
const src = read();
|
|
104
|
+
assert.match(src, /glob\(f"\{docs_path\}\/docs\/adr\/\*\.md"\)/, 'shape A: a directory of files');
|
|
105
|
+
assert.match(src, /f"\{docs_path\}\/docs\/ADR\.md"/, 'shape B: /replicate\'s single file');
|
|
106
|
+
assert.match(src, /has_adr = len\(collect_adrs\(docs_path\)\) > 0/,
|
|
107
|
+
'and the one definition that unifies them');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Every validation lens compared documents to documents. MEASURED — replicate.md's Phase-2 table
|
|
4
|
+
// listed five: stories→PRD, AC→stories, Architecture.md, Pseudocode.md, cross-document coherence.
|
|
5
|
+
// Nothing anywhere asked whether the outside world agrees, so a requirement could rest on a
|
|
6
|
+
// capability the provider does not have and reach code generation unchallenged. It is the only
|
|
7
|
+
// contact-with-reality defect in this pipeline; everything else it checks, it can check by reading
|
|
8
|
+
// its own output.
|
|
9
|
+
//
|
|
10
|
+
// TWO honesty constraints bind this suite, both from the field report:
|
|
11
|
+
// 1. check-pipeline-gaps.sh, which the report cites, DOES NOT EXIST. P7 keeps it uncited.
|
|
12
|
+
// 2. No external fact may be baked in. What an API can do DRIFTS: a fixture asserting one would
|
|
13
|
+
// fail when the world changes and pass when our own rule breaks. P6 asserts the ABSENCE of
|
|
14
|
+
// vendor names, and no assertion here mentions a capability of any real service.
|
|
15
|
+
|
|
16
|
+
const { test, describe } = require('node:test');
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const TPL = path.join(__dirname, '..', '..', 'templates', '.claude');
|
|
22
|
+
const read = (rel) => fs.readFileSync(path.join(TPL, rel), 'utf-8');
|
|
23
|
+
|
|
24
|
+
const SPARC = 'skills/sparc-prd-mini/SKILL.md';
|
|
25
|
+
const REPLICATE = 'commands/replicate.md';
|
|
26
|
+
|
|
27
|
+
/** The External Dependencies section of the Phase-5 Architecture.md template. Scoped, because an
|
|
28
|
+
* assertion over a whole file passes on any prose that happens to mention the words. */
|
|
29
|
+
function inventory() {
|
|
30
|
+
const src = read(SPARC);
|
|
31
|
+
const start = src.indexOf('## External Dependencies\n');
|
|
32
|
+
assert.ok(start > 0, 'the Phase-5 template must carry an External Dependencies section');
|
|
33
|
+
const end = src.indexOf('## Data Architecture', start);
|
|
34
|
+
assert.ok(end > start, 'and it must sit before Data Architecture, where the stack is still in view');
|
|
35
|
+
return src.slice(start, end);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Phase 2 of replicate.md — a lens declared in a later phase validates nothing. */
|
|
39
|
+
function phase2() {
|
|
40
|
+
const src = read(REPLICATE);
|
|
41
|
+
const start = src.indexOf('### Phase 2: VALIDATION');
|
|
42
|
+
const end = src.indexOf('### Phase 3', start);
|
|
43
|
+
assert.ok(start > 0 && end > start, 'replicate.md must have a Phase 2 before Phase 3');
|
|
44
|
+
return src.slice(start, end);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('the pipeline asks whether the outside world can do what a requirement needs (PR-010)', () => {
|
|
48
|
+
test('P1 — the inventory exists and carries all five columns', () => {
|
|
49
|
+
const block = inventory();
|
|
50
|
+
// The HEADER ROW, not five words scattered through prose. Cross-family QE: a paragraph
|
|
51
|
+
// containing all five labels would have replaced the table without failing.
|
|
52
|
+
const header = block.split('\n').find((l) => l.startsWith('| Capability needed |')) || '';
|
|
53
|
+
assert.notEqual(header, '', 'the inventory must have a table header row starting with the '
|
|
54
|
+
+ 'capability column');
|
|
55
|
+
for (const col of ['Capability needed', 'Provider / API', 'Evidence', 'Verdict',
|
|
56
|
+
'Requirements relying on it']) {
|
|
57
|
+
assert.ok(header.includes(col), 'the header row must carry the column: ' + col);
|
|
58
|
+
}
|
|
59
|
+
// One row per CAPABILITY, not per vendor: a provider can do one of two things you need.
|
|
60
|
+
assert.match(block, /One row per capability, not one row\s+per vendor/,
|
|
61
|
+
'the granularity must be stated, or one row hides two questions');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('P2 — the evidence rule is CLOSED on both sides', () => {
|
|
65
|
+
// Saying what evidence IS leaves every reader to decide what it is not. The three ways this
|
|
66
|
+
// check gets faked are named, so a row using one of them is visibly not evidence.
|
|
67
|
+
const block = inventory();
|
|
68
|
+
assert.match(block, /link to the PROVIDER'S OWN documentation/,
|
|
69
|
+
'what counts must be stated precisely — not "a source"');
|
|
70
|
+
assert.match(block, /plus the date it was checked/,
|
|
71
|
+
'a citation without a date cannot be re-checked later');
|
|
72
|
+
// Cross-family QE, the load-bearing finding: CONFIRMED was self-attested, and a plausible URL is
|
|
73
|
+
// the cheapest forgery there is. A verbatim quote is what a link alone cannot fake.
|
|
74
|
+
assert.match(block, /verbatim QUOTE from\s+that page stating the capability/,
|
|
75
|
+
'a non-fabricable artifact must be required, not just a link');
|
|
76
|
+
assert.match(block, /a URL nobody opened/,
|
|
77
|
+
'and the link-alone forgery must be named as not-evidence');
|
|
78
|
+
assert.match(block, /do NOT count/, 'and the rule must be closed on the other side too');
|
|
79
|
+
for (const fake of ['landing page', 'pricing page', 'recollection']) {
|
|
80
|
+
assert.ok(block.includes(fake), 'the not-evidence list must name: ' + fake);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('P3 — exactly three verdicts, and the empty case is explicit', () => {
|
|
85
|
+
const block = inventory();
|
|
86
|
+
// Each verdict must be a ROW of the verdict table, not a word somewhere in the prose. Found by
|
|
87
|
+
// mutation: renaming the UNCONFIRMED row to UNKNOWN left the vocabulary broken and the first
|
|
88
|
+
// version of this assertion green, because the word still appeared in the paragraph below it.
|
|
89
|
+
for (const v of ['CONFIRMED', 'UNCONFIRMED', 'CONTRADICTED']) {
|
|
90
|
+
assert.match(block, new RegExp('^\\| ' + v + ' \\|', 'm'),
|
|
91
|
+
'the verdict must be a row of the verdict table, not merely mentioned: ' + v);
|
|
92
|
+
}
|
|
93
|
+
// Two values would collapse "we checked and it cannot" into "nobody checked", which overstates
|
|
94
|
+
// one and hides the other. The reason has to be written down or the third value looks redundant.
|
|
95
|
+
assert.match(block, /exactly three, because two would hide a difference that\s+matters/,
|
|
96
|
+
'the reason for three values must be recorded, or a later edit will merge two of them');
|
|
97
|
+
// EXACTLY three: the first version asserted the three exist and let a fourth in silently.
|
|
98
|
+
const verdictRows = block.split('\n')
|
|
99
|
+
.filter((l) => /^\| [A-Z_]+ \|/.test(l) && !l.startsWith('| Verdict |'));
|
|
100
|
+
assert.equal(verdictRows.length, 3,
|
|
101
|
+
'the verdict table must have exactly three rows, not merely contain the three: '
|
|
102
|
+
+ JSON.stringify(verdictRows));
|
|
103
|
+
assert.match(block, /No external dependencies —\s+this product calls no third-party service/,
|
|
104
|
+
'a project with none must say so: an empty section and an absent one look identical');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('P4 — Phase 2 declares the sixth lens, inside Phase 2', () => {
|
|
108
|
+
const block = phase2();
|
|
109
|
+
const lensRow = block.split('\n').find((l) => l.startsWith('| `validator-dependencies` |')) || '';
|
|
110
|
+
assert.notEqual(lensRow, '',
|
|
111
|
+
'the lens must be a ROW of the validator table, not a mention in prose');
|
|
112
|
+
assert.match(lensRow, /External Dependencies/,
|
|
113
|
+
'and its scope column must name the section it reads: ' + lensRow);
|
|
114
|
+
assert.match(lensRow, /evidence that names that capability/,
|
|
115
|
+
'and its criteria column must say what it checks for: ' + lensRow);
|
|
116
|
+
assert.match(block, /the only one that looks OUTSIDE the\s+documents/,
|
|
117
|
+
'and say what makes it different from the other five, or it reads as a sixth of the same kind');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('P5 — the exit criteria carry BOTH consequences, and UNCONFIRMED does not pass silently', () => {
|
|
121
|
+
// Anchored to the exit-criteria ROW each term belongs to. Cross-family QE: three independent
|
|
122
|
+
// phrase matches anywhere in Phase 2 would pass on explanatory prose that changed no verdict.
|
|
123
|
+
const rows = {};
|
|
124
|
+
for (const l of phase2().split('\n')) {
|
|
125
|
+
const m = l.match(/^\| (🟢 READY|🟡 CAVEATS|🔴 NEEDS WORK) \|/);
|
|
126
|
+
if (m) rows[m[1]] = l;
|
|
127
|
+
}
|
|
128
|
+
assert.ok(rows['🟢 READY'] && rows['🟡 CAVEATS'] && rows['🔴 NEEDS WORK'],
|
|
129
|
+
'the exit-criteria table must carry all three verdict rows');
|
|
130
|
+
assert.match(rows['🔴 NEEDS WORK'], /any external dependency is `CONTRADICTED`/,
|
|
131
|
+
'a contradicted dependency must send the run back, in the row that sends it back');
|
|
132
|
+
assert.match(rows['🟡 CAVEATS'], /every `UNCONFIRMED` dependency NAMED row by row/,
|
|
133
|
+
'an unconfirmed dependency must be NAMED in the CAVEATS row — an unnamed caveat is one '
|
|
134
|
+
+ 'nobody acts on');
|
|
135
|
+
assert.match(rows['🟢 READY'], /no external dependency `UNCONFIRMED` or `CONTRADICTED`/,
|
|
136
|
+
'and READY must exclude both, or the strongest verdict is the one that checks least');
|
|
137
|
+
// And the consequence must be scoped to the REQUIREMENT: without that, an inventory of nothing
|
|
138
|
+
// but UNCONFIRMED rows reaches Phase 3 with a caveat and feasibility is never established.
|
|
139
|
+
assert.match(inventory(), /cannot enter Phase 3 — defer, remove or replace them/,
|
|
140
|
+
'UNCONFIRMED must stop the requirements that rest on it, or the check is optional in practice');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('P6 — NO vendor fact is baked in: external facts drift', () => {
|
|
144
|
+
// A fixture asserting what some API can do would fail when the world changes and pass when our
|
|
145
|
+
// own rule breaks. The template must teach the SHAPE and name nobody.
|
|
146
|
+
const block = inventory();
|
|
147
|
+
const vendors = ['Anthropic', 'OpenAI', 'Stripe', 'Twilio', 'SendGrid', 'AWS', 'Google Cloud',
|
|
148
|
+
'Azure', 'Firebase', 'Supabase'];
|
|
149
|
+
for (const v of vendors) {
|
|
150
|
+
assert.ok(!block.includes(v),
|
|
151
|
+
'the template must name no real provider — capabilities drift and a stale fact recorded as '
|
|
152
|
+
+ 'evidence is worse than none. Found: ' + v);
|
|
153
|
+
}
|
|
154
|
+
// A denylist can only ever catch the names on it. The positive assertion is what actually holds:
|
|
155
|
+
// the example row's own cells must be bracketed placeholders, so there is no cell for a real
|
|
156
|
+
// vendor to sit in. The list above stays as a cheap second net.
|
|
157
|
+
const exampleRow = block.split('\n')
|
|
158
|
+
.find((l) => l.startsWith('| [') && l.includes('CONFIRMED')) || '';
|
|
159
|
+
assert.notEqual(exampleRow, '', 'the template must show one example row');
|
|
160
|
+
const cells = exampleRow.split('|').map((c) => c.trim()).filter(Boolean);
|
|
161
|
+
for (const cell of cells) {
|
|
162
|
+
assert.ok(/^\[|^CONFIRMED$/.test(cell) || cell.startsWith('['),
|
|
163
|
+
'every example cell must be a bracketed placeholder or the verdict itself, so no real '
|
|
164
|
+
+ 'provider can sit in the row: ' + cell);
|
|
165
|
+
}
|
|
166
|
+
assert.match(block, /are PLACEHOLDERS/, 'and it must say the example names are placeholders');
|
|
167
|
+
assert.match(block, /a stale fact recorded as evidence is worse than\s+none/,
|
|
168
|
+
'with the reason, so a later editor does not helpfully fill in a real one');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('P8 — the agent that DISPATCHES the swarm lists the same lenses as the command', () => {
|
|
172
|
+
// Found by an independent reviewer after the fact: commands/replicate.md gained the sixth lens
|
|
173
|
+
// and templates/.claude/agents/doc-validator.md did not. Two files describing one swarm, one of
|
|
174
|
+
// them a lens short — and the sha256 snapshot test cannot see it, because both files hash fine.
|
|
175
|
+
const cmd = read(REPLICATE);
|
|
176
|
+
const agent = fs.readFileSync(path.join(TPL, 'agents', 'doc-validator.md'), 'utf-8');
|
|
177
|
+
const names = (src) => new Set([...src.matchAll(/\| `(validator-[a-z-]+)`/g)].map((m) => m[1]));
|
|
178
|
+
const inCmd = names(cmd);
|
|
179
|
+
const inAgent = names(agent);
|
|
180
|
+
assert.ok(inCmd.size >= 6, 'the command must dispatch at least six lenses: ' + [...inCmd]);
|
|
181
|
+
assert.deepEqual([...inAgent].sort(), [...inCmd].sort(),
|
|
182
|
+
'the dispatching agent and the command disagree about the swarm: '
|
|
183
|
+
+ JSON.stringify({ agent: [...inAgent], command: [...inCmd] }));
|
|
184
|
+
// And the count in the agent's own prose must match the table it introduces.
|
|
185
|
+
const stated = agent.match(/Launch (\d+) parallel validation agents/);
|
|
186
|
+
assert.ok(stated, 'doc-validator.md must state how many it launches');
|
|
187
|
+
assert.equal(Number(stated[1]), inAgent.size,
|
|
188
|
+
'doc-validator.md says ' + stated[1] + ' but tables ' + inAgent.size);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('P7 — nothing cites check-pipeline-gaps.sh, which does not exist', () => {
|
|
192
|
+
// The report cites it. MEASURED — a repo-wide find returns nothing. Pointing a reader at a
|
|
193
|
+
// script that is not there is a worse failure than the gap it was cited to close.
|
|
194
|
+
for (const f of [SPARC, REPLICATE]) {
|
|
195
|
+
assert.ok(!read(f).includes('check-pipeline-gaps'),
|
|
196
|
+
f + ' references a script that does not exist in this repository');
|
|
197
|
+
}
|
|
198
|
+
// The premise, asserted rather than assumed: if someone ever ADDS the script, this goes red and
|
|
199
|
+
// whoever added it has to revisit the rule instead of leaving a stale prohibition behind.
|
|
200
|
+
const pkgRoot = path.join(__dirname, '..', '..');
|
|
201
|
+
const candidates = ['check-pipeline-gaps.sh', path.join('scripts', 'check-pipeline-gaps.sh'),
|
|
202
|
+
path.join('bin', 'check-pipeline-gaps.sh')];
|
|
203
|
+
for (const c of candidates) {
|
|
204
|
+
assert.ok(!fs.existsSync(path.join(pkgRoot, c)),
|
|
205
|
+
'the script now exists at ' + c + ' — this rule was written on the premise that it does '
|
|
206
|
+
+ 'not, so revisit it rather than deleting this assertion');
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Two prose defects in 05-growth-engine.md, shipped as one change because the file has four
|
|
4
|
+
// byte-identical copies and splitting them means two three-package releases.
|
|
5
|
+
//
|
|
6
|
+
// P4: the growth-type list forced ONE choice across TWO different questions — how a company reaches
|
|
7
|
+
// buyers, and what makes usage produce more usage. A sales-led company running a referral loop was
|
|
8
|
+
// literally unsayable. Three loop mechanics were also absent.
|
|
9
|
+
//
|
|
10
|
+
// P2: the Research Protocol's three modes mentioned no constraint of any kind, so the module could
|
|
11
|
+
// design a violation with full confidence — confidence here comes from data quality, not legality.
|
|
12
|
+
//
|
|
13
|
+
// The load-bearing rule in P2's fix, and the evidence for it is the source report's own failure: it
|
|
14
|
+
// warned that a 2024 penalty figure was stale and then quoted the 2025 figure as current, in August
|
|
15
|
+
// 2026. Warning and error, one document, one year apart. So: cite the norm, never the amount — and
|
|
16
|
+
// P5 exists to make a future edit that re-adds a figure go red.
|
|
17
|
+
|
|
18
|
+
const { test, describe } = require('node:test');
|
|
19
|
+
const assert = require('node:assert/strict');
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const MODULE = path.join(__dirname, '..', '..', 'templates', '.claude', 'skills',
|
|
24
|
+
'reverse-engineering-unicorn', 'modules', '05-growth-engine.md');
|
|
25
|
+
|
|
26
|
+
const read = () => fs.readFileSync(MODULE, 'utf-8');
|
|
27
|
+
|
|
28
|
+
/** A named section, from its heading to the next heading at the same level. */
|
|
29
|
+
function section(heading, level) {
|
|
30
|
+
const src = read();
|
|
31
|
+
const start = src.indexOf(heading);
|
|
32
|
+
assert.ok(start > 0, 'the module must carry: ' + heading);
|
|
33
|
+
const end = src.indexOf('\n' + '#'.repeat(level) + ' ', start + 1);
|
|
34
|
+
assert.ok(end > start, heading + ' must be followed by another section');
|
|
35
|
+
return src.slice(start, end);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const compliance = () => section('### ⚖️ Чеклист допустимости', 3);
|
|
39
|
+
|
|
40
|
+
describe('growth type is two questions, and the protocol knows constraints exist', () => {
|
|
41
|
+
test('P1 - the two axes are separate and independently chosen', () => {
|
|
42
|
+
const src = read();
|
|
43
|
+
assert.match(src, /\*\*Ось 1 — МОТИОН/, 'the go-to-market axis must be its own list');
|
|
44
|
+
assert.match(src, /\*\*Ось 2 — ПЕТЛЯ/, 'and the loop axis its own');
|
|
45
|
+
assert.match(src, /Оси НЕЗАВИСИМЫ/, 'independence must be stated, not left to be inferred');
|
|
46
|
+
assert.match(src, /Sales-Led` \+ `Поощряемая реферальная/,
|
|
47
|
+
'and shown with the combination the old single list made unsayable');
|
|
48
|
+
|
|
49
|
+
// The defect was ONE instruction spanning both questions. Its removal is the property.
|
|
50
|
+
assert.ok(!/\*\*Выбранный тип:\*\* \[ONE из:\]/.test(src),
|
|
51
|
+
'the single cross-axis selector must be gone, or the fix is cosmetic');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('P2 - the three missing loop mechanics are present', () => {
|
|
55
|
+
const src = read();
|
|
56
|
+
for (const m of ['Значковая / встраиваемая', 'Поощряемая реферальная', 'Сетевой эффект']) {
|
|
57
|
+
assert.ok(src.includes(m), 'the loop axis must offer: ' + m);
|
|
58
|
+
}
|
|
59
|
+
// "no loop" must be sayable, or every project claims one it does not have.
|
|
60
|
+
assert.match(src, /Нет петли/, 'absence of a loop must be a choice');
|
|
61
|
+
assert.match(src, /полноценный ответ, а не пропуск/,
|
|
62
|
+
'and must be marked as an answer, not a blank');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('P3 - the artifact does not claim the module was blind to virality', () => {
|
|
66
|
+
// The backlog item's own correction: the K-factor is already tracked and referrals already
|
|
67
|
+
// appear in the TRIZ table. A false problem statement outlives the fix that follows it.
|
|
68
|
+
const src = read();
|
|
69
|
+
assert.ok(/K-фактор|K-factor/.test(src), 'the pre-existing K-factor site must survive the edit');
|
|
70
|
+
assert.ok(/[Рр]еферал|referral/i.test(src), 'and the pre-existing referral mentions');
|
|
71
|
+
for (const claim of ['не знает про виральность', 'слеп к виральности', 'ignores virality']) {
|
|
72
|
+
assert.ok(!src.includes(claim), 'the module must not be accused of what it already did: ' + claim);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('P4 - the compliance checklist covers all three research modes', () => {
|
|
77
|
+
const src = read();
|
|
78
|
+
const c = src.indexOf('### ⚖️ Чеклист допустимости');
|
|
79
|
+
const quick = src.indexOf('### 🟢 Режим QUICK');
|
|
80
|
+
assert.ok(c > 0 && quick > c,
|
|
81
|
+
'the checklist must precede the modes, or it reads as an afterthought to QUICK alone');
|
|
82
|
+
assert.match(compliance(), /все три режима/,
|
|
83
|
+
'and must say it applies to all three, not only the one it sits above');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// The two detectors P5 runs. Declared at module scope so P8 can drive them over counter-examples:
|
|
87
|
+
// a detector asserted only against text that happens to be clean proves nothing about its reach.
|
|
88
|
+
//
|
|
89
|
+
// Cross-family review found both too narrow. The statute pattern REQUIRED a leading number, so the
|
|
90
|
+
// ordinary form `Article 6 GDPR` slipped through; the money pattern required a currency marker, so
|
|
91
|
+
// a bare threshold such as `30 дней` did too — and the rule forbids thresholds, not only prices.
|
|
92
|
+
const MONEY = /(?:[0-9][0-9\s,.]*\s*(?:долл|USD|\$|€|руб|₽|%|дн(?:я|ей)|мес|год|лет|час|тыс|млн))|(?:(?:не более|не менее|до|свыше|от)\s+[0-9])/gi;
|
|
93
|
+
const L = '(?<![\\p{L}\\p{N}])'; // a real left boundary, Cyrillic included
|
|
94
|
+
const STATUTE = new RegExp(
|
|
95
|
+
'(?:' + L + '\\d+\\s*(?:CFR|U\\.?S\\.?C|USC|ФЗ))'
|
|
96
|
+
+ '|(?:' + L + '(?:Article|Art\\.|Статья|ст\\.)\\s*\\d+)'
|
|
97
|
+
+ '|(?:' + L + '(?:GDPR|CCPA))'
|
|
98
|
+
+ '|(?:' + L + 'ФЗ[- ]?\\d+)'
|
|
99
|
+
+ '|(?:' + L + '\\d+-ФЗ)', 'giu');
|
|
100
|
+
|
|
101
|
+
test('P5 - no monetary figure and no statute is asserted', () => {
|
|
102
|
+
// THE rule. A number in a template is wrong within a year; a statute is jurisdiction-specific and
|
|
103
|
+
// unverified in this repository. Written so a future edit re-adding either goes red.
|
|
104
|
+
const c = compliance();
|
|
105
|
+
MONEY.lastIndex = 0; STATUTE.lastIndex = 0;
|
|
106
|
+
const money = c.match(MONEY) || [];
|
|
107
|
+
assert.deepEqual(money, [],
|
|
108
|
+
'a monetary figure or threshold appeared in the checklist: ' + JSON.stringify(money));
|
|
109
|
+
const statutes = c.match(STATUTE) || [];
|
|
110
|
+
assert.deepEqual(statutes, [],
|
|
111
|
+
'a statute is asserted as authoritative, and none was verified here: ' + JSON.stringify(statutes));
|
|
112
|
+
assert.match(c, /Ссылайтесь на \*\*норму и на то, где её смотреть\*\*, никогда — на сумму/,
|
|
113
|
+
'the rule itself must be written down, or the absence above is an accident');
|
|
114
|
+
assert.match(c, /Здесь намеренно нет ни одной\s*\n?цифры/,
|
|
115
|
+
'and the absence must be declared deliberate, so a later editor does not "helpfully" add one');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('P6 - it is questions, and it says it is not a gate', () => {
|
|
119
|
+
const c = compliance();
|
|
120
|
+
assert.match(c, /это вопросы, а не утверждения о праве/,
|
|
121
|
+
'a template installed into arbitrary projects cannot assert any project law');
|
|
122
|
+
assert.match(c, /Это не юридическая проверка и не застава/,
|
|
123
|
+
'and it must refuse to be read as a gate — it has no verifiable input');
|
|
124
|
+
assert.match(c, /у неё нет проверяемого входа/, 'with the reason stated');
|
|
125
|
+
assert.match(c, /Ответ «нет» — это находка, а не формальность/,
|
|
126
|
+
'an unanswered question and a cleared one must not look alike');
|
|
127
|
+
assert.ok((c.match(/^\| \d+ \|/gm) || []).length >= 5,
|
|
128
|
+
'the checklist needs enough questions to be worth running');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('P8 - the detectors CATCH the forms a future edit would actually add', () => {
|
|
132
|
+
// A detector run only against clean text is vacuous evidence: it cannot be distinguished from a
|
|
133
|
+
// detector that matches nothing at all. These are the exact strings that slipped through the
|
|
134
|
+
// first version, plus the ones it did catch, so the fix is proven to have widened reach without
|
|
135
|
+
// losing it.
|
|
136
|
+
const mustCatch = [
|
|
137
|
+
['штраф 53088 долларов', MONEY, 'the stale figure this whole rule exists because of'],
|
|
138
|
+
['30 дней', MONEY, 'a bare time threshold — the rule forbids thresholds, not only prices'],
|
|
139
|
+
['не более 3 месяцев', MONEY, 'a threshold written in words plus a number'],
|
|
140
|
+
['до 20% оборота', MONEY, 'a percentage cap'],
|
|
141
|
+
['Article 6 GDPR', STATUTE, 'the ordinary citation form, which needed no leading number'],
|
|
142
|
+
['16 CFR Part 465', STATUTE, 'the US form'],
|
|
143
|
+
['ст. 15 ФЗ-152', STATUTE, 'the Russian form'],
|
|
144
|
+
['152-ФЗ', STATUTE, 'and its other spelling'],
|
|
145
|
+
];
|
|
146
|
+
for (const [text, re, why] of mustCatch) {
|
|
147
|
+
re.lastIndex = 0;
|
|
148
|
+
assert.ok(re.test(text), 'detector missed (' + why + '): ' + JSON.stringify(text));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// And it must not fire on ordinary prose, or the guard becomes noise someone disables.
|
|
152
|
+
const mustNotCatch = ['вопрос 1', 'семь вопросов', 'Ось 2 — ПЕТЛЯ', 'K-фактор'];
|
|
153
|
+
for (const text of mustNotCatch) {
|
|
154
|
+
MONEY.lastIndex = 0; STATUTE.lastIndex = 0;
|
|
155
|
+
assert.ok(!MONEY.test(text) && !STATUTE.test(text),
|
|
156
|
+
'detector false-fires on ordinary text: ' + JSON.stringify(text));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('P7 - the staleness lesson is recorded where the next editor will read it', () => {
|
|
161
|
+
// The reason for the norm-not-amount rule is a real, dated failure. Keeping the reason next to
|
|
162
|
+
// the rule is what stops the rule from being softened by someone who does not know why it exists.
|
|
163
|
+
const c = compliance();
|
|
164
|
+
assert.match(c, /предупреждал, что цифра позапрошлого\s*\n?года устарела/,
|
|
165
|
+
'the source failure must be recorded');
|
|
166
|
+
assert.match(c, /Предупреждение и ошибка в одном\s*\n?документе/,
|
|
167
|
+
'including what made it instructive');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The Growth Traceability criterion is CONDITIONAL, and that is where the danger is.
|
|
4
|
+
//
|
|
5
|
+
// An unconditional growth threshold sends a project with no acquisition objective (internal tool,
|
|
6
|
+
// on-prem) into a permanent NEEDS WORK loop — the exact trap already closed once for the Measurable
|
|
7
|
+
// criterion. But the cure has its own failure mode, and it is the quieter one: a condition that can
|
|
8
|
+
// never be true makes a gate that never fires, and a gate that never fires reads exactly like a gate
|
|
9
|
+
// that always passes.
|
|
10
|
+
//
|
|
11
|
+
// Recalled lesson (dz recall, 0.85): "a verification probe that CAN run in a mode where it checks
|
|
12
|
+
// nothing is vacuous evidence". So both directions are asserted here — the -10 path must be
|
|
13
|
+
// REACHABLE, and the +0 path must exist for the projects the condition exempts.
|
|
14
|
+
|
|
15
|
+
const { test, describe } = require('node:test');
|
|
16
|
+
const assert = require('node:assert/strict');
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const TPL = path.join(__dirname, '..', '..', 'templates', '.claude');
|
|
21
|
+
const VALIDATOR = path.join(TPL, 'skills', 'requirements-validator', 'SKILL.md');
|
|
22
|
+
|
|
23
|
+
const read = () => fs.readFileSync(VALIDATOR, 'utf-8');
|
|
24
|
+
|
|
25
|
+
/** The criterion, from its heading to the next one. */
|
|
26
|
+
function criterion() {
|
|
27
|
+
const src = read();
|
|
28
|
+
const start = src.indexOf('### Growth Traceability');
|
|
29
|
+
assert.ok(start > 0, 'requirements-validator must carry the Growth Traceability criterion');
|
|
30
|
+
const end = src.indexOf('\n### ', start + 1);
|
|
31
|
+
assert.ok(end > start, 'the criterion must be followed by another section');
|
|
32
|
+
return src.slice(start, end);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('the growth gate has teeth AND an exemption, and neither swallows the other', () => {
|
|
36
|
+
test('P1 - the penalty path is reachable and the exempt path is not', () => {
|
|
37
|
+
const c = criterion();
|
|
38
|
+
// REACHABLE: a project with a filled seed table and no trace scores the penalty.
|
|
39
|
+
assert.match(c, /-10 not traced|-10 if the seed table carries rows/,
|
|
40
|
+
'the -10 path must exist for a project the criterion applies to');
|
|
41
|
+
assert.match(c, /≥1 `FR-GROWTH-nnn` row \| YES/,
|
|
42
|
+
'and the applicable row of the table must say YES, or nothing ever reaches -10');
|
|
43
|
+
// EXEMPT: three named situations score +0, and each is a row of the same table.
|
|
44
|
+
for (const exempt of ['says `нет` / is empty', 'internal tool, on-prem', 'is ABSENT']) {
|
|
45
|
+
assert.ok(c.includes(exempt), 'the exemption table must name: ' + exempt);
|
|
46
|
+
}
|
|
47
|
+
// The two must be distinguishable by a reader: an applicability table with only YES rows or
|
|
48
|
+
// only no rows is one of the two failure modes above wearing the other's clothes.
|
|
49
|
+
const yes = (c.match(/\| YES \|/g) || []).length;
|
|
50
|
+
const no = (c.match(/\| no \|/g) || []).length;
|
|
51
|
+
assert.ok(yes >= 1 && no >= 3,
|
|
52
|
+
'both verdicts must be reachable in the table: YES=' + yes + ' no=' + no);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('P2 - an absent brief is +0, never -10, with the reason stated', () => {
|
|
56
|
+
const c = criterion();
|
|
57
|
+
assert.match(c, /An absent brief is \+0, never -10/,
|
|
58
|
+
'the --from-docs entry skips Phase 0 — penalising it would loop those projects forever');
|
|
59
|
+
assert.match(c, /Phase 0 did not run.*is not.*the growth requirements are missing/is,
|
|
60
|
+
'and the two must be distinguished in words, not only in a table cell');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('P3 - the condition is APPLICABILITY, not project type', () => {
|
|
64
|
+
// Gating on type was tried upstream and refuted: replicate.md:171 records that gating M5 on
|
|
65
|
+
// PRODUCT TYPE disabled the one branch M5 declares, and every B2B project got an empty slot.
|
|
66
|
+
// Repeating that mistake one phase later would re-open it.
|
|
67
|
+
const c = criterion();
|
|
68
|
+
assert.match(c, /acquisition or adoption is in scope/,
|
|
69
|
+
'the condition must be the same fact replicate.md gates M5 on');
|
|
70
|
+
assert.match(c, /incl\. B2B/, 'including B2B explicitly, or the refuted type-gate returns');
|
|
71
|
+
assert.match(c, /it is not about project type/i,
|
|
72
|
+
'and the artifact must refuse the type reading out loud');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('P4 - the scoring stays OUTSIDE the 100-point table', () => {
|
|
76
|
+
// validation-gate-teeth.test.js pins 72/100 in its own title, derived from the current weight
|
|
77
|
+
// table. A new weight changes that arithmetic and reddens the test whose job is holding the
|
|
78
|
+
// gate's teeth. The Security criterion already solved this; this one copies the mechanism.
|
|
79
|
+
const c = criterion();
|
|
80
|
+
assert.match(c, /scores OUTSIDE the 100-point/i, 'the criterion must say where it scores');
|
|
81
|
+
assert.match(c, /adds no\s*\n?weight to any existing criterion/i,
|
|
82
|
+
'and that it adds no weight');
|
|
83
|
+
// Structural proof, not a promise: the INVEST/SMART weight headings must be untouched.
|
|
84
|
+
const src = read();
|
|
85
|
+
assert.match(src, /### INVEST Criteria \(User Stories\) — 50% weight/,
|
|
86
|
+
'the INVEST weight must still be 50%');
|
|
87
|
+
assert.match(src, /### SMART Criteria \(Acceptance Criteria\) — 30% weight/,
|
|
88
|
+
'and SMART still 30% — a growth weight here is what reddens validation-gate-teeth');
|
|
89
|
+
assert.ok(!/Growth.*—\s*\d+% weight/i.test(src),
|
|
90
|
+
'no growth criterion may carry a percentage weight');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('P5 - TRACED is defined, and a silent drop is separated from a recorded rejection', () => {
|
|
94
|
+
const c = criterion();
|
|
95
|
+
assert.match(c, /case-sensitive, the exact token/,
|
|
96
|
+
'"mention" must be defined or every reader draws a different line');
|
|
97
|
+
assert.match(c, /not a title, not a paraphrase/i, 'and what does NOT count must be named');
|
|
98
|
+
assert.match(c, /A silently dropped row is the defect/,
|
|
99
|
+
'the defect class must be stated');
|
|
100
|
+
assert.match(c, /A row rejected on the record is not/,
|
|
101
|
+
'and a conscious rejection must be permitted, or the gate forbids saying no');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('P6 - the criterion states its own limit and does not claim legality', () => {
|
|
105
|
+
const c = criterion();
|
|
106
|
+
assert.match(c, /proves an obligation was CARRIED FORWARD, not that it was built/,
|
|
107
|
+
'a traceability gate looks like proof of implementation and must refuse the reading');
|
|
108
|
+
assert.match(c, /Legality is not assessed anywhere in this pipeline/,
|
|
109
|
+
'and legality must be disclaimed here too — it is a separate backlog item');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('P7 - the prose gate points at its deterministic counterpart and does not claim to be one', () => {
|
|
113
|
+
// AR-2: this section is read by a model — layer 3. Calling it layer 1 would be exactly the
|
|
114
|
+
// documented-but-false safety story this project has been burned by.
|
|
115
|
+
const c = criterion();
|
|
116
|
+
assert.match(c, /check-growth-trace\.cjs/, 'the deterministic checker must be named');
|
|
117
|
+
assert.match(c, /This\s*\n?section is a prose gate read by a model/i,
|
|
118
|
+
'and the prose gate must say what it is');
|
|
119
|
+
assert.match(c, /the utility is the deterministic one/,
|
|
120
|
+
'and which of the two is deterministic');
|
|
121
|
+
});
|
|
122
|
+
});
|