@holmes-lab/holmes-kit 0.15.0 → 0.17.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 +118 -0
- package/README.md +5 -1
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/doctor.js +15 -1
- package/dist/holmes/cli/mcp-version.d.ts +4 -1
- package/dist/holmes/cli/mcp-version.js +5 -2
- package/dist/holmes/governance/approval-queue.d.ts +6 -0
- package/dist/holmes/governance/approval-queue.js +11 -3
- package/dist/holmes/governance/autonomy.js +16 -1
- package/dist/holmes/governance/session-context.d.ts +74 -0
- package/dist/holmes/governance/session-context.js +179 -0
- package/dist/holmes/hooks/pre-tool-use.js +5 -2
- package/dist/holmes/hooks/rtm-refresh-child.d.ts +1 -0
- package/dist/holmes/hooks/rtm-refresh-child.js +56 -0
- package/dist/holmes/hooks/rtm-refresh.d.ts +13 -0
- package/dist/holmes/hooks/rtm-refresh.js +76 -0
- package/dist/holmes/hooks/stop.js +42 -0
- package/dist/holmes/mcp/handlers.d.ts +8 -7
- package/dist/holmes/mcp/handlers.js +114 -5
- package/dist/holmes/mcp/server.js +12 -0
- package/dist/holmes/mcp/tool-schemas.js +1 -1
- package/dist/holmes/review/judgement-bundle.d.ts +49 -0
- package/dist/holmes/review/judgement-bundle.js +108 -0
- package/dist/holmes/review/run-replay.d.ts +5 -0
- package/dist/holmes/review/run-replay.js +32 -0
- package/dist/holmes/rtm/anchor-density.d.ts +43 -0
- package/dist/holmes/rtm/anchor-density.js +117 -0
- package/dist/holmes/rtm/impact-advisory.d.ts +52 -0
- package/dist/holmes/rtm/impact-advisory.js +182 -0
- package/dist/holmes/rtm/localize.js +7 -0
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +50 -1
- package/dist/holmes/rtm/rtm-graph.d.ts +28 -1
- package/dist/holmes/rtm/rtm-graph.js +61 -8
- package/dist/holmes/spec/compat-impact.d.ts +5 -0
- package/dist/holmes/spec/compat-impact.js +1 -0
- package/package.json +1 -1
- package/playbooks/publish/PLAYBOOK.md +16 -7
|
@@ -0,0 +1,117 @@
|
|
|
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.MIN_ANCHORS = void 0;
|
|
37
|
+
exports.anchorDensityFindings = anchorDensityFindings;
|
|
38
|
+
exports.appendAnchorDensity = appendAnchorDensity;
|
|
39
|
+
exports.readAnchorDensity = readAnchorDensity;
|
|
40
|
+
// @implements A-SPEC-569.5
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const replica_id_1 = require("../governance/replica-id");
|
|
44
|
+
/** Files below this live-anchor count are never flagged, whatever the distribution — a floor. */
|
|
45
|
+
exports.MIN_ANCHORS = 8;
|
|
46
|
+
/**
|
|
47
|
+
* FtT files whose live anchor count sits at or above max(MIN_ANCHORS, p90 of the distribution).
|
|
48
|
+
* p90 is the value at index ceil(0.9·n)-1 of the ascending counts — deterministic, no interpolation.
|
|
49
|
+
* Pure: same inputs, same findings, in sorted path order.
|
|
50
|
+
*/
|
|
51
|
+
function anchorDensityFindings(fttFiles, counts) {
|
|
52
|
+
if (counts.length === 0 || fttFiles.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
const sorted = counts.map((c) => c.anchors).sort((a, b) => a - b);
|
|
55
|
+
const p90 = sorted[Math.ceil(0.9 * sorted.length) - 1];
|
|
56
|
+
const threshold = Math.max(exports.MIN_ANCHORS, p90);
|
|
57
|
+
const byPath = new Map(counts.map((c) => [c.sourcePath, c.anchors]));
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const f of [...new Set(fttFiles.map((p) => p.replace(/\\/g, '/')))].sort()) {
|
|
60
|
+
const anchors = byPath.get(f);
|
|
61
|
+
if (anchors !== undefined && anchors >= threshold)
|
|
62
|
+
out.push({ path: f, anchors, p90 });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
const DENSITY_FILE_RE = /^anchor-density\.([^.]+)\.jsonl$/;
|
|
67
|
+
function appendAnchorDensity(root, rec) {
|
|
68
|
+
try {
|
|
69
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
70
|
+
return false;
|
|
71
|
+
let replica = 'local';
|
|
72
|
+
try {
|
|
73
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
74
|
+
}
|
|
75
|
+
catch { /* keep the fallback */ }
|
|
76
|
+
const file = path.join(root, '.ax', 'ledger', `anchor-density.${replica}.jsonl`);
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
78
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function readAnchorDensity(root) {
|
|
86
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
87
|
+
let names;
|
|
88
|
+
try {
|
|
89
|
+
names = fs.readdirSync(dir).filter((n) => DENSITY_FILE_RE.test(n)).sort();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const name of names) {
|
|
96
|
+
let text;
|
|
97
|
+
try {
|
|
98
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const line of text.split('\n')) {
|
|
104
|
+
const s = line.trim();
|
|
105
|
+
if (!s)
|
|
106
|
+
continue;
|
|
107
|
+
try {
|
|
108
|
+
const r = JSON.parse(s);
|
|
109
|
+
if (r && typeof r === 'object' && typeof r.aspec === 'string' && Array.isArray(r.files) && typeof r.ts === 'string') {
|
|
110
|
+
out.push(r);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch { /* a corrupt line never breaks the read */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The impact advisory: what the Files-to-Touch declaration MISSES, computed from the call graph at
|
|
3
|
+
* the moment of sealing. jarvis proposal #1, admitted on our own measured record — of fourteen
|
|
4
|
+
* graph mechanisms, the only win was the IMPACT axis (PPR, three corpora), and REQ-565 established
|
|
5
|
+
* the "declaration vs machine" gate pattern this extends.
|
|
6
|
+
*
|
|
7
|
+
* ONE HOP, NEVER CLOSURE: caller closure measured 59 extra files and zero answers across twelve
|
|
8
|
+
* commits ("closure is impact, not localization"). ADVISORY, NEVER VERDICT: nothing here can change
|
|
9
|
+
* ok/refuse/grant (Judgments must not be budgeted). NO GUESSING: an unreadable graph yields null,
|
|
10
|
+
* not an estimate.
|
|
11
|
+
*/
|
|
12
|
+
export interface GraphLike {
|
|
13
|
+
nodeIdsInFile(relPath: string): string[];
|
|
14
|
+
callersOf(id: string): string[];
|
|
15
|
+
/** @implements A-SPEC-568.2 — the node's intent sentence, or null; information only, never a verdict input. */
|
|
16
|
+
summaryOf(id: string): string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface ImpactAdvisory {
|
|
19
|
+
files: Array<{
|
|
20
|
+
path: string;
|
|
21
|
+
anchors: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
summary: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
/** @implements A-SPEC-569.3 — anchors beyond ANCHOR_CAP, counted rather than silently dropped. */
|
|
26
|
+
anchorsOmitted?: number;
|
|
27
|
+
}>;
|
|
28
|
+
more: number;
|
|
29
|
+
graphAsOf?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare const ADVISORY_CAP = 10;
|
|
32
|
+
/** @implements A-SPEC-569.3 — anchors annotated per advisory file; a prose constant, never a verdict input. */
|
|
33
|
+
export declare const ANCHOR_CAP = 10;
|
|
34
|
+
export declare function declaredImpactGap(fttFiles: string[], graph: GraphLike, readFile: (rel: string) => string | null, opts?: {
|
|
35
|
+
cap?: number;
|
|
36
|
+
}): ImpactAdvisory | null;
|
|
37
|
+
/**
|
|
38
|
+
* @implements A-SPEC-566.2
|
|
39
|
+
* The observation ledger: every advisory the act produced, so its false-positive rate can be
|
|
40
|
+
* MEASURED before anyone proposes promoting it to a hard gate (the August rtm_reindex lesson).
|
|
41
|
+
* Repo-relative paths, spec ids and integers only — nothing else has a field.
|
|
42
|
+
*/
|
|
43
|
+
export interface AdvisoryRecord {
|
|
44
|
+
aspec: string;
|
|
45
|
+
files: string[];
|
|
46
|
+
more: number;
|
|
47
|
+
graphAsOf?: string;
|
|
48
|
+
ts: string;
|
|
49
|
+
replica?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function appendImpactAdvisory(root: string, rec: AdvisoryRecord): boolean;
|
|
52
|
+
export declare function readImpactAdvisories(root: string): AdvisoryRecord[];
|
|
@@ -0,0 +1,182 @@
|
|
|
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.ANCHOR_CAP = exports.ADVISORY_CAP = void 0;
|
|
37
|
+
exports.declaredImpactGap = declaredImpactGap;
|
|
38
|
+
exports.appendImpactAdvisory = appendImpactAdvisory;
|
|
39
|
+
exports.readImpactAdvisories = readImpactAdvisories;
|
|
40
|
+
// @implements A-SPEC-566.1
|
|
41
|
+
// @implements A-SPEC-566.2
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const replica_id_1 = require("../governance/replica-id");
|
|
45
|
+
exports.ADVISORY_CAP = 10;
|
|
46
|
+
/** @implements A-SPEC-569.3 — anchors annotated per advisory file; a prose constant, never a verdict input. */
|
|
47
|
+
exports.ANCHOR_CAP = 10;
|
|
48
|
+
/** The ONLY shape allowed into the ledger: `seg/seg/…` of word characters, dots and dashes —
|
|
49
|
+
* no leading slash, no drive letter, no backslash, no empty or `..` segment. */
|
|
50
|
+
function isRepoRelative(p) {
|
|
51
|
+
if (p === '' || p.includes('\\') || p.includes(':'))
|
|
52
|
+
return false;
|
|
53
|
+
const segs = p.split('/');
|
|
54
|
+
// Round-4: '.' segments made `src/./b.ts` dodge a declared `src/b.ts`, and the ASCII-only \w
|
|
55
|
+
// silently dropped findings in non-ASCII filenames (the 한글파일 evidence discipline of 563.3).
|
|
56
|
+
// Unicode letters/digits are leak-safe; dot-segments are not path characters, they are aliases.
|
|
57
|
+
return segs.every((s) => /^[\p{L}\p{N}_.-]+$/u.test(s) && s !== '..' && s !== '.');
|
|
58
|
+
}
|
|
59
|
+
/** `@implements <spec-id>` ids out of a file's text — the anchor idiom, read-only. */
|
|
60
|
+
function anchorsIn(text) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const m of text.matchAll(/@implements\s+((?:A|T|H|C)-SPEC-[\d.]+|REQ-\d+)/g)) {
|
|
63
|
+
if (!out.includes(m[1]))
|
|
64
|
+
out.push(m[1]);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function declaredImpactGap(fttFiles, graph, readFile, opts) {
|
|
69
|
+
try {
|
|
70
|
+
const cap = opts?.cap ?? exports.ADVISORY_CAP;
|
|
71
|
+
const declared = new Set(fttFiles.map((f) => f.replace(/\\/g, '/')));
|
|
72
|
+
const outside = new Set();
|
|
73
|
+
for (const ftt of declared) {
|
|
74
|
+
for (const nodeId of graph.nodeIdsInFile(ftt)) {
|
|
75
|
+
for (const caller of graph.callersOf(nodeId)) {
|
|
76
|
+
const at = caller.lastIndexOf('@');
|
|
77
|
+
if (at === -1)
|
|
78
|
+
continue; // a node without a file cannot be a finding
|
|
79
|
+
const file = caller.slice(at + 1);
|
|
80
|
+
// Adversarial rounds 2-3: the record's no-secret contract says REPO-RELATIVE ONLY. A deny
|
|
81
|
+
// list (round-2: '/', 'C:', '..') let UNC paths through one round later — so this is an
|
|
82
|
+
// ALLOW list: plain slash-separated word segments, no '..' segment, nothing else. What
|
|
83
|
+
// does not look like an in-repo relative path is dropped, never normalized into one.
|
|
84
|
+
if (!isRepoRelative(file))
|
|
85
|
+
continue;
|
|
86
|
+
if (!declared.has(file))
|
|
87
|
+
outside.add(file);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (outside.size === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const sorted = [...outside].sort();
|
|
94
|
+
const shown = sorted.slice(0, cap);
|
|
95
|
+
return {
|
|
96
|
+
files: shown.map((path) => {
|
|
97
|
+
let ids = [];
|
|
98
|
+
try {
|
|
99
|
+
const text = readFile(path);
|
|
100
|
+
if (typeof text === 'string')
|
|
101
|
+
ids = anchorsIn(text);
|
|
102
|
+
}
|
|
103
|
+
catch { /* anchors stay [] */ }
|
|
104
|
+
// @implements A-SPEC-569.3 — capped BEFORE the summary lookups, so the omitted tail costs
|
|
105
|
+
// no SELECTs either; the omission is counted, never silent.
|
|
106
|
+
const shownIds = ids.slice(0, exports.ANCHOR_CAP);
|
|
107
|
+
const anchorsOmitted = ids.length - shownIds.length;
|
|
108
|
+
// @implements A-SPEC-568.2 — the anchor's intent sentence rides BESIDE the id, read from the
|
|
109
|
+
// graph S1 built. Information only: nothing below this line feeds files/more/ordering, and a
|
|
110
|
+
// failing lookup downgrades to null rather than killing the finding (an advisory never guesses).
|
|
111
|
+
return {
|
|
112
|
+
path,
|
|
113
|
+
anchors: shownIds.map((id) => {
|
|
114
|
+
let summary = null;
|
|
115
|
+
try {
|
|
116
|
+
summary = graph.summaryOf(`SPEC:${id}`);
|
|
117
|
+
}
|
|
118
|
+
catch { /* summary stays null */ }
|
|
119
|
+
return { id, summary };
|
|
120
|
+
}),
|
|
121
|
+
...(anchorsOmitted > 0 ? { anchorsOmitted } : {}),
|
|
122
|
+
};
|
|
123
|
+
}),
|
|
124
|
+
more: sorted.length - shown.length,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
} // an advisory never guesses
|
|
130
|
+
}
|
|
131
|
+
const ADVISORY_FILE_RE = /^impact-advisories\.([^.]+)\.jsonl$/;
|
|
132
|
+
function appendImpactAdvisory(root, rec) {
|
|
133
|
+
try {
|
|
134
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
135
|
+
return false;
|
|
136
|
+
let replica = 'local';
|
|
137
|
+
try {
|
|
138
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
139
|
+
}
|
|
140
|
+
catch { /* keep the fallback */ }
|
|
141
|
+
const file = path.join(root, '.ax', 'ledger', `impact-advisories.${replica}.jsonl`);
|
|
142
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
143
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function readImpactAdvisories(root) {
|
|
151
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
152
|
+
let names;
|
|
153
|
+
try {
|
|
154
|
+
names = fs.readdirSync(dir).filter((n) => ADVISORY_FILE_RE.test(n)).sort();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const name of names) {
|
|
161
|
+
let text;
|
|
162
|
+
try {
|
|
163
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const line of text.split('\n')) {
|
|
169
|
+
const s = line.trim();
|
|
170
|
+
if (!s)
|
|
171
|
+
continue;
|
|
172
|
+
try {
|
|
173
|
+
const r = JSON.parse(s);
|
|
174
|
+
if (r && typeof r === 'object' && typeof r.aspec === 'string' && Array.isArray(r.files) && typeof r.ts === 'string') {
|
|
175
|
+
out.push(r);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch { /* a corrupt line never breaks the read */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
@@ -223,6 +223,13 @@ function localizeIssue(issueText, scanned, specs, topN = 10) {
|
|
|
223
223
|
for (const id of citations.cited)
|
|
224
224
|
if (!matchedSpecs.includes(id))
|
|
225
225
|
matchedSpecs.push(id);
|
|
226
|
+
// A spec-intent-vector injection was wired here (REQ-568 S3) and REVERTED on its pre-registered
|
|
227
|
+
// replay: over 441 held-out traceability cases the assist was byte-identical to this lexical
|
|
228
|
+
// path — long queries already match ~371 specs lexically (the semantic top-3 was a subset in
|
|
229
|
+
// 23/23 samples), and in the lexical-zero segment the A-SPEC-399 reorder-not-admit rule forbids
|
|
230
|
+
// exactly the admission recovery would need. The S-491 embedding win belongs to direct
|
|
231
|
+
// query→file ranking, not to this layer. Do not re-add without a NEW pre-registered corpus of
|
|
232
|
+
// short uncited requests AND an admission rule of its own.
|
|
226
233
|
const matchedSpecSet = new Set(matchedSpecs);
|
|
227
234
|
// Vendored/mirrored trees (review D4): an identical copy under reference/vendor must not outrank —
|
|
228
235
|
// or lexically tie-break above — the live source. Their scores are halved and ties break toward
|
|
@@ -57,6 +57,14 @@ export interface BuildRtmOptions {
|
|
|
57
57
|
*/
|
|
58
58
|
fileDigest?: (sourcePath: string) => string | undefined;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* @implements A-SPEC-568.1
|
|
62
|
+
* `"<title> — <intent first sentence>"`, EXTRACTED — never generated. Same spec store, same bytes:
|
|
63
|
+
* the whole function is whitespace folding, one regex, one slice. A missing, blank or still-TODO
|
|
64
|
+
* intent section degrades to the title alone (spec_create stubs sections as `TODO`, and "TODO" as
|
|
65
|
+
* an intent sentence would be noise wearing a dash).
|
|
66
|
+
*/
|
|
67
|
+
export declare function specSummary(s: Spec): string;
|
|
60
68
|
/**
|
|
61
69
|
* Adds one scanned file's CODE nodes and `implements` edges to the graph,
|
|
62
70
|
* tagged with that file's sourcePath so RtmGraph.removeBySource(f.sourcePath)
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.DEFAULT_IMPACT = void 0;
|
|
37
|
+
exports.specSummary = specSummary;
|
|
37
38
|
exports.buildFileSubgraph = buildFileSubgraph;
|
|
38
39
|
exports.buildRtm = buildRtm;
|
|
39
40
|
exports.addCallEdges = addCallEdges;
|
|
@@ -62,6 +63,52 @@ function fact(opts, sourceLocation, derivation, confidence, evidenceDigest = nul
|
|
|
62
63
|
derivation,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* @implements A-SPEC-568.1
|
|
68
|
+
* Which section states a spec's INTENT, per kind. Only the three kinds whose required sections
|
|
69
|
+
* carry a prose intent are mapped; every other kind (T-SPEC's quadrants, C-SPEC) summarizes as its
|
|
70
|
+
* title alone — a rule deferred until a consumer measures the lack (H-SPEC-568 Open Questions).
|
|
71
|
+
*/
|
|
72
|
+
const INTENT_SECTION = {
|
|
73
|
+
'REQ': 'Problem / Need',
|
|
74
|
+
'H-SPEC': 'Intent',
|
|
75
|
+
'A-SPEC': 'Objective',
|
|
76
|
+
};
|
|
77
|
+
/** Longest intent sentence carried into the graph — a bound, applied deterministically. */
|
|
78
|
+
const SUMMARY_SENTENCE_CAP = 200;
|
|
79
|
+
/**
|
|
80
|
+
* @implements A-SPEC-568.1
|
|
81
|
+
* `"<title> — <intent first sentence>"`, EXTRACTED — never generated. Same spec store, same bytes:
|
|
82
|
+
* the whole function is whitespace folding, one regex, one slice. A missing, blank or still-TODO
|
|
83
|
+
* intent section degrades to the title alone (spec_create stubs sections as `TODO`, and "TODO" as
|
|
84
|
+
* an intent sentence would be noise wearing a dash).
|
|
85
|
+
*/
|
|
86
|
+
function specSummary(s) {
|
|
87
|
+
// The Spec type promises strings, but runtime records disagree (test fixtures without a title;
|
|
88
|
+
// 20 legacy store documents with title ''). Extraction is fail-open: it degrades, never throws.
|
|
89
|
+
const title = String(s.title ?? '').trim();
|
|
90
|
+
const section = INTENT_SECTION[s.type];
|
|
91
|
+
const text = (section ? (s.sections?.[section] ?? '') : '').replace(/\s+/g, ' ').trim();
|
|
92
|
+
// Measured 2026-09-07 on this store: 20 legacy specs carry title '' — the summary falls back to
|
|
93
|
+
// the sentence alone, then to the id, because SC1 admits no empty summary on any path.
|
|
94
|
+
let result;
|
|
95
|
+
if (text === '' || text === 'TODO')
|
|
96
|
+
result = title !== '' ? title : s.id;
|
|
97
|
+
else {
|
|
98
|
+
// First sentence: up to the first . ! or ? that ends a word — the lookahead keeps `A-SPEC-129.2`
|
|
99
|
+
// whole, because its dot is followed by a digit, not by whitespace or the end.
|
|
100
|
+
const m = /^(.*?[.!?])(?=\s|$)/.exec(text);
|
|
101
|
+
let sentence = m ? m[1] : text;
|
|
102
|
+
if (sentence.length > SUMMARY_SENTENCE_CAP)
|
|
103
|
+
sentence = `${sentence.slice(0, SUMMARY_SENTENCE_CAP)}…`;
|
|
104
|
+
result = title !== '' ? `${title} — ${sentence}` : sentence;
|
|
105
|
+
}
|
|
106
|
+
// @implements A-SPEC-569.1 — the WHOLE summary folds, title included. The first cut normalized
|
|
107
|
+
// only the sentence, and a YAML double-quoted title carried \n/\t straight into dumpCanonical,
|
|
108
|
+
// where graphViewOf parsed the forged row as a real call edge (0.16.0 adversarial reproduction).
|
|
109
|
+
// Prose must never carry the dump's structural characters.
|
|
110
|
+
return result.replace(/\s+/g, ' ').trim();
|
|
111
|
+
}
|
|
65
112
|
/**
|
|
66
113
|
* Adds one scanned file's CODE nodes and `implements` edges to the graph,
|
|
67
114
|
* tagged with that file's sourcePath so RtmGraph.removeBySource(f.sourcePath)
|
|
@@ -96,7 +143,9 @@ function buildRtm(specs, scanned, graph, opts) {
|
|
|
96
143
|
// Add spec nodes and dependencies
|
|
97
144
|
for (const s of specs) {
|
|
98
145
|
const sourcePath = opts?.specSourcePath?.(s.id);
|
|
99
|
-
|
|
146
|
+
// @implements A-SPEC-568.1 — the intent property rides in with the node; CODE/FILE/COMMIT/
|
|
147
|
+
// DECISION nodes pass nothing and stay null.
|
|
148
|
+
graph.addNode(`SPEC:${s.id}`, s.type, sourcePath, fact(opts, sourcePath ?? null, 'spec-store', 1), specSummary(s));
|
|
100
149
|
// DECLARED in frontmatter.
|
|
101
150
|
for (const p of s.dependsOn) {
|
|
102
151
|
graph.addEdge(`SPEC:${s.id}`, `SPEC:${p}`, 'depends_on', sourcePath, fact(opts, sourcePath ?? null, 'frontmatter', 1));
|
|
@@ -51,8 +51,19 @@ export declare class RtmGraph {
|
|
|
51
51
|
constructor(dbPath?: string);
|
|
52
52
|
private nodeStmt?;
|
|
53
53
|
private edgeStmt?;
|
|
54
|
-
addNode(id: string, kind: string, sourcePath?: string, provenance?: Provenance): void;
|
|
54
|
+
addNode(id: string, kind: string, sourcePath?: string, provenance?: Provenance, summary?: string | null): void;
|
|
55
|
+
/**
|
|
56
|
+
* @implements A-SPEC-568.1
|
|
57
|
+
* The node's intent sentence, extracted from the spec store at build time — or null: for a CODE
|
|
58
|
+
* node (no prose was extracted), for a node written before rtm-graph/3, for an unknown id. The
|
|
59
|
+
* summary is INFORMATION, never a verdict input (REQ-568 c6 rule) — nothing in this class or its
|
|
60
|
+
* consumers ranks, gates or filters on it.
|
|
61
|
+
*/
|
|
62
|
+
summaryOf(id: string): string | null;
|
|
55
63
|
addEdge(src: string, dst: string, rel: string, sourcePath?: string, provenance?: Provenance): void;
|
|
64
|
+
/** @implements A-SPEC-569.1 — does the graph hold this node? SELECT 1, no row materialization:
|
|
65
|
+
* the approved-only channel filter (A-SPEC-569.2) asks exactly this and nothing more. */
|
|
66
|
+
hasNode(id: string): boolean;
|
|
56
67
|
/** @implements A-SPEC-281 — how this node got here, or null if the node is unknown. */
|
|
57
68
|
provenanceOfNode(id: string): Provenance | null;
|
|
58
69
|
/** @implements A-SPEC-281 — how this edge got here, or null if the edge is unknown. */
|
|
@@ -72,6 +83,13 @@ export declare class RtmGraph {
|
|
|
72
83
|
* this and not `incoming`, which would also drag in `implements` and `depends_on` and turn a
|
|
73
84
|
* "who calls me" question into "what is attached to me in any way at all".
|
|
74
85
|
*/
|
|
86
|
+
/**
|
|
87
|
+
* @implements A-SPEC-566.1
|
|
88
|
+
* Every node the graph holds for one source file — the impact advisory's entry point. Exact
|
|
89
|
+
* `source_path` equality on the indexed column (idx_nodes_source): no LIKE, so % and _ in a path
|
|
90
|
+
* are literal by construction and `a.ts` can never absorb `xa.ts`.
|
|
91
|
+
*/
|
|
92
|
+
nodeIdsInFile(relPath: string): string[];
|
|
75
93
|
callersOf(id: string): string[];
|
|
76
94
|
/**
|
|
77
95
|
* @implements A-SPEC-288
|
|
@@ -197,6 +215,15 @@ export declare class RtmGraph {
|
|
|
197
215
|
clear(): void;
|
|
198
216
|
/** @implements A-SPEC-283 — the journal mode actually in force, so the pragma can be asserted. */
|
|
199
217
|
journalMode(): string;
|
|
218
|
+
/**
|
|
219
|
+
* @implements A-SPEC-569.5
|
|
220
|
+
* Live anchors per source file — distinct SPEC targets of `implements` edges, one GROUP BY.
|
|
221
|
+
* Feeds the anchor-density OBSERVATION at sealing time; nothing reads it into a verdict.
|
|
222
|
+
*/
|
|
223
|
+
implementsAnchorCounts(): Array<{
|
|
224
|
+
sourcePath: string;
|
|
225
|
+
anchors: number;
|
|
226
|
+
}>;
|
|
200
227
|
nodeCount(): number;
|
|
201
228
|
edgeCount(): number;
|
|
202
229
|
close(): void;
|
|
@@ -75,7 +75,7 @@ class RtmGraph {
|
|
|
75
75
|
this.db.pragma('journal_mode = WAL');
|
|
76
76
|
this.db.pragma('busy_timeout = 5000');
|
|
77
77
|
}
|
|
78
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_path TEXT);
|
|
78
|
+
this.db.exec(`CREATE TABLE IF NOT EXISTS nodes (id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_path TEXT, summary TEXT);
|
|
79
79
|
CREATE TABLE IF NOT EXISTS edges (src TEXT NOT NULL, dst TEXT NOT NULL, rel TEXT NOT NULL, source_path TEXT, PRIMARY KEY (src,dst,rel));
|
|
80
80
|
-- The PK covers src-prefixed lookups; nothing covered dst or source_path, so every reverse
|
|
81
81
|
-- traversal and every removeBySource was a full table scan. Measured on a 240k-node graph:
|
|
@@ -103,6 +103,11 @@ class RtmGraph {
|
|
|
103
103
|
const type = column === 'confidence' ? 'REAL' : 'TEXT';
|
|
104
104
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
|
105
105
|
}
|
|
106
|
+
// @implements A-SPEC-568.1 — same in-place migration rule as provenance: an rtm-graph/2 file
|
|
107
|
+
// must keep OPENING (openReusableGraph decides reuse; an unreadable file would look corrupt).
|
|
108
|
+
if (table === 'nodes' && !present.has('summary')) {
|
|
109
|
+
this.db.exec('ALTER TABLE nodes ADD COLUMN summary TEXT');
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
// @implements A-SPEC-139
|
|
@@ -113,14 +118,34 @@ class RtmGraph {
|
|
|
113
118
|
// Semantics are unchanged: same signatures, same INSERT OR IGNORE idempotence, same columns.
|
|
114
119
|
nodeStmt;
|
|
115
120
|
edgeStmt;
|
|
116
|
-
addNode(id, kind, sourcePath, provenance) {
|
|
117
|
-
this.nodeStmt ??= this.db.prepare(`INSERT OR IGNORE INTO nodes (id,kind,source_path,${PROVENANCE_COLUMNS.join(',')}) VALUES (
|
|
118
|
-
|
|
121
|
+
addNode(id, kind, sourcePath, provenance, summary) {
|
|
122
|
+
this.nodeStmt ??= this.db.prepare(`INSERT OR IGNORE INTO nodes (id,kind,source_path,summary,${PROVENANCE_COLUMNS.join(',')}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
123
|
+
// @implements A-SPEC-569.1 — the STORAGE boundary folds structural characters too, so a future
|
|
124
|
+
// caller that never went through specSummary still cannot forge dumpCanonical rows. Second,
|
|
125
|
+
// independent face of the seal (each face is verified alone).
|
|
126
|
+
const foldedSummary = summary == null ? null : summary.replace(/\s+/g, ' ').trim();
|
|
127
|
+
this.nodeStmt.run(id, kind, sourcePath ?? null, foldedSummary, ...provenanceRow(provenance));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* @implements A-SPEC-568.1
|
|
131
|
+
* The node's intent sentence, extracted from the spec store at build time — or null: for a CODE
|
|
132
|
+
* node (no prose was extracted), for a node written before rtm-graph/3, for an unknown id. The
|
|
133
|
+
* summary is INFORMATION, never a verdict input (REQ-568 c6 rule) — nothing in this class or its
|
|
134
|
+
* consumers ranks, gates or filters on it.
|
|
135
|
+
*/
|
|
136
|
+
summaryOf(id) {
|
|
137
|
+
const row = this.db.prepare('SELECT summary FROM nodes WHERE id = ?').get(id);
|
|
138
|
+
return row?.summary ?? null;
|
|
119
139
|
}
|
|
120
140
|
addEdge(src, dst, rel, sourcePath, provenance) {
|
|
121
141
|
this.edgeStmt ??= this.db.prepare(`INSERT OR IGNORE INTO edges (src,dst,rel,source_path,${PROVENANCE_COLUMNS.join(',')}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
122
142
|
this.edgeStmt.run(src, dst, rel, sourcePath ?? null, ...provenanceRow(provenance));
|
|
123
143
|
}
|
|
144
|
+
/** @implements A-SPEC-569.1 — does the graph hold this node? SELECT 1, no row materialization:
|
|
145
|
+
* the approved-only channel filter (A-SPEC-569.2) asks exactly this and nothing more. */
|
|
146
|
+
hasNode(id) {
|
|
147
|
+
return this.db.prepare('SELECT 1 FROM nodes WHERE id = ?').get(id) !== undefined;
|
|
148
|
+
}
|
|
124
149
|
/** @implements A-SPEC-281 — how this node got here, or null if the node is unknown. */
|
|
125
150
|
provenanceOfNode(id) {
|
|
126
151
|
return provenanceOf(this.db.prepare('SELECT * FROM nodes WHERE id = ?').get(id));
|
|
@@ -149,6 +174,15 @@ class RtmGraph {
|
|
|
149
174
|
* this and not `incoming`, which would also drag in `implements` and `depends_on` and turn a
|
|
150
175
|
* "who calls me" question into "what is attached to me in any way at all".
|
|
151
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* @implements A-SPEC-566.1
|
|
179
|
+
* Every node the graph holds for one source file — the impact advisory's entry point. Exact
|
|
180
|
+
* `source_path` equality on the indexed column (idx_nodes_source): no LIKE, so % and _ in a path
|
|
181
|
+
* are literal by construction and `a.ts` can never absorb `xa.ts`.
|
|
182
|
+
*/
|
|
183
|
+
nodeIdsInFile(relPath) {
|
|
184
|
+
return this.db.prepare('SELECT id FROM nodes WHERE source_path = ? ORDER BY id ASC').all(relPath).map((x) => x.id);
|
|
185
|
+
}
|
|
152
186
|
callersOf(id) {
|
|
153
187
|
return this.db.prepare("SELECT DISTINCT src FROM edges WHERE dst=? AND rel='calls' ORDER BY src ASC").all(id).map(x => x.src);
|
|
154
188
|
}
|
|
@@ -339,12 +373,21 @@ class RtmGraph {
|
|
|
339
373
|
// @implements A-SPEC-281 — provenance is part of the value, so a provenance-only divergence
|
|
340
374
|
// between a full rebuild and an incremental update is caught rather than passing as equal.
|
|
341
375
|
const cols = PROVENANCE_COLUMNS.join(', ');
|
|
342
|
-
|
|
343
|
-
|
|
376
|
+
// @implements A-SPEC-569.1 (revision) — EVERY text column folds at emission. Sealing columns
|
|
377
|
+
// one at a time (title → summary) was whack-a-mole: high-effort review found id/depends_on
|
|
378
|
+
// carrying tabs into these rows and shifting columns under graphViewOf. The dump's structural
|
|
379
|
+
// characters are the dump's own concern, so this is the single choke point; the row-shape
|
|
380
|
+
// invariant (exactly 13 cells, structural-char-free) is pinned by test.
|
|
381
|
+
const fold = (v) => (v === null || v === undefined ? '' : String(v).replace(/[\t\n\r]+/g, ' '));
|
|
382
|
+
const prov = (r) => PROVENANCE_COLUMNS.map((c) => fold(r[c])).join('\t');
|
|
383
|
+
const nodes = this.db.prepare(`SELECT id, kind, source_path, summary, ${cols} FROM nodes ORDER BY id`).all();
|
|
344
384
|
const edges = this.db.prepare(`SELECT src, dst, rel, source_path, ${cols} FROM edges ORDER BY src, dst, rel`).all();
|
|
345
385
|
return [
|
|
346
|
-
|
|
347
|
-
|
|
386
|
+
// @implements A-SPEC-568.1 — summary is part of the VALUE (appended last so every positional
|
|
387
|
+
// consumer of the earlier columns is untouched); a summary-only divergence between two builds
|
|
388
|
+
// must fail the convergence comparison rather than pass as equal.
|
|
389
|
+
...nodes.map((n) => `N\t${fold(n.id)}\t${fold(n.kind)}\t${fold(n.source_path)}\t${prov(n)}\t${fold(n.summary)}`),
|
|
390
|
+
...edges.map((e) => `E\t${fold(e.src)}\t${fold(e.dst)}\t${fold(e.rel)}\t${fold(e.source_path)}\t${prov(e)}`),
|
|
348
391
|
].join('\n');
|
|
349
392
|
}
|
|
350
393
|
/**
|
|
@@ -388,6 +431,16 @@ class RtmGraph {
|
|
|
388
431
|
journalMode() {
|
|
389
432
|
return String(this.db.pragma('journal_mode', { simple: true }) ?? '');
|
|
390
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* @implements A-SPEC-569.5
|
|
436
|
+
* Live anchors per source file — distinct SPEC targets of `implements` edges, one GROUP BY.
|
|
437
|
+
* Feeds the anchor-density OBSERVATION at sealing time; nothing reads it into a verdict.
|
|
438
|
+
*/
|
|
439
|
+
implementsAnchorCounts() {
|
|
440
|
+
return this.db.prepare(`SELECT source_path AS sourcePath, COUNT(DISTINCT dst) AS anchors FROM edges
|
|
441
|
+
WHERE rel='implements' AND source_path IS NOT NULL GROUP BY source_path ORDER BY source_path ASC`)
|
|
442
|
+
.all();
|
|
443
|
+
}
|
|
391
444
|
nodeCount() { return this.db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c; }
|
|
392
445
|
edgeCount() { return this.db.prepare('SELECT COUNT(*) AS c FROM edges').get().c; }
|
|
393
446
|
close() { this.db.close(); }
|
|
@@ -23,4 +23,9 @@ export interface CompatCheckOpts {
|
|
|
23
23
|
/** Repo-relative reader for the OS cross-check; null = file absent (skip, fail-open). */
|
|
24
24
|
readFile?: (rel: string) => string | null;
|
|
25
25
|
}
|
|
26
|
+
/** `src/…` path tokens out of the Files to Touch section — backticks, bullets and commas tolerated.
|
|
27
|
+
* Backslashes normalize to `/` FIRST (adversarial round-1): a Windows author legitimately writes
|
|
28
|
+
* `src\holmes\hooks\stop.ts`, and un-normalized it walked straight past the surface prefixes —
|
|
29
|
+
* an OS-compat gate defeated by an OS path convention would be its own counterexample. */
|
|
30
|
+
export declare function filesToTouch(spec: Spec): string[];
|
|
26
31
|
export declare function checkCompatDeclared(spec: Spec, opts?: CompatCheckOpts): string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CELL_VERDICTS = exports.OS_CELLS = exports.HARNESS_CELLS = void 0;
|
|
4
|
+
exports.filesToTouch = filesToTouch;
|
|
4
5
|
exports.checkCompatDeclared = checkCompatDeclared;
|
|
5
6
|
/**
|
|
6
7
|
* REQ-565's enforcement device: the obligation to declare HARNESS (claude/codex/agy) and OS
|
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.17.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",
|