@dzhechkov/p-replicator 1.10.4 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +312 -76
- package/CHANGELOG.md +232 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +3 -3
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/04_api_reference.md +11 -5
- package/README/eng/05_architecture.md +1 -1
- package/README/eng/README.md +2 -1
- package/README/ru/01_quickstart.md +3 -3
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/04_api_reference.md +11 -5
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +9 -9
- package/README.md +278 -39
- package/package.json +5 -4
- package/sbom.json +665 -75
- package/scripts/check-pipeline-gaps.sh +413 -0
- package/src/commands/doctor.js +94 -4
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +15 -0
- package/src/utils.js +35 -11
- package/templates/.claude/agents/harvest-coordinator.md +10 -1
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +81 -9
- package/templates/.claude/commands/go.md +9 -0
- package/templates/.claude/commands/harvest.md +39 -3
- package/templates/.claude/commands/myinsights.md +21 -26
- package/templates/.claude/commands/replicate.md +171 -37
- package/templates/.claude/commands/start.md +29 -0
- package/templates/.claude/hooks/capture-source-path.cjs +795 -0
- package/templates/.claude/hooks/check-canon.cjs +493 -0
- package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
- package/templates/.claude/hooks/check-external-deps.cjs +288 -0
- package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
- package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
- package/templates/.claude/hooks/check-job-contract.cjs +501 -0
- package/templates/.claude/hooks/check-look-origin.cjs +240 -0
- package/templates/.claude/hooks/check-look-trace.cjs +385 -0
- package/templates/.claude/hooks/check-metric-source.cjs +296 -0
- package/templates/.claude/hooks/check-model-cost.cjs +470 -0
- package/templates/.claude/hooks/check-ports.cjs +434 -24
- package/templates/.claude/hooks/check-source-version.cjs +312 -0
- package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
- package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
- package/templates/.claude/hooks/session-insights.cjs +158 -25
- package/templates/.claude/hooks/statusline.cjs +2 -2
- package/templates/.claude/hooks/write-insight.cjs +253 -0
- package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
- package/templates/.claude/rules/docker-ports.md +41 -19
- package/templates/.claude/rules/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +13 -3
- package/templates/.claude/rules/honest-configuration.md +54 -0
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- package/templates/.claude/rules/insights-capture.md +10 -5
- package/templates/.claude/rules/long-running-job.md +73 -0
- package/templates/.claude/rules/model-call-cost.md +85 -0
- package/templates/.claude/rules/replicate-pipeline.md +123 -52
- package/templates/.claude/rules/skill-interface-protocol.md +1 -0
- package/templates/.claude/rules/swarm-file-evidence.md +46 -0
- package/templates/.claude/settings.json +13 -1
- package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
- package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
- package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
- package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
- package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
- package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
- package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
- package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
- package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
- package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +234 -716
- package/tests/e2e/lifecycle.test.js +55 -9
- package/tests/e2e/packed-insights-writer.test.js +308 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
- package/tests/snapshot/baseline.json +72 -46
- package/tests/snapshot/templates.test.js +47 -0
- package/tests/unit/absence-is-not-emptiness.test.js +15 -1
- package/tests/unit/capture-source-path.test.js +492 -0
- package/tests/unit/check-canon.test.js +403 -0
- package/tests/unit/check-embed-contract.test.js +422 -0
- package/tests/unit/check-external-deps.test.js +363 -0
- package/tests/unit/check-file-ownership.test.js +388 -0
- package/tests/unit/check-handoff-manifest.test.js +410 -0
- package/tests/unit/check-job-contract.test.js +514 -0
- package/tests/unit/check-look-origin.test.js +180 -0
- package/tests/unit/check-look-trace.test.js +420 -0
- package/tests/unit/check-metric-source.test.js +325 -0
- package/tests/unit/check-model-cost.test.js +425 -0
- package/tests/unit/check-pipeline-gaps.test.js +94 -0
- package/tests/unit/check-ports.test.js +773 -2
- package/tests/unit/check-source-version.test.js +344 -0
- package/tests/unit/check-swarm-receipts.test.js +231 -0
- package/tests/unit/check-webhook-contract.test.js +536 -0
- package/tests/unit/db-port-rule.test.js +43 -6
- package/tests/unit/detection-ladder-contract.test.js +302 -0
- package/tests/unit/detection-ladder-registry.test.js +52 -0
- package/tests/unit/doctor-insight-flow.test.js +315 -0
- package/tests/unit/external-dependency-check.test.js +19 -19
- package/tests/unit/generator-swarm-contract.test.js +287 -0
- package/tests/unit/guard-honest-input-meta.test.js +64 -0
- package/tests/unit/honest-failure-rules.test.js +574 -0
- package/tests/unit/hooks-project-anchored.test.js +67 -3
- package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
- package/tests/unit/insights-dz-delegation.test.js +197 -0
- package/tests/unit/insights-writer.test.js +285 -0
- package/tests/unit/look-phase-contract.test.js +231 -0
- package/tests/unit/negative-conclusion-gate.test.js +300 -0
- package/tests/unit/quote-provenance.test.js +122 -0
- package/tests/unit/shipped-suite-context.test.js +3 -1
- package/tests/unit/traceability-machine-ids.test.js +413 -0
- package/tests/unit/traceability-negative-fixture.test.js +322 -0
- package/tests/unit/utils.test.js +40 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Deterministic filesystem/string checks plus one real local subprocess: no model or network call.
|
|
4
|
+
// CLAIM BOUND: P1-P3 are fixture-side evidence; P4/P5 are pipeline-side evidence.
|
|
5
|
+
// P1-P3 prove that this change authors a faithful gap-carrier. P4/P5 read the shipped skill.
|
|
6
|
+
// No assertion here proves that a live model would miss the gap.
|
|
7
|
+
|
|
8
|
+
const { test, describe } = require('node:test');
|
|
9
|
+
const assert = require('node:assert/strict');
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const os = require('node:os');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { spawnSync } = require('node:child_process');
|
|
14
|
+
|
|
15
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
16
|
+
const FIXTURE_FEATURE = path.join(
|
|
17
|
+
PACKAGE_ROOT, 'tests', 'fixtures', 'prep-traceability-fixture',
|
|
18
|
+
'docs', 'features', 'order-refund',
|
|
19
|
+
);
|
|
20
|
+
const SPECIFICATION = path.join(FIXTURE_FEATURE, '01_specification.md');
|
|
21
|
+
const PSEUDOCODE = path.join(FIXTURE_FEATURE, '02_pseudocode.md');
|
|
22
|
+
const SKILL = path.join(
|
|
23
|
+
PACKAGE_ROOT, 'templates', '.claude', 'skills', 'sparc-prd-mini', 'SKILL.md',
|
|
24
|
+
);
|
|
25
|
+
const FEATURE_COMMAND = path.join(
|
|
26
|
+
PACKAGE_ROOT, 'templates', '.claude', 'commands', 'feature.md',
|
|
27
|
+
);
|
|
28
|
+
const PACKAGE_JSON = path.join(PACKAGE_ROOT, 'package.json');
|
|
29
|
+
const CHECKER = path.join(PACKAGE_ROOT, 'scripts', 'check-pipeline-gaps.sh');
|
|
30
|
+
|
|
31
|
+
const read = (file) => fs.readFileSync(file, 'utf8');
|
|
32
|
+
|
|
33
|
+
function h2Section(src, heading) {
|
|
34
|
+
const marker = `## ${heading}`;
|
|
35
|
+
const start = src.indexOf(marker);
|
|
36
|
+
assert.ok(start >= 0, `02_pseudocode.md must contain the ${heading} block`);
|
|
37
|
+
|
|
38
|
+
const contentStart = start + marker.length;
|
|
39
|
+
const tail = src.slice(contentStart);
|
|
40
|
+
const nextHeading = tail.search(/^## /m);
|
|
41
|
+
const endMarker = tail.indexOf(`<!-- END ${heading} -->`);
|
|
42
|
+
const boundaries = [nextHeading, endMarker].filter((offset) => offset >= 0);
|
|
43
|
+
const end = boundaries.length > 0
|
|
44
|
+
? contentStart + Math.min(...boundaries)
|
|
45
|
+
: src.length;
|
|
46
|
+
|
|
47
|
+
return src.slice(start, end);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function algorithmEvidence(src) {
|
|
51
|
+
return h2Section(src, 'Core Algorithms');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function scenarioCoverage(src) {
|
|
55
|
+
return h2Section(src, 'Scenario Coverage');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function markdownSection(src, heading) {
|
|
59
|
+
const level = heading.match(/^#+/)[0].length;
|
|
60
|
+
const start = src.indexOf(heading);
|
|
61
|
+
assert.ok(start >= 0, `${heading} must remain addressable`);
|
|
62
|
+
|
|
63
|
+
const contentStart = start + heading.length;
|
|
64
|
+
const tail = src.slice(contentStart);
|
|
65
|
+
const nextHeading = tail.search(new RegExp(`^#{1,${level}}\\s`, 'm'));
|
|
66
|
+
const end = nextHeading >= 0 ? contentStart + nextHeading : src.length;
|
|
67
|
+
return src.slice(start, end);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function textBetween(src, startMarker, endMarker) {
|
|
71
|
+
const start = src.indexOf(startMarker);
|
|
72
|
+
const end = src.indexOf(endMarker, start + startMarker.length);
|
|
73
|
+
assert.ok(start >= 0 && end > start, `${startMarker} must precede ${endMarker}`);
|
|
74
|
+
return src.slice(start, end);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function roleMap(section) {
|
|
78
|
+
const match = section.match(/```yaml[\s\S]*?DOCUMENT_ROLE_MAP:\r?\n([\s\S]*?)```/);
|
|
79
|
+
assert.ok(match, 'the scoped section must contain a DOCUMENT_ROLE_MAP YAML block');
|
|
80
|
+
return Object.fromEntries(
|
|
81
|
+
[...match[1].matchAll(/^ (specification|pseudocode|architecture|refinement|completion): (\S+)$/gm)]
|
|
82
|
+
.map((entry) => [entry[1], entry[2]]),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stepInstruction(skill, startMarker, endMarker) {
|
|
87
|
+
return textBetween(skill, startMarker, endMarker);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function registeredUnitTests(script) {
|
|
91
|
+
return new Set(script.split(/\s+/).filter((part) => /^tests\/unit\/.*\.test\.js$/.test(part)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function unregisteredUnitTests(fileNames, script) {
|
|
95
|
+
const registered = registeredUnitTests(script);
|
|
96
|
+
return fileNames
|
|
97
|
+
.filter((name) => name.endsWith('.test.js'))
|
|
98
|
+
.filter((name) => !registered.has(`tests/unit/${name}`))
|
|
99
|
+
.sort();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe('the per-feature traceability gap is a named negative fixture (PR-021)', () => {
|
|
103
|
+
test('P1 — the fixture carries exactly one requirement with no algorithm', () => {
|
|
104
|
+
const specification = read(SPECIFICATION);
|
|
105
|
+
const pseudocode = read(PSEUDOCODE);
|
|
106
|
+
const requirementIds = [...specification.matchAll(/^### (FR-order-refund-\d+)\b/gm)]
|
|
107
|
+
.map((match) => match[1]);
|
|
108
|
+
const algorithmIds = [...pseudocode.matchAll(/^REQUIREMENT: `(FR-order-refund-\d+)`$/gm)]
|
|
109
|
+
.map((match) => match[1]);
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(requirementIds, [
|
|
112
|
+
'FR-order-refund-1', 'FR-order-refund-2', 'FR-order-refund-3',
|
|
113
|
+
]);
|
|
114
|
+
assert.deepEqual(algorithmIds, ['FR-order-refund-1', 'FR-order-refund-3']);
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
requirementIds.filter((id) => !algorithmIds.includes(id)),
|
|
117
|
+
['FR-order-refund-2'],
|
|
118
|
+
);
|
|
119
|
+
assert.doesNotMatch(algorithmEvidence(pseudocode), /FR-order-refund-2/,
|
|
120
|
+
'the deliberately un-algorithmed requirement must be absent from algorithm evidence');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('P2 — the Scenario Coverage block is structurally present with both tables', () => {
|
|
124
|
+
const block = scenarioCoverage(read(PSEUDOCODE));
|
|
125
|
+
assert.match(block, /^## Scenario Coverage$/m);
|
|
126
|
+
assert.match(block, /Not claimed by any algorithm:\s*\n\s*\| Scenario \| Reason \|/);
|
|
127
|
+
assert.match(block,
|
|
128
|
+
/Claimed by an algorithm but absent from Specification\.md:\s*\n\s*\| Algorithm \| Claimed ID \|/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('P3 — Scenario Coverage names the missing requirement in the unclaimed table', () => {
|
|
132
|
+
const block = scenarioCoverage(read(PSEUDOCODE));
|
|
133
|
+
const unclaimed = textBetween(
|
|
134
|
+
block,
|
|
135
|
+
'Not claimed by any algorithm:',
|
|
136
|
+
'Claimed by an algorithm but absent from Specification.md:',
|
|
137
|
+
);
|
|
138
|
+
const dangling = block.slice(block.indexOf('Claimed by an algorithm but absent from Specification.md:'));
|
|
139
|
+
|
|
140
|
+
assert.match(unclaimed, /^\| FR-order-refund-2 \| out-of-mvp-scope \|$/m,
|
|
141
|
+
'the unclaimed-scenario table must name the deliberately un-algorithmed requirement');
|
|
142
|
+
assert.doesNotMatch(dangling, /FR-order-refund-2/,
|
|
143
|
+
'the dangling-algorithm table must not make the opposite claim about FR-order-refund-2');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('P4 — the skill defines the complete project-default document role map', () => {
|
|
147
|
+
const contract = markdownSection(read(SKILL), '## Document Role Map Contract');
|
|
148
|
+
const defaults = roleMap(markdownSection(contract, '### Project-level default'));
|
|
149
|
+
|
|
150
|
+
assert.deepEqual(defaults, {
|
|
151
|
+
specification: 'Specification.md',
|
|
152
|
+
pseudocode: 'Pseudocode.md',
|
|
153
|
+
architecture: 'Architecture.md',
|
|
154
|
+
refinement: 'Refinement.md',
|
|
155
|
+
completion: 'Completion.md',
|
|
156
|
+
}, 'the project contour must retain its complete five-role filename map');
|
|
157
|
+
assert.match(contract, /Resolve the active map once before Phase 3/,
|
|
158
|
+
'the skill must resolve the role map before any mapped document or gate is written');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('P5 — Scenario Coverage resolves its inputs and output through document roles', () => {
|
|
162
|
+
const skill = read(SKILL);
|
|
163
|
+
const instruction = stepInstruction(
|
|
164
|
+
skill,
|
|
165
|
+
'**Шаг 4.9 — ПОКРЫТИЕ СЦЕНАРИЕВ',
|
|
166
|
+
'**[MANUAL] CP4:**',
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
assert.match(instruction,
|
|
170
|
+
/Resolve role `specification` through `DOCUMENT_ROLE_MAP` as `SPECIFICATION_FILE`/,
|
|
171
|
+
'Scenario Coverage must resolve the specification input through its role');
|
|
172
|
+
assert.match(instruction,
|
|
173
|
+
/Resolve role `pseudocode` through `DOCUMENT_ROLE_MAP` as `PSEUDOCODE_FILE`/,
|
|
174
|
+
'Scenario Coverage must resolve the pseudocode input and output through its role');
|
|
175
|
+
assert.match(instruction, /Re-read `SPECIFICATION_FILE`/,
|
|
176
|
+
'Scenario Coverage must read the resolved specification target');
|
|
177
|
+
assert.match(instruction, /Write a `## Scenario Coverage` block into `PSEUDOCODE_FILE`/,
|
|
178
|
+
'Scenario Coverage must write to the resolved pseudocode target');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('the skill resolves gate inputs through the role map in both the project and per-feature contours', () => {
|
|
182
|
+
const skill = read(SKILL);
|
|
183
|
+
const featureCommand = read(FEATURE_COMMAND);
|
|
184
|
+
const contract = markdownSection(skill, '## Document Role Map Contract');
|
|
185
|
+
const projectMap = roleMap(markdownSection(contract, '### Project-level default'));
|
|
186
|
+
const featureMap = roleMap(markdownSection(featureCommand, '### Phase 1 document role map'));
|
|
187
|
+
const scenarioGate = stepInstruction(
|
|
188
|
+
skill,
|
|
189
|
+
'**Шаг 4.9 — ПОКРЫТИЕ СЦЕНАРИЕВ',
|
|
190
|
+
'**[MANUAL] CP4:**',
|
|
191
|
+
);
|
|
192
|
+
const reconciliationGate = stepInstruction(
|
|
193
|
+
skill,
|
|
194
|
+
'**Шаг 5.9 — СВЕРКА С ПСЕВДОКОДОМ',
|
|
195
|
+
'**[MANUAL] CP5:**',
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
assert.deepEqual(projectMap, {
|
|
199
|
+
specification: 'Specification.md',
|
|
200
|
+
pseudocode: 'Pseudocode.md',
|
|
201
|
+
architecture: 'Architecture.md',
|
|
202
|
+
refinement: 'Refinement.md',
|
|
203
|
+
completion: 'Completion.md',
|
|
204
|
+
}, 'the project contour must resolve roles to the existing project-level filenames');
|
|
205
|
+
assert.deepEqual(featureMap, {
|
|
206
|
+
specification: '01_specification.md',
|
|
207
|
+
pseudocode: '02_pseudocode.md',
|
|
208
|
+
architecture: '03_architecture.md',
|
|
209
|
+
refinement: '04_refinement.md',
|
|
210
|
+
completion: '05_completion.md',
|
|
211
|
+
}, '/feature must pass its complete per-feature role map to the skill');
|
|
212
|
+
assert.doesNotMatch(skill,
|
|
213
|
+
/01_specification\.md|02_pseudocode\.md|03_architecture\.md|04_refinement\.md|05_completion\.md/,
|
|
214
|
+
'per-feature filenames must remain owned by the feature command, not duplicated into the skill');
|
|
215
|
+
assert.match(scenarioGate,
|
|
216
|
+
/Resolve role `specification` through `DOCUMENT_ROLE_MAP` as `SPECIFICATION_FILE`/,
|
|
217
|
+
'Scenario Coverage must resolve its specification input by role in either contour');
|
|
218
|
+
assert.match(scenarioGate,
|
|
219
|
+
/Resolve role `pseudocode` through `DOCUMENT_ROLE_MAP` as `PSEUDOCODE_FILE`/,
|
|
220
|
+
'Scenario Coverage must resolve its pseudocode input and output by role in either contour');
|
|
221
|
+
assert.match(reconciliationGate,
|
|
222
|
+
/Resolve role `pseudocode` through `DOCUMENT_ROLE_MAP` as `PSEUDOCODE_FILE`/,
|
|
223
|
+
'Reconciliation must resolve its pseudocode input by role in either contour');
|
|
224
|
+
assert.match(reconciliationGate,
|
|
225
|
+
/Resolve role `architecture` through `DOCUMENT_ROLE_MAP` as `ARCHITECTURE_FILE`/,
|
|
226
|
+
'Reconciliation must resolve its architecture input and output by role in either contour');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('P6 — every tests/unit/*.test.js is registered in package.json', () => {
|
|
230
|
+
const pkg = JSON.parse(read(PACKAGE_JSON));
|
|
231
|
+
const unitFiles = fs.readdirSync(path.join(PACKAGE_ROOT, 'tests', 'unit'));
|
|
232
|
+
|
|
233
|
+
assert.deepEqual(unregisteredUnitTests(unitFiles, pkg.scripts['test:unit']), [],
|
|
234
|
+
'test:unit has a file on disk that it never executes');
|
|
235
|
+
assert.deepEqual(unregisteredUnitTests(unitFiles, pkg.scripts.test), [],
|
|
236
|
+
'test has a unit file on disk that it never executes');
|
|
237
|
+
|
|
238
|
+
const probe = '__unregistered-probe.test.js';
|
|
239
|
+
assert.deepEqual(
|
|
240
|
+
unregisteredUnitTests([...unitFiles, probe], pkg.scripts['test:unit']),
|
|
241
|
+
[probe],
|
|
242
|
+
'the registration guard must fire on a real injected unregistered filename',
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('P7 — the file states the bound of its own claim', () => {
|
|
247
|
+
const source = read(__filename);
|
|
248
|
+
assert.match(source,
|
|
249
|
+
/CLAIM BOUND: P1-P3 are fixture-side evidence; P4\/P5 are pipeline-side evidence\./);
|
|
250
|
+
assert.match(source, /No assertion here proves that a live model would miss the gap\./);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('P8 — packaged checker rejects the planted FR-order-refund-2 gap and a healed twin passes', (t) => {
|
|
254
|
+
const fixtureRoot = path.join(
|
|
255
|
+
PACKAGE_ROOT, 'tests', 'fixtures', 'prep-traceability-fixture',
|
|
256
|
+
);
|
|
257
|
+
const args = [
|
|
258
|
+
CHECKER, fixtureRoot, '--traceability', '--role-map-source', FEATURE_COMMAND,
|
|
259
|
+
'--project-role-map-source', SKILL,
|
|
260
|
+
];
|
|
261
|
+
const broken = spawnSync('bash', args, { encoding: 'utf8' });
|
|
262
|
+
const brokenOutput = `${broken.stdout || ''}${broken.stderr || ''}`;
|
|
263
|
+
assert.equal(broken.status, 1, brokenOutput);
|
|
264
|
+
assert.match(brokenOutput,
|
|
265
|
+
/GAP order-refund specification->pseudocode FR-order-refund-2/);
|
|
266
|
+
assert.match(brokenOutput, /missing-algorithm=1 orphan-algorithm=0/);
|
|
267
|
+
assert.doesNotMatch(brokenOutput,
|
|
268
|
+
/GAP order-refund pseudocode->specification FR-order-refund-2/);
|
|
269
|
+
|
|
270
|
+
const healedRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'traceability-healed-'));
|
|
271
|
+
t.after(() => fs.rmSync(healedRoot, { recursive: true, force: true }));
|
|
272
|
+
fs.cpSync(fixtureRoot, healedRoot, { recursive: true });
|
|
273
|
+
const healedPseudo = path.join(
|
|
274
|
+
healedRoot, 'docs', 'features', 'order-refund', '02_pseudocode.md',
|
|
275
|
+
);
|
|
276
|
+
fs.appendFileSync(healedPseudo, [
|
|
277
|
+
'',
|
|
278
|
+
'### Algorithm: Notify the buyer',
|
|
279
|
+
'',
|
|
280
|
+
'REQUIREMENT: `FR-order-refund-2`',
|
|
281
|
+
'',
|
|
282
|
+
].join('\n'));
|
|
283
|
+
const healed = spawnSync('bash', [
|
|
284
|
+
CHECKER, healedRoot, '--traceability', '--role-map-source', FEATURE_COMMAND,
|
|
285
|
+
'--project-role-map-source', SKILL,
|
|
286
|
+
], { encoding: 'utf8' });
|
|
287
|
+
assert.equal(healed.status, 0, `${healed.stdout || ''}${healed.stderr || ''}`);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// AM-1's Confirmation (plan 06, line 291): the flip must be COMPLETE, not cosmetic. P3/P4/P5 were
|
|
291
|
+
// written as pins asserting the DEFECT, carrying instructions to invert them once cd0b52f6 landed.
|
|
292
|
+
// Any such instruction surviving under a pin that now asserts the FIXED state is a reader trap:
|
|
293
|
+
// the next maintainer either re-inverts it (breaking the guard) or trusts a stale note.
|
|
294
|
+
// SCOPE NOTE, learned the hard way while writing this: the first version scanned the whole file
|
|
295
|
+
// and failed on ITS OWN explanatory comment, which necessarily quotes the forbidden phrases. A
|
|
296
|
+
// self-reading guard must exclude itself, so this one slices out exactly the three pin bodies.
|
|
297
|
+
test('P9 — P3/P4/P5 carry only corrected positive labels and messages', () => {
|
|
298
|
+
const source = read(__filename);
|
|
299
|
+
const pinBody = (name) => {
|
|
300
|
+
const start = source.indexOf(`test('${name} —`);
|
|
301
|
+
assert.ok(start >= 0, `${name} must remain present and addressable`);
|
|
302
|
+
const next = source.indexOf("\n test('", start + 1);
|
|
303
|
+
return source.slice(start, next >= 0 ? next : source.length);
|
|
304
|
+
};
|
|
305
|
+
const forbidden = [/INVERTED\s+PIN/i, /\bflip\b/i, /pins?\s+the\s+(?:current\s+)?defect/i,
|
|
306
|
+
/will\s+be\s+(?:flipped|inverted)/i, /cd0b52f6/];
|
|
307
|
+
const positive = {
|
|
308
|
+
P3: /names the missing requirement in the unclaimed table/,
|
|
309
|
+
P4: /defines the complete project-default document role map/,
|
|
310
|
+
P5: /resolves its inputs and output through document roles/,
|
|
311
|
+
};
|
|
312
|
+
for (const name of ['P3', 'P4', 'P5']) {
|
|
313
|
+
const body = pinBody(name);
|
|
314
|
+
for (const pattern of forbidden) {
|
|
315
|
+
assert.ok(!pattern.test(body),
|
|
316
|
+
`${name} still carries pre-flip language (${pattern}) — the flip was cosmetic`);
|
|
317
|
+
}
|
|
318
|
+
assert.match(body, positive[name],
|
|
319
|
+
`${name} must state its corrected positive property, not the absence of the old defect`);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
package/tests/unit/utils.test.js
CHANGED
|
@@ -341,11 +341,12 @@ describe('COMPONENTS.items SSOT', () => {
|
|
|
341
341
|
}
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
test('item counts: 10 skills, 11 commands, 4 agents,
|
|
344
|
+
test('item counts: 10 skills, 11 commands, 4 agents, 13 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,
|
|
348
|
+
assert.equal(Object.keys(utils.COMPONENTS.rules.items).length, 13,
|
|
349
|
+
'rules count (+ honest failure rules, + embeddable-widget, + incoming-webhooks, + long-running-job, + model-call-cost)');
|
|
349
350
|
assert.ok(utils.COMPONENTS.settings, 'settings component group added in v1.4');
|
|
350
351
|
assert.equal(Object.keys(utils.COMPONENTS.settings.items).length, 1, 'settings.json count');
|
|
351
352
|
});
|
|
@@ -634,3 +635,40 @@ describe('COMPONENTS.hooks (v1.5.0)', () => {
|
|
|
634
635
|
`expected >= 6 hook scripts (post v1.5.0), got ${hookCount}`);
|
|
635
636
|
});
|
|
636
637
|
});
|
|
638
|
+
|
|
639
|
+
describe('mergeHookMatchers — identity survives the user respelling of a variable (a1ef0e41)', () => {
|
|
640
|
+
// MEASURED (backlog a1ef0e41): a user who hand-rewrote `$CLAUDE_PROJECT_DIR` into
|
|
641
|
+
// `${CLAUDE_PROJECT_DIR}` got every event hook DOUBLED on the next update, because the
|
|
642
|
+
// byte-equality Set saw two strings for one command. Both spellings expand identically in sh.
|
|
643
|
+
const shipped = (cmd) => [{ matcher: '*', hooks: [{ type: 'command', command: cmd }] }];
|
|
644
|
+
|
|
645
|
+
test('${VAR} respelling of a shipped $VAR hook does not double on merge', () => {
|
|
646
|
+
const user = shipped('node "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-insights.cjs"');
|
|
647
|
+
const tpl = shipped('node "$CLAUDE_PROJECT_DIR/.claude/hooks/session-insights.cjs"');
|
|
648
|
+
const merged = utils.mergeHookMatchers(user, tpl);
|
|
649
|
+
assert.equal(merged[0].hooks.length, 1, 'one command, two spellings — must stay ONE hook');
|
|
650
|
+
// The USER's own spelling survives; the stored command is never rewritten.
|
|
651
|
+
assert.match(merged[0].hooks[0].command, /\$\{CLAUDE_PROJECT_DIR\}/);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test('whitespace-run respelling does not double either', () => {
|
|
655
|
+
const user = shipped('node a.cjs --flag');
|
|
656
|
+
const tpl = shipped('node a.cjs --flag');
|
|
657
|
+
assert.equal(utils.mergeHookMatchers(user, tpl)[0].hooks.length, 1);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('genuinely different commands still both survive (dedup must not over-collapse)', () => {
|
|
661
|
+
const user = shipped('node a.cjs');
|
|
662
|
+
const tpl = shipped('node b.cjs');
|
|
663
|
+
assert.equal(utils.mergeHookMatchers(user, tpl)[0].hooks.length, 2);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('MUTATION: byte-equality dedup (the old code) goes red on the respelling case', () => {
|
|
667
|
+
// The RED half in miniature: with canonicalization stripped, the two spellings differ.
|
|
668
|
+
const oldDedup = (a, b) => new Set(a.map((h) => h.command)).has(b.command);
|
|
669
|
+
const userHook = { command: 'node "${X}/y.cjs"' };
|
|
670
|
+
const tplHook = { command: 'node "$X/y.cjs"' };
|
|
671
|
+
assert.equal(oldDedup([userHook], tplHook), false,
|
|
672
|
+
'byte equality cannot see through the respelling — this is the measured doubling');
|
|
673
|
+
});
|
|
674
|
+
});
|