@holmes-lab/holmes-kit 0.20.2 → 0.21.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 +48 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/governance/constitution.d.ts +11 -0
- package/dist/holmes/governance/constitution.js +15 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +10 -1
- package/dist/holmes/guardrail/impact-gate.js +19 -0
- package/dist/holmes/guardrail/risk-classifier.d.ts +1 -0
- package/dist/holmes/guardrail/risk-classifier.js +26 -3
- package/dist/holmes/hooks/stop.d.ts +9 -0
- package/dist/holmes/hooks/stop.js +63 -1
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +1 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +18 -1
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +4 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +16 -1
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +2 -1
- package/dist/holmes/mcp/handlers/operator-inspection.js +25 -3
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +1 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +29 -1
- package/dist/holmes/mcp/handlers.d.ts +4 -1
- package/dist/holmes/mcp/handlers.js +4 -0
- package/dist/holmes/rtm/anchor-comment.d.ts +2 -0
- package/dist/holmes/rtm/anchor-comment.js +8 -0
- package/dist/holmes/rtm/file-anchors.d.ts +9 -0
- package/dist/holmes/rtm/file-anchors.js +128 -0
- package/dist/holmes/rtm/ftt-fulfilment.d.ts +42 -0
- package/dist/holmes/rtm/ftt-fulfilment.js +195 -0
- package/dist/holmes/rtm/known-defects.d.ts +26 -0
- package/dist/holmes/rtm/known-defects.js +77 -0
- package/dist/holmes/rtm/link-census.d.ts +61 -0
- package/dist/holmes/rtm/link-census.js +90 -0
- package/dist/holmes/rtm/trace-gaps.d.ts +20 -0
- package/dist/holmes/rtm/trace-gaps.js +64 -0
- package/dist/holmes/server/dashboard-launcher.d.ts +20 -0
- package/dist/holmes/server/dashboard-launcher.js +24 -1
- package/dist/holmes/server/dashboard.js +40 -2
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +11 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +4 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-655
|
|
3
|
+
/**
|
|
4
|
+
* RTM link census — which approved A-SPECs the code graph cannot see, and why.
|
|
5
|
+
*
|
|
6
|
+
* Measured 2026-09-16 on this repository: the dashboard census said coveragePct 100 while only
|
|
7
|
+
* 558/630 approved A-SPECs (88.6%) carried an `implements` edge; the other 72 were anchored only
|
|
8
|
+
* in test files the scanner deliberately excludes. "Unknown" and "absent" were the same number.
|
|
9
|
+
* The census here keeps the graph's contracts (tests and non-AST files stay OUT of the graph —
|
|
10
|
+
* cpg-scanner.ts:141-147 is a sealed decision) and classifies the gap instead:
|
|
11
|
+
*
|
|
12
|
+
* test-only anchored only in test files (a characterization / docs / CI slice — legitimate)
|
|
13
|
+
* file-anchor anchored only in files the injector can write but the indexer never parses
|
|
14
|
+
* test-and-file both of the above
|
|
15
|
+
* weak-anchor anchored nowhere, but a file inside the spec's own Files to Touch names the id
|
|
16
|
+
* none no trace at all
|
|
17
|
+
*
|
|
18
|
+
* The weak-anchor predicate is deliberately NARROW. The backlog card that proposed it ("any
|
|
19
|
+
* `A-SPEC-N` mention in a comment without a matching @implements") scores 817 false positives and
|
|
20
|
+
* 0 true positives on this repository; the narrowed form scores 0 and 0. Pure: every reader is
|
|
21
|
+
* injected, so the classifier is testable without a filesystem.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.UNLINKED_REASONS = void 0;
|
|
25
|
+
exports.emptyByReason = emptyByReason;
|
|
26
|
+
exports.pct1 = pct1;
|
|
27
|
+
exports.weakMentionsFor = weakMentionsFor;
|
|
28
|
+
exports.linkCensus = linkCensus;
|
|
29
|
+
exports.UNLINKED_REASONS = ['test-only', 'file-anchor', 'test-and-file', 'weak-anchor', 'none'];
|
|
30
|
+
function emptyByReason() {
|
|
31
|
+
return { 'test-only': 0, 'file-anchor': 0, 'test-and-file': 0, 'weak-anchor': 0, none: 0 };
|
|
32
|
+
}
|
|
33
|
+
/** Percentage with one decimal; a zero denominator reads as 0, never NaN. */
|
|
34
|
+
function pct1(num, den) {
|
|
35
|
+
return den > 0 ? Math.round((num / den) * 1000) / 10 : 0;
|
|
36
|
+
}
|
|
37
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
|
+
/**
|
|
39
|
+
* Files-to-Touch paths whose text names EXACTLY `id`. `A-SPEC-12` does not match `A-SPEC-120` or
|
|
40
|
+
* `A-SPEC-12.1`. A reader returning null (unreadable, absent) contributes nothing — an unreadable
|
|
41
|
+
* file is neither a mention nor evidence of its absence.
|
|
42
|
+
*/
|
|
43
|
+
function weakMentionsFor(id, fttFiles, readText) {
|
|
44
|
+
const re = new RegExp(String.raw `(?<![\w.-])${escapeRe(id)}(?!\w|\.\d)`);
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const rel of fttFiles) {
|
|
47
|
+
const text = readText(rel);
|
|
48
|
+
if (text !== null && text !== undefined && re.test(text))
|
|
49
|
+
out.push(rel);
|
|
50
|
+
}
|
|
51
|
+
return [...new Set(out)].sort();
|
|
52
|
+
}
|
|
53
|
+
function invert(map) {
|
|
54
|
+
const byId = new Map();
|
|
55
|
+
for (const [file, ids] of Object.entries(map ?? {})) {
|
|
56
|
+
for (const id of ids ?? []) {
|
|
57
|
+
const list = byId.get(id) ?? [];
|
|
58
|
+
if (!list.includes(file))
|
|
59
|
+
list.push(file);
|
|
60
|
+
byId.set(id, list);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return byId;
|
|
64
|
+
}
|
|
65
|
+
function linkCensus(input) {
|
|
66
|
+
const implemented = new Set(input.implemented);
|
|
67
|
+
const tests = invert(input.testAnchors);
|
|
68
|
+
const files = invert(input.fileAnchors);
|
|
69
|
+
const byReason = emptyByReason();
|
|
70
|
+
const unlinked = [];
|
|
71
|
+
const ids = [...new Set(input.approvedIds)].sort();
|
|
72
|
+
let codeLinked = 0;
|
|
73
|
+
for (const id of ids) {
|
|
74
|
+
if (implemented.has(id)) {
|
|
75
|
+
codeLinked++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const t = tests.get(id) ?? [];
|
|
79
|
+
const f = files.get(id) ?? [];
|
|
80
|
+
const mentions = t.length === 0 && f.length === 0 ? [...(input.weakMentions?.[id] ?? [])].sort() : [];
|
|
81
|
+
const reason = t.length && f.length ? 'test-and-file'
|
|
82
|
+
: t.length ? 'test-only'
|
|
83
|
+
: f.length ? 'file-anchor'
|
|
84
|
+
: mentions.length ? 'weak-anchor'
|
|
85
|
+
: 'none';
|
|
86
|
+
byReason[reason]++;
|
|
87
|
+
unlinked.push({ id, reason, anchoredIn: [...t, ...f].sort(), mentionedIn: mentions });
|
|
88
|
+
}
|
|
89
|
+
return { total: ids.length, codeLinked, codeLinkedPct: pct1(codeLinked, ids.length), unlinked, byReason };
|
|
90
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface UnlinkedSpec {
|
|
2
|
+
id: string;
|
|
3
|
+
ftt: string;
|
|
4
|
+
}
|
|
5
|
+
export interface TraceGap {
|
|
6
|
+
id: string;
|
|
7
|
+
file: string;
|
|
8
|
+
summary?: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** Approved A-SPECs no scanned (production) file anchors, with their Files-to-Touch text; sorted by id. */
|
|
11
|
+
export declare function unlinkedApproved(specs: ReadonlyArray<{
|
|
12
|
+
id: string;
|
|
13
|
+
type: string;
|
|
14
|
+
status: string;
|
|
15
|
+
sections?: Record<string, string>;
|
|
16
|
+
}>, scanned: ReadonlyArray<{
|
|
17
|
+
implementsSpecs?: string[];
|
|
18
|
+
}>): UnlinkedSpec[];
|
|
19
|
+
/** (id, file) for every changed file an unlinked spec declares; sorted by id then file; no duplicates. */
|
|
20
|
+
export declare function traceGaps(changedFiles: string[], unlinked: UnlinkedSpec[], summaryOf?: (id: string) => string | null): TraceGap[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.unlinkedApproved = unlinkedApproved;
|
|
4
|
+
exports.traceGaps = traceGaps;
|
|
5
|
+
// @implements A-SPEC-658
|
|
6
|
+
/**
|
|
7
|
+
* Trace gaps — the approved A-SPECs the graph cannot see when a file they declare changes.
|
|
8
|
+
*
|
|
9
|
+
* `rtm_impact` walks `implements` edges, and an approved A-SPEC that no production source anchors
|
|
10
|
+
* has none: it can never appear in any impact set, however directly the change concerns it. That
|
|
11
|
+
* is not "no impact", it is "cannot see" — and until now the two had the same shape (silence).
|
|
12
|
+
* Measured 2026-09-17 on this repository: 24 of the 72 unlinked approved specs (REQ-655's census)
|
|
13
|
+
* declare 34 scanned production files in their Files to Touch — change `hooks/pre-tool-use.ts`
|
|
14
|
+
* today and A-SPEC-202 / A-SPEC-250 stay unmentioned. The jarvis incident was the same shape
|
|
15
|
+
* (`_fmt_at` changed, REQ-347 unreported).
|
|
16
|
+
*
|
|
17
|
+
* Pure: the population is the same one REQ-655 counts (approved A-SPEC minus scanned anchors), the
|
|
18
|
+
* declared files come from REQ-656's item parser (REQ-654's token rule, globs excluded), and the
|
|
19
|
+
* result is an intersection. Information for `rtm_impact`, a `widen` reason for the impact gate;
|
|
20
|
+
* never a verdict on its own.
|
|
21
|
+
*/
|
|
22
|
+
const ftt_fulfilment_1 = require("./ftt-fulfilment");
|
|
23
|
+
/** Approved A-SPECs no scanned (production) file anchors, with their Files-to-Touch text; sorted by id. */
|
|
24
|
+
function unlinkedApproved(specs, scanned) {
|
|
25
|
+
const anchored = new Set();
|
|
26
|
+
for (const f of scanned)
|
|
27
|
+
for (const id of f.implementsSpecs ?? [])
|
|
28
|
+
anchored.add(id);
|
|
29
|
+
return specs
|
|
30
|
+
.filter((s) => s.type === 'A-SPEC' && s.status === 'approved' && !anchored.has(s.id))
|
|
31
|
+
.map((s) => ({ id: s.id, ftt: String(s.sections?.['Files to Touch'] ?? '') }))
|
|
32
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
33
|
+
}
|
|
34
|
+
/** (id, file) for every changed file an unlinked spec declares; sorted by id then file; no duplicates. */
|
|
35
|
+
function traceGaps(changedFiles, unlinked, summaryOf) {
|
|
36
|
+
const files = new Set(changedFiles.map((p) => p.replace(/\\/g, '/')));
|
|
37
|
+
if (files.size === 0)
|
|
38
|
+
return [];
|
|
39
|
+
const out = [];
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
for (const u of unlinked) {
|
|
42
|
+
for (const item of (0, ftt_fulfilment_1.fttItems)(u.ftt)) {
|
|
43
|
+
if (!files.has(item.path))
|
|
44
|
+
continue;
|
|
45
|
+
const key = `${u.id} ${item.path}`;
|
|
46
|
+
if (seen.has(key))
|
|
47
|
+
continue;
|
|
48
|
+
seen.add(key);
|
|
49
|
+
const gap = { id: u.id, file: item.path };
|
|
50
|
+
if (summaryOf) {
|
|
51
|
+
let s = null;
|
|
52
|
+
try {
|
|
53
|
+
s = summaryOf(`SPEC:${u.id}`);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
s = null;
|
|
57
|
+
}
|
|
58
|
+
gap.summary = s;
|
|
59
|
+
}
|
|
60
|
+
out.push(gap);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
64
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UnlinkedReason } from '../rtm/link-census';
|
|
1
2
|
/** Honesty summary of what a launched dashboard shows — derived from the endpoints it serves. */
|
|
2
3
|
export interface DashboardCensus {
|
|
3
4
|
reqCount: number;
|
|
@@ -13,6 +14,25 @@ export interface DashboardCensus {
|
|
|
13
14
|
* coveragePct never divides by zero.
|
|
14
15
|
*/
|
|
15
16
|
export declare function dashboardCensus(rtm: any, heatmap: any): DashboardCensus;
|
|
17
|
+
/** The honesty census plus the code-link axis. `dashboardCensus` above stays byte-identical (pinned). */
|
|
18
|
+
export interface DashboardCensusExtended extends DashboardCensus {
|
|
19
|
+
codeLinkedPct: number;
|
|
20
|
+
codeLinkedCount: number;
|
|
21
|
+
unlinkedCount: number;
|
|
22
|
+
unlinkedByReason: Record<UnlinkedReason, number>;
|
|
23
|
+
excluded: {
|
|
24
|
+
total: number;
|
|
25
|
+
retired: number;
|
|
26
|
+
unmapped: number;
|
|
27
|
+
nonSpec: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Pure: the pinned census plus the code-link axis from `/api/rtm`'s `linkCensus`, `coveragePct`
|
|
32
|
+
* with one decimal, and the documents the axes exclude (retired / unmapped / non-spec) counted
|
|
33
|
+
* with their reasons. A payload without `linkCensus` (an older server) folds to zeros, never throws.
|
|
34
|
+
*/
|
|
35
|
+
export declare function dashboardCensusExtended(rtm: any, heatmap: any): DashboardCensusExtended;
|
|
16
36
|
export interface LaunchResult {
|
|
17
37
|
url: string;
|
|
18
38
|
port: number;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// @implements A-SPEC-545.3
|
|
3
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
3
|
exports.dashboardCensus = dashboardCensus;
|
|
4
|
+
exports.dashboardCensusExtended = dashboardCensusExtended;
|
|
5
5
|
exports.ensureDashboard = ensureDashboard;
|
|
6
6
|
exports._resetLauncher = _resetLauncher;
|
|
7
7
|
exports._stopAll = _stopAll;
|
|
8
|
+
// @implements A-SPEC-545.3
|
|
9
|
+
const link_census_1 = require("../rtm/link-census");
|
|
8
10
|
/**
|
|
9
11
|
* @implements A-SPEC-545.3
|
|
10
12
|
* Pure: fold the /api/rtm and /api/rtm/heatmap payloads into a census. Missing fields read as 0/false;
|
|
@@ -24,6 +26,27 @@ function dashboardCensus(rtm, heatmap) {
|
|
|
24
26
|
findingsScanned: heatmap?.findingsScanned === true,
|
|
25
27
|
};
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Pure: the pinned census plus the code-link axis from `/api/rtm`'s `linkCensus`, `coveragePct`
|
|
31
|
+
* with one decimal, and the documents the axes exclude (retired / unmapped / non-spec) counted
|
|
32
|
+
* with their reasons. A payload without `linkCensus` (an older server) folds to zeros, never throws.
|
|
33
|
+
*/
|
|
34
|
+
function dashboardCensusExtended(rtm, heatmap) {
|
|
35
|
+
const base = dashboardCensus(rtm, heatmap);
|
|
36
|
+
const lc = rtm?.linkCensus;
|
|
37
|
+
const retired = rtm?.retired?.count ?? 0;
|
|
38
|
+
const unmapped = rtm?.unmapped?.count ?? 0;
|
|
39
|
+
const nonSpec = rtm?.nonSpec?.count ?? 0;
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
coveragePct: (0, link_census_1.pct1)(heatmap?.completeCount ?? 0, heatmap?.pipelineCount ?? 0),
|
|
43
|
+
codeLinkedPct: typeof lc?.codeLinkedPct === 'number' ? lc.codeLinkedPct : 0,
|
|
44
|
+
codeLinkedCount: typeof lc?.codeLinked === 'number' ? lc.codeLinked : 0,
|
|
45
|
+
unlinkedCount: Array.isArray(lc?.unlinked) ? lc.unlinked.length : 0,
|
|
46
|
+
unlinkedByReason: { ...(0, link_census_1.emptyByReason)(), ...(lc?.byReason ?? {}) },
|
|
47
|
+
excluded: { total: retired + unmapped + nonSpec, retired, unmapped, nonSpec },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
27
50
|
const live = new Map();
|
|
28
51
|
/**
|
|
29
52
|
* @implements A-SPEC-545.3
|
|
@@ -55,6 +55,9 @@ const ast_store_1 = require("../cpg/foundation/ast-store");
|
|
|
55
55
|
const cfg_1 = require("../cpg/foundation/cfg");
|
|
56
56
|
const cdg_1 = require("../cpg/foundation/cdg");
|
|
57
57
|
const cfg_view_1 = require("./cfg-view");
|
|
58
|
+
const link_census_1 = require("../rtm/link-census");
|
|
59
|
+
const test_scope_1 = require("../rtm/test-scope");
|
|
60
|
+
const file_anchors_1 = require("../rtm/file-anchors");
|
|
58
61
|
/**
|
|
59
62
|
* Start a lightweight standalone Node.js HTTP server for interactive dashboard & RTM visualization.
|
|
60
63
|
*
|
|
@@ -94,10 +97,44 @@ async function startDashboardServer(options) {
|
|
|
94
97
|
for (const s of f.implementsSpecs)
|
|
95
98
|
implementedSpecIds.add(s);
|
|
96
99
|
}
|
|
100
|
+
// @implements A-SPEC-655 — the code-link axis, with a reason on every unlinked A-SPEC.
|
|
101
|
+
// Tests and non-AST files stay OUT of the graph (cpg-scanner.ts:141-147 is sealed); their
|
|
102
|
+
// anchors are read off-graph so the census can tell "anchored where the graph cannot see"
|
|
103
|
+
// from "no trace at all". Measured 2026-09-16 on this repository: 72 of 630 approved
|
|
104
|
+
// A-SPECs were invisible to the graph and every one of them was a test-only slice.
|
|
105
|
+
const approvedAspecs = specs.filter((s) => s.id.startsWith('A-SPEC') && s.status === 'approved');
|
|
106
|
+
const testAnchors = (0, test_scope_1.scanTestAnchors)(root);
|
|
107
|
+
const fileAnchors = (0, file_anchors_1.scanFileAnchors)(root);
|
|
108
|
+
const anchoredAnywhere = new Set(implementedSpecIds);
|
|
109
|
+
for (const ids of [...Object.values(testAnchors), ...Object.values(fileAnchors)])
|
|
110
|
+
for (const id of ids)
|
|
111
|
+
anchoredAnywhere.add(id);
|
|
112
|
+
const readRel = (rel) => {
|
|
113
|
+
const abs = path.resolve(root, rel);
|
|
114
|
+
if (!abs.startsWith(root + path.sep))
|
|
115
|
+
return null; // a Files-to-Touch line is text, not a path we trust
|
|
116
|
+
try {
|
|
117
|
+
return fs.readFileSync(abs, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const weakMentions = {};
|
|
124
|
+
for (const s of approvedAspecs) {
|
|
125
|
+
if (anchoredAnywhere.has(s.id))
|
|
126
|
+
continue;
|
|
127
|
+
const ftt = String(s.sections?.['Files to Touch'] ?? '');
|
|
128
|
+
const tokens = [...new Set(ftt.match(/[\w@.*-]+(?:\/[\w@.*-]+)+/g) ?? [])].filter((t) => !t.includes('*'));
|
|
129
|
+
weakMentions[s.id] = (0, link_census_1.weakMentionsFor)(s.id, tokens, readRel);
|
|
130
|
+
}
|
|
131
|
+
const census = (0, link_census_1.linkCensus)({ approvedIds: approvedAspecs.map((s) => s.id), implemented: implementedSpecIds, testAnchors, fileAnchors, weakMentions });
|
|
132
|
+
const reasonOf = new Map(census.unlinked.map((u) => [u.id, u.reason]));
|
|
97
133
|
const enrichedSpecs = specs.map((s) => {
|
|
98
134
|
const isA = s.id.startsWith('A-SPEC');
|
|
99
135
|
const covered = isA ? implementedSpecIds.has(s.id) : s.status === 'approved';
|
|
100
|
-
|
|
136
|
+
const reason = reasonOf.get(s.id);
|
|
137
|
+
return { ...s, covered, legacyStatus: !isCanonicalStatus(s.status), ...(reason ? { unlinkedReason: reason } : {}) };
|
|
101
138
|
});
|
|
102
139
|
// Two axes, never one number. Approval (a spec was signed off) and implementation (code is
|
|
103
140
|
// anchored to it) answer different questions, and a spec whose status is outside the
|
|
@@ -156,6 +193,7 @@ async function startDashboardServer(options) {
|
|
|
156
193
|
coveredCount,
|
|
157
194
|
approval,
|
|
158
195
|
implementation,
|
|
196
|
+
linkCensus: census,
|
|
159
197
|
unmapped,
|
|
160
198
|
nonSpec,
|
|
161
199
|
retired,
|
|
@@ -1131,7 +1169,7 @@ function renderDashboardHtml() {
|
|
|
1131
1169
|
<div class="tree-row tree-row-aspec">
|
|
1132
1170
|
<div class="tree-label">🔵 <strong>\${escapeHtml(a.id)}</strong>: \${escapeHtml(a.title || '')}</div>
|
|
1133
1171
|
<div>
|
|
1134
|
-
<span class="neighbor-rel \${a.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${a.covered ? 'COVERED' : '
|
|
1172
|
+
<span class="neighbor-rel \${a.covered ? 'badge-covered' : 'badge-uncovered'}" style="margin-right:8px;">\${a.covered ? 'COVERED' : 'UNLINKED · ' + escapeHtml(a.unlinkedReason || 'none')}</span>
|
|
1135
1173
|
<button class="jump-btn" onclick="focusSpecInGraph('\${escapeHtml(a.id)}')">🕸️ View in Graph</button>
|
|
1136
1174
|
</div>
|
|
1137
1175
|
</div>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@implements A-SPEC-209",
|
|
3
3
|
"name": "@holmes-lab/holmes-kit",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.21.0",
|
|
5
5
|
"description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
|
|
6
6
|
"main": "dist/holmes/mcp/server.js",
|
|
7
7
|
"types": "dist/holmes/mcp/server.d.ts",
|
|
@@ -82,6 +82,13 @@ Criteria · Non-Functional · Assumptions · Open Questions.** 필수 필드: `r
|
|
|
82
82
|
부모 REQ의 Success Criteria가 이 설계로 어떻게 달성되는지가 본문이다. Open Questions는 비워두는
|
|
83
83
|
칸이 아니다 — 아직 결정하지 않은 것을 결정하지 않았다고 적는 곳이고, 닫을 때는 근거와 함께 닫는다.
|
|
84
84
|
|
|
85
|
+
**관측을 신설하거나 좌표를 바꾸는 슬라이스**(계측·원장·카운터·지표·이벤트 기록)는 `## Non-Functional`에
|
|
86
|
+
`- [observability] <무엇을 기록하고 어디서 어떤 키로 읽는가>` 의무를 적는다. 기록 쪽만 검증한 테스트는
|
|
87
|
+
읽는 쪽이 보는 좌표에 없는 기록을 잡지 못한다 — jarvis 실사고(2026-09-08): `kind`가 고정돼 조회가
|
|
88
|
+
구조적으로 0이었고, 그 0 위에서 설계 결정이 두 번 내려졌다. 이 의무는 T-SPEC의 `[observability]`
|
|
89
|
+
케이스와 대응되어야 하며, 대응이 없으면 `review_scope`·`maintenance_analyze`가 `obligationGaps`로
|
|
90
|
+
**보고**한다 — 차단이 아니라 보고다(`obligationGapsFor`; 태그 어휘는 열려 있어 새 필드가 필요 없다).
|
|
91
|
+
|
|
85
92
|
## Files to Touch를 확정하기 전에 — 설계-시점 영향 범위 읽기
|
|
86
93
|
|
|
87
94
|
A-SPEC의 Files to Touch는 **스코프 선언**이고, 그래프는 그 선언이 무엇을 빠뜨리는지 이미 안다.
|
|
@@ -117,6 +124,10 @@ T-SPEC 필수 섹션은 4분면 그대로다: **Normal · Corner · Negative ·
|
|
|
117
124
|
케이스인지**가 드러나게 쓴다. 그리고 frontmatter `depends_on: [대상 A-SPEC]` — 이 간선이 없으면
|
|
118
125
|
승인해도 코드 게이트가 열리지 않는다 — 그때 게이트가 `depends_on`을 지목한다(`promote-slice`의 「T-SPEC 거부는 상태를 구별해 말한다」).
|
|
119
126
|
|
|
127
|
+
H-SPEC에 `[observability]` 의무가 있으면 T-SPEC은 **기록→조회 왕복** 케이스를
|
|
128
|
+
`- [observability] Given … When … Then …`으로 적는다: 쓰기 API를 부르고, **읽는 쪽이 실제로 쓰는 읽기
|
|
129
|
+
API**로 같은 좌표를 조회해 행이 있음을 단언한다. 태그는 줄 시작에서만 인식된다(`nonfunctional.ts`).
|
|
130
|
+
|
|
120
131
|
## 흔한 오해
|
|
121
132
|
|
|
122
133
|
| 오해 | 사실 |
|
|
@@ -57,6 +57,10 @@ holmes는 기계적으로 판별한다: `red-error`로는 red→green 시퀀스
|
|
|
57
57
|
요구한다.
|
|
58
58
|
- `[명예제]` 테스트를 쓰기 전에 **그 테스트를 실패시킬 프로덕션 변경을 한 문장으로 말하라**. 말할
|
|
59
59
|
수 없다면 그 테스트는 무엇도 지키지 못한다. (C층 `kills:`가 이를 스펙 필드로 승격한다.)
|
|
60
|
+
- `[obligationGaps]` 관측을 신설하는 슬라이스는 **기록→조회 왕복**을 단언한다 — 기록 함수를 부른 뒤
|
|
61
|
+
소비자가 쓰는 읽기 경로로 같은 좌표를 읽는다. H-SPEC `[observability]` 의무와 T-SPEC 케이스 태그의
|
|
62
|
+
대응을 `obligationGapsFor`가 검사해 `obligationGaps`로 보고한다(집행 표는 그대로: 이것은 게이트가
|
|
63
|
+
아니라 보고다).
|
|
60
64
|
|
|
61
65
|
## 집행 요약
|
|
62
66
|
|