@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,76 @@
|
|
|
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.RTM_REFRESH_MARKER = exports.RTM_REFRESH_TTL_MS = void 0;
|
|
37
|
+
exports.maybeSpawnRtmRefresh = maybeSpawnRtmRefresh;
|
|
38
|
+
// @implements A-SPEC-566.4
|
|
39
|
+
/**
|
|
40
|
+
* Graph freshness, kept by the product itself. The impact advisory's quality factor was MEASURED to
|
|
41
|
+
* be staleness (an 8-day-old graph found 7 advisories; a 1-second reindex found 17), so the Stop
|
|
42
|
+
* hook — the moment changes exist — spawns a DETACHED reindex behind a TTL gate. The idiom is
|
|
43
|
+
* A-SPEC-547.2's update refresh verbatim: TTL file, detached child, fail-soft everywhere; the
|
|
44
|
+
* parent never waits (zero turn latency) and the approval path still only ever REOPENS the graph.
|
|
45
|
+
*/
|
|
46
|
+
const fs = __importStar(require("node:fs"));
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
exports.RTM_REFRESH_TTL_MS = 30 * 60 * 1000;
|
|
49
|
+
exports.RTM_REFRESH_MARKER = path.join('.ax', 'state', 'rtm-refresh-last');
|
|
50
|
+
function maybeSpawnRtmRefresh(opts) {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(path.join(opts.root, '.ax')))
|
|
53
|
+
return; // nothing to refresh
|
|
54
|
+
const marker = path.join(opts.root, exports.RTM_REFRESH_MARKER);
|
|
55
|
+
const ttl = opts.ttlMs ?? exports.RTM_REFRESH_TTL_MS;
|
|
56
|
+
try {
|
|
57
|
+
const age = opts.now - fs.statSync(marker).mtimeMs;
|
|
58
|
+
if (age < ttl)
|
|
59
|
+
return; // one refresh per window
|
|
60
|
+
}
|
|
61
|
+
catch { /* absent marker → refresh is due */ }
|
|
62
|
+
// Touch BEFORE spawning: a burst of Stops inside one window must not stampede children.
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
65
|
+
fs.writeFileSync(marker, String(opts.now));
|
|
66
|
+
fs.utimesSync(marker, new Date(opts.now), new Date(opts.now));
|
|
67
|
+
}
|
|
68
|
+
catch { /* an unwritable marker is not a reason to skip the refresh itself */ }
|
|
69
|
+
const child = opts.spawn(opts.execPath, [opts.scriptPath, opts.root], { detached: true, stdio: 'ignore' });
|
|
70
|
+
child?.unref?.();
|
|
71
|
+
}
|
|
72
|
+
catch { /* freshness is maintenance, never a hook failure */ }
|
|
73
|
+
}
|
|
74
|
+
// The child lives in rtm-refresh-child.ts, NOT here: this module is required by the Stop hook, and
|
|
75
|
+
// the gate path must never carry the AST substrate (A-SPEC-510.2 pinned it, and its static check
|
|
76
|
+
// caught the first cut of this slice doing exactly that). The gate side is fs+path only.
|
|
@@ -910,6 +910,48 @@ if (require.main === module) {
|
|
|
910
910
|
const unrecorded = unrecordedApprovals(stopProjectRoot());
|
|
911
911
|
// @implements A-SPEC-455
|
|
912
912
|
const rolledBack = rolledBackLedgers(stopProjectRoot());
|
|
913
|
+
// @implements A-SPEC-564.2 — the Claude usage enrichment: numbers out of transcript_path into
|
|
914
|
+
// the session-context ledger, once per session. Capability-declared: Codex/AGY pass no
|
|
915
|
+
// transcript_path and skip silently. Fail-open around EVERYTHING — observation never touches
|
|
916
|
+
// the verdict below (Judgments must not be budgeted), and a giant transcript yields a capped
|
|
917
|
+
// partial sum, never a slow Stop.
|
|
918
|
+
try {
|
|
919
|
+
const tPath = input.transcript_path;
|
|
920
|
+
if (typeof tPath === 'string' && tPath !== '') {
|
|
921
|
+
const sc = require('../governance/session-context');
|
|
922
|
+
const root = stopProjectRoot();
|
|
923
|
+
const already = sc.readSessionContext(root).some((r) => r.kind === 'usage' && r.sessionKey === sessionId);
|
|
924
|
+
if (!already) {
|
|
925
|
+
// Adversarial round (2026-09-06): a FIFO at transcript_path blocked this read FOREVER,
|
|
926
|
+
// wedging every turn end — the queue writer's round-7 lesson, replayed on the observer.
|
|
927
|
+
// TYPE BEFORE READ: only a regular file is a transcript; anything else is a silent skip.
|
|
928
|
+
if (!fs.lstatSync(tPath).isFile())
|
|
929
|
+
throw new Error('not a regular file');
|
|
930
|
+
const text = fs.readFileSync(tPath, 'utf8');
|
|
931
|
+
const sum = sc.summarizeTranscript(text.split('\n'));
|
|
932
|
+
if (sum.turns > 0) {
|
|
933
|
+
sc.appendSessionContext(root, {
|
|
934
|
+
kind: 'usage', sessionKey: sessionId, models: sum.models, usage: sum.usage,
|
|
935
|
+
turns: sum.turns, ...(sum.truncated ? { truncated: true } : {}), ts: new Date().toISOString(),
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch { /* silent skip — the other harnesses' path, and any read failure, land here */ }
|
|
942
|
+
// @implements A-SPEC-566.4 — graph freshness rides the same turn boundary, detached and
|
|
943
|
+
// TTL-gated (A-SPEC-547.2's idiom): staleness was MEASURED to be the advisory's quality
|
|
944
|
+
// factor (7 findings on an 8-day graph, 17 after a 1s reindex). Never waits, never judges.
|
|
945
|
+
try {
|
|
946
|
+
const { maybeSpawnRtmRefresh } = require('./rtm-refresh');
|
|
947
|
+
const cp = require('node:child_process');
|
|
948
|
+
maybeSpawnRtmRefresh({
|
|
949
|
+
root: stopProjectRoot(), now: Date.now(), execPath: process.execPath,
|
|
950
|
+
scriptPath: path.resolve(__dirname, 'rtm-refresh-child.js'),
|
|
951
|
+
spawn: (cmd, args, o) => cp.spawn(cmd, args, o),
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
catch { /* maintenance, never a hook failure */ }
|
|
913
955
|
let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec });
|
|
914
956
|
// @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
|
|
915
957
|
// the operator observes RED-first gaps before an owner promotes the posture to strict.
|
|
@@ -261,23 +261,19 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
261
261
|
reason: string;
|
|
262
262
|
findings?: undefined;
|
|
263
263
|
conflict?: undefined;
|
|
264
|
-
approved?: undefined;
|
|
265
|
-
digest?: undefined;
|
|
266
264
|
} | {
|
|
267
265
|
ok: boolean;
|
|
268
266
|
reason: string;
|
|
269
267
|
findings: import("../spec/validator").Finding[];
|
|
270
268
|
conflict?: undefined;
|
|
271
|
-
approved?: undefined;
|
|
272
|
-
digest?: undefined;
|
|
273
269
|
} | {
|
|
274
270
|
ok: boolean;
|
|
275
271
|
reason: string;
|
|
276
272
|
conflict: import("../spec/version-conflict").ConflictDetail;
|
|
277
273
|
findings?: undefined;
|
|
278
|
-
approved?: undefined;
|
|
279
|
-
digest?: undefined;
|
|
280
274
|
} | {
|
|
275
|
+
anchorDensity?: import("../rtm/anchor-density").AnchorDensityFinding[] | undefined;
|
|
276
|
+
impactAdvisory?: import("../rtm/impact-advisory").ImpactAdvisory | undefined;
|
|
281
277
|
approved: string;
|
|
282
278
|
digest: string;
|
|
283
279
|
ok?: undefined;
|
|
@@ -535,7 +531,6 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
535
531
|
changed: string[];
|
|
536
532
|
}): Promise<{
|
|
537
533
|
breadthWarning?: string | undefined;
|
|
538
|
-
impacted: string[];
|
|
539
534
|
rankedImpact: {
|
|
540
535
|
file: string;
|
|
541
536
|
score: number;
|
|
@@ -546,6 +541,12 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
546
541
|
reason: "hub" | "depth";
|
|
547
542
|
inDegree?: number;
|
|
548
543
|
}[] | undefined;
|
|
544
|
+
summariesOmitted?: number | undefined;
|
|
545
|
+
impacted: string[];
|
|
546
|
+
impactedSummaries: {
|
|
547
|
+
id: string;
|
|
548
|
+
summary: string | null;
|
|
549
|
+
}[];
|
|
549
550
|
}>;
|
|
550
551
|
rtm_reindex(a: {
|
|
551
552
|
root: string;
|
|
@@ -151,8 +151,16 @@ const cacheDirFor = (root) => {
|
|
|
151
151
|
};
|
|
152
152
|
// @implements A-SPEC-283 — bumped whenever the graph's node/edge shape changes, so a store written
|
|
153
153
|
// by an older build is rebuilt rather than read with new assumptions.
|
|
154
|
-
|
|
155
|
-
const
|
|
154
|
+
// @implements A-SPEC-568.1 — /3: nodes gained the intent `summary` column.
|
|
155
|
+
const RTM_GRAPH_SCHEMA = 'rtm-graph/3';
|
|
156
|
+
// @implements A-SPEC-569.3 — how many impacted specs get their intent sentence attached. A prose
|
|
157
|
+
// constant, never a verdict input: the impacted list itself is never truncated.
|
|
158
|
+
const SUMMARY_CAP = 40;
|
|
159
|
+
// @implements A-SPEC-569.1 (revision) — /2: pre-fix 0.16.0 builds could persist forged structural
|
|
160
|
+
// characters in the summary column, and every other basis field would still match after upgrading.
|
|
161
|
+
// A-SPEC-283's own rule applies to us too: an older build's artifact is rebuilt, never read with
|
|
162
|
+
// new assumptions.
|
|
163
|
+
const RTM_EXTRACTOR_VERSION = 'holmes-rtm/2';
|
|
156
164
|
const cachedScan = (root, repoRoot = root) => new cpg_scanner_1.CpgScanner(undefined, new scan_cache_1.ScanFileCache(cacheDirFor(root))).scan(root, repoRoot);
|
|
157
165
|
// @implements A-SPEC-131
|
|
158
166
|
// Same scan, with the skip report kept: the callers that make honesty claims (cpg_scan's surface,
|
|
@@ -1684,7 +1692,75 @@ function makeRawHandlers(store, opts) {
|
|
|
1684
1692
|
if (approveResolved.source === 'grant' && approveResolved.root && approveResolved.approval.nonce) {
|
|
1685
1693
|
(0, approval_grants_1.consumeGrantFile)(approveResolved.root, approveResolved.approval.nonce);
|
|
1686
1694
|
}
|
|
1687
|
-
|
|
1695
|
+
// @implements A-SPEC-566.2 — the impact advisory rides the SUCCESS, after the seal is done:
|
|
1696
|
+
// the verdict is already committed, so nothing here can change it (advisory, never gate —
|
|
1697
|
+
// Judgments must not be budgeted). Reuses the persisted graph READ-ONLY; it never scans,
|
|
1698
|
+
// parses or builds (scan:build measured 20~38x — an approval must not pay that), and every
|
|
1699
|
+
// failure below degrades to "no advisory field" on an otherwise identical response.
|
|
1700
|
+
let impactAdvisory;
|
|
1701
|
+
let anchorDensity;
|
|
1702
|
+
try {
|
|
1703
|
+
if (spec.type === 'A-SPEC' && a.root) {
|
|
1704
|
+
const dbPath = path.join(a.root, '.ax', 'rtm.sqlite');
|
|
1705
|
+
if (fs.existsSync(dbPath)) {
|
|
1706
|
+
const { declaredImpactGap, appendImpactAdvisory } = require('../rtm/impact-advisory');
|
|
1707
|
+
const { filesToTouch } = require('../spec/compat-impact');
|
|
1708
|
+
const { RtmGraph } = require('../rtm/rtm-graph');
|
|
1709
|
+
// Closed in finally (high-effort review F4): this handler lives in a long-running MCP
|
|
1710
|
+
// server, and an unclosed native handle per approval accumulates for the process
|
|
1711
|
+
// lifetime — and on Windows can hold rtm.sqlite locked against the next rebuild.
|
|
1712
|
+
const graph = new RtmGraph(dbPath);
|
|
1713
|
+
try {
|
|
1714
|
+
const ftt = filesToTouch(candidate);
|
|
1715
|
+
const gap = declaredImpactGap(ftt, graph, (rel) => { try {
|
|
1716
|
+
return fs.readFileSync(path.join(a.root, rel), 'utf8');
|
|
1717
|
+
}
|
|
1718
|
+
catch {
|
|
1719
|
+
return null;
|
|
1720
|
+
} });
|
|
1721
|
+
if (gap) {
|
|
1722
|
+
const graphAsOf = (() => { try {
|
|
1723
|
+
return fs.statSync(dbPath).mtime.toISOString();
|
|
1724
|
+
}
|
|
1725
|
+
catch {
|
|
1726
|
+
return undefined;
|
|
1727
|
+
} })();
|
|
1728
|
+
impactAdvisory = { ...gap, ...(graphAsOf ? { graphAsOf } : {}) };
|
|
1729
|
+
appendImpactAdvisory(a.root, {
|
|
1730
|
+
aspec: a.id, files: gap.files.map((f) => f.path), more: gap.more,
|
|
1731
|
+
...(graphAsOf ? { graphAsOf } : {}), ts: new Date().toISOString(),
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
// @implements A-SPEC-569.5 — anchor-density OBSERVATION, same reopened graph, same
|
|
1735
|
+
// no-scan contract, same lifecycle as the advisory above (observe → ledger → measure
|
|
1736
|
+
// before anyone proposes promotion). Never a verdict input: the seal is already done,
|
|
1737
|
+
// and its own failure degrades to "no field" on an otherwise identical response.
|
|
1738
|
+
try {
|
|
1739
|
+
const { anchorDensityFindings, appendAnchorDensity } = require('../rtm/anchor-density');
|
|
1740
|
+
const findings = anchorDensityFindings(ftt, graph.implementsAnchorCounts());
|
|
1741
|
+
if (findings.length > 0) {
|
|
1742
|
+
anchorDensity = findings;
|
|
1743
|
+
appendAnchorDensity(a.root, {
|
|
1744
|
+
aspec: a.id, files: findings.map((f) => ({ path: f.path, anchors: f.anchors })),
|
|
1745
|
+
p90: findings[0].p90, ts: new Date().toISOString(),
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
catch {
|
|
1750
|
+
anchorDensity = undefined;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
finally {
|
|
1754
|
+
graph.close();
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
catch {
|
|
1760
|
+
impactAdvisory = undefined;
|
|
1761
|
+
anchorDensity = undefined;
|
|
1762
|
+
}
|
|
1763
|
+
return { approved: a.id, digest, ...(impactAdvisory ? { impactAdvisory } : {}), ...(anchorDensity ? { anchorDensity } : {}) };
|
|
1688
1764
|
},
|
|
1689
1765
|
async spec_list(a) {
|
|
1690
1766
|
assertSpecStoreReachable('spec_list', store, a.root); // @implements A-SPEC-419
|
|
@@ -2205,6 +2281,9 @@ function makeRawHandlers(store, opts) {
|
|
|
2205
2281
|
// (cached vectors only, set fixed, covered hits move, why-line attached). Any missing
|
|
2206
2282
|
// signal — no tier, no key, cold cache, embed failure — leaves the report untouched;
|
|
2207
2283
|
// localization itself never fails because of the semantic layer.
|
|
2284
|
+
//
|
|
2285
|
+
// A spec-intent-vector assist (REQ-568 S3) was wired ahead of localizeIssue here and REVERTED
|
|
2286
|
+
// on its pre-registered replay — see rtm/localize.ts at the matchedSpecs join for the numbers.
|
|
2208
2287
|
try {
|
|
2209
2288
|
if (report.hits.length > 1
|
|
2210
2289
|
&& (0, localize_1.citationsIn)(a.issue, new Set(governed.map((s) => s.id))).cited.length === 0) {
|
|
@@ -2470,7 +2549,14 @@ function makeRawHandlers(store, opts) {
|
|
|
2470
2549
|
// was asked. Bind the derivation and use it.
|
|
2471
2550
|
const root = projectRootOf(a.root);
|
|
2472
2551
|
const scanned = cachedScan(root);
|
|
2473
|
-
|
|
2552
|
+
// @implements A-SPEC-569.2 — the impact/advisory graph is APPROVED-ONLY. A draft needs no
|
|
2553
|
+
// approval to exist, and the 0.16.0 adversarial round showed one reaching the agent-visible
|
|
2554
|
+
// channels (impacted closure, advisory anchor summaries) — the trust boundary for those
|
|
2555
|
+
// channels is the act of approval. NOT filterGoverned: that predicate passes drafts (it only
|
|
2556
|
+
// drops outdated/legacy), which is exactly what let this in. Diagnosis (rtm_check) and
|
|
2557
|
+
// matching (issue_localize / maintenance_analyze) keep their own populations — the replay
|
|
2558
|
+
// pins were measured on them.
|
|
2559
|
+
const specs = (await store.list()).filter((s) => s.status === 'approved');
|
|
2474
2560
|
// @implements A-SPEC-283
|
|
2475
2561
|
// Reuse the persisted graph when its basis still holds. Measured: on the warm path the graph
|
|
2476
2562
|
// build is ~81% of the cost and reopening is ~0ms. `scanDigest` is the field that makes this
|
|
@@ -2497,7 +2583,12 @@ function makeRawHandlers(store, opts) {
|
|
|
2497
2583
|
// explainImpact, not impactedBy: the bounds and the breadth signal must reach the caller.
|
|
2498
2584
|
// An impact set is not just a list — a broad one means "review the contract", and a consumer
|
|
2499
2585
|
// that cannot tell the difference will try to bundle two hundred call sites.
|
|
2500
|
-
const { specs:
|
|
2586
|
+
const { specs: impactedRaw, reachedByDepth, stoppedAt, seedIsHub } = (0, rtm_builder_1.explainImpact)(g, a.changed);
|
|
2587
|
+
// @implements A-SPEC-569.2 — the closure walks EDGES, and an implements edge is owned by
|
|
2588
|
+
// the code file, so an anchor naming a draft (or a spec nobody wrote) still emits one —
|
|
2589
|
+
// deliberately, for rtm_check's dangling diagnosis. The CHANNEL filter is node existence:
|
|
2590
|
+
// approved-only specs were given nodes above, so only sealed intent reaches the caller.
|
|
2591
|
+
const impacted = impactedRaw.filter((id) => g.hasNode(id));
|
|
2501
2592
|
// @implements A-SPEC-469 — the graded FILE surface beside the spec closure, same code path
|
|
2502
2593
|
// as the S-484 measurement (identity, not reimplementation). Seeds are the changed symbols'
|
|
2503
2594
|
// nodes; the files that own them are excluded — a prediction naming the change itself is
|
|
@@ -2508,8 +2599,26 @@ function makeRawHandlers(store, opts) {
|
|
|
2508
2599
|
.map((id) => (id.includes('@') ? id.slice(id.lastIndexOf('@') + 1) : ''))
|
|
2509
2600
|
.filter((f) => f !== ''));
|
|
2510
2601
|
const rankedImpact = (0, assoc_arm_1.pprImpactRanked)((0, assoc_arm_1.graphViewOf)(g.dumpCanonical()), riSeeds, riExclude, assoc_arm_1.RANKED_IMPACT_K, assoc_arm_1.RANKED_IMPACT_CONFIG);
|
|
2602
|
+
// @implements A-SPEC-568.2 — the intent sentence beside every impacted spec id, same order
|
|
2603
|
+
// as `impacted` (which stays a bare id list for its existing consumers). Information only:
|
|
2604
|
+
// nothing reads it back into the walk, the ranking or any gate.
|
|
2605
|
+
// @implements A-SPEC-569.3 — capped: measured on this repository, uncapped summaries were
|
|
2606
|
+
// 94% of a 104,706-byte response (a hub-grade impact of 339 specs). The omission is COUNTED,
|
|
2607
|
+
// never silent, and `impacted` itself stays complete — only the annotation is bounded.
|
|
2608
|
+
const shownSummaries = impacted.slice(0, SUMMARY_CAP);
|
|
2609
|
+
const impactedSummaries = shownSummaries.map((id) => {
|
|
2610
|
+
let summary = null;
|
|
2611
|
+
try {
|
|
2612
|
+
summary = g.summaryOf(id);
|
|
2613
|
+
}
|
|
2614
|
+
catch { /* summary stays null */ }
|
|
2615
|
+
return { id, summary };
|
|
2616
|
+
});
|
|
2617
|
+
const summariesOmitted = impacted.length - shownSummaries.length;
|
|
2511
2618
|
return {
|
|
2512
2619
|
impacted,
|
|
2620
|
+
impactedSummaries,
|
|
2621
|
+
...(summariesOmitted > 0 ? { summariesOmitted } : {}),
|
|
2513
2622
|
rankedImpact,
|
|
2514
2623
|
reachedByDepth,
|
|
2515
2624
|
bounded: stoppedAt.length > 0 ? stoppedAt.slice(0, 20) : undefined,
|
|
@@ -42,6 +42,17 @@ const handlers = (0, handlers_1.makeHandlers)(store, {
|
|
|
42
42
|
return 'unknown';
|
|
43
43
|
} },
|
|
44
44
|
});
|
|
45
|
+
// @implements A-SPEC-564.1 — the session-context stamp: WHO attached, name AND version (the version
|
|
46
|
+
// was received and dropped for months). Written HERE because the server is the one place all three
|
|
47
|
+
// harnesses pass through; lazy-once via the call path (clientInfo exists only after initialize, and
|
|
48
|
+
// the ledger's root arrives with the first rooted call). Fail-open by construction.
|
|
49
|
+
const { makeSessionStamper } = require('../governance/session-context');
|
|
50
|
+
const stampSession = makeSessionStamper(() => { try {
|
|
51
|
+
return server.getClientVersion();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
} });
|
|
45
56
|
// @implements A-SPEC-259 — the advertised version is the package's own, not a literal that froze at
|
|
46
57
|
// 0.1.0: a hardcoded serverInfo.version blinds any client-side drift diagnosis.
|
|
47
58
|
const PKG_VERSION = (() => {
|
|
@@ -109,6 +120,7 @@ const TOOLS = Object.keys(handlers)
|
|
|
109
120
|
});
|
|
110
121
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
111
122
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
123
|
+
stampSession(req.params.arguments?.root);
|
|
112
124
|
// @implements A-SPEC-189
|
|
113
125
|
// The server is the first consumer of its own advertised schemas. Before this check, 15 of 26
|
|
114
126
|
// handlers threw raw internal errors at `{}` over the wire, and a one-key typo in reverse_anchor
|
|
@@ -330,7 +330,7 @@ exports.TOOL_SCHEMAS = {
|
|
|
330
330
|
},
|
|
331
331
|
},
|
|
332
332
|
rtm_impact: {
|
|
333
|
-
description: 'Given changed symbol qualified-names, return the impacted SPEC node ids reachable through @implements/depends_on edges ({ impacted }).',
|
|
333
|
+
description: 'Given changed symbol qualified-names, return the impacted SPEC node ids reachable through @implements/depends_on edges ({ impacted, impactedSummaries: [{id, summary}] — the spec intent sentence beside each id, informational only }).',
|
|
334
334
|
inputSchema: {
|
|
335
335
|
type: 'object',
|
|
336
336
|
properties: {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The judgement bundle: what the reviewing agent RECEIVES, split by epistemic kind.
|
|
3
|
+
*
|
|
4
|
+
* The owner's correction is the whole design: this is a TECHNICAL graph, and technical facts are
|
|
5
|
+
* crisp — a symbol is defined in one file or it is not. Every measured failure of graded tools on
|
|
6
|
+
* crisp facts (the ranker deleting its own answer, anchor noise, the 0.25-constant confidence)
|
|
7
|
+
* says the same thing, and the S0 misjudgement decomposition (revision 7) named the exact shapes:
|
|
8
|
+
* 17% of false picks were facts a single crisp query would have refuted (symbol ownership, surface
|
|
9
|
+
* presence), and the other 83% — target attribution — are best treated with HISTORY facts
|
|
10
|
+
* (git log -S is deterministic), not with more similarity.
|
|
11
|
+
*
|
|
12
|
+
* So: FACT results carry a verdict, the query that produced it, and the tree it was true of. There
|
|
13
|
+
* is no field for a similarity or a score — the type is the wall (the QueueEventLite idiom).
|
|
14
|
+
* Candidates stay labeled 'candidate' and never get promoted. One self-evaluation, one optional
|
|
15
|
+
* reinforcement, bounded by structure.
|
|
16
|
+
*/
|
|
17
|
+
export interface FactResult {
|
|
18
|
+
kind: 'symbol-owner' | 'surface-presence' | 'feature-history';
|
|
19
|
+
query: string;
|
|
20
|
+
verdict: boolean | string[];
|
|
21
|
+
basis: string;
|
|
22
|
+
asOf: string;
|
|
23
|
+
}
|
|
24
|
+
export interface GitFacts {
|
|
25
|
+
/** Files defining the named symbol (assignment/def/class shapes) in the pinned tree. */
|
|
26
|
+
grepOwners(name: string): string[];
|
|
27
|
+
/** Does the file already carry any of these terms, in the pinned tree? */
|
|
28
|
+
grepInFile(file: string, terms: string[]): boolean;
|
|
29
|
+
/** Files last touching this term/feature across pre-pin history (git log -S shape). */
|
|
30
|
+
logTouched(term: string): string[];
|
|
31
|
+
/** The pinned tree these answers are true of. */
|
|
32
|
+
asOf(): string;
|
|
33
|
+
}
|
|
34
|
+
export interface JudgementBundle {
|
|
35
|
+
header: string;
|
|
36
|
+
facts: FactResult[];
|
|
37
|
+
reinforced: boolean;
|
|
38
|
+
candidates: Array<{
|
|
39
|
+
file: string;
|
|
40
|
+
excerpt: string;
|
|
41
|
+
label: 'candidate';
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export declare const MAX_FACT_QUERIES = 8;
|
|
45
|
+
export declare const BUNDLE_HEADER: string;
|
|
46
|
+
export declare function buildJudgementBundle(subject: string, candidates: Array<{
|
|
47
|
+
file: string;
|
|
48
|
+
excerpt: string;
|
|
49
|
+
}>, facts: GitFacts): JudgementBundle;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @implements A-SPEC-567.1
|
|
3
|
+
/**
|
|
4
|
+
* The judgement bundle: what the reviewing agent RECEIVES, split by epistemic kind.
|
|
5
|
+
*
|
|
6
|
+
* The owner's correction is the whole design: this is a TECHNICAL graph, and technical facts are
|
|
7
|
+
* crisp — a symbol is defined in one file or it is not. Every measured failure of graded tools on
|
|
8
|
+
* crisp facts (the ranker deleting its own answer, anchor noise, the 0.25-constant confidence)
|
|
9
|
+
* says the same thing, and the S0 misjudgement decomposition (revision 7) named the exact shapes:
|
|
10
|
+
* 17% of false picks were facts a single crisp query would have refuted (symbol ownership, surface
|
|
11
|
+
* presence), and the other 83% — target attribution — are best treated with HISTORY facts
|
|
12
|
+
* (git log -S is deterministic), not with more similarity.
|
|
13
|
+
*
|
|
14
|
+
* So: FACT results carry a verdict, the query that produced it, and the tree it was true of. There
|
|
15
|
+
* is no field for a similarity or a score — the type is the wall (the QueueEventLite idiom).
|
|
16
|
+
* Candidates stay labeled 'candidate' and never get promoted. One self-evaluation, one optional
|
|
17
|
+
* reinforcement, bounded by structure.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.BUNDLE_HEADER = exports.MAX_FACT_QUERIES = void 0;
|
|
21
|
+
exports.buildJudgementBundle = buildJudgementBundle;
|
|
22
|
+
exports.MAX_FACT_QUERIES = 8;
|
|
23
|
+
exports.BUNDLE_HEADER = 'fact 는 검증된 참/거짓(조회식·기준 트리 동봉), candidate 는 미검증 후보다. 조회가 기억을 이긴다 — '
|
|
24
|
+
+ 'fact 와 충돌하는 전제는 버려라. 기존 소유 파일이 후보에 없으면 신규-파일 가설(빈 선택)을 고려하라.';
|
|
25
|
+
/** Decidable subquery tokens out of a subject line: quoted strings, identifier-shaped words and
|
|
26
|
+
* feature tags — deterministic order, capped. Whatever cannot be extracted stays EVIDENCE-only. */
|
|
27
|
+
function extractTokens(subject) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const push = (t) => { if (t.length >= 3 && !out.includes(t))
|
|
30
|
+
out.push(t); };
|
|
31
|
+
for (const m of subject.matchAll(/"([^"]+)"|'([^']+)'/g))
|
|
32
|
+
push(m[1] ?? m[2]);
|
|
33
|
+
for (const m of subject.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*(?:_[A-Za-z0-9_]+)+|[A-Z]{2,}[A-Z0-9_]*)\b/g))
|
|
34
|
+
push(m[1]);
|
|
35
|
+
for (const m of subject.matchAll(/\bF\d{3}\b/g))
|
|
36
|
+
push(m[0]);
|
|
37
|
+
return out.slice(0, exports.MAX_FACT_QUERIES);
|
|
38
|
+
}
|
|
39
|
+
function buildJudgementBundle(subject, candidates, facts) {
|
|
40
|
+
const asOf = (() => { try {
|
|
41
|
+
return facts.asOf();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return 'unknown';
|
|
45
|
+
} })();
|
|
46
|
+
const results = [];
|
|
47
|
+
const candidateFiles = new Set(candidates.map((c) => c.file));
|
|
48
|
+
const tokens = extractTokens(subject);
|
|
49
|
+
let historyRan = false;
|
|
50
|
+
let reinforced = false;
|
|
51
|
+
// Decidable-first: close what a crisp query CAN close. Each answer carries the query that made it
|
|
52
|
+
// and the tree it is true of — an unanswerable (throwing) query is silently absent, never guessed.
|
|
53
|
+
for (const tok of tokens) {
|
|
54
|
+
try {
|
|
55
|
+
const owners = facts.grepOwners(tok);
|
|
56
|
+
if (owners.length > 0) {
|
|
57
|
+
results.push({ kind: 'symbol-owner', query: tok, verdict: owners,
|
|
58
|
+
basis: `git grep -l '${tok} =' | def/class — 정의 소유 파일`, asOf });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { /* absent, not guessed */ }
|
|
62
|
+
}
|
|
63
|
+
for (const c of candidates) {
|
|
64
|
+
try {
|
|
65
|
+
if (tokens.length > 0 && facts.grepInFile(c.file, tokens)) {
|
|
66
|
+
results.push({ kind: 'surface-presence', query: `${c.file} ∋ {${tokens.slice(0, 3).join(',')}}`,
|
|
67
|
+
verdict: true, basis: 'git grep — 후보가 해당 표면을 이미 보유', asOf });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { /* absent, not guessed */ }
|
|
71
|
+
}
|
|
72
|
+
const featureTag = tokens.find((t) => /^F\d{3}$/.test(t)) ?? tokens[0];
|
|
73
|
+
if (featureTag !== undefined) {
|
|
74
|
+
try {
|
|
75
|
+
const touched = facts.logTouched(featureTag);
|
|
76
|
+
historyRan = true;
|
|
77
|
+
if (touched.length > 0) {
|
|
78
|
+
results.push({ kind: 'feature-history', query: featureTag, verdict: touched,
|
|
79
|
+
basis: `git log -S '${featureTag}' — 이 피처를 마지막으로 만진 파일들`, asOf });
|
|
80
|
+
// Self-evaluation, once: owners entirely OUTSIDE the candidate set is the S0 shape that
|
|
81
|
+
// misled the judge (c0/c17/c25 …) — reinforce with ONE more history probe on the next token.
|
|
82
|
+
if (!touched.some((f) => candidateFiles.has(f))) {
|
|
83
|
+
reinforced = true;
|
|
84
|
+
const second = tokens.find((t) => t !== featureTag);
|
|
85
|
+
if (second !== undefined) {
|
|
86
|
+
try {
|
|
87
|
+
const more = facts.logTouched(second);
|
|
88
|
+
if (more.length > 0) {
|
|
89
|
+
results.push({ kind: 'feature-history', query: second, verdict: more,
|
|
90
|
+
basis: `git log -S '${second}' — 보강 1회(소유 파일이 후보 밖)`, asOf });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { /* the reinforcement is best-effort too */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
void historyRan;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
header: exports.BUNDLE_HEADER,
|
|
104
|
+
facts: results,
|
|
105
|
+
reinforced,
|
|
106
|
+
candidates: candidates.map((c) => ({ ...c, label: 'candidate' })),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -203,6 +203,9 @@ export declare function runReplay(corpus: ReplayCorpus, limit: number, opts?: {
|
|
|
203
203
|
contentVerify?: boolean;
|
|
204
204
|
/** @implements A-SPEC-485 — holdout window start; default 0 is the pinned main window. */
|
|
205
205
|
offset?: number;
|
|
206
|
+
/** @implements A-SPEC-567.2 — attach the judgement bundle (FACT channel + labels) to each
|
|
207
|
+
* caseDump row. Absent = the row is byte-identical to the pre-option shape. */
|
|
208
|
+
judgementBundle?: boolean;
|
|
206
209
|
/**
|
|
207
210
|
* @implements A-SPEC-487 — per-case dump for the blind judgment protocol. The pins stay on
|
|
208
211
|
* the 1-pass result; the dump carries the 2-pass (semantic-injected) output when
|
|
@@ -225,6 +228,8 @@ export declare function runReplay(corpus: ReplayCorpus, limit: number, opts?: {
|
|
|
225
228
|
truthFiles: string[];
|
|
226
229
|
/** @implements A-SPEC-489 — present only when dumpBodies was asked. */
|
|
227
230
|
bodies?: Record<string, string>;
|
|
231
|
+
/** @implements A-SPEC-567.2 — present only when judgementBundle was asked. */
|
|
232
|
+
judgementBundle?: import('./judgement-bundle').JudgementBundle;
|
|
228
233
|
}) => void;
|
|
229
234
|
/** @implements A-SPEC-487 — 2-pass semantic injection for the dump only, never the pins. */
|
|
230
235
|
productSemantic?: {
|
|
@@ -42,6 +42,7 @@ exports.semanticCaseRanking = semanticCaseRanking;
|
|
|
42
42
|
// @implements A-SPEC-378
|
|
43
43
|
// @implements A-SPEC-402
|
|
44
44
|
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const node_child_process_1 = require("node:child_process");
|
|
45
46
|
const os = __importStar(require("node:os"));
|
|
46
47
|
const path = __importStar(require("node:path"));
|
|
47
48
|
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
@@ -275,12 +276,43 @@ async function runReplay(corpus, limit, opts = {}) {
|
|
|
275
276
|
catch { /* unreadable candidate: omitted */ }
|
|
276
277
|
}
|
|
277
278
|
}
|
|
279
|
+
// @implements A-SPEC-567.2 — the FACT channel reads the PARENT tree and pre-case history
|
|
280
|
+
// only (git object queries by sha — deterministic, and the case commit itself is never
|
|
281
|
+
// consulted, so the blind protocol survives). Every query failure is a silent absence.
|
|
282
|
+
let judgementBundle;
|
|
283
|
+
if (opts.judgementBundle === true) {
|
|
284
|
+
try {
|
|
285
|
+
const { buildJudgementBundle } = require('./judgement-bundle');
|
|
286
|
+
const gitQ = (args) => {
|
|
287
|
+
try {
|
|
288
|
+
return (0, node_child_process_1.execFileSync)('git', ['-C', corpus.root, ...args], { encoding: 'utf8', stdio: 'pipe' });
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const stripRef = (line) => line.replace(/^[^:]*:/, '');
|
|
295
|
+
const gitFacts = {
|
|
296
|
+
grepOwners: (name) => [...new Set(gitQ(['grep', '-l', '-E', `(^|[^A-Za-z0-9_])${name}\\s*=|def ${name}|class ${name}`, parent, '--', ...corpus.sourcePathspec])
|
|
297
|
+
.split('\n').filter(Boolean).map(stripRef))].slice(0, 5),
|
|
298
|
+
grepInFile: (file, terms) => gitQ(['grep', '-c', '-E', terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), parent, '--', file]).trim() !== '',
|
|
299
|
+
logTouched: (term) => [...new Set(gitQ(['log', '-S', term, '--name-only', '--format=', '-n', '8', parent, '--', ...corpus.sourcePathspec])
|
|
300
|
+
.split('\n').filter((f) => f && corpus.isSource(f)))].slice(0, 5),
|
|
301
|
+
asOf: () => parent,
|
|
302
|
+
};
|
|
303
|
+
judgementBundle = buildJudgementBundle(c.subject, dumpResult.candidates.map((x) => ({ file: x.file, excerpt: bodies?.[x.file] ?? '' })), gitFacts);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
judgementBundle = undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
278
309
|
opts.caseDump({
|
|
279
310
|
commit: c.commit, subject: c.subject,
|
|
280
311
|
candidates: dumpResult.candidates,
|
|
281
312
|
rankedImpact: dumpResult.impacts?.rankedImpact ?? [],
|
|
282
313
|
truthFiles: c.files,
|
|
283
314
|
...(bodies !== undefined ? { bodies } : {}),
|
|
315
|
+
...(judgementBundle !== undefined ? { judgementBundle } : {}),
|
|
284
316
|
});
|
|
285
317
|
}
|
|
286
318
|
// Same case, same truth, same `topN` the product used — an arm scored against a different
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor-density advisory — OBSERVATION ONLY, never a gate.
|
|
3
|
+
*
|
|
4
|
+
* The measured ground: anchor density taxes localization precision (authoring ONE spec moved
|
|
5
|
+
* replay recall 0.5476→0.5060; A-SPEC-270's √-dilution exists because a 113-anchor file brushes
|
|
6
|
+
* some spec for almost any request). A count GATE was considered and REFUSED (position-dependent
|
|
7
|
+
* refusals, an incentive to stop anchoring, mechanical file splits) — so this surfaces the fact at
|
|
8
|
+
* sealing time and records it, and the observation ledger decides any future promotion, exactly
|
|
9
|
+
* the impactAdvisory lifecycle. The thresholds below are prose constants: nothing reads them into
|
|
10
|
+
* a verdict (judgments must not be budgeted).
|
|
11
|
+
*/
|
|
12
|
+
export interface AnchorDensityFinding {
|
|
13
|
+
path: string;
|
|
14
|
+
anchors: number;
|
|
15
|
+
p90: number;
|
|
16
|
+
}
|
|
17
|
+
/** Files below this live-anchor count are never flagged, whatever the distribution — a floor. */
|
|
18
|
+
export declare const MIN_ANCHORS = 8;
|
|
19
|
+
/**
|
|
20
|
+
* FtT files whose live anchor count sits at or above max(MIN_ANCHORS, p90 of the distribution).
|
|
21
|
+
* p90 is the value at index ceil(0.9·n)-1 of the ascending counts — deterministic, no interpolation.
|
|
22
|
+
* Pure: same inputs, same findings, in sorted path order.
|
|
23
|
+
*/
|
|
24
|
+
export declare function anchorDensityFindings(fttFiles: string[], counts: Array<{
|
|
25
|
+
sourcePath: string;
|
|
26
|
+
anchors: number;
|
|
27
|
+
}>): AnchorDensityFinding[];
|
|
28
|
+
/**
|
|
29
|
+
* The observation ledger — paths and integers only, no prose, no secrets: what a future
|
|
30
|
+
* promotion/rejection judgment will be measured on.
|
|
31
|
+
*/
|
|
32
|
+
export interface AnchorDensityRecord {
|
|
33
|
+
aspec: string;
|
|
34
|
+
files: Array<{
|
|
35
|
+
path: string;
|
|
36
|
+
anchors: number;
|
|
37
|
+
}>;
|
|
38
|
+
p90: number;
|
|
39
|
+
ts: string;
|
|
40
|
+
replica?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function appendAnchorDensity(root: string, rec: AnchorDensityRecord): boolean;
|
|
43
|
+
export declare function readAnchorDensity(root: string): AnchorDensityRecord[];
|