@holmes-lab/holmes-kit 0.1.9 → 0.1.10
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 +99 -0
- package/README.md +48 -4
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/test-platform.d.ts +25 -0
- package/dist/holmes/cli/test-platform.js +38 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +41 -0
- package/dist/holmes/cpg/cpg-scanner.js +53 -1
- package/dist/holmes/cpg/forbidden-edges.d.ts +73 -0
- package/dist/holmes/cpg/forbidden-edges.js +140 -0
- package/dist/holmes/cpg/hash-cache.js +13 -5
- package/dist/holmes/cpg/language-parser-walk.js +70 -4
- package/dist/holmes/cpg/proposed-content.d.ts +51 -0
- package/dist/holmes/cpg/proposed-content.js +72 -0
- package/dist/holmes/cpg/required-calls.d.ts +62 -0
- package/dist/holmes/cpg/required-calls.js +93 -0
- package/dist/holmes/guardrail/cspec-change.d.ts +23 -0
- package/dist/holmes/guardrail/cspec-change.js +70 -0
- package/dist/holmes/guardrail/risk-classifier.js +122 -0
- package/dist/holmes/guardrail/write-target.d.ts +42 -0
- package/dist/holmes/guardrail/write-target.js +69 -18
- package/dist/holmes/hooks/pre-tool-use.js +90 -5
- package/dist/holmes/hooks/stop.d.ts +17 -0
- package/dist/holmes/hooks/stop.js +39 -2
- package/dist/holmes/mcp/handlers.d.ts +41 -0
- package/dist/holmes/mcp/handlers.js +173 -3
- package/dist/holmes/mcp/tool-schemas.js +12 -0
- package/dist/holmes/project/dependencies.d.ts +15 -0
- package/dist/holmes/project/dependencies.js +58 -0
- package/dist/holmes/project/json-state.d.ts +24 -0
- package/dist/holmes/project/json-state.js +30 -0
- package/dist/holmes/reverse/scan.js +8 -1
- package/dist/holmes/review/scope.d.ts +29 -0
- package/dist/holmes/review/scope.js +44 -0
- package/dist/holmes/rtm/test-scope.d.ts +44 -0
- package/dist/holmes/rtm/test-scope.js +92 -2
- package/dist/holmes/server/dashboard.d.ts +22 -0
- package/dist/holmes/server/dashboard.js +114 -24
- package/dist/holmes/spec/approval-blockers.d.ts +21 -5
- package/dist/holmes/spec/approval-blockers.js +49 -6
- package/dist/holmes/spec/legacy-format.d.ts +14 -0
- package/dist/holmes/spec/legacy-format.js +15 -1
- package/dist/holmes/spec/nonfunctional.d.ts +70 -0
- package/dist/holmes/spec/nonfunctional.js +119 -0
- package/dist/holmes/spec/spec-parser.d.ts +25 -0
- package/dist/holmes/spec/spec-parser.js +46 -2
- package/dist/holmes/spec/spec-types.d.ts +4 -1
- package/dist/holmes/spec/spec-types.js +13 -1
- package/dist/holmes/testing/effects.d.ts +54 -0
- package/dist/holmes/testing/effects.js +107 -0
- package/package.json +3 -2
- package/playbooks/promote-slice/PLAYBOOK.md +20 -0
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SUPPORTED_TEST_FRAMEWORKS = void 0;
|
|
37
|
+
exports.architectureGoverningFiles = architectureGoverningFiles;
|
|
37
38
|
exports.accountChangedFiles = accountChangedFiles;
|
|
38
39
|
exports.countTestCases = countTestCases;
|
|
39
40
|
exports.extractAnchors = extractAnchors;
|
|
@@ -42,8 +43,50 @@ exports.computeTestScope = computeTestScope;
|
|
|
42
43
|
// @implements A-SPEC-121.2
|
|
43
44
|
const fs = __importStar(require("node:fs"));
|
|
44
45
|
const path = __importStar(require("node:path"));
|
|
46
|
+
const spec_types_1 = require("../spec/spec-types");
|
|
45
47
|
const rtm_check_1 = require("./rtm-check");
|
|
46
48
|
const DEFAULTS = { partialBlast: 3, fullBlast: 12 };
|
|
49
|
+
/**
|
|
50
|
+
* Architecture-governing documents, picked out of a change set.
|
|
51
|
+
*
|
|
52
|
+
* @implements A-SPEC-233
|
|
53
|
+
* WHY THIS EXISTS. `assessRisk`'s architecture axis already returns `hard-hitl` for exactly these
|
|
54
|
+
* changes — "supersedes accepted ADR(s)" and "C-SPEC Forbidden Edges / Layer Rules changed". Nothing
|
|
55
|
+
* on the test side consumed that verdict. Measured 2026-08-22 on the built dist: a change impacting
|
|
56
|
+
* A-SPEC-224 (the structural-constraint slice itself) got tier `unit`, ONE test file, while
|
|
57
|
+
* `assessRisk` returned `hard-hitl`. The most dangerous change received the narrowest test run.
|
|
58
|
+
*
|
|
59
|
+
* WHY THIS PREDICATE IS COARSER THAN THE RISK AXIS, AND WHY THAT IS RIGHT. The risk axis asks "did
|
|
60
|
+
* the CONSTRAINTS change?" and answers precisely, via `cspecConstraintChanged(current, proposed)`.
|
|
61
|
+
* That needs before-and-after content, which is not available here: the change source is not always
|
|
62
|
+
* git — `change-source.ts` also diffs against a baseline hash tree, where no "before" exists to
|
|
63
|
+
* read. So this asks the weaker question it can actually answer: "was an architecture-governing
|
|
64
|
+
* document touched?" The two are allowed to differ because their cost asymmetries are opposite. A
|
|
65
|
+
* false positive on the risk axis interrupts a human for nothing; a false positive here runs more
|
|
66
|
+
* tests. Widening is this module's standing answer to uncertainty everywhere else, and it is the
|
|
67
|
+
* answer here too.
|
|
68
|
+
*
|
|
69
|
+
* WHAT IT DOES NOT SEE. Whether the edit changed a rule or only its prose. Deletions (`changedFiles`
|
|
70
|
+
* carries added/modified/renamed; `.ax/specs`/`.ax/decisions` deletion is gated separately). Any
|
|
71
|
+
* architecture decision recorded somewhere other than these two locations.
|
|
72
|
+
*/
|
|
73
|
+
function architectureGoverningFiles(changedFiles) {
|
|
74
|
+
if (!Array.isArray(changedFiles))
|
|
75
|
+
return [];
|
|
76
|
+
// The C-SPEC folder is READ from the type table, never spelled here. A second spelling of a
|
|
77
|
+
// location is a second thing to keep in step, and this repository has already paid for that
|
|
78
|
+
// (`isTestFile` drifted into two copies; `parseSpec`'s section split was restated and returned an
|
|
79
|
+
// empty section while the check that used it went green).
|
|
80
|
+
const cspecDir = `.ax/specs/${spec_types_1.SPEC_TYPES['C-SPEC'].folder}/`;
|
|
81
|
+
const out = new Set();
|
|
82
|
+
for (const f of changedFiles) {
|
|
83
|
+
if (typeof f !== 'string' || !f.endsWith('.md'))
|
|
84
|
+
continue;
|
|
85
|
+
if (f.startsWith('.ax/decisions/') || f.startsWith(cspecDir))
|
|
86
|
+
out.add(f);
|
|
87
|
+
}
|
|
88
|
+
return [...out].sort();
|
|
89
|
+
}
|
|
47
90
|
const stripSpecPrefix = (id) => (id.startsWith('SPEC:') ? id.slice('SPEC:'.length) : id);
|
|
48
91
|
// "Is this a test file?" must be ONE predicate. Two copies had already drifted: the scanner excluded
|
|
49
92
|
// JVM/.NET/Rust/C++ conventions from the production graph while this side still only recognized
|
|
@@ -279,15 +322,62 @@ function computeTestScope(impactedSpecsRaw, testAnchors, specs, coverageGaps = [
|
|
|
279
322
|
// incomplete graph is not evidence of no impact — same epistemic state as C3 below, same answer.
|
|
280
323
|
// Checked FIRST so a contradictory input (empty impact + unresolved present) resolves toward
|
|
281
324
|
// full, never toward "nothing to re-test".
|
|
325
|
+
const architectureTouched = [...new Set(extras?.architectureTouched ?? [])].sort();
|
|
326
|
+
const obligationGaps = [...new Set(extras?.obligationGaps ?? [])].sort();
|
|
282
327
|
if (unresolvedFiles.length > 0) {
|
|
283
328
|
const shown = unresolvedFiles.slice(0, 5).join(', ');
|
|
284
329
|
const suffix = unresolvedFiles.length > 5 ? ` (${unresolvedFiles.length} unresolved total)` : '';
|
|
285
330
|
return {
|
|
286
331
|
tier: 'full', impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius: 0,
|
|
287
|
-
hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles,
|
|
332
|
+
hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles, architectureTouched, obligationGaps,
|
|
288
333
|
reason: `${unresolvedFiles.length} changed source file(s) resolved to no spec — cannot narrow safely, full regression: ${shown}${suffix}`,
|
|
289
334
|
};
|
|
290
335
|
}
|
|
336
|
+
// @implements A-SPEC-233
|
|
337
|
+
// This branch RETURNS. That, not its position, is what makes it beat the branches below, which
|
|
338
|
+
// only assign `tier`/`reason` and fall through to the shared return.
|
|
339
|
+
//
|
|
340
|
+
// The first cut of this comment claimed the placement was the contract, and gave a reason that is
|
|
341
|
+
// false: that below `impactedSpecs.length === 0` the signal would vanish. Measured — moving this
|
|
342
|
+
// block to just above the final return left all twelve cases green, because that branch does not
|
|
343
|
+
// return either. A comment asserting a guarantee the code does not make is the exact state this
|
|
344
|
+
// slice's own REQ exists to remove, so it is written as it actually is.
|
|
345
|
+
//
|
|
346
|
+
// The one real ordering relation is with the OTHER early return, `unresolvedFiles`. Both answer
|
|
347
|
+
// `full`, so the tier is never in question between them; what the order decides is which reason
|
|
348
|
+
// is reported, and `unresolvedFiles` wins because it names the files to anchor — an operator can
|
|
349
|
+
// act on that.
|
|
350
|
+
//
|
|
351
|
+
// What carries the weight instead of placement: an architecture document having changed is true
|
|
352
|
+
// regardless of blast, and true even when `impactedSpecs` is empty — the shape of a change that
|
|
353
|
+
// edits ONLY a C-SPEC, which resolves to no impacted spec at all.
|
|
354
|
+
if (architectureTouched.length > 0) {
|
|
355
|
+
const shown = architectureTouched.slice(0, 5).join(', ');
|
|
356
|
+
const suffix = architectureTouched.length > 5 ? ` (${architectureTouched.length} total)` : '';
|
|
357
|
+
return {
|
|
358
|
+
tier: 'full', impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius: 0,
|
|
359
|
+
hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles: [], architectureTouched, obligationGaps,
|
|
360
|
+
reason: `architecture-governing document(s) changed — full regression: ${shown}${suffix}`,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// @implements A-SPEC-235
|
|
364
|
+
// Another early return, so it wins over the assigning branches below for the same reason the last
|
|
365
|
+
// one does. Its position among the OTHER early returns decides only which reason is reported —
|
|
366
|
+
// all three answer `full`. It comes last of the three because the two above signal larger
|
|
367
|
+
// uncertainty: a graph that cannot see a changed file, and an architecture document that moved.
|
|
368
|
+
//
|
|
369
|
+
// The reason names the TAG, not just the spec id. "coverage gap in A-SPEC-235" sends the reader
|
|
370
|
+
// to grep; "A-SPEC-235 [purity]" tells them the case to write.
|
|
371
|
+
if (obligationGaps.length > 0) {
|
|
372
|
+
const shown = obligationGaps.slice(0, 5).join(', ');
|
|
373
|
+
const suffix = obligationGaps.length > 5 ? ` (${obligationGaps.length} total)` : '';
|
|
374
|
+
return {
|
|
375
|
+
tier: 'full', impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius: 0,
|
|
376
|
+
hitsFoundational: false, coverageGaps: [...coverageGaps], unresolvedFiles: [],
|
|
377
|
+
architectureTouched, obligationGaps,
|
|
378
|
+
reason: `declared obligation(s) with no test — full regression: ${shown}${suffix}`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
291
381
|
// Tier on the DIRECTLY-impacted A-SPECs (what the changed code implements), NOT the whole impacted
|
|
292
382
|
// set — impactedBy always returns the ancestor chain up to the REQ root, so keying on it made every
|
|
293
383
|
// change "reach a foundational REQ" → always full (verification H4). Downstream blast of the direct
|
|
@@ -332,5 +422,5 @@ function computeTestScope(impactedSpecsRaw, testAnchors, specs, coverageGaps = [
|
|
|
332
422
|
tier = 'unit';
|
|
333
423
|
reason = `localized impact (blast=${blastRadius}) — run the ${impactedTestFiles.length} impacted test file(s)`;
|
|
334
424
|
}
|
|
335
|
-
return { tier, impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius, hitsFoundational, coverageGaps: [...coverageGaps], reason, unresolvedFiles: [] };
|
|
425
|
+
return { tier, impactedSpecs, impactedTestFiles: selectedTestFiles, blastRadius, hitsFoundational, coverageGaps: [...coverageGaps], reason, unresolvedFiles: [], architectureTouched, obligationGaps };
|
|
336
426
|
}
|
|
@@ -70,6 +70,28 @@ export interface PipelineRow {
|
|
|
70
70
|
mutationScoreMeasured: false;
|
|
71
71
|
}
|
|
72
72
|
export declare function isCanonicalStatus(status: unknown): boolean;
|
|
73
|
+
/**
|
|
74
|
+
* The kinds that are governed specs at all.
|
|
75
|
+
*
|
|
76
|
+
* @implements A-SPEC-219.1
|
|
77
|
+
* Held here rather than imported from SPEC_TYPES because REQ is a spec kind that SPEC_TYPES does
|
|
78
|
+
* list — the distinction this set draws is "document in the spec store that the pipeline governs"
|
|
79
|
+
* versus "document that lives there but never will", and JOB is the whole of the second group.
|
|
80
|
+
*/
|
|
81
|
+
export declare const SPEC_KINDS: Set<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Drop retired documents before anything reads the corpus.
|
|
84
|
+
*
|
|
85
|
+
* @implements A-SPEC-222.2
|
|
86
|
+
* A-SPEC-222.1 excluded `outdated` from `isGoverned` and claimed every other check would follow.
|
|
87
|
+
* `tspec-mirror.test.ts` refuted that, and the spec was corrected to name one known site. Measured
|
|
88
|
+
* 2026-08-22 it was three: /api/rtm exposed 132 retired ids, the heatmap 108, the graph 132 — a
|
|
89
|
+
* screen still pointing at documents retired the day before. Stated once here so the three sites
|
|
90
|
+
* cannot drift, with the suite enumerating the endpoints so a fourth cannot be added silently.
|
|
91
|
+
*/
|
|
92
|
+
export declare function activeSpecs<T extends {
|
|
93
|
+
status?: unknown;
|
|
94
|
+
}>(specs: T[]): T[];
|
|
73
95
|
/** `A-SPEC-219.1` -> `A-SPEC`, `REQ-021` -> `REQ`. */
|
|
74
96
|
export declare function specKindOf(id: string): string;
|
|
75
97
|
/**
|
|
@@ -33,12 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SPEC_KINDS = void 0;
|
|
36
37
|
exports.startDashboardServer = startDashboardServer;
|
|
37
38
|
exports.isCanonicalStatus = isCanonicalStatus;
|
|
39
|
+
exports.activeSpecs = activeSpecs;
|
|
38
40
|
exports.specKindOf = specKindOf;
|
|
39
41
|
exports.buildPipelineRows = buildPipelineRows;
|
|
40
42
|
// @implements A-SPEC-215
|
|
41
43
|
// @implements A-SPEC-219
|
|
44
|
+
// @implements A-SPEC-222.2
|
|
42
45
|
const fs = __importStar(require("node:fs"));
|
|
43
46
|
const http = __importStar(require("node:http"));
|
|
44
47
|
const path = __importStar(require("node:path"));
|
|
@@ -77,7 +80,9 @@ async function startDashboardServer(options) {
|
|
|
77
80
|
}
|
|
78
81
|
if (parsedUrl === '/api/rtm') {
|
|
79
82
|
try {
|
|
80
|
-
const
|
|
83
|
+
const allSpecs = await store.list();
|
|
84
|
+
const retiredSpecs = allSpecs.filter((s) => s.status === 'outdated');
|
|
85
|
+
const specs = activeSpecs(allSpecs);
|
|
81
86
|
const files = scanner.scan(root);
|
|
82
87
|
const implementedSpecIds = new Set();
|
|
83
88
|
for (const f of files) {
|
|
@@ -93,16 +98,29 @@ async function startDashboardServer(options) {
|
|
|
93
98
|
// anchored to it) answer different questions, and a spec whose status is outside the
|
|
94
99
|
// canonical vocabulary answers neither — it is excluded and counted separately instead of
|
|
95
100
|
// being silently scored 0, which read as "47 uncovered specs" when it meant "47 unmapped".
|
|
96
|
-
const canonical = enrichedSpecs.filter((s) => !s.legacyStatus);
|
|
101
|
+
const canonical = enrichedSpecs.filter((s) => !s.legacyStatus && exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
97
102
|
const approvedCount = canonical.filter((s) => s.status === 'approved').length;
|
|
98
103
|
const anchorable = enrichedSpecs.filter((s) => s.id.startsWith('A-SPEC') && !s.legacyStatus);
|
|
99
104
|
const anchoredCount = anchorable.filter((s) => s.covered).length;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
// @implements A-SPEC-219.1
|
|
106
|
+
// Two populations sit outside the axes, and merging them into one number misleads: a reader
|
|
107
|
+
// of "legacy 49" concludes 49 documents need cleaning up, when 23 of them will never become
|
|
108
|
+
// specs no matter how long anyone waits. The population that prompted this split was
|
|
109
|
+
// `06_job/JOB-*`, work orders carrying an `assignee`; those were deleted under REQ-223
|
|
110
|
+
// once measurement showed all 23 pointed at retired REQs and at 46 source paths of which
|
|
111
|
+
// none still existed. The split stays because it is the guard: the next stray document
|
|
112
|
+
// lands here instead of in the cleanup backlog.
|
|
113
|
+
const outside = enrichedSpecs.filter((s) => s.legacyStatus || !exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
114
|
+
const unmappedSpecs = outside.filter((s) => exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
115
|
+
const nonSpecDocs = outside.filter((s) => !exports.SPEC_KINDS.has(specKindOf(s.id)));
|
|
116
|
+
const tally = (rows) => {
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const s of rows) {
|
|
119
|
+
const kind = specKindOf(s.id);
|
|
120
|
+
out[kind] = (out[kind] || 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
};
|
|
106
124
|
const approval = {
|
|
107
125
|
total: canonical.length,
|
|
108
126
|
approved: approvedCount,
|
|
@@ -113,7 +131,18 @@ async function startDashboardServer(options) {
|
|
|
113
131
|
anchored: anchoredCount,
|
|
114
132
|
pct: anchorable.length > 0 ? Math.round((anchoredCount / anchorable.length) * 100) : 0,
|
|
115
133
|
};
|
|
116
|
-
|
|
134
|
+
// Reported, never silently dropped: without this the 132 excluded documents vanish and the
|
|
135
|
+
// screen cannot say where they went.
|
|
136
|
+
const retiredByKind = {};
|
|
137
|
+
for (const s of retiredSpecs) {
|
|
138
|
+
const kind = specKindOf(s.id);
|
|
139
|
+
retiredByKind[kind] = (retiredByKind[kind] || 0) + 1;
|
|
140
|
+
}
|
|
141
|
+
// Count and kinds only. Shipping the ids would put retired documents back into the very
|
|
142
|
+
// response this slice removes them from, and nothing on the screen renders them.
|
|
143
|
+
const retired = { count: retiredSpecs.length, byKind: retiredByKind };
|
|
144
|
+
const unmapped = { count: unmappedSpecs.length, byKind: tally(unmappedSpecs) };
|
|
145
|
+
const nonSpec = { count: nonSpecDocs.length, byKind: tally(nonSpecDocs) };
|
|
117
146
|
const coveredCount = enrichedSpecs.filter((s) => s.covered).length;
|
|
118
147
|
const totalCount = enrichedSpecs.length;
|
|
119
148
|
const body = JSON.stringify({
|
|
@@ -122,7 +151,9 @@ async function startDashboardServer(options) {
|
|
|
122
151
|
coveredCount,
|
|
123
152
|
approval,
|
|
124
153
|
implementation,
|
|
125
|
-
|
|
154
|
+
unmapped,
|
|
155
|
+
nonSpec,
|
|
156
|
+
retired,
|
|
126
157
|
specs: enrichedSpecs,
|
|
127
158
|
files,
|
|
128
159
|
});
|
|
@@ -140,7 +171,8 @@ async function startDashboardServer(options) {
|
|
|
140
171
|
// @implements A-SPEC-219
|
|
141
172
|
if (parsedUrl === '/api/rtm/heatmap') {
|
|
142
173
|
try {
|
|
143
|
-
|
|
174
|
+
// @implements A-SPEC-222.2 — retired documents never reach a rendered view.
|
|
175
|
+
const specs = activeSpecs(await store.list());
|
|
144
176
|
const files = scanner.scan(root);
|
|
145
177
|
// FindingsLedger.list() returns [] for a missing file, which is indistinguishable from a
|
|
146
178
|
// clean scan. The dashboard therefore asks the filesystem directly: an audit lens that
|
|
@@ -188,7 +220,8 @@ async function startDashboardServer(options) {
|
|
|
188
220
|
// @implements A-SPEC-219
|
|
189
221
|
if (parsedUrl === '/api/graph') {
|
|
190
222
|
try {
|
|
191
|
-
|
|
223
|
+
// @implements A-SPEC-222.2 — retired documents never reach a rendered view.
|
|
224
|
+
const specs = activeSpecs(await store.list());
|
|
192
225
|
const files = scanner.scan(root);
|
|
193
226
|
const nodes = [];
|
|
194
227
|
const edges = [];
|
|
@@ -472,6 +505,8 @@ function renderDashboardHtml() {
|
|
|
472
505
|
.axis-stat { display: inline-flex; align-items: center; gap: 6px; }
|
|
473
506
|
.legacy-chip { cursor: pointer; border: 1px dashed #64748b; border-radius: 999px; padding: 4px 10px; }
|
|
474
507
|
.legacy-chip:hover { border-color: #f59e0b; color: #f59e0b; }
|
|
508
|
+
.nonspec-chip { cursor: pointer; border: 1px solid #334155; border-radius: 999px; padding: 4px 10px; color: #64748b; }
|
|
509
|
+
.nonspec-chip:hover { border-color: #94a3b8; color: #94a3b8; }
|
|
475
510
|
.scope-notice { font-size: 12px; color: #94a3b8; background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 8px 12px; margin-bottom: 10px; }
|
|
476
511
|
.scope-notice-truncated { color: #fbbf24; border-color: #b45309; border-style: dashed; }
|
|
477
512
|
.stage-missing { color: #64748b; font-style: italic; cursor: default; background: repeating-linear-gradient(45deg, rgba(100,116,139,0.06) 0 6px, transparent 6px 12px); }
|
|
@@ -500,7 +535,8 @@ function renderDashboardHtml() {
|
|
|
500
535
|
<div class="axis-stats">
|
|
501
536
|
<span class="axis-stat" title="Specs signed off, over specs using the canonical status vocabulary">📋 Approval <span id="approvalStat" class="stat-badge">--%</span></span>
|
|
502
537
|
<span class="axis-stat" title="A-SPECs with code anchored to them, over canonical A-SPECs">⚙️ Implementation <span id="implStat" class="stat-badge">--%</span></span>
|
|
503
|
-
<span class="axis-stat legacy-chip" id="
|
|
538
|
+
<span class="axis-stat legacy-chip" id="unmappedChip" title="Spec documents whose status is outside draft|review|approved|outdated — these are the cleanup backlog" onclick="switchTab('legacy')">🏷️ <span id="unmappedStat">--</span></span>
|
|
539
|
+
<span class="axis-stat nonspec-chip" id="nonSpecChip" title="Documents in the spec store that are not a governed spec kind — never a cleanup target" onclick="switchTab('legacy')">📋 <span id="nonSpecStat">--</span></span>
|
|
504
540
|
</div>
|
|
505
541
|
</div>
|
|
506
542
|
|
|
@@ -520,10 +556,17 @@ function renderDashboardHtml() {
|
|
|
520
556
|
</div>
|
|
521
557
|
|
|
522
558
|
<div id="legacyTab" style="display: none;">
|
|
523
|
-
<div class="ux-hint-banner" style="border-left-color: #
|
|
524
|
-
<span>🏷️ <strong>
|
|
559
|
+
<div class="ux-hint-banner" style="border-left-color: #f59e0b;">
|
|
560
|
+
<span>🏷️ <strong>Cleanup backlog:</strong> spec documents whose <code>status</code> falls outside <code>draft | review | approved | outdated</code>, so no governance gate has scored them. Each is waiting for <code>spec_upgrade</code> → <code>spec_retire</code>, or a rewrite into the current format. Excluded from both headline axes — an unmapped vocabulary is not an uncovered spec.</span>
|
|
561
|
+
</div>
|
|
562
|
+
<div id="unmappedGrid" class="rtm-container">Loading…</div>
|
|
563
|
+
|
|
564
|
+
<div id="nonSpecSection" style="display: none;">
|
|
565
|
+
<div class="ux-hint-banner" style="border-left-color: #334155; margin-top: 28px;">
|
|
566
|
+
<span>📋 <strong>Not specifications:</strong> documents that live in the spec store but are not a governed spec kind. They are <strong>not</strong> a cleanup target and never will be, and they have always sat outside both axes. Nothing should normally appear here — <code>spec-store-boundary.test.ts</code> fails the build when it does.</span>
|
|
567
|
+
</div>
|
|
568
|
+
<div id="nonSpecGrid" class="rtm-container"></div>
|
|
525
569
|
</div>
|
|
526
|
-
<div id="legacyGrid" class="rtm-container">Loading unmapped specifications...</div>
|
|
527
570
|
</div>
|
|
528
571
|
|
|
529
572
|
<div id="heatmapTab" style="display: none;">
|
|
@@ -682,12 +725,32 @@ function renderDashboardHtml() {
|
|
|
682
725
|
|
|
683
726
|
// Specs excluded from both axes get their own surface. Previously 47 documents were invisible
|
|
684
727
|
// on every tab while still sitting in the headline denominator, which read as 0% coverage.
|
|
685
|
-
|
|
686
|
-
|
|
728
|
+
// @implements A-SPEC-219.1 — two populations, two destinations. Summing them is what misled:
|
|
729
|
+
// "49" read as "49 to clean up" while 23 of them will never become specs.
|
|
730
|
+
const SPEC_KINDS_UI = ['REQ', 'H-SPEC', 'A-SPEC', 'C-SPEC', 'T-SPEC'];
|
|
731
|
+
const kindOfId = (id) => (String(id).match(/^([A-Za-z-]+?)-\d/) || [null, 'UNKNOWN'])[1];
|
|
732
|
+
|
|
733
|
+
function renderUnmappedGrid(specs) {
|
|
734
|
+
const outside = specs.filter(s => s.legacyStatus || SPEC_KINDS_UI.indexOf(kindOfId(s.id)) === -1);
|
|
735
|
+
renderDocGroup(
|
|
736
|
+
document.getElementById('unmappedGrid'),
|
|
737
|
+
outside.filter(s => SPEC_KINDS_UI.indexOf(kindOfId(s.id)) !== -1),
|
|
738
|
+
'Every spec uses the canonical status vocabulary.',
|
|
739
|
+
);
|
|
740
|
+
// @implements A-SPEC-223 — an absent category is not explained. T-SPEC-219.1 asked for this
|
|
741
|
+
// as a boundary case and it was never implemented; the store is now empty of such documents,
|
|
742
|
+
// so the banner would describe something that does not exist.
|
|
743
|
+
const nonSpec = outside.filter(s => SPEC_KINDS_UI.indexOf(kindOfId(s.id)) === -1);
|
|
744
|
+
const section = document.getElementById('nonSpecSection');
|
|
745
|
+
if (section) section.style.display = nonSpec.length > 0 ? 'block' : 'none';
|
|
746
|
+
renderDocGroup(document.getElementById('nonSpecGrid'), nonSpec, '');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderDocGroup(container, rows, emptyMessage) {
|
|
687
750
|
if (!container) return;
|
|
688
|
-
const legacy =
|
|
751
|
+
const legacy = rows;
|
|
689
752
|
if (legacy.length === 0) {
|
|
690
|
-
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">
|
|
753
|
+
container.innerHTML = '<div style="color:#94a3b8; font-size:14px;">' + emptyMessage + '</div>';
|
|
691
754
|
return;
|
|
692
755
|
}
|
|
693
756
|
const byKind = new Map();
|
|
@@ -744,12 +807,17 @@ function renderDashboardHtml() {
|
|
|
744
807
|
allFiles = data.files || [];
|
|
745
808
|
const ap = data.approval || { pct: 0, approved: 0, total: 0 };
|
|
746
809
|
const im = data.implementation || { pct: 0, anchored: 0, total: 0 };
|
|
747
|
-
const
|
|
810
|
+
const um = data.unmapped || { count: 0, byKind: {} };
|
|
811
|
+
const ns = data.nonSpec || { count: 0, byKind: {} };
|
|
748
812
|
document.getElementById('approvalStat').innerText = ap.pct + '% (' + ap.approved + '/' + ap.total + ')';
|
|
749
813
|
document.getElementById('implStat').innerText = im.pct + '% (' + im.anchored + '/' + im.total + ')';
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
814
|
+
const umKinds = Object.keys(um.byKind || {}).map(k => k + ' ' + um.byKind[k]).join(' · ');
|
|
815
|
+
const nsKinds = Object.keys(ns.byKind || {}).map(k => k + ' ' + ns.byKind[k]).join(' · ');
|
|
816
|
+
document.getElementById('unmappedStat').innerText = um.count + ' to clean up' + (umKinds ? ' (' + umKinds + ')' : '');
|
|
817
|
+
const nsChip = document.getElementById('nonSpecChip');
|
|
818
|
+
if (nsChip) nsChip.style.display = ns.count > 0 ? 'inline-flex' : 'none';
|
|
819
|
+
document.getElementById('nonSpecStat').innerText = ns.count + ' not specs' + (nsKinds ? ' (' + nsKinds + ')' : '');
|
|
820
|
+
renderUnmappedGrid(data.specs || []);
|
|
753
821
|
renderReqQuantitativeGrid(allSpecs, allFiles);
|
|
754
822
|
}
|
|
755
823
|
} catch (err) {
|
|
@@ -2057,6 +2125,28 @@ const CANONICAL_STATUSES = new Set(['draft', 'review', 'approved', 'outdated']);
|
|
|
2057
2125
|
function isCanonicalStatus(status) {
|
|
2058
2126
|
return typeof status === 'string' && CANONICAL_STATUSES.has(status);
|
|
2059
2127
|
}
|
|
2128
|
+
/**
|
|
2129
|
+
* The kinds that are governed specs at all.
|
|
2130
|
+
*
|
|
2131
|
+
* @implements A-SPEC-219.1
|
|
2132
|
+
* Held here rather than imported from SPEC_TYPES because REQ is a spec kind that SPEC_TYPES does
|
|
2133
|
+
* list — the distinction this set draws is "document in the spec store that the pipeline governs"
|
|
2134
|
+
* versus "document that lives there but never will", and JOB is the whole of the second group.
|
|
2135
|
+
*/
|
|
2136
|
+
exports.SPEC_KINDS = new Set(['REQ', 'H-SPEC', 'A-SPEC', 'C-SPEC', 'T-SPEC']);
|
|
2137
|
+
/**
|
|
2138
|
+
* Drop retired documents before anything reads the corpus.
|
|
2139
|
+
*
|
|
2140
|
+
* @implements A-SPEC-222.2
|
|
2141
|
+
* A-SPEC-222.1 excluded `outdated` from `isGoverned` and claimed every other check would follow.
|
|
2142
|
+
* `tspec-mirror.test.ts` refuted that, and the spec was corrected to name one known site. Measured
|
|
2143
|
+
* 2026-08-22 it was three: /api/rtm exposed 132 retired ids, the heatmap 108, the graph 132 — a
|
|
2144
|
+
* screen still pointing at documents retired the day before. Stated once here so the three sites
|
|
2145
|
+
* cannot drift, with the suite enumerating the endpoints so a fourth cannot be added silently.
|
|
2146
|
+
*/
|
|
2147
|
+
function activeSpecs(specs) {
|
|
2148
|
+
return specs.filter((s) => s.status !== 'outdated');
|
|
2149
|
+
}
|
|
2060
2150
|
/** `A-SPEC-219.1` -> `A-SPEC`, `REQ-021` -> `REQ`. */
|
|
2061
2151
|
function specKindOf(id) {
|
|
2062
2152
|
const m = /^([A-Za-z-]+?)-\d/.exec(String(id ?? ''));
|
|
@@ -2,12 +2,28 @@ import { Spec } from './spec-parser';
|
|
|
2
2
|
/**
|
|
3
3
|
* Sections that still hold nothing but a generated placeholder.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - `reverse_draft` writes `TODO — <prompt>` plus the INFERRED marker comment. Both are
|
|
7
|
-
* required together: 11 of this repository's 257 approved specs use the word "TODO" in
|
|
8
|
-
* legitimate prose ("남아 있는 TODO 주석을 제거한다"), and a word search refuses every one.
|
|
5
|
+
* Three shapes are judged, and the last two are a UNION rather than a replacement:
|
|
9
6
|
* - `spec_create` writes the bare literal `TODO` (`FIELD_PLACEHOLDER`) into each required section.
|
|
10
|
-
*
|
|
7
|
+
* Unambiguous on its own — an entire section equal to that string is never prose.
|
|
8
|
+
* - `reverse_draft` writes `TODO — <prompt>` plus the INFERRED marker. Requiring both together is
|
|
9
|
+
* what lets a section that is FILLED with scan evidence and still carries a TODO count as
|
|
10
|
+
* unfinished (`evidenceThenTodo`, the main brownfield shape REQ-181 created).
|
|
11
|
+
* - A `TODO` line with nothing but furniture around it, marker or no marker.
|
|
12
|
+
*
|
|
13
|
+
* @implements A-SPEC-240
|
|
14
|
+
* WHY THE THIRD SHAPE EXISTS. Measured 2026-08-23 against the installed package: an H-SPEC whose
|
|
15
|
+
* eight prose sections all read `TODO — 사람이 쓸 것` — title and owner placeholders too — received
|
|
16
|
+
* `status: approved` and an `approved_digest`. The marker was absent, so nothing looked. `INFERRED`
|
|
17
|
+
* is an HTML comment: invisible when rendered, and easy to strip while editing the section around
|
|
18
|
+
* it. Keying the judgement on it means the protection can vanish silently.
|
|
19
|
+
*
|
|
20
|
+
* WHY IT IS A UNION AND NOT A REPLACEMENT. The first cut replaced the marker rule with the emptiness
|
|
21
|
+
* rule, and `evidenceThenTodo` walked through it — a file list reads as substance. Measured across
|
|
22
|
+
* 482 approved specs the two rules disagree on nothing, so the union costs no false refusals while
|
|
23
|
+
* each covers what the other cannot.
|
|
24
|
+
*
|
|
25
|
+
* The word "TODO" in ordinary prose is still safe: 37 approved sections use it that way
|
|
26
|
+
* ("남아 있는 TODO 주석을 제거한다"), and every one carries real content beside it.
|
|
11
27
|
*/
|
|
12
28
|
export declare function placeholderSections(spec: Spec): string[];
|
|
13
29
|
/**
|
|
@@ -79,15 +79,56 @@ const stripFences = (s) => {
|
|
|
79
79
|
* emptiness matters most.
|
|
80
80
|
*/
|
|
81
81
|
const TODO_LINE = /^TODO\s*[—-]/m;
|
|
82
|
+
/**
|
|
83
|
+
* Furniture a generator writes around the prose a human still owes.
|
|
84
|
+
*
|
|
85
|
+
* @implements A-SPEC-240
|
|
86
|
+
* Deliberately a closed, short list: an HTML comment (the markers), the reverse-engineering note,
|
|
87
|
+
* and the prompt line itself. Lengthening it widens what counts as "empty", which is a decision to
|
|
88
|
+
* refuse more documents — not one to make without measuring.
|
|
89
|
+
*/
|
|
90
|
+
const FURNITURE = [/^TODO\s*[—-]/, /^NOTE: this spec was REVERSE-ENGINEERED/];
|
|
91
|
+
/**
|
|
92
|
+
* Is there anything left once the generator's furniture is removed?
|
|
93
|
+
*
|
|
94
|
+
* @implements A-SPEC-240
|
|
95
|
+
* A `TODO` line is the precondition for asking at all — see `placeholderSections`. What counts as
|
|
96
|
+
* adequate prose is a question a machine cannot answer, so a short but genuine section is left
|
|
97
|
+
* alone; only a section that is nothing BUT scaffolding is judged empty.
|
|
98
|
+
*/
|
|
99
|
+
function hasNoSubstance(text) {
|
|
100
|
+
const rest = stripFences(text)
|
|
101
|
+
.replace(/<!--[\s\S]*?-->/g, '')
|
|
102
|
+
.split('\n')
|
|
103
|
+
.map((l) => l.trim())
|
|
104
|
+
.filter((l) => l !== '' && !FURNITURE.some((re) => re.test(l)));
|
|
105
|
+
return rest.length === 0;
|
|
106
|
+
}
|
|
82
107
|
/**
|
|
83
108
|
* Sections that still hold nothing but a generated placeholder.
|
|
84
109
|
*
|
|
85
|
-
*
|
|
86
|
-
* - `reverse_draft` writes `TODO — <prompt>` plus the INFERRED marker comment. Both are
|
|
87
|
-
* required together: 11 of this repository's 257 approved specs use the word "TODO" in
|
|
88
|
-
* legitimate prose ("남아 있는 TODO 주석을 제거한다"), and a word search refuses every one.
|
|
110
|
+
* Three shapes are judged, and the last two are a UNION rather than a replacement:
|
|
89
111
|
* - `spec_create` writes the bare literal `TODO` (`FIELD_PLACEHOLDER`) into each required section.
|
|
90
|
-
*
|
|
112
|
+
* Unambiguous on its own — an entire section equal to that string is never prose.
|
|
113
|
+
* - `reverse_draft` writes `TODO — <prompt>` plus the INFERRED marker. Requiring both together is
|
|
114
|
+
* what lets a section that is FILLED with scan evidence and still carries a TODO count as
|
|
115
|
+
* unfinished (`evidenceThenTodo`, the main brownfield shape REQ-181 created).
|
|
116
|
+
* - A `TODO` line with nothing but furniture around it, marker or no marker.
|
|
117
|
+
*
|
|
118
|
+
* @implements A-SPEC-240
|
|
119
|
+
* WHY THE THIRD SHAPE EXISTS. Measured 2026-08-23 against the installed package: an H-SPEC whose
|
|
120
|
+
* eight prose sections all read `TODO — 사람이 쓸 것` — title and owner placeholders too — received
|
|
121
|
+
* `status: approved` and an `approved_digest`. The marker was absent, so nothing looked. `INFERRED`
|
|
122
|
+
* is an HTML comment: invisible when rendered, and easy to strip while editing the section around
|
|
123
|
+
* it. Keying the judgement on it means the protection can vanish silently.
|
|
124
|
+
*
|
|
125
|
+
* WHY IT IS A UNION AND NOT A REPLACEMENT. The first cut replaced the marker rule with the emptiness
|
|
126
|
+
* rule, and `evidenceThenTodo` walked through it — a file list reads as substance. Measured across
|
|
127
|
+
* 482 approved specs the two rules disagree on nothing, so the union costs no false refusals while
|
|
128
|
+
* each covers what the other cannot.
|
|
129
|
+
*
|
|
130
|
+
* The word "TODO" in ordinary prose is still safe: 37 approved sections use it that way
|
|
131
|
+
* ("남아 있는 TODO 주석을 제거한다"), and every one carries real content beside it.
|
|
91
132
|
*/
|
|
92
133
|
function placeholderSections(spec) {
|
|
93
134
|
const out = [];
|
|
@@ -98,7 +139,9 @@ function placeholderSections(spec) {
|
|
|
98
139
|
continue;
|
|
99
140
|
}
|
|
100
141
|
const prose = stripFences(text);
|
|
101
|
-
if (TODO_LINE.test(prose)
|
|
142
|
+
if (!TODO_LINE.test(prose))
|
|
143
|
+
continue;
|
|
144
|
+
if (prose.includes(draft_1.INFERRED) || hasNoSubstance(text))
|
|
102
145
|
out.push(name);
|
|
103
146
|
}
|
|
104
147
|
return out;
|
|
@@ -35,6 +35,20 @@ export type FormatState =
|
|
|
35
35
|
};
|
|
36
36
|
/** Frontmatter key that preserves the pre-upgrade status. Named once so writer and reader agree. */
|
|
37
37
|
export declare const LEGACY_STATUS_FIELD = "legacy_status";
|
|
38
|
+
/**
|
|
39
|
+
* Where a legacy `depends_on` string is kept verbatim.
|
|
40
|
+
*
|
|
41
|
+
* @implements A-SPEC-221
|
|
42
|
+
* The old format wrote the parent as `REQ-001:<hash>`. The parser only understood arrays, so the
|
|
43
|
+
* whole value became `[]` — measured on H-SPEC-001, the upgrade destroyed the parent edge and then
|
|
44
|
+
* reported `orphan: H-SPEC requires a parent of type REQ` about the edge it had just removed.
|
|
45
|
+
*
|
|
46
|
+
* The digest is preserved rather than promoted. Writing it into `parent_digests` would assert that
|
|
47
|
+
* a `version_token`-era value is an approval digest, and there is no evidence the two are the same
|
|
48
|
+
* computation — asserting it anyway is the fabrication this repository keeps removing. Verbatim
|
|
49
|
+
* preservation leaves the judgement to a person who can actually make it.
|
|
50
|
+
*/
|
|
51
|
+
export declare const LEGACY_DEPENDS_FIELD = "legacy_depends_on";
|
|
38
52
|
/**
|
|
39
53
|
* Classify a document by FORMAT, not by validity.
|
|
40
54
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LEGACY_STATUS_FIELD = void 0;
|
|
3
|
+
exports.LEGACY_DEPENDS_FIELD = exports.LEGACY_STATUS_FIELD = void 0;
|
|
4
4
|
exports.formatStateOf = formatStateOf;
|
|
5
5
|
exports.legacyMessage = legacyMessage;
|
|
6
6
|
exports.upgradePlan = upgradePlan;
|
|
@@ -8,6 +8,20 @@ exports.upgradedFrontmatter = upgradedFrontmatter;
|
|
|
8
8
|
const spec_types_1 = require("./spec-types");
|
|
9
9
|
/** Frontmatter key that preserves the pre-upgrade status. Named once so writer and reader agree. */
|
|
10
10
|
exports.LEGACY_STATUS_FIELD = 'legacy_status';
|
|
11
|
+
/**
|
|
12
|
+
* Where a legacy `depends_on` string is kept verbatim.
|
|
13
|
+
*
|
|
14
|
+
* @implements A-SPEC-221
|
|
15
|
+
* The old format wrote the parent as `REQ-001:<hash>`. The parser only understood arrays, so the
|
|
16
|
+
* whole value became `[]` — measured on H-SPEC-001, the upgrade destroyed the parent edge and then
|
|
17
|
+
* reported `orphan: H-SPEC requires a parent of type REQ` about the edge it had just removed.
|
|
18
|
+
*
|
|
19
|
+
* The digest is preserved rather than promoted. Writing it into `parent_digests` would assert that
|
|
20
|
+
* a `version_token`-era value is an approval digest, and there is no evidence the two are the same
|
|
21
|
+
* computation — asserting it anyway is the fabrication this repository keeps removing. Verbatim
|
|
22
|
+
* preservation leaves the judgement to a person who can actually make it.
|
|
23
|
+
*/
|
|
24
|
+
exports.LEGACY_DEPENDS_FIELD = 'legacy_depends_on';
|
|
11
25
|
/**
|
|
12
26
|
* Classify a document by FORMAT, not by validity.
|
|
13
27
|
*
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One non-functional obligation, read from an H-SPEC's `## Non-Functional`.
|
|
3
|
+
*
|
|
4
|
+
* @implements A-SPEC-234
|
|
5
|
+
* WHY THIS EXISTS. `## Non-Functional` is a REQUIRED section — measured 2026-08-22, all 105 approved
|
|
6
|
+
* H-SPECs in this repository have one filled in. Nothing read it. The T-SPEC approval gate asks only
|
|
7
|
+
* for the four input quadrants (Normal/Corner/Negative/Boundary), which are an input-selection
|
|
8
|
+
* taxonomy: equivalence partitioning, boundary values, negative cases. Concurrency, resource
|
|
9
|
+
* exhaustion and partial failure are not kinds of INPUT but kinds of EXECUTION CONDITION, so they
|
|
10
|
+
* never belonged to that question and no gate ever asked about them.
|
|
11
|
+
*
|
|
12
|
+
* The cost is on record in this session: an H-SPEC declared a cost obligation, its T-SPEC was green
|
|
13
|
+
* on all four quadrants, and 10,558ms shipped for a 2KB file.
|
|
14
|
+
*/
|
|
15
|
+
export interface NonFunctionalObligation {
|
|
16
|
+
/** Author-chosen, lower-cased. The vocabulary is NOT fixed — see `parseNonFunctional`. */
|
|
17
|
+
tag: string;
|
|
18
|
+
/** The obligation as written, minus the tag. */
|
|
19
|
+
text: string;
|
|
20
|
+
/** The line as written, so a report can quote it. */
|
|
21
|
+
line: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the obligations out of a `## Non-Functional` section.
|
|
25
|
+
*
|
|
26
|
+
* @implements A-SPEC-234
|
|
27
|
+
* A malformed list item is RETURNED, never dropped. A typo that silently parses to nothing reads as
|
|
28
|
+
* "the rule passed" — the shape this repository has paid for in A-SPEC-191 §17, §29 and again in
|
|
29
|
+
* A-SPEC-222.2. Prose and blank lines are neither: a section is allowed to explain itself.
|
|
30
|
+
*
|
|
31
|
+
* THE TAG VOCABULARY IS NOT FIXED, AND THAT IS THE POINT. A closed set of allowed tags becomes a
|
|
32
|
+
* thing to maintain and then falls behind — the enumeration-chasing this repository already rejected
|
|
33
|
+
* for tool names (`write-target.ts`). Whatever the author writes IS the vocabulary; the machine
|
|
34
|
+
* judges CORRESPONDENCE between an H-SPEC tag and a T-SPEC tag, never meaning.
|
|
35
|
+
*
|
|
36
|
+
* WHAT THIS DOES NOT SEE. Whether the obligation is a good one. Whether the case tagged with it
|
|
37
|
+
* actually exercises it — a case reading `- [perf] Given nothing When nothing Then nothing` counts
|
|
38
|
+
* as met. Obligations written as prose without a tag (the other 105 documents). Those are human
|
|
39
|
+
* judgements, and simulating them would recreate the "documented but unenforced" state this slice
|
|
40
|
+
* exists to remove.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseNonFunctional(section: unknown): {
|
|
43
|
+
obligations: NonFunctionalObligation[];
|
|
44
|
+
malformed: string[];
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The obligation tags a T-SPEC's CASES carry.
|
|
48
|
+
*
|
|
49
|
+
* @implements A-SPEC-234
|
|
50
|
+
* Only the four case sections count, and the section names are read from the type table rather than
|
|
51
|
+
* spelled here — a second spelling of a location is a second thing to keep in step, and this
|
|
52
|
+
* repository has already paid for that twice (`isTestFile` drifted into two copies; a restated
|
|
53
|
+
* section-splitting regex returned an empty section while the check using it went green).
|
|
54
|
+
*
|
|
55
|
+
* A tag anywhere else in the document states an INTENTION. Only a case is evidence that something
|
|
56
|
+
* was tested.
|
|
57
|
+
*/
|
|
58
|
+
export declare function caseObligationTags(sections: unknown): Set<string>;
|
|
59
|
+
/**
|
|
60
|
+
* Obligations an H-SPEC declares that no child T-SPEC case answers.
|
|
61
|
+
*
|
|
62
|
+
* @implements A-SPEC-234
|
|
63
|
+
* Matching is EXACT after lower-casing. A prefix match would let `perf` be satisfied by
|
|
64
|
+
* `performance`, and a false "met" is a gate switched off that says nothing — the same asymmetry
|
|
65
|
+
* that made `call` targets exact in `forbidden-edges.ts`.
|
|
66
|
+
*
|
|
67
|
+
* `malformed` lines are NOT reported here. Flagging syntax errors and judging fulfilment are
|
|
68
|
+
* different questions; mixed together, one typo invents an obligation nobody declared.
|
|
69
|
+
*/
|
|
70
|
+
export declare function unmetObligations(nonFunctional: unknown, tspecSections: unknown[]): NonFunctionalObligation[];
|