@rsaglobaltech/specgate 0.8.1 → 0.10.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/CHANGELOG.md +196 -0
- package/README.md +17 -25
- package/bin/specgate.ts +23 -5
- package/dist/bin/specgate.js +19 -4
- package/dist/bin/specgate.js.map +1 -1
- package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js +56 -0
- package/dist/packages/core/src/application/GenerateAgentPromptUseCase.js.map +1 -1
- package/dist/packages/core/src/application/ValidateProjectUseCase.js +54 -0
- package/dist/packages/core/src/application/ValidateProjectUseCase.js.map +1 -1
- package/dist/packages/core/src/domain/AdversarialProbe.js +75 -0
- package/dist/packages/core/src/domain/AdversarialProbe.js.map +1 -0
- package/dist/packages/core/src/domain/AgentPrompt.js +52 -3
- package/dist/packages/core/src/domain/AgentPrompt.js.map +1 -1
- package/dist/packages/core/src/domain/DeclaredArtifacts.js +25 -0
- package/dist/packages/core/src/domain/DeclaredArtifacts.js.map +1 -1
- package/dist/packages/core/src/domain/DoneVerification.js +89 -0
- package/dist/packages/core/src/domain/DoneVerification.js.map +1 -0
- package/dist/packages/core/src/domain/GateCommandIntegrity.js +139 -0
- package/dist/packages/core/src/domain/GateCommandIntegrity.js.map +1 -0
- package/dist/packages/core/src/domain/GherkinQuality.js +19 -3
- package/dist/packages/core/src/domain/GherkinQuality.js.map +1 -1
- package/dist/packages/core/src/domain/HarnessConfig.js +6 -0
- package/dist/packages/core/src/domain/HarnessConfig.js.map +1 -1
- package/dist/packages/core/src/domain/MultiStack.js +96 -0
- package/dist/packages/core/src/domain/MultiStack.js.map +1 -0
- package/dist/packages/core/src/domain/NewRequirement.js +46 -0
- package/dist/packages/core/src/domain/NewRequirement.js.map +1 -0
- package/dist/packages/core/src/domain/Precedents.js +96 -0
- package/dist/packages/core/src/domain/Precedents.js.map +1 -0
- package/dist/packages/core/src/domain/RequirementPlan.js +1 -0
- package/dist/packages/core/src/domain/RequirementPlan.js.map +1 -1
- package/dist/packages/core/src/domain/RequirementRemoval.js +161 -0
- package/dist/packages/core/src/domain/RequirementRemoval.js.map +1 -0
- package/dist/packages/core/src/domain/ScenarioCoverage.js +178 -0
- package/dist/packages/core/src/domain/ScenarioCoverage.js.map +1 -0
- package/dist/packages/core/src/domain/SpecSections.js +206 -0
- package/dist/packages/core/src/domain/SpecSections.js.map +1 -0
- package/dist/packages/core/src/domain/TestCommand.js +82 -0
- package/dist/packages/core/src/domain/TestCommand.js.map +1 -0
- package/dist/packages/core/src/domain/TraceabilityFormat.js +45 -2
- package/dist/packages/core/src/domain/TraceabilityFormat.js.map +1 -1
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js +31 -0
- package/dist/packages/core/src/infrastructure/HarnessConfigFile.js.map +1 -1
- package/dist/packages/core/test/unit/adversarial-probe.test.js +81 -0
- package/dist/packages/core/test/unit/adversarial-probe.test.js.map +1 -0
- package/dist/packages/core/test/unit/done-verification.test.js +84 -0
- package/dist/packages/core/test/unit/done-verification.test.js.map +1 -0
- package/dist/packages/core/test/unit/gate-command-integrity.test.js +114 -0
- package/dist/packages/core/test/unit/gate-command-integrity.test.js.map +1 -0
- package/dist/packages/core/test/unit/gherkin-quality.test.js +28 -0
- package/dist/packages/core/test/unit/gherkin-quality.test.js.map +1 -1
- package/dist/packages/core/test/unit/multi-stack.test.js +86 -0
- package/dist/packages/core/test/unit/multi-stack.test.js.map +1 -0
- package/dist/packages/core/test/unit/new-requirement.test.js +67 -0
- package/dist/packages/core/test/unit/new-requirement.test.js.map +1 -0
- package/dist/packages/core/test/unit/precedents.test.js +99 -0
- package/dist/packages/core/test/unit/precedents.test.js.map +1 -0
- package/dist/packages/core/test/unit/requirement-removal.test.js +124 -0
- package/dist/packages/core/test/unit/requirement-removal.test.js.map +1 -0
- package/dist/packages/core/test/unit/scenario-coverage.test.js +146 -0
- package/dist/packages/core/test/unit/scenario-coverage.test.js.map +1 -0
- package/dist/packages/core/test/unit/spec-sections.test.js +146 -0
- package/dist/packages/core/test/unit/spec-sections.test.js.map +1 -0
- package/dist/packages/core/test/unit/test-command.test.js +81 -0
- package/dist/packages/core/test/unit/test-command.test.js.map +1 -0
- package/dist/packages/core/test/unit/validate-project.test.js +21 -0
- package/dist/packages/core/test/unit/validate-project.test.js.map +1 -1
- package/dist/scripts/agents/commands.js +1 -1
- package/dist/scripts/agents/commands.js.map +1 -1
- package/dist/scripts/agents/init.js +59 -15
- package/dist/scripts/agents/init.js.map +1 -1
- package/dist/scripts/change/instructions.js +2 -2
- package/dist/scripts/change/instructions.js.map +1 -1
- package/dist/scripts/check.js +10 -0
- package/dist/scripts/check.js.map +1 -0
- package/dist/scripts/cli/commands/change/ChangeCommand.js +2 -2
- package/dist/scripts/cli/commands/change/ChangeCommand.js.map +1 -1
- package/dist/scripts/cli/commands/config/CiInitCommand.js +1 -1
- package/dist/scripts/cli/commands/config/CiInitCommand.js.map +1 -1
- package/dist/scripts/cli/commands/harness/RunCommand.js +214 -9
- package/dist/scripts/cli/commands/harness/RunCommand.js.map +1 -1
- package/dist/scripts/cli/commands/pack/InferPackCommand.js +331 -31
- package/dist/scripts/cli/commands/pack/InferPackCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/AdoptCommand.js +86 -9
- package/dist/scripts/cli/commands/project/AdoptCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/InitProjectCommand.js +286 -45
- package/dist/scripts/cli/commands/project/InitProjectCommand.js.map +1 -1
- package/dist/scripts/cli/commands/project/UpdateCommand.js +118 -8
- package/dist/scripts/cli/commands/project/UpdateCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/CheckCommand.js +184 -0
- package/dist/scripts/cli/commands/quality/CheckCommand.js.map +1 -0
- package/dist/scripts/cli/commands/quality/DoctorCommand.js +20 -2
- package/dist/scripts/cli/commands/quality/DoctorCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/FixCommand.js +1 -1
- package/dist/scripts/cli/commands/quality/FixCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ReportCommand.js +1 -1
- package/dist/scripts/cli/commands/quality/ReportCommand.js.map +1 -1
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js +406 -24
- package/dist/scripts/cli/commands/quality/ValidateSpecsCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/DoneCommand.js +108 -0
- package/dist/scripts/cli/commands/spec/DoneCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/NewCommand.js +178 -0
- package/dist/scripts/cli/commands/spec/NewCommand.js.map +1 -0
- package/dist/scripts/cli/commands/spec/PlanCommand.js +8 -2
- package/dist/scripts/cli/commands/spec/PlanCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/ReqCommand.js +124 -4
- package/dist/scripts/cli/commands/spec/ReqCommand.js.map +1 -1
- package/dist/scripts/cli/commands/spec/StatusCommand.js +84 -32
- package/dist/scripts/cli/commands/spec/StatusCommand.js.map +1 -1
- package/dist/scripts/docs/build-site.js +145 -35
- package/dist/scripts/docs/build-site.js.map +1 -1
- package/dist/scripts/docs/components.js +208 -0
- package/dist/scripts/docs/components.js.map +1 -0
- package/dist/scripts/docs/highlight.js +180 -0
- package/dist/scripts/docs/highlight.js.map +1 -0
- package/dist/scripts/docs/icons.js +63 -0
- package/dist/scripts/docs/icons.js.map +1 -0
- package/dist/scripts/docs/nav.js +9 -3
- package/dist/scripts/docs/nav.js.map +1 -1
- package/dist/scripts/gen_mcp_tools.js +22 -2
- package/dist/scripts/gen_mcp_tools.js.map +1 -1
- package/dist/scripts/harness/init.js +101 -40
- package/dist/scripts/harness/init.js.map +1 -1
- package/dist/scripts/harness/worktree-lock.js +161 -0
- package/dist/scripts/harness/worktree-lock.js.map +1 -0
- package/dist/scripts/infer_pack.js +4 -1
- package/dist/scripts/infer_pack.js.map +1 -1
- package/dist/scripts/lib/surface.js +88 -38
- package/dist/scripts/lib/surface.js.map +1 -1
- package/dist/scripts/new.js +10 -0
- package/dist/scripts/new.js.map +1 -0
- package/dist/scripts/plugin/gate-hook.js +2 -2
- package/dist/scripts/plugin/gate-hook.js.map +1 -1
- package/package.json +5 -2
- package/templates/adopt/AI_RULES.md.tpl +1 -1
- package/templates/adopt/spec.md.tpl +4 -4
- package/templates/base/spec.md.tpl +12 -0
- package/templates/ci/azure.yml.tpl +2 -2
- package/templates/ci/github.yml.tpl +2 -2
- package/templates/ci/gitlab.yml.tpl +1 -1
- package/templates/ci/jenkins.tpl +1 -1
- package/templates/contracts/test-strategy.md.tpl +2 -2
- package/templates/harness/harness.config.yaml.tpl +3 -3
- package/templates/harness/prompt-prefix.md.tpl +2 -5
|
@@ -44,16 +44,26 @@ const DiskProjectRepository_1 = require("../../../../packages/core/src/infrastru
|
|
|
44
44
|
const diagnostics_1 = require("../../../lib/diagnostics");
|
|
45
45
|
const against_lock_1 = require("../../../specops/against_lock");
|
|
46
46
|
const requirement_graph_1 = require("../../../lib/requirement-graph");
|
|
47
|
+
const MultiStack_1 = require("../../../../packages/core/src/domain/MultiStack");
|
|
48
|
+
const ScenarioCoverage_1 = require("../../../../packages/core/src/domain/ScenarioCoverage");
|
|
49
|
+
const TraceabilityFormat_1 = require("../../../../packages/core/src/domain/TraceabilityFormat");
|
|
50
|
+
const crypto = __importStar(require("node:crypto"));
|
|
47
51
|
const agent_1 = require("../../../lib/agent");
|
|
48
52
|
const placeholders_1 = require("../../../lib/placeholders");
|
|
49
53
|
const command_1 = require("../../../lib/command");
|
|
50
54
|
const ValidateProjectUseCase_1 = require("../../../../packages/core/src/application/ValidateProjectUseCase");
|
|
51
55
|
const DiskTraceabilityRepository_1 = require("../../../../packages/core/src/infrastructure/DiskTraceabilityRepository");
|
|
52
|
-
const
|
|
56
|
+
const TraceabilityFormat_2 = require("../../../../packages/core/src/domain/TraceabilityFormat");
|
|
53
57
|
const project_root_1 = require("../../../lib/project-root");
|
|
54
58
|
const GherkinQuality_1 = require("../../../../packages/core/src/domain/GherkinQuality");
|
|
59
|
+
/** Scenario findings that make a suite report a pass it did not run. */
|
|
60
|
+
const SUITE_LIES = new Set([
|
|
61
|
+
GherkinQuality_1.QUALITY_CODES.NO_STEPS,
|
|
62
|
+
GherkinQuality_1.QUALITY_CODES.KEYWORD_CASE,
|
|
63
|
+
GherkinQuality_1.QUALITY_CODES.OUTLINE_WITHOUT_EXAMPLES,
|
|
64
|
+
]);
|
|
55
65
|
const GherkinTags_1 = require("../../../../packages/core/src/domain/GherkinTags");
|
|
56
|
-
const
|
|
66
|
+
const TraceabilityFormat_3 = require("../../../../packages/core/src/domain/TraceabilityFormat");
|
|
57
67
|
const RequirementSyntax_1 = require("../../../../packages/core/src/domain/RequirementSyntax");
|
|
58
68
|
const DeclaredArtifacts_1 = require("../../../../packages/core/src/domain/DeclaredArtifacts");
|
|
59
69
|
const SpecParser_1 = require("../../../../packages/core/src/domain/SpecParser");
|
|
@@ -69,7 +79,8 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
69
79
|
}
|
|
70
80
|
usage() {
|
|
71
81
|
process.stdout.write("🔎 Usage:\n" +
|
|
72
|
-
"
|
|
82
|
+
" specgate validate <project_dir> [--strict] [--strict-tdd] [--strict-scenarios] " +
|
|
83
|
+
" [--strict-coverage] " +
|
|
73
84
|
"[--strict-requirements] [--strict-links] [--against-lock]\n\n" +
|
|
74
85
|
"Checks:\n" +
|
|
75
86
|
"- minimum SDD structure\n" +
|
|
@@ -93,6 +104,14 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
93
104
|
"--strict-requirements additionally enforces, over docs/specs/capabilities/**/spec.md:\n" +
|
|
94
105
|
"- Every requirement states an obligation (SHALL / MUST / SHOULD / MAY / DEBE / DEBERÁ)\n" +
|
|
95
106
|
"- A requirement that opens with IF resolves with THEN in the same sentence\n\n" +
|
|
107
|
+
"--strict is the gate: every check below, in one flag. Prefer it — the\n" +
|
|
108
|
+
"individual flags exist so an existing CI invocation keeps meaning what it meant.\n\n" +
|
|
109
|
+
"--strict-coverage additionally enforces:\n" +
|
|
110
|
+
"- Every scenario in a declared feature file is named by the test artifact that\n" +
|
|
111
|
+
" claims to prove it\n" +
|
|
112
|
+
"- Every declared test artifact names its requirement or one of its scenarios —\n" +
|
|
113
|
+
" a path that exists is not evidence that it proves anything\n" +
|
|
114
|
+
" Both are name matches, so this is opt-in: it does not run your suite\n\n" +
|
|
96
115
|
"--strict-links additionally enforces:\n" +
|
|
97
116
|
"- Every Feature file / Technical artifact / Test artifact the matrix declares as a\n" +
|
|
98
117
|
" path still exists on disk\n");
|
|
@@ -138,7 +157,64 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
138
157
|
this.logFix(fixLines);
|
|
139
158
|
process.exit(exitCode);
|
|
140
159
|
}
|
|
141
|
-
|
|
160
|
+
/**
|
|
161
|
+
* A shared spec that is only shared by convention is not shared.
|
|
162
|
+
*
|
|
163
|
+
* `init --multi-stack` symlinks spec.md, features/ and the ADR index into
|
|
164
|
+
* every stack, so there is one file with several names. Windows refuses to
|
|
165
|
+
* create symlinks without Developer Mode, and scaffolding copies instead —
|
|
166
|
+
* at which point "shared" is a claim rather than a fact, and the three stacks
|
|
167
|
+
* can quietly start describing different products.
|
|
168
|
+
*
|
|
169
|
+
* So the guarantee moves into the gate: a copy that no longer matches the
|
|
170
|
+
* root is an error. Symlinked trees skip this, because they cannot drift.
|
|
171
|
+
*/
|
|
172
|
+
sharedSpecDrift(rootDir, projectRel) {
|
|
173
|
+
const digest = (target) => {
|
|
174
|
+
const h = crypto.createHash("sha256");
|
|
175
|
+
const stat = fs.statSync(target);
|
|
176
|
+
if (stat.isFile()) {
|
|
177
|
+
h.update(fs.readFileSync(target));
|
|
178
|
+
return h.digest("hex");
|
|
179
|
+
}
|
|
180
|
+
const entries = [];
|
|
181
|
+
const walkInto = (dir) => {
|
|
182
|
+
for (const name of fs.readdirSync(dir).sort()) {
|
|
183
|
+
const full = path.join(dir, name);
|
|
184
|
+
if (fs.statSync(full).isDirectory())
|
|
185
|
+
walkInto(full);
|
|
186
|
+
else
|
|
187
|
+
entries.push(path.relative(target, full).split(path.sep).join("/"));
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
walkInto(target);
|
|
191
|
+
for (const rel of entries) {
|
|
192
|
+
h.update(rel);
|
|
193
|
+
h.update(fs.readFileSync(path.join(target, rel)));
|
|
194
|
+
}
|
|
195
|
+
return h.digest("hex");
|
|
196
|
+
};
|
|
197
|
+
const drifted = [];
|
|
198
|
+
for (const shared of MultiStack_1.SHARED_PATHS) {
|
|
199
|
+
const atRoot = path.join(rootDir, shared);
|
|
200
|
+
const inProject = path.join(projectRel, shared);
|
|
201
|
+
if (!fs.existsSync(atRoot) || !fs.existsSync(inProject))
|
|
202
|
+
continue;
|
|
203
|
+
// A link cannot drift from what it points at.
|
|
204
|
+
const link = fs.lstatSync(inProject, { throwIfNoEntry: false });
|
|
205
|
+
if (link && link.isSymbolicLink())
|
|
206
|
+
continue;
|
|
207
|
+
try {
|
|
208
|
+
if (digest(atRoot) !== digest(inProject))
|
|
209
|
+
drifted.push(shared);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Unreadable is a different problem, and the normal checks report it.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return drifted;
|
|
216
|
+
}
|
|
217
|
+
validateMonorepo(targetDir, strictTdd, strictScenarios, strictRequirements, strictLinks, strictCoverage) {
|
|
142
218
|
const cfgPath = path.join(targetDir, "specops.config.yaml");
|
|
143
219
|
if (!fs.existsSync(cfgPath))
|
|
144
220
|
return null;
|
|
@@ -181,9 +257,24 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
181
257
|
args.push("--strict-requirements");
|
|
182
258
|
if (strictLinks)
|
|
183
259
|
args.push("--strict-links");
|
|
260
|
+
if (strictCoverage)
|
|
261
|
+
args.push("--strict-coverage");
|
|
184
262
|
const r = (0, node_child_process_1.spawnSync)(process.execPath, args, { encoding: "utf8" });
|
|
185
263
|
process.stdout.write(r.stdout || "");
|
|
186
264
|
process.stderr.write(r.stderr || "");
|
|
265
|
+
const drifted = this.sharedSpecDrift(targetDir, subDir);
|
|
266
|
+
if (drifted.length > 0) {
|
|
267
|
+
this.logError(`Shared spec has drifted in ${rel}: ${drifted.join(", ")} no longer matches the root`);
|
|
268
|
+
this.logFix([
|
|
269
|
+
"These paths are meant to be one artifact for the whole tree. They were copied",
|
|
270
|
+
"rather than symlinked (this platform would not create links), so an edit in one",
|
|
271
|
+
"stack does not reach the others.",
|
|
272
|
+
`Fix: copy the root's version over ${rel}'s, or move the edit to the root and`,
|
|
273
|
+
"copy outward — then commit both.",
|
|
274
|
+
]);
|
|
275
|
+
results.push({ project: rel, ok: false, detail: "shared spec drift" });
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
187
278
|
results.push({ project: rel, ok: r.status === 0 });
|
|
188
279
|
}
|
|
189
280
|
const failures = results.filter((r) => !r.ok);
|
|
@@ -211,11 +302,15 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
211
302
|
* this tool uses everywhere else. Under the flag, warnings fail too — asking
|
|
212
303
|
* for strict and getting lenient is the H14 mistake in a different costume.
|
|
213
304
|
*/
|
|
214
|
-
checkScenarioQuality(targetDir, featureFiles) {
|
|
305
|
+
checkScenarioQuality(targetDir, featureFiles, delivering = "") {
|
|
306
|
+
const owed = this.featureFilesOwed(targetDir, delivering);
|
|
215
307
|
const findings = [];
|
|
216
308
|
for (const file of featureFiles.slice().sort()) {
|
|
217
309
|
const rel = path.relative(targetDir, file).split(path.sep).join("/");
|
|
218
|
-
|
|
310
|
+
const found = (0, GherkinQuality_1.analyseGherkinSource)(fs.readFileSync(file, "utf8"), rel);
|
|
311
|
+
// A scenario Cucumber reports as passing without running anything lies
|
|
312
|
+
// whatever the row's status (H14). The rest is owed on delivery.
|
|
313
|
+
findings.push(...(owed(rel) ? found : found.filter((d) => SUITE_LIES.has(d.code))));
|
|
219
314
|
}
|
|
220
315
|
if (findings.length === 0)
|
|
221
316
|
return;
|
|
@@ -234,6 +329,41 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
234
329
|
]);
|
|
235
330
|
process.exit(1);
|
|
236
331
|
}
|
|
332
|
+
/**
|
|
333
|
+
* Which feature files the scenario rules apply to: the same line as
|
|
334
|
+
* `--strict-links` — a Draft row owes nothing yet, a delivered one owes
|
|
335
|
+
* everything. `specgate new` writes a scenario of `<placeholder>` steps for a
|
|
336
|
+
* person to fill in; failing the build the moment it exists would teach
|
|
337
|
+
* people to skip the gate, and passing it once delivered would be a green
|
|
338
|
+
* that means nothing. A file no row claims is always checked.
|
|
339
|
+
*/
|
|
340
|
+
featureFilesOwed(targetDir, delivering) {
|
|
341
|
+
const tracePath = path.join(targetDir, "docs/specs/traceability.md");
|
|
342
|
+
if (!fs.existsSync(tracePath))
|
|
343
|
+
return () => true;
|
|
344
|
+
let rows = [];
|
|
345
|
+
try {
|
|
346
|
+
rows = (0, TraceabilityFormat_3.parseTraceabilityRows)(fs.readFileSync(tracePath, "utf8")).rows || [];
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return () => true;
|
|
350
|
+
}
|
|
351
|
+
const claimed = new Set();
|
|
352
|
+
const owed = new Set();
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
const delivered = TraceabilityFormat_1.DELIVERED_STATUS.has(String(row.status || "").trim()) ||
|
|
355
|
+
(delivering !== "" && row.requirement === delivering);
|
|
356
|
+
for (const declared of (0, DeclaredArtifacts_1.declaredPaths)(row.featureFile)) {
|
|
357
|
+
const rel = (0, DeclaredArtifacts_1.artifactFile)(declared);
|
|
358
|
+
if (!rel)
|
|
359
|
+
continue;
|
|
360
|
+
claimed.add(rel);
|
|
361
|
+
if (delivered)
|
|
362
|
+
owed.add(rel);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return (rel) => owed.has(rel) || !claimed.has(rel);
|
|
366
|
+
}
|
|
237
367
|
/**
|
|
238
368
|
* Does the scenario the matrix names actually exist in the file? (F4)
|
|
239
369
|
*
|
|
@@ -254,7 +384,7 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
254
384
|
checkScenarioTags(targetDir, traceContent) {
|
|
255
385
|
let rows = [];
|
|
256
386
|
try {
|
|
257
|
-
rows = (0,
|
|
387
|
+
rows = (0, TraceabilityFormat_3.parseTraceabilityRows)(traceContent).rows || [];
|
|
258
388
|
}
|
|
259
389
|
catch {
|
|
260
390
|
return;
|
|
@@ -309,10 +439,10 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
309
439
|
* Opt-in, same promise as `--strict-scenarios`: a project with aspirational
|
|
310
440
|
* rows does not fail its next `validate` over this.
|
|
311
441
|
*/
|
|
312
|
-
checkDeclaredArtifactsExist(targetDir, traceContent) {
|
|
442
|
+
checkDeclaredArtifactsExist(targetDir, traceContent, delivering = "") {
|
|
313
443
|
let rows = [];
|
|
314
444
|
try {
|
|
315
|
-
rows = (0,
|
|
445
|
+
rows = (0, TraceabilityFormat_3.parseTraceabilityRows)(traceContent).rows || [];
|
|
316
446
|
}
|
|
317
447
|
catch {
|
|
318
448
|
return;
|
|
@@ -325,11 +455,20 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
325
455
|
["test artifact", "testArtifact"],
|
|
326
456
|
];
|
|
327
457
|
for (const row of rows) {
|
|
458
|
+
// A row that has not been delivered names where the work is going to
|
|
459
|
+
// land; only a delivered one claims the file is there. Measured
|
|
460
|
+
// 2026-10-06: with `--strict` as the CI gate, every planned row failed
|
|
461
|
+
// the build — and in the harness, one pending requirement's paths failed
|
|
462
|
+
// its sibling's worktree.
|
|
463
|
+
const owesFiles = TraceabilityFormat_1.DELIVERED_STATUS.has(String(row.status || "").trim()) ||
|
|
464
|
+
(delivering !== "" && row.requirement === delivering);
|
|
465
|
+
if (!owesFiles)
|
|
466
|
+
continue;
|
|
328
467
|
for (const [label, key] of columns) {
|
|
329
468
|
for (const declared of (0, DeclaredArtifacts_1.declaredPaths)(row[key])) {
|
|
330
|
-
// A cell may
|
|
331
|
-
//
|
|
332
|
-
const rel =
|
|
469
|
+
// A cell may point into a file — an anchor (`login.ts#L15-L89`) or a
|
|
470
|
+
// test selector (`test_x.py::test_y`). Neither is part of the filename.
|
|
471
|
+
const rel = (0, DeclaredArtifacts_1.artifactFile)(declared);
|
|
333
472
|
if (!rel || seen.has(`${row.requirement || row.feature}::${rel}`))
|
|
334
473
|
continue;
|
|
335
474
|
if (fs.existsSync(path.join(targetDir, rel)))
|
|
@@ -358,6 +497,162 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
358
497
|
]);
|
|
359
498
|
process.exit(1);
|
|
360
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* `--strict-coverage`: every declared scenario is named by something that
|
|
502
|
+
* claims to prove it (#168).
|
|
503
|
+
*
|
|
504
|
+
* A row names one `Scenario ID` and one feature file; the file may hold five
|
|
505
|
+
* scenarios, and nothing related the other four to anything. Measured: a
|
|
506
|
+
* feature declaring three scenarios with tests covering two passed
|
|
507
|
+
* `--strict-tdd --strict-scenarios --strict-links` all at once, with the row
|
|
508
|
+
* on `Implemented`.
|
|
509
|
+
*
|
|
510
|
+
* It is behind its own flag on purpose. The check is a name match — this tool
|
|
511
|
+
* does not run the suite and cannot know which assertion belongs to which
|
|
512
|
+
* scenario — and a project that names its tests some other way would fail
|
|
513
|
+
* through no fault of its own. Opting in is the honest interface for a
|
|
514
|
+
* heuristic.
|
|
515
|
+
*/
|
|
516
|
+
checkScenarioCoverage(targetDir, traceContent) {
|
|
517
|
+
let rows = [];
|
|
518
|
+
try {
|
|
519
|
+
rows = (0, TraceabilityFormat_3.parseTraceabilityRows)(traceContent).rows || [];
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const readAll = (rel) => {
|
|
525
|
+
const full = path.join(targetDir, (0, DeclaredArtifacts_1.artifactFile)(rel));
|
|
526
|
+
if (!fs.existsSync(full))
|
|
527
|
+
return [];
|
|
528
|
+
const stat = fs.statSync(full);
|
|
529
|
+
if (stat.isFile())
|
|
530
|
+
return [fs.readFileSync(full, "utf8")];
|
|
531
|
+
const out = [];
|
|
532
|
+
const walkTests = (dir) => {
|
|
533
|
+
for (const name of fs.readdirSync(dir)) {
|
|
534
|
+
const child = path.join(dir, name);
|
|
535
|
+
if (fs.statSync(child).isDirectory())
|
|
536
|
+
walkTests(child);
|
|
537
|
+
else
|
|
538
|
+
out.push(fs.readFileSync(child, "utf8"));
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
walkTests(full);
|
|
542
|
+
return out;
|
|
543
|
+
};
|
|
544
|
+
const findings = [];
|
|
545
|
+
const advisories = [];
|
|
546
|
+
const seen = new Set();
|
|
547
|
+
// Read every row once first, so the check can calibrate before it judges.
|
|
548
|
+
const readRow = (row) => {
|
|
549
|
+
const declaredTests = (0, DeclaredArtifacts_1.declaredPaths)(row.testArtifact).filter((t) => t && t !== "-" && t.toUpperCase() !== "TBD");
|
|
550
|
+
const featureRel = (0, DeclaredArtifacts_1.declaredPaths)(row.featureFile)[0];
|
|
551
|
+
const featurePath = featureRel ? path.join(targetDir, (0, DeclaredArtifacts_1.artifactFile)(featureRel)) : null;
|
|
552
|
+
return {
|
|
553
|
+
requirement: row.requirement || "",
|
|
554
|
+
featureRel,
|
|
555
|
+
feature: featurePath && fs.existsSync(featurePath) ? fs.readFileSync(featurePath, "utf8") : "",
|
|
556
|
+
tests: declaredTests.flatMap(readAll),
|
|
557
|
+
declaredTests,
|
|
558
|
+
};
|
|
559
|
+
};
|
|
560
|
+
const read = rows.map(readRow).filter((r) => r.declaredTests.length > 0);
|
|
561
|
+
// Does this project name requirements in its tests at all? A brownfield
|
|
562
|
+
// adoption points rows at tests written years before this tool existed, and
|
|
563
|
+
// the only way to satisfy a name match there is to edit them — which is the
|
|
564
|
+
// one thing `adopt` promises it will never make you do.
|
|
565
|
+
const conventionInUse = read.length > 0 && (0, ScenarioCoverage_1.usesNamingConvention)(read);
|
|
566
|
+
if (read.length > 0 && !conventionInUse) {
|
|
567
|
+
this.logWarn("--strict-coverage could not run: no test artifact in this project names its " +
|
|
568
|
+
"requirement or any of its scenarios.");
|
|
569
|
+
this.logFix([
|
|
570
|
+
"This check matches names, and nothing here uses that convention — which is normal",
|
|
571
|
+
"for a repository whose tests predate its specifications. It is not a finding:",
|
|
572
|
+
"failing every row for a convention nobody adopted would be the gate lying.",
|
|
573
|
+
"",
|
|
574
|
+
"To turn it on, name the requirement in the test that proves it — a comment or a",
|
|
575
|
+
"test name mentioning REQ-NNN is enough — and this check starts holding the rest",
|
|
576
|
+
"of the matrix to the same standard.",
|
|
577
|
+
]);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
for (const row of rows) {
|
|
581
|
+
// A `TBD` or `-` test artifact is --strict-tdd's finding, not this one:
|
|
582
|
+
// reporting it twice under two flags helps nobody.
|
|
583
|
+
const declaredTests = (0, DeclaredArtifacts_1.declaredPaths)(row.testArtifact).filter((t) => t && t !== "-" && t.toUpperCase() !== "TBD");
|
|
584
|
+
if (declaredTests.length === 0)
|
|
585
|
+
continue;
|
|
586
|
+
const sources = declaredTests.flatMap(readAll);
|
|
587
|
+
const featureRel = (0, DeclaredArtifacts_1.declaredPaths)(row.featureFile)[0];
|
|
588
|
+
const featurePath = featureRel ? path.join(targetDir, (0, DeclaredArtifacts_1.artifactFile)(featureRel)) : null;
|
|
589
|
+
const feature = featurePath && fs.existsSync(featurePath) ? fs.readFileSync(featurePath, "utf8") : "";
|
|
590
|
+
// A row with no scenario has nothing for the per-scenario check to match
|
|
591
|
+
// on — and those are the rows that lie most easily, because `adopt` seeds
|
|
592
|
+
// them with a use case and no scenario. Measured on a real adoption: a
|
|
593
|
+
// "Vet" requirement declared `PetValidatorTests.java` as its proof and
|
|
594
|
+
// every gate stayed green. The requirement id is the only evidence left.
|
|
595
|
+
const requirementId = row.requirement || "";
|
|
596
|
+
// Advisory, not a failure. At row level this cannot tell "the team has
|
|
597
|
+
// not adopted the convention yet" from "this link is a lie", and failing
|
|
598
|
+
// the first to catch the second is what made adding one comment turn
|
|
599
|
+
// three unrelated rows red.
|
|
600
|
+
if ((0, ScenarioCoverage_1.linkIsUnevidenced)(requirementId, feature, sources)) {
|
|
601
|
+
const key = `${requirementId}::unevidenced`;
|
|
602
|
+
if (!seen.has(key)) {
|
|
603
|
+
seen.add(key);
|
|
604
|
+
advisories.push((0, diagnostics_1.warning)("link_without_evidence", `${requirementId}'s test artifact names neither ${requirementId} nor any of ` +
|
|
605
|
+
`its scenarios.`, {
|
|
606
|
+
target: requirementId,
|
|
607
|
+
file: "docs/specs/traceability.md",
|
|
608
|
+
fix: `Name ${requirementId} or one of its scenarios in ` +
|
|
609
|
+
`${declaredTests.join(" or ")}, or point the row at the test that ` +
|
|
610
|
+
`really proves it. A path that exists is not evidence that it proves ` +
|
|
611
|
+
`anything.`,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (!featurePath || !fs.existsSync(featurePath))
|
|
617
|
+
continue; // --strict-links reports this.
|
|
618
|
+
for (const scenario of (0, ScenarioCoverage_1.skippedScenarios)(feature, sources)) {
|
|
619
|
+
const key = `${featureRel}::${scenario.title}`;
|
|
620
|
+
if (seen.has(key))
|
|
621
|
+
continue;
|
|
622
|
+
seen.add(key);
|
|
623
|
+
findings.push((0, diagnostics_1.error)("scenario_not_covered", `Nothing proves "${scenario.title}" (${featureRel}).`, {
|
|
624
|
+
target: row.requirement || row.feature,
|
|
625
|
+
file: featureRel,
|
|
626
|
+
fix: `Add a test naming ${scenario.needles[0]} to ` +
|
|
627
|
+
`${declaredTests.join(" or ")}, or delete the scenario if it no ` +
|
|
628
|
+
`longer describes behaviour this project promises.`,
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Formatted like the errors, so an advisory carries its code and can be
|
|
633
|
+
// grepped for in CI output the same way.
|
|
634
|
+
for (const a of advisories) {
|
|
635
|
+
process.stderr.write(` ${(0, diagnostics_1.formatDiagnostic)(a)}\n`);
|
|
636
|
+
}
|
|
637
|
+
if (findings.length === 0)
|
|
638
|
+
return;
|
|
639
|
+
if (this.io.json) {
|
|
640
|
+
this.io.fail({ validation: null }, [...findings, ...advisories]);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
this.logError(`Declared scenarios nothing proves: ${findings.length}`);
|
|
644
|
+
for (const f of findings)
|
|
645
|
+
process.stderr.write(` ${(0, diagnostics_1.formatDiagnostic)(f)}\n`);
|
|
646
|
+
// Each finding already carries its own fix; this is the shared why. It used
|
|
647
|
+
// to say "a scenario no test names" for both codes, which was the wrong
|
|
648
|
+
// advice for half of them.
|
|
649
|
+
this.logFix([
|
|
650
|
+
"A promise with nothing behind it is the thing this gate exists to catch —",
|
|
651
|
+
"and it is what an agent leaves behind when it cannot satisfy a scenario.",
|
|
652
|
+
"Other rows in this project do name what proves them, so these stand out.",
|
|
653
|
+
]);
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
361
656
|
/**
|
|
362
657
|
* `--strict-requirements`: EARS-checkable requirement prose, at rest (F6).
|
|
363
658
|
*
|
|
@@ -378,6 +673,29 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
378
673
|
* scaffolded projects never grow one — it is the change-lifecycle structure,
|
|
379
674
|
* not something `specgate init` writes. Nothing to check is not a violation.
|
|
380
675
|
*/
|
|
676
|
+
/**
|
|
677
|
+
* The root `spec.md` plus every capability spec, concatenated.
|
|
678
|
+
*
|
|
679
|
+
* Concatenation is enough because the only question asked of it is "does this
|
|
680
|
+
* requirement have text somewhere". Nothing downstream cares which file the
|
|
681
|
+
* text came from.
|
|
682
|
+
*/
|
|
683
|
+
collectSpecProse(targetDir, specPath) {
|
|
684
|
+
const parts = [];
|
|
685
|
+
if (fs.existsSync(specPath))
|
|
686
|
+
parts.push(fs.readFileSync(specPath, "utf8"));
|
|
687
|
+
const capabilitiesDir = path.join(targetDir, ChangeWorkspace_2.CAPABILITIES_DIR);
|
|
688
|
+
if (fs.existsSync(capabilitiesDir)) {
|
|
689
|
+
for (const entry of fs.readdirSync(capabilitiesDir, { withFileTypes: true }).sort()) {
|
|
690
|
+
if (!entry.isDirectory())
|
|
691
|
+
continue;
|
|
692
|
+
const capSpec = path.join(capabilitiesDir, entry.name, "spec.md");
|
|
693
|
+
if (fs.existsSync(capSpec))
|
|
694
|
+
parts.push(fs.readFileSync(capSpec, "utf8"));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return parts.length > 0 ? parts.join("\n\n") : null;
|
|
698
|
+
}
|
|
381
699
|
checkRequirementSyntax(targetDir) {
|
|
382
700
|
const capabilitiesDir = path.join(targetDir, ChangeWorkspace_2.CAPABILITIES_DIR);
|
|
383
701
|
if (!fs.existsSync(capabilitiesDir))
|
|
@@ -423,12 +741,39 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
423
741
|
const rawArgs = this.args[0] === "validate" ? this.args.slice(1) : this.args;
|
|
424
742
|
const argv = rawArgs;
|
|
425
743
|
this.io = (0, agent_1.agentIo)((0, agent_1.wantsJson)(argv));
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
744
|
+
// `--strict` is the gate, singular.
|
|
745
|
+
//
|
|
746
|
+
// A cold evaluator counted ten places calling `--strict-tdd` "the gate" —
|
|
747
|
+
// the quickstart, `adopt`'s epilogue, the generated AI_RULES.md, `harness
|
|
748
|
+
// init`, the harness prompt — while `ci init` alone emitted
|
|
749
|
+
// `--strict-tdd --strict-links`. So a user who followed any of the ten got
|
|
750
|
+
// a check that passes on an Implemented requirement whose test file does
|
|
751
|
+
// not exist, and the one place that was right made the other nine look
|
|
752
|
+
// like a mistake rather than a policy.
|
|
753
|
+
//
|
|
754
|
+
// Fixing the wording in ten places would have left the same trap for the
|
|
755
|
+
// eleventh. One name that means all of them is the fix; the individual
|
|
756
|
+
// flags keep working, so every existing CI invocation still means what it
|
|
757
|
+
// meant.
|
|
758
|
+
const strict = argv.includes("--strict");
|
|
759
|
+
const strictTdd = strict || argv.includes("--strict-tdd");
|
|
760
|
+
const strictScenarios = strict || argv.includes("--strict-scenarios");
|
|
761
|
+
const strictRequirements = strict || argv.includes("--strict-requirements");
|
|
762
|
+
const strictLinks = strict || argv.includes("--strict-links");
|
|
763
|
+
const strictCoverage = strict || argv.includes("--strict-coverage");
|
|
430
764
|
const againstLock = argv.includes("--against-lock");
|
|
431
|
-
|
|
765
|
+
// `--delivering REQ-NNN` holds one requirement to what a delivered row
|
|
766
|
+
// owes while its Status still says Draft. `done --check` and the harness
|
|
767
|
+
// gate run before the status flips, so without it they would skip the very
|
|
768
|
+
// requirement they are about to mark Implemented.
|
|
769
|
+
const deliveringAt = argv.indexOf("--delivering");
|
|
770
|
+
const delivering = deliveringAt >= 0 ? argv[deliveringAt + 1] || "" : "";
|
|
771
|
+
const positional = argv.filter((a, i) => !a.startsWith("-") && !(deliveringAt >= 0 && i === deliveringAt + 1));
|
|
772
|
+
// Asking what a command does is not a usage error, and it exits 0.
|
|
773
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
774
|
+
this.usage();
|
|
775
|
+
process.exit(0);
|
|
776
|
+
}
|
|
432
777
|
const targetDir = positional[0];
|
|
433
778
|
if (!targetDir) {
|
|
434
779
|
this.usage();
|
|
@@ -440,7 +785,7 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
440
785
|
" specgate init",
|
|
441
786
|
]);
|
|
442
787
|
}
|
|
443
|
-
const monorepo = this.validateMonorepo(targetDir, strictTdd, strictScenarios, strictRequirements, strictLinks);
|
|
788
|
+
const monorepo = this.validateMonorepo(targetDir, strictTdd, strictScenarios, strictRequirements, strictLinks, strictCoverage);
|
|
444
789
|
if (monorepo !== null) {
|
|
445
790
|
process.exit(monorepo.failures === 0 ? 0 : 1);
|
|
446
791
|
}
|
|
@@ -494,7 +839,7 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
494
839
|
"spec.md": "Author it from the shipped template (templates/base/spec.md.tpl) — one `## REQ-NNN — <title>` section per requirement.",
|
|
495
840
|
"AI_RULES.md": "Author it from templates/backend/AI_RULES.md.tpl (or frontend) — it is the agent's project rulebook.",
|
|
496
841
|
"README.md": "Add a README.md describing how to build and test the project.",
|
|
497
|
-
"docs/specs/traceability.md": `Create it with the rich matrix header:\n ${
|
|
842
|
+
"docs/specs/traceability.md": `Create it with the rich matrix header:\n ${TraceabilityFormat_2.RICH_HEADER}`,
|
|
498
843
|
"docs/specs/adr/README.md": "Create docs/specs/adr/README.md as the index of your Architecture Decision Records.",
|
|
499
844
|
};
|
|
500
845
|
for (const f of REQUIRED_FILES) {
|
|
@@ -515,7 +860,7 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
515
860
|
}
|
|
516
861
|
const featureCount = featureFiles.length;
|
|
517
862
|
if (strictScenarios) {
|
|
518
|
-
this.checkScenarioQuality(targetDir, featureFiles);
|
|
863
|
+
this.checkScenarioQuality(targetDir, featureFiles, delivering);
|
|
519
864
|
}
|
|
520
865
|
if (strictRequirements) {
|
|
521
866
|
this.checkRequirementSyntax(targetDir);
|
|
@@ -549,7 +894,14 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
549
894
|
const specPath = path.join(targetDir, "spec.md");
|
|
550
895
|
const { report, mode, requirements: reqsInMatrix, } = new ValidateProjectUseCase_1.ValidateProjectUseCase(new DiskTraceabilityRepository_1.DiskTraceabilityRepository()).checkMatrix(traceContent, {
|
|
551
896
|
strictTdd,
|
|
552
|
-
|
|
897
|
+
// Every place a requirement's prose is allowed to live. Three shapes are
|
|
898
|
+
// in the wild and all three are legitimate: `## REQ-NNN` sections (what
|
|
899
|
+
// `adopt` writes), the `| ID | Requirement |` table in §8 (what the
|
|
900
|
+
// original `init` template wrote, and what this repository still uses),
|
|
901
|
+
// and a capability spec under docs/specs/capabilities/ (ADR-0022).
|
|
902
|
+
// Reading only the root spec.md flagged 14 requirements in our own repo
|
|
903
|
+
// that are documented perfectly well one directory down.
|
|
904
|
+
specContent: this.collectSpecProse(targetDir, specPath),
|
|
553
905
|
});
|
|
554
906
|
const strictTddViolations = report.findings
|
|
555
907
|
.filter((f) => f.code === "strict_tdd_violation")
|
|
@@ -557,6 +909,15 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
557
909
|
for (const finding of report.findings) {
|
|
558
910
|
if (finding.code === "strict_tdd_violation")
|
|
559
911
|
continue; // reported together, below
|
|
912
|
+
// Severity decides. This loop used to fail on anything the report
|
|
913
|
+
// carried, which turned the first warning added to `checkMatrix` into a
|
|
914
|
+
// red build — the exact green-to-red flip ADR-0026 exists to avoid.
|
|
915
|
+
if (finding.severity === "warning") {
|
|
916
|
+
this.logWarn(finding.message);
|
|
917
|
+
if (finding.fixLines?.length)
|
|
918
|
+
this.logFix([...finding.fixLines]);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
560
921
|
this.fail(finding.code, finding.message, 1, [...(finding.fixLines || [])]);
|
|
561
922
|
return;
|
|
562
923
|
}
|
|
@@ -586,7 +947,10 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
586
947
|
}
|
|
587
948
|
this.checkScenarioTags(targetDir, traceContent);
|
|
588
949
|
if (strictLinks) {
|
|
589
|
-
this.checkDeclaredArtifactsExist(targetDir, traceContent);
|
|
950
|
+
this.checkDeclaredArtifactsExist(targetDir, traceContent, delivering);
|
|
951
|
+
}
|
|
952
|
+
if (strictCoverage) {
|
|
953
|
+
this.checkScenarioCoverage(targetDir, traceContent);
|
|
590
954
|
}
|
|
591
955
|
for (const ff of featureFiles.sort()) {
|
|
592
956
|
const rel = path.relative(targetDir, ff).split(path.sep).join("/");
|
|
@@ -746,8 +1110,26 @@ class ValidateSpecsCommand extends command_1.BaseCommand {
|
|
|
746
1110
|
}
|
|
747
1111
|
this.logInfo("- Base SDD structure: complete");
|
|
748
1112
|
this.logInfo(`- Traceability mode: ${traceMode}`);
|
|
749
|
-
if (strictTdd)
|
|
750
|
-
|
|
1113
|
+
if (strictTdd) {
|
|
1114
|
+
// The count travels with the pass. `passed` over rows that were never
|
|
1115
|
+
// checked is how the tool and its own documentation ended up disagreeing.
|
|
1116
|
+
let exempt = 0;
|
|
1117
|
+
try {
|
|
1118
|
+
exempt = (0, TraceabilityFormat_1.countDraftExempt)((0, TraceabilityFormat_3.parseTraceabilityRows)(traceContent).rows || []);
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
exempt = 0;
|
|
1122
|
+
}
|
|
1123
|
+
this.logInfo(exempt > 0
|
|
1124
|
+
? `- Strict TDD gate: passed (${exempt} row(s) exempt — still Draft, so not checked)`
|
|
1125
|
+
: "- Strict TDD gate: passed");
|
|
1126
|
+
if (exempt > 0) {
|
|
1127
|
+
this.logFix([
|
|
1128
|
+
"A Draft row owes nothing yet, so --strict-tdd skips it. It starts being",
|
|
1129
|
+
"checked the moment its Status leaves Draft — `specgate done <REQ>` does that.",
|
|
1130
|
+
]);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
751
1133
|
if (skeletonOnly) {
|
|
752
1134
|
this.logWarn("Adoption never retro-filled — the only scenario is the adoption baseline.");
|
|
753
1135
|
this.logFix([
|