@holmes-lab/holmes-kit 0.21.0 → 0.22.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 +52 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.js +1 -0
- package/dist/holmes/cli/doctor.d.ts +1 -0
- package/dist/holmes/cli/doctor.js +42 -0
- package/dist/holmes/cli/index.js +1 -0
- package/dist/holmes/cli/init.js +1 -0
- package/dist/holmes/cpg/language-parser-walk.js +1 -0
- package/dist/holmes/hooks/stop.d.ts +7 -0
- package/dist/holmes/hooks/stop.js +54 -1
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +27 -1
- package/dist/holmes/mcp/handlers/operator-inspection.js +68 -2
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +5 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +59 -1
- package/dist/holmes/mcp/handlers/test-execution.d.ts +4 -0
- package/dist/holmes/mcp/handlers/test-execution.js +6 -2
- package/dist/holmes/mcp/handlers.d.ts +31 -1
- package/dist/holmes/mcp/handlers.js +1 -0
- package/dist/holmes/mcp/maintenance-analyze.js +1 -0
- package/dist/holmes/mcp/tool-schemas.js +1 -0
- package/dist/holmes/project/ci-runs.d.ts +46 -0
- package/dist/holmes/project/ci-runs.js +137 -0
- package/dist/holmes/project/install-scripts-policy.js +1 -0
- package/dist/holmes/review/evaluation-metrics.js +1 -0
- package/dist/holmes/review/kills-check.d.ts +40 -0
- package/dist/holmes/review/kills-check.js +147 -0
- package/dist/holmes/review/manual-baseline.js +1 -0
- package/dist/holmes/rtm/advisory-outcomes.d.ts +137 -0
- package/dist/holmes/rtm/advisory-outcomes.js +314 -0
- package/dist/holmes/rtm/rtm-graph.js +1 -0
- package/dist/holmes/rtm/taint-benchmark.js +1 -0
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +19 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ADVISORY_KINDS = void 0;
|
|
37
|
+
exports.canonical = canonical;
|
|
38
|
+
exports.advisoryId = advisoryId;
|
|
39
|
+
exports.foldAdvisoryState = foldAdvisoryState;
|
|
40
|
+
exports.judgeAdvisories = judgeAdvisories;
|
|
41
|
+
exports.dismissAdvisories = dismissAdvisories;
|
|
42
|
+
exports.advisoryCensus = advisoryCensus;
|
|
43
|
+
exports.appendAdvisoryOutcomes = appendAdvisoryOutcomes;
|
|
44
|
+
exports.readAdvisoryOutcomes = readAdvisoryOutcomes;
|
|
45
|
+
exports.traceGapAdvisories = traceGapAdvisories;
|
|
46
|
+
exports.currentAdvisoryKeys = currentAdvisoryKeys;
|
|
47
|
+
exports.recordAdvisoryOutcomes = recordAdvisoryOutcomes;
|
|
48
|
+
exports.traceGapFiles = traceGapFiles;
|
|
49
|
+
exports.gitHeadOf = gitHeadOf;
|
|
50
|
+
exports.traceGapFor = traceGapFor;
|
|
51
|
+
// @implements A-SPEC-663
|
|
52
|
+
/**
|
|
53
|
+
* The advisory REACTION ledger — the numerator every promotion decision has been missing.
|
|
54
|
+
*
|
|
55
|
+
* This repository already issues five advisory kinds (impact-advisory, anchor-density, ftt-fulfilment,
|
|
56
|
+
* trace-gap, kills-unapplicable) and records the ISSUE of most of them. Measured 2026-09-18: 77
|
|
57
|
+
* impact-advisory rows, 76 anchor-density rows, and not one row anywhere saying what the author did
|
|
58
|
+
* next. Tricorder's "not useful" button and Google's "productive mutant" metric count the reaction,
|
|
59
|
+
* not the emission; without that count "promote to a hard gate once the ledger answers the false-
|
|
60
|
+
* positive rate" has no ledger to ask.
|
|
61
|
+
*
|
|
62
|
+
* Pure: identity, judgement and folding take rows in and give rows out. The append/read helpers are
|
|
63
|
+
* the only I/O and follow the other advisory ledgers (per-replica jsonl, repo-relative content only).
|
|
64
|
+
*/
|
|
65
|
+
const crypto = __importStar(require("node:crypto"));
|
|
66
|
+
const node_child_process_1 = require("node:child_process");
|
|
67
|
+
const fs = __importStar(require("node:fs"));
|
|
68
|
+
const path = __importStar(require("node:path"));
|
|
69
|
+
const replica_id_1 = require("../governance/replica-id");
|
|
70
|
+
const ftt_fulfilment_1 = require("./ftt-fulfilment");
|
|
71
|
+
const trace_gaps_1 = require("./trace-gaps");
|
|
72
|
+
/** Canonical JSON: sorted keys, arrays kept in order — the same payload always gives the same id. */
|
|
73
|
+
function canonical(value) {
|
|
74
|
+
if (Array.isArray(value))
|
|
75
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
76
|
+
if (value && typeof value === 'object') {
|
|
77
|
+
const o = value;
|
|
78
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canonical(o[k])}`).join(',')}}`;
|
|
79
|
+
}
|
|
80
|
+
return JSON.stringify(value);
|
|
81
|
+
}
|
|
82
|
+
/** Deterministic identity of one finding: kind + spec + canonical payload → sha256, first 16 hex. */
|
|
83
|
+
function advisoryId(kind, aspec, payload) {
|
|
84
|
+
return crypto.createHash('sha256').update(`${kind}|${aspec}|${canonical(payload)}`).digest('hex').slice(0, 16);
|
|
85
|
+
}
|
|
86
|
+
/** Latest state per id, in first-seen order. `dismissed` and `resolved` are terminal until re-issued. */
|
|
87
|
+
function foldAdvisoryState(rows) {
|
|
88
|
+
const byId = new Map();
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
const cur = byId.get(r.id);
|
|
91
|
+
if (!cur) {
|
|
92
|
+
byId.set(r.id, { id: r.id, kind: r.kind, aspec: r.aspec, firstSeen: r.firstSeen, last: r.outcome, lastAt: r.judgedAt, rows: 1 });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
cur.rows += 1;
|
|
96
|
+
if (r.judgedAt >= cur.lastAt) {
|
|
97
|
+
cur.last = r.outcome;
|
|
98
|
+
cur.lastAt = r.judgedAt;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [...byId.values()];
|
|
102
|
+
}
|
|
103
|
+
const day = (iso) => iso.slice(0, 10);
|
|
104
|
+
/**
|
|
105
|
+
* Judge the advisories a re-evaluation produced against what the ledger already knows.
|
|
106
|
+
* - a current finding with no state, or whose last state is resolved/dismissed → `issued`
|
|
107
|
+
* - a current finding whose last state is issued/persisted → `persisted`
|
|
108
|
+
* - an open (issued/persisted) finding of the judged scope that is no longer current → `resolved`
|
|
109
|
+
* One row per (id, outcome) per UTC day: re-running the same query the same day adds nothing.
|
|
110
|
+
* `scope` names which open findings this evaluation could have seen (so an unrelated spec's finding
|
|
111
|
+
* is never resolved by a query that did not look at it).
|
|
112
|
+
*/
|
|
113
|
+
function judgeAdvisories(state, current, scope, now, head, recent = [], opts = {}) {
|
|
114
|
+
const issue = opts.issue !== false;
|
|
115
|
+
const out = [];
|
|
116
|
+
const today = recent.filter((r) => day(r.judgedAt) === day(now));
|
|
117
|
+
const seen = new Set(today.map((r) => `${r.id}|${r.outcome}`));
|
|
118
|
+
// `persisted` is a heartbeat, not an event: at most one row per id per day, and never on the day
|
|
119
|
+
// the finding was issued — the same query repeated the same day must add nothing.
|
|
120
|
+
const touchedToday = new Set(today.map((r) => r.id));
|
|
121
|
+
const push = (k, outcome, firstSeen) => {
|
|
122
|
+
if (seen.has(`${k.id}|${outcome}`))
|
|
123
|
+
return;
|
|
124
|
+
if (outcome === 'persisted' && touchedToday.has(k.id))
|
|
125
|
+
return;
|
|
126
|
+
seen.add(`${k.id}|${outcome}`);
|
|
127
|
+
touchedToday.add(k.id);
|
|
128
|
+
out.push({ id: k.id, kind: k.kind, aspec: k.aspec, outcome, firstSeen, judgedAt: now, head });
|
|
129
|
+
};
|
|
130
|
+
const byId = new Map(state.map((s) => [s.id, s]));
|
|
131
|
+
const currentIds = new Set(current.map((c) => c.id));
|
|
132
|
+
for (const c of current) {
|
|
133
|
+
const s = byId.get(c.id);
|
|
134
|
+
if (!s || s.last === 'resolved' || s.last === 'dismissed') {
|
|
135
|
+
if (issue)
|
|
136
|
+
push(c, 'issued', now);
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
push(c, 'persisted', s.firstSeen);
|
|
140
|
+
}
|
|
141
|
+
for (const s of state) {
|
|
142
|
+
if (!scope(s) || currentIds.has(s.id))
|
|
143
|
+
continue;
|
|
144
|
+
if (s.last === 'issued' || s.last === 'persisted')
|
|
145
|
+
push(s, 'resolved', s.firstSeen);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Explicit dismissal by the author: only for findings the ledger knows and that are still open. */
|
|
150
|
+
function dismissAdvisories(state, ids, now, head) {
|
|
151
|
+
const byId = new Map(state.map((s) => [s.id, s]));
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const id of ids) {
|
|
154
|
+
const s = byId.get(id);
|
|
155
|
+
if (!s || (s.last !== 'issued' && s.last !== 'persisted'))
|
|
156
|
+
continue;
|
|
157
|
+
out.push({ id: s.id, kind: s.kind, aspec: s.aspec, outcome: 'dismissed', firstSeen: s.firstSeen, judgedAt: now, head });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
exports.ADVISORY_KINDS = ['impact-advisory', 'anchor-density', 'ftt-fulfilment', 'trace-gap', 'kills-unapplicable'];
|
|
162
|
+
/** Per kind: how many findings were ever issued, and how many stand resolved / persisted / dismissed now. */
|
|
163
|
+
function advisoryCensus(rows) {
|
|
164
|
+
const out = Object.fromEntries(exports.ADVISORY_KINDS.map((k) => [k, { issued: 0, resolved: 0, persisted: 0, dismissed: 0 }]));
|
|
165
|
+
for (const s of foldAdvisoryState(rows)) {
|
|
166
|
+
const c = out[s.kind];
|
|
167
|
+
if (!c)
|
|
168
|
+
continue;
|
|
169
|
+
c.issued += 1;
|
|
170
|
+
if (s.last === 'resolved')
|
|
171
|
+
c.resolved += 1;
|
|
172
|
+
else if (s.last === 'persisted')
|
|
173
|
+
c.persisted += 1;
|
|
174
|
+
else if (s.last === 'dismissed')
|
|
175
|
+
c.dismissed += 1;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
// Scanned files carry ABSOLUTE paths (CpgScanner); Files to Touch are repo-relative. Compare in one spelling.
|
|
180
|
+
function relPosix(p, root) {
|
|
181
|
+
const rel = root && path.isAbsolute(p) ? path.relative(root, p) : p;
|
|
182
|
+
return rel.split(path.sep).join('/').replace(/\\/g, '/');
|
|
183
|
+
}
|
|
184
|
+
const FILE_RE = /^advisory-outcomes\.([^.]+)\.jsonl$/;
|
|
185
|
+
function appendAdvisoryOutcomes(root, rows) {
|
|
186
|
+
if (rows.length === 0)
|
|
187
|
+
return true;
|
|
188
|
+
try {
|
|
189
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
190
|
+
return false;
|
|
191
|
+
let replica = 'local';
|
|
192
|
+
try {
|
|
193
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
194
|
+
}
|
|
195
|
+
catch { /* keep the fallback */ }
|
|
196
|
+
const file = path.join(root, '.ax', 'ledger', `advisory-outcomes.${replica}.jsonl`);
|
|
197
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
198
|
+
fs.appendFileSync(file, rows.map((r) => `${JSON.stringify({ ...r, replica })}\n`).join(''));
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function readAdvisoryOutcomes(root) {
|
|
206
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
207
|
+
let names;
|
|
208
|
+
try {
|
|
209
|
+
names = fs.readdirSync(dir).filter((n) => FILE_RE.test(n)).sort();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const n of names) {
|
|
216
|
+
let text;
|
|
217
|
+
try {
|
|
218
|
+
text = fs.readFileSync(path.join(dir, n), 'utf8');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
for (const line of text.split('\n')) {
|
|
224
|
+
if (!line.trim())
|
|
225
|
+
continue;
|
|
226
|
+
try {
|
|
227
|
+
out.push(JSON.parse(line));
|
|
228
|
+
}
|
|
229
|
+
catch { /* a torn line is not a record */ }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return out.sort((a, b) => (a.judgedAt < b.judgedAt ? -1 : a.judgedAt > b.judgedAt ? 1 : 0));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* One trace-gap finding per unlinked approved A-SPEC that declares at least one SCANNED production
|
|
236
|
+
* file in its Files to Touch — the same population REQ-658 reports per change, taken whole. The
|
|
237
|
+
* payload is the sorted file list, so moving one anchor changes the id (a smaller gap is a new
|
|
238
|
+
* finding, the old one resolved) — honest, if noisy; the census counts findings, not specs.
|
|
239
|
+
*/
|
|
240
|
+
function traceGapAdvisories(specs, scanned, root = '') {
|
|
241
|
+
const scannedPaths = new Set(scanned.map((f) => relPosix(f.path, root)));
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const u of (0, trace_gaps_1.unlinkedApproved)(specs, scanned)) {
|
|
244
|
+
const files = [...new Set((0, ftt_fulfilment_1.fttItems)(u.ftt).map((i) => i.path).filter((p) => scannedPaths.has(p)))].sort();
|
|
245
|
+
if (files.length === 0)
|
|
246
|
+
continue;
|
|
247
|
+
out.push({ id: advisoryId('trace-gap', u.id, { files }), kind: 'trace-gap', aspec: u.id });
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
function currentAdvisoryKeys(aspec, c) {
|
|
252
|
+
const keys = [];
|
|
253
|
+
const ids = {};
|
|
254
|
+
const add = (kind, payload) => { const id = advisoryId(kind, aspec, payload); keys.push({ id, kind, aspec }); ids[kind] = id; };
|
|
255
|
+
if (c.impactAdvisory && c.impactAdvisory.files.length > 0)
|
|
256
|
+
add('impact-advisory', { files: c.impactAdvisory.files.map((f) => f.path) });
|
|
257
|
+
if (c.anchorDensity && c.anchorDensity.length > 0)
|
|
258
|
+
add('anchor-density', { files: c.anchorDensity.map((f) => f.path) });
|
|
259
|
+
if (c.fttFulfilment)
|
|
260
|
+
add('ftt-fulfilment', { missing: c.fttFulfilment.missing.map((m) => m.path), moved: c.fttFulfilment.moved.map((m) => m.path), alternatives: c.fttFulfilment.alternatives.map((m) => m.path) });
|
|
261
|
+
if (c.killsAdvisory && c.killsAdvisory.unapplicable.length > 0)
|
|
262
|
+
add('kills-unapplicable', { unapplicable: c.killsAdvisory.unapplicable.map((u) => ({ where: u.where, reason: u.reason })) });
|
|
263
|
+
if (c.traceGap && c.traceGap.files.length > 0)
|
|
264
|
+
add('trace-gap', { files: [...c.traceGap.files].sort() });
|
|
265
|
+
return { keys, ids };
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* One act: read the ledger, judge `current` against it within `scope`, apply explicit dismissals,
|
|
269
|
+
* append, and return what the response should carry. I/O failures leave the response without the
|
|
270
|
+
* fields (the seal or the query is unaffected). Never throws.
|
|
271
|
+
*/
|
|
272
|
+
function recordAdvisoryOutcomes(root, aspec, current, scope, head, dismiss = [], now = new Date().toISOString(), opts = {}) {
|
|
273
|
+
try {
|
|
274
|
+
const rows = readAdvisoryOutcomes(root);
|
|
275
|
+
const state = foldAdvisoryState(rows);
|
|
276
|
+
const judged = judgeAdvisories(state, current, scope, now, head, rows, opts);
|
|
277
|
+
const after = foldAdvisoryState([...rows, ...judged]);
|
|
278
|
+
const dismissed = dismissAdvisories(after, dismiss, now, head);
|
|
279
|
+
const known = new Set(dismissed.map((d) => d.id));
|
|
280
|
+
const dismissUnknown = dismiss.filter((id) => !known.has(id));
|
|
281
|
+
appendAdvisoryOutcomes(root, [...judged, ...dismissed]);
|
|
282
|
+
const history = foldAdvisoryState([...rows, ...judged, ...dismissed]).filter((s) => s.aspec === aspec);
|
|
283
|
+
return { history, dismissUnknown };
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return { history: [], dismissUnknown: dismiss };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** The scanned production files an A-SPEC declares — the trace-gap payload for one spec. */
|
|
290
|
+
function traceGapFiles(spec, scanned, root = '') {
|
|
291
|
+
const scannedPaths = new Set(scanned.map((f) => relPosix(f.path, root)));
|
|
292
|
+
return [...new Set((0, ftt_fulfilment_1.fttItems)(String(spec.sections?.['Files to Touch'] ?? '')).map((i) => i.path).filter((p) => scannedPaths.has(p)))].sort();
|
|
293
|
+
}
|
|
294
|
+
/** The commit a reaction row is judged at; 'nogit' when git cannot say. Never throws. */
|
|
295
|
+
function gitHeadOf(root) {
|
|
296
|
+
try {
|
|
297
|
+
return (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || 'nogit';
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return 'nogit';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The trace-gap finding for ONE spec, status aside: the scanned production files it declares, or
|
|
305
|
+
* null when a scanned file already anchors it or it declares none. Used at the seal (where the
|
|
306
|
+
* spec is approved by the time this runs) and in the preview.
|
|
307
|
+
*/
|
|
308
|
+
function traceGapFor(specId, spec, scanned, root = '') {
|
|
309
|
+
for (const f of scanned)
|
|
310
|
+
if ((f.implementsSpecs ?? []).includes(specId))
|
|
311
|
+
return null;
|
|
312
|
+
const files = traceGapFiles(spec, scanned, root);
|
|
313
|
+
return files.length > 0 ? { files } : null;
|
|
314
|
+
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.RtmGraph = void 0;
|
|
7
|
+
// @implements A-SPEC-588
|
|
7
8
|
// @implements A-SPEC-293
|
|
8
9
|
// @implements A-SPEC-644
|
|
9
10
|
// @implements A-SPEC-289
|
|
@@ -38,6 +38,7 @@ exports.runTaintBenchmarkAsync = runTaintBenchmarkAsync;
|
|
|
38
38
|
exports.compareEngines = compareEngines;
|
|
39
39
|
exports.runTaintBenchmark = runTaintBenchmark;
|
|
40
40
|
exports.taintOnRealCorpus = taintOnRealCorpus;
|
|
41
|
+
// @implements A-SPEC-355
|
|
41
42
|
// @implements A-SPEC-354
|
|
42
43
|
const language_parser_1 = require("../cpg/language-parser");
|
|
43
44
|
const dataflow_taint_1 = require("./dataflow-taint");
|
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.22.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",
|
|
@@ -124,6 +124,25 @@ T-SPEC 필수 섹션은 4분면 그대로다: **Normal · Corner · Negative ·
|
|
|
124
124
|
케이스인지**가 드러나게 쓴다. 그리고 frontmatter `depends_on: [대상 A-SPEC]` — 이 간선이 없으면
|
|
125
125
|
승인해도 코드 게이트가 열리지 않는다 — 그때 게이트가 `depends_on`을 지목한다(`promote-slice`의 「T-SPEC 거부는 상태를 구별해 말한다」).
|
|
126
126
|
|
|
127
|
+
### `kills:` 문법 — `where`는 소스 조각이지 파일 경로가 아니다
|
|
128
|
+
|
|
129
|
+
T-SPEC frontmatter의 `kills`는 판별력을 스펙 필드로 승격한 것이다: 엔진(`test_run --mutate`)이 A-SPEC을
|
|
130
|
+
`@implements`하는 **프로덕션 소스 안의 리터럴 `where`를 `mutate`로 치환**하고, 커버 테스트가 붉어지는지
|
|
131
|
+
본다. 그래서 `where`는 그 소스에 **글자 그대로 존재하는 조각**이어야 한다.
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
kills:
|
|
135
|
+
- where: "if (reason === '')"
|
|
136
|
+
mutate: "if (false)"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`where: src/holmes/foo.ts`처럼 파일 경로를 적거나 `mutate`에 산문("검사를 제거한다")을 적으면 아무것도
|
|
140
|
+
적용되지 않는다 — 2026-09-17 실측에서 이 저장소의 22개 엔트리 전부가 그 모양이었다. 이제 두 곳이 그것을
|
|
141
|
+
말한다: T-SPEC 승인(`spec_approve`·`approval_status`)이 적용 불가 엔트리를 `killsAdvisory`
|
|
142
|
+
(`kills-unapplicable`) **소견으로 보고**하고(봉인은 그대로 된다), `test_run --mutate`는 적용되지 않은
|
|
143
|
+
엔트리를 `unapplied`로 `survivors`와 구별해 싣는다. `survivors: []`는 `unapplied`가 비어 있을 때만 "생존
|
|
144
|
+
변이 없음"이다.
|
|
145
|
+
|
|
127
146
|
H-SPEC에 `[observability]` 의무가 있으면 T-SPEC은 **기록→조회 왕복** 케이스를
|
|
128
147
|
`- [observability] Given … When … Then …`으로 적는다: 쓰기 API를 부르고, **읽는 쪽이 실제로 쓰는 읽기
|
|
129
148
|
API**로 같은 좌표를 조회해 행이 있음을 단언한다. 태그는 줄 시작에서만 인식된다(`nonfunctional.ts`).
|