@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
|
@@ -204,6 +204,7 @@ exports.TOOL_SCHEMAS = {
|
|
|
204
204
|
properties: {
|
|
205
205
|
root: str('Optional when the server is bound to a file store — the ledger location is derived from the store itself; if supplied it must resolve to the SAME project, otherwise the approval is refused before anything is written.'),
|
|
206
206
|
id: str('Id of the spec to approve and seal.'),
|
|
207
|
+
dismiss: { type: 'array', items: { type: 'string' }, description: 'A-SPEC-663 — advisory ids (from approval_status.advisoryIds) the author judged not useful; recorded as `dismissed` in the reaction ledger, never blocking. Unknown ids are echoed back in `dismissUnknown`.' },
|
|
207
208
|
},
|
|
208
209
|
required: ['id'],
|
|
209
210
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type CiStatus = 'green' | 'red' | 'clone-failed' | 'checkout-failed' | 'install-failed' | 'build-failed' | 'jest-crashed' | 'vm-unreachable';
|
|
2
|
+
export interface CiRun {
|
|
3
|
+
rev: string;
|
|
4
|
+
os: string;
|
|
5
|
+
arch?: string;
|
|
6
|
+
machine?: string;
|
|
7
|
+
node?: string;
|
|
8
|
+
status: CiStatus | string;
|
|
9
|
+
suites: number;
|
|
10
|
+
tests: number;
|
|
11
|
+
failed: string[];
|
|
12
|
+
durationMs: number;
|
|
13
|
+
at: string;
|
|
14
|
+
note?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CiVerdict {
|
|
17
|
+
os: string;
|
|
18
|
+
state: 'green' | 'red' | 'failed' | 'not-run';
|
|
19
|
+
rev?: string;
|
|
20
|
+
at?: string;
|
|
21
|
+
behind?: number;
|
|
22
|
+
failed: string[];
|
|
23
|
+
status?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @implements A-SPEC-664 — the ADOPTION predicate: does this workspace use the CI matrix at all?
|
|
27
|
+
*
|
|
28
|
+
* Existence, not contents. A `ci-runs.<host>.jsonl` that exists but is empty means a run was
|
|
29
|
+
* attempted here, so silence about a commit is a signal worth saying out loud. No file at all means
|
|
30
|
+
* nobody ever ran the matrix in this workspace — and the runner script is not in the shipped package
|
|
31
|
+
* (`files` carries bin/, dist/, grammars/, playbooks/, scripts/install.ps1, docs/, CHANGELOG), so
|
|
32
|
+
* advising every consumer to run it would be advising a path they do not have. Measured 2026-09-18
|
|
33
|
+
* on a consumer-shaped tree: the Stop hook printed that advice every turn.
|
|
34
|
+
*/
|
|
35
|
+
export declare function hasCiLedger(root: string): boolean;
|
|
36
|
+
export declare function readCiRuns(root: string): CiRun[];
|
|
37
|
+
/** The most recent run for an OS, by `at`. */
|
|
38
|
+
export declare function latestCiRun(runs: CiRun[], os?: string): CiRun | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* What the matrix says about `head`. `behind(rev)` is the injected git distance from that rev to
|
|
41
|
+
* head (commits), or undefined when git cannot answer. No run → `not-run`; an infrastructure
|
|
42
|
+
* status (clone/install/build failed, VM unreachable) → `failed`, which is not red and not green.
|
|
43
|
+
*/
|
|
44
|
+
export declare function ciVerdict(runs: CiRun[], os: string, head: string | undefined, behind: (rev: string) => number | undefined): CiVerdict;
|
|
45
|
+
/** One line for the Stop hook's tracked channel or doctor. Never says green without a green row. */
|
|
46
|
+
export declare function ciStatusLine(v: CiVerdict): string;
|
|
@@ -0,0 +1,137 @@
|
|
|
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.hasCiLedger = hasCiLedger;
|
|
37
|
+
exports.readCiRuns = readCiRuns;
|
|
38
|
+
exports.latestCiRun = latestCiRun;
|
|
39
|
+
exports.ciVerdict = ciVerdict;
|
|
40
|
+
exports.ciStatusLine = ciStatusLine;
|
|
41
|
+
// @implements A-SPEC-664
|
|
42
|
+
/**
|
|
43
|
+
* The CI-runs ledger reader — the product side of the minimal Linux matrix.
|
|
44
|
+
*
|
|
45
|
+
* `scripts/ci-orb-linux.sh` appends one row per judged commit to `.ax/ledger/ci-runs.<host>.jsonl`.
|
|
46
|
+
* This module reads those rows and says, for a given HEAD, what the matrix last said — and says
|
|
47
|
+
* "not run" when it has nothing, never "green". Measured 2026-09-17: the 0.21.0 release's first Linux
|
|
48
|
+
* run showed 136 red suites, all environmental; the cost of not having a row to look at was three
|
|
49
|
+
* diagnostic rounds. Pure except the ledger read; git distance is injected.
|
|
50
|
+
*/
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const FILE_RE = /^ci-runs\.([^.]+)\.jsonl$/;
|
|
54
|
+
/**
|
|
55
|
+
* @implements A-SPEC-664 — the ADOPTION predicate: does this workspace use the CI matrix at all?
|
|
56
|
+
*
|
|
57
|
+
* Existence, not contents. A `ci-runs.<host>.jsonl` that exists but is empty means a run was
|
|
58
|
+
* attempted here, so silence about a commit is a signal worth saying out loud. No file at all means
|
|
59
|
+
* nobody ever ran the matrix in this workspace — and the runner script is not in the shipped package
|
|
60
|
+
* (`files` carries bin/, dist/, grammars/, playbooks/, scripts/install.ps1, docs/, CHANGELOG), so
|
|
61
|
+
* advising every consumer to run it would be advising a path they do not have. Measured 2026-09-18
|
|
62
|
+
* on a consumer-shaped tree: the Stop hook printed that advice every turn.
|
|
63
|
+
*/
|
|
64
|
+
function hasCiLedger(root) {
|
|
65
|
+
try {
|
|
66
|
+
return fs.readdirSync(path.join(root, '.ax', 'ledger')).some((n) => FILE_RE.test(n));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function readCiRuns(root) {
|
|
73
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
74
|
+
let names;
|
|
75
|
+
try {
|
|
76
|
+
names = fs.readdirSync(dir).filter((n) => FILE_RE.test(n)).sort();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const n of names) {
|
|
83
|
+
let text;
|
|
84
|
+
try {
|
|
85
|
+
text = fs.readFileSync(path.join(dir, n), 'utf8');
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const line of text.split('\n')) {
|
|
91
|
+
if (!line.trim())
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
const r = JSON.parse(line);
|
|
95
|
+
if (typeof r.rev !== 'string' || typeof r.os !== 'string' || typeof r.at !== 'string')
|
|
96
|
+
continue;
|
|
97
|
+
out.push({ ...r, rev: r.rev, os: r.os, at: r.at, suites: Number(r.suites ?? 0), tests: Number(r.tests ?? 0),
|
|
98
|
+
failed: Array.isArray(r.failed) ? r.failed.map(String) : [], durationMs: Number(r.durationMs ?? 0), status: String(r.status ?? 'unknown') });
|
|
99
|
+
}
|
|
100
|
+
catch { /* a torn line is not a run */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out.sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0));
|
|
104
|
+
}
|
|
105
|
+
/** The most recent run for an OS, by `at`. */
|
|
106
|
+
function latestCiRun(runs, os = 'linux') {
|
|
107
|
+
let best;
|
|
108
|
+
for (const r of runs)
|
|
109
|
+
if (r.os === os && (!best || r.at >= best.at))
|
|
110
|
+
best = r;
|
|
111
|
+
return best;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* What the matrix says about `head`. `behind(rev)` is the injected git distance from that rev to
|
|
115
|
+
* head (commits), or undefined when git cannot answer. No run → `not-run`; an infrastructure
|
|
116
|
+
* status (clone/install/build failed, VM unreachable) → `failed`, which is not red and not green.
|
|
117
|
+
*/
|
|
118
|
+
function ciVerdict(runs, os, head, behind) {
|
|
119
|
+
const latest = latestCiRun(runs, os);
|
|
120
|
+
if (!latest)
|
|
121
|
+
return { os, state: 'not-run', failed: [] };
|
|
122
|
+
const state = latest.status === 'green' ? 'green' : latest.status === 'red' ? 'red' : 'failed';
|
|
123
|
+
const b = head && latest.rev !== head ? behind(latest.rev) : head ? 0 : undefined;
|
|
124
|
+
return { os, state, rev: latest.rev, at: latest.at, ...(b === undefined ? {} : { behind: b }), failed: latest.failed, status: latest.status };
|
|
125
|
+
}
|
|
126
|
+
/** One line for the Stop hook's tracked channel or doctor. Never says green without a green row. */
|
|
127
|
+
function ciStatusLine(v) {
|
|
128
|
+
if (v.state === 'not-run')
|
|
129
|
+
return `CI: ${v.os} not run (no ci-runs ledger row) — run scripts/ci-orb-linux.sh`;
|
|
130
|
+
const short = (v.rev ?? '').slice(0, 8);
|
|
131
|
+
const behind = v.behind === undefined ? '' : v.behind === 0 ? ' (at HEAD)' : ` (${v.behind} commit${v.behind === 1 ? '' : 's'} behind HEAD)`;
|
|
132
|
+
if (v.state === 'green')
|
|
133
|
+
return `CI: ${v.os} ${short} green${behind}`;
|
|
134
|
+
if (v.state === 'red')
|
|
135
|
+
return `CI: ${v.os} ${short} red${behind}: ${v.failed.slice(0, 5).join(', ')}${v.failed.length > 5 ? ` +${v.failed.length - 5}` : ''}`;
|
|
136
|
+
return `CI: ${v.os} ${short} ${v.status}${behind} — the matrix could not judge this commit`;
|
|
137
|
+
}
|
|
@@ -7,6 +7,7 @@ exports.ceilingMetrics = ceilingMetrics;
|
|
|
7
7
|
exports.achievementRate = achievementRate;
|
|
8
8
|
exports.boundsImpact = boundsImpact;
|
|
9
9
|
exports.overlapOf = overlapOf;
|
|
10
|
+
// @implements A-SPEC-314
|
|
10
11
|
// @implements A-SPEC-296
|
|
11
12
|
/**
|
|
12
13
|
* Objective evaluation of a replay corpus (Goal Phase 8).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Mutation } from '../spec/kills';
|
|
2
|
+
export type UnapplicableReason = 'where-empty' | 'no-anchored-source' | 'where-looks-like-path' | 'where-not-found';
|
|
3
|
+
export interface UnapplicableKill {
|
|
4
|
+
where: string;
|
|
5
|
+
mutate: string;
|
|
6
|
+
reason: UnapplicableReason;
|
|
7
|
+
}
|
|
8
|
+
export interface KillsAdvisory {
|
|
9
|
+
kind: 'kills-unapplicable';
|
|
10
|
+
tspec: string;
|
|
11
|
+
aspec: string;
|
|
12
|
+
total: number;
|
|
13
|
+
applicable: number;
|
|
14
|
+
unapplicable: UnapplicableKill[];
|
|
15
|
+
}
|
|
16
|
+
/** Reason precedence: empty → no source at all → found (applicable) → path-shaped → not found. Pure. */
|
|
17
|
+
export declare function killsApplicability(kills: Mutation[], sourceTexts: Record<string, string>): {
|
|
18
|
+
applicable: Mutation[];
|
|
19
|
+
unapplicable: UnapplicableKill[];
|
|
20
|
+
};
|
|
21
|
+
/** The advisory a T-SPEC approval carries; null when there is nothing to say. Pure. */
|
|
22
|
+
export declare function killsAdvisory(tspec: string, aspec: string, kills: Mutation[], sourceTexts: Record<string, string>): KillsAdvisory | null;
|
|
23
|
+
/**
|
|
24
|
+
* `applied:false` results are UNAPPLIED, never survivors: a mutation that was never made cannot have
|
|
25
|
+
* survived anything. Survivors are the applied mutations the covering tests did not catch. Pure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function partitionMutateResults<R extends {
|
|
28
|
+
mutation: Mutation;
|
|
29
|
+
applied: boolean;
|
|
30
|
+
reason?: string;
|
|
31
|
+
verdict?: 'killed' | 'survived';
|
|
32
|
+
}>(results: R[]): {
|
|
33
|
+
survivors: Mutation[];
|
|
34
|
+
unapplied: {
|
|
35
|
+
where: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
/** Text of every production source that @implements `aspecId`. Unreadable root → {}; unreadable file → skipped. */
|
|
40
|
+
export declare function anchoredSourceTexts(root: string, aspecId: string): Record<string, string>;
|
|
@@ -0,0 +1,147 @@
|
|
|
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.killsApplicability = killsApplicability;
|
|
37
|
+
exports.killsAdvisory = killsAdvisory;
|
|
38
|
+
exports.partitionMutateResults = partitionMutateResults;
|
|
39
|
+
exports.anchoredSourceTexts = anchoredSourceTexts;
|
|
40
|
+
// @implements A-SPEC-662
|
|
41
|
+
/**
|
|
42
|
+
* `kills` applicability — the honesty half of the C-layer mutation field.
|
|
43
|
+
*
|
|
44
|
+
* Measured 2026-09-17 (docs/goals/EVIDENCE-kills-cost-2026-09-17.md): every one of the 22 `kills`
|
|
45
|
+
* entries in this repository wrote `where` as a file path and `mutate` as prose, so the literal-
|
|
46
|
+
* replacement engine (`spec/kills.ts`) applied none of them — and `test_run --mutate` then answered
|
|
47
|
+
* `survivors: []`, the same shape a clean run has. Nothing at approval time said a word.
|
|
48
|
+
*
|
|
49
|
+
* Pure judgement here; the walk that collects anchored source text is the only I/O, kept separate
|
|
50
|
+
* and injectable. The rule is the engine's own: `where` must occur literally in a production source
|
|
51
|
+
* that @implements the A-SPEC. This module never blocks — it names what will not apply.
|
|
52
|
+
*/
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const path = __importStar(require("node:path"));
|
|
55
|
+
// A bare relative path with an extension and no whitespace — the shape all 22 measured entries had.
|
|
56
|
+
const LOOKS_LIKE_PATH = /^[\w@./-]+\.[A-Za-z0-9]{1,6}$/;
|
|
57
|
+
/** Reason precedence: empty → no source at all → found (applicable) → path-shaped → not found. Pure. */
|
|
58
|
+
function killsApplicability(kills, sourceTexts) {
|
|
59
|
+
const texts = Object.values(sourceTexts);
|
|
60
|
+
const applicable = [];
|
|
61
|
+
const unapplicable = [];
|
|
62
|
+
for (const m of kills) {
|
|
63
|
+
const where = String(m.where ?? '');
|
|
64
|
+
let reason = null;
|
|
65
|
+
if (where.trim() === '')
|
|
66
|
+
reason = 'where-empty';
|
|
67
|
+
else if (texts.length === 0)
|
|
68
|
+
reason = 'no-anchored-source';
|
|
69
|
+
else if (texts.some((t) => t.includes(where)))
|
|
70
|
+
reason = null;
|
|
71
|
+
else if (LOOKS_LIKE_PATH.test(where))
|
|
72
|
+
reason = 'where-looks-like-path';
|
|
73
|
+
else
|
|
74
|
+
reason = 'where-not-found';
|
|
75
|
+
if (reason === null)
|
|
76
|
+
applicable.push(m);
|
|
77
|
+
else
|
|
78
|
+
unapplicable.push({ where: m.where, mutate: m.mutate, reason });
|
|
79
|
+
}
|
|
80
|
+
return { applicable, unapplicable };
|
|
81
|
+
}
|
|
82
|
+
/** The advisory a T-SPEC approval carries; null when there is nothing to say. Pure. */
|
|
83
|
+
function killsAdvisory(tspec, aspec, kills, sourceTexts) {
|
|
84
|
+
if (kills.length === 0)
|
|
85
|
+
return null;
|
|
86
|
+
const r = killsApplicability(kills, sourceTexts);
|
|
87
|
+
if (r.unapplicable.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
return { kind: 'kills-unapplicable', tspec, aspec, total: kills.length, applicable: r.applicable.length, unapplicable: r.unapplicable };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `applied:false` results are UNAPPLIED, never survivors: a mutation that was never made cannot have
|
|
93
|
+
* survived anything. Survivors are the applied mutations the covering tests did not catch. Pure.
|
|
94
|
+
*/
|
|
95
|
+
function partitionMutateResults(results) {
|
|
96
|
+
const survivors = [];
|
|
97
|
+
const unapplied = [];
|
|
98
|
+
for (const r of results) {
|
|
99
|
+
if (r.applied === false) {
|
|
100
|
+
unapplied.push({ where: r.mutation.where, reason: r.reason ?? 'not applied' });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (r.verdict === 'survived')
|
|
104
|
+
survivors.push(r.mutation);
|
|
105
|
+
}
|
|
106
|
+
return { survivors, unapplied };
|
|
107
|
+
}
|
|
108
|
+
// The same walk `handlers/test-execution.ts` (`sourceFileWithMutation`) makes: bounded, skips vendored,
|
|
109
|
+
// hidden, built and test files. Keys are repo-relative POSIX paths.
|
|
110
|
+
const SOURCE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|py|go|rs|java|kt|cs|rb|php|swift)$/;
|
|
111
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'reference', 'vendor', 'third_party']);
|
|
112
|
+
/** Text of every production source that @implements `aspecId`. Unreadable root → {}; unreadable file → skipped. */
|
|
113
|
+
function anchoredSourceTexts(root, aspecId) {
|
|
114
|
+
const out = {};
|
|
115
|
+
const marker = `@implements ${aspecId}`;
|
|
116
|
+
const walk = (d) => {
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = fs.readdirSync(d, { withFileTypes: true });
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const e of entries) {
|
|
125
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.'))
|
|
126
|
+
continue;
|
|
127
|
+
const p = path.join(d, e.name);
|
|
128
|
+
if (e.isDirectory()) {
|
|
129
|
+
walk(p);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (!SOURCE.test(e.name) || /\.test\./.test(e.name))
|
|
133
|
+
continue;
|
|
134
|
+
let text;
|
|
135
|
+
try {
|
|
136
|
+
text = fs.readFileSync(p, 'utf8');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (text.includes(marker))
|
|
142
|
+
out[path.relative(root, p).split(path.sep).join('/')] = text;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
walk(root);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
@@ -5,6 +5,7 @@ exports.manualBaselineOutcomes = manualBaselineOutcomes;
|
|
|
5
5
|
exports.shippedOutcomes = shippedOutcomes;
|
|
6
6
|
exports.shippedBeatsManual = shippedBeatsManual;
|
|
7
7
|
exports.poolCoverage = poolCoverage;
|
|
8
|
+
// @implements A-SPEC-412, A-SPEC-413, A-SPEC-414
|
|
8
9
|
// @implements A-SPEC-404
|
|
9
10
|
/**
|
|
10
11
|
* The "strong model with plain file search" baseline (Goal Phase B).
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type AdvisoryKind = 'impact-advisory' | 'anchor-density' | 'ftt-fulfilment' | 'trace-gap' | 'kills-unapplicable';
|
|
2
|
+
export type AdvisoryOutcome = 'issued' | 'resolved' | 'persisted' | 'dismissed';
|
|
3
|
+
export interface AdvisoryKey {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: AdvisoryKind;
|
|
6
|
+
aspec: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AdvisoryOutcomeRecord extends AdvisoryKey {
|
|
9
|
+
outcome: AdvisoryOutcome;
|
|
10
|
+
firstSeen: string;
|
|
11
|
+
judgedAt: string;
|
|
12
|
+
head: string;
|
|
13
|
+
replica?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AdvisoryState extends AdvisoryKey {
|
|
16
|
+
firstSeen: string;
|
|
17
|
+
last: AdvisoryOutcome;
|
|
18
|
+
lastAt: string;
|
|
19
|
+
rows: number;
|
|
20
|
+
}
|
|
21
|
+
/** Canonical JSON: sorted keys, arrays kept in order — the same payload always gives the same id. */
|
|
22
|
+
export declare function canonical(value: unknown): string;
|
|
23
|
+
/** Deterministic identity of one finding: kind + spec + canonical payload → sha256, first 16 hex. */
|
|
24
|
+
export declare function advisoryId(kind: AdvisoryKind, aspec: string, payload: unknown): string;
|
|
25
|
+
/** Latest state per id, in first-seen order. `dismissed` and `resolved` are terminal until re-issued. */
|
|
26
|
+
export declare function foldAdvisoryState(rows: AdvisoryOutcomeRecord[]): AdvisoryState[];
|
|
27
|
+
/**
|
|
28
|
+
* Judge the advisories a re-evaluation produced against what the ledger already knows.
|
|
29
|
+
* - a current finding with no state, or whose last state is resolved/dismissed → `issued`
|
|
30
|
+
* - a current finding whose last state is issued/persisted → `persisted`
|
|
31
|
+
* - an open (issued/persisted) finding of the judged scope that is no longer current → `resolved`
|
|
32
|
+
* One row per (id, outcome) per UTC day: re-running the same query the same day adds nothing.
|
|
33
|
+
* `scope` names which open findings this evaluation could have seen (so an unrelated spec's finding
|
|
34
|
+
* is never resolved by a query that did not look at it).
|
|
35
|
+
*/
|
|
36
|
+
export declare function judgeAdvisories(state: AdvisoryState[], current: AdvisoryKey[], scope: (s: AdvisoryState) => boolean, now: string, head: string, recent?: AdvisoryOutcomeRecord[], opts?: {
|
|
37
|
+
issue?: boolean;
|
|
38
|
+
}): AdvisoryOutcomeRecord[];
|
|
39
|
+
/** Explicit dismissal by the author: only for findings the ledger knows and that are still open. */
|
|
40
|
+
export declare function dismissAdvisories(state: AdvisoryState[], ids: string[], now: string, head: string): AdvisoryOutcomeRecord[];
|
|
41
|
+
export type AdvisoryCensus = Record<AdvisoryKind, {
|
|
42
|
+
issued: number;
|
|
43
|
+
resolved: number;
|
|
44
|
+
persisted: number;
|
|
45
|
+
dismissed: number;
|
|
46
|
+
}>;
|
|
47
|
+
export declare const ADVISORY_KINDS: AdvisoryKind[];
|
|
48
|
+
/** Per kind: how many findings were ever issued, and how many stand resolved / persisted / dismissed now. */
|
|
49
|
+
export declare function advisoryCensus(rows: AdvisoryOutcomeRecord[]): AdvisoryCensus;
|
|
50
|
+
export declare function appendAdvisoryOutcomes(root: string, rows: AdvisoryOutcomeRecord[]): boolean;
|
|
51
|
+
export declare function readAdvisoryOutcomes(root: string): AdvisoryOutcomeRecord[];
|
|
52
|
+
/**
|
|
53
|
+
* One trace-gap finding per unlinked approved A-SPEC that declares at least one SCANNED production
|
|
54
|
+
* file in its Files to Touch — the same population REQ-658 reports per change, taken whole. The
|
|
55
|
+
* payload is the sorted file list, so moving one anchor changes the id (a smaller gap is a new
|
|
56
|
+
* finding, the old one resolved) — honest, if noisy; the census counts findings, not specs.
|
|
57
|
+
*/
|
|
58
|
+
export declare function traceGapAdvisories(specs: ReadonlyArray<{
|
|
59
|
+
id: string;
|
|
60
|
+
type: string;
|
|
61
|
+
status: string;
|
|
62
|
+
sections?: Record<string, string>;
|
|
63
|
+
}>, scanned: ReadonlyArray<{
|
|
64
|
+
path: string;
|
|
65
|
+
implementsSpecs?: string[];
|
|
66
|
+
}>, root?: string): AdvisoryKey[];
|
|
67
|
+
/**
|
|
68
|
+
* The findings a seal or a preview computed, as ledger keys. Each kind's payload is its minimal
|
|
69
|
+
* canonical form — paths and reasons only, never summaries or counts that vary without the finding
|
|
70
|
+
* changing. Absent inputs contribute nothing.
|
|
71
|
+
*/
|
|
72
|
+
export interface ComputedAdvisories {
|
|
73
|
+
impactAdvisory?: {
|
|
74
|
+
files: Array<{
|
|
75
|
+
path: string;
|
|
76
|
+
}>;
|
|
77
|
+
} | null;
|
|
78
|
+
anchorDensity?: Array<{
|
|
79
|
+
path: string;
|
|
80
|
+
}> | null;
|
|
81
|
+
fttFulfilment?: {
|
|
82
|
+
missing: Array<{
|
|
83
|
+
path: string;
|
|
84
|
+
}>;
|
|
85
|
+
moved: Array<{
|
|
86
|
+
path: string;
|
|
87
|
+
}>;
|
|
88
|
+
alternatives: Array<{
|
|
89
|
+
path: string;
|
|
90
|
+
}>;
|
|
91
|
+
} | null;
|
|
92
|
+
killsAdvisory?: {
|
|
93
|
+
unapplicable: Array<{
|
|
94
|
+
where: string;
|
|
95
|
+
reason: string;
|
|
96
|
+
}>;
|
|
97
|
+
} | null;
|
|
98
|
+
traceGap?: {
|
|
99
|
+
files: string[];
|
|
100
|
+
} | null;
|
|
101
|
+
}
|
|
102
|
+
export declare function currentAdvisoryKeys(aspec: string, c: ComputedAdvisories): {
|
|
103
|
+
keys: AdvisoryKey[];
|
|
104
|
+
ids: Partial<Record<AdvisoryKind, string>>;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* One act: read the ledger, judge `current` against it within `scope`, apply explicit dismissals,
|
|
108
|
+
* append, and return what the response should carry. I/O failures leave the response without the
|
|
109
|
+
* fields (the seal or the query is unaffected). Never throws.
|
|
110
|
+
*/
|
|
111
|
+
export declare function recordAdvisoryOutcomes(root: string, aspec: string, current: AdvisoryKey[], scope: (s: AdvisoryState) => boolean, head: string, dismiss?: string[], now?: string, opts?: {
|
|
112
|
+
issue?: boolean;
|
|
113
|
+
}): {
|
|
114
|
+
history: AdvisoryState[];
|
|
115
|
+
dismissUnknown: string[];
|
|
116
|
+
};
|
|
117
|
+
/** The scanned production files an A-SPEC declares — the trace-gap payload for one spec. */
|
|
118
|
+
export declare function traceGapFiles(spec: {
|
|
119
|
+
sections?: Record<string, string>;
|
|
120
|
+
}, scanned: ReadonlyArray<{
|
|
121
|
+
path: string;
|
|
122
|
+
}>, root?: string): string[];
|
|
123
|
+
/** The commit a reaction row is judged at; 'nogit' when git cannot say. Never throws. */
|
|
124
|
+
export declare function gitHeadOf(root: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* The trace-gap finding for ONE spec, status aside: the scanned production files it declares, or
|
|
127
|
+
* null when a scanned file already anchors it or it declares none. Used at the seal (where the
|
|
128
|
+
* spec is approved by the time this runs) and in the preview.
|
|
129
|
+
*/
|
|
130
|
+
export declare function traceGapFor(specId: string, spec: {
|
|
131
|
+
sections?: Record<string, string>;
|
|
132
|
+
}, scanned: ReadonlyArray<{
|
|
133
|
+
path: string;
|
|
134
|
+
implementsSpecs?: string[];
|
|
135
|
+
}>, root?: string): {
|
|
136
|
+
files: string[];
|
|
137
|
+
} | null;
|