@holmes-lab/holmes-kit 0.2.1 → 0.3.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 +21 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -21
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +311 -5
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +41 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +597 -16
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the extractor actually recovers, per language.
|
|
3
|
+
*
|
|
4
|
+
* Goal Phase 4 requires that per-language coverage and its limits always travel with the result.
|
|
5
|
+
* They did not, and the capability is genuinely uneven — measured with fixtures 2026-08-28, symbols
|
|
6
|
+
* come out of every advertised language, `imports` only out of Python and TypeScript-family files,
|
|
7
|
+
* and `inherits` out of NONE of them, not even where the declaration is completely unambiguous
|
|
8
|
+
* (`class Child(Base)`, `extends Base`).
|
|
9
|
+
*
|
|
10
|
+
* The problem was never the unevenness; it was the silence. Coverage reported only extensions
|
|
11
|
+
* OUTSIDE the scannable set, so an impact answer over a Java file looked exactly as trustworthy as
|
|
12
|
+
* one over Python while resting on strictly less evidence, and every downstream answer inherited
|
|
13
|
+
* that asymmetry without anyone being told.
|
|
14
|
+
*
|
|
15
|
+
* This table is pinned against the real scanner by fixtures in the sibling test. If it drifts from
|
|
16
|
+
* reality in either direction that test fails — a capability table that cannot fail guarantees
|
|
17
|
+
* nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare const EXTRACTABLE_RELATIONS: readonly ["calls", "imports", "inherits"];
|
|
20
|
+
export type ExtractableRelation = typeof EXTRACTABLE_RELATIONS[number];
|
|
21
|
+
export interface LanguageCapability {
|
|
22
|
+
symbols: boolean;
|
|
23
|
+
/** What the SCANNER recovers from the source text. */
|
|
24
|
+
relations: ExtractableRelation[];
|
|
25
|
+
/**
|
|
26
|
+
* @implements A-SPEC-286
|
|
27
|
+
* What actually becomes a GRAPH edge — always a subset of `relations`.
|
|
28
|
+
*
|
|
29
|
+
* These are different claims and conflating them misleads. Measured 2026-08-28: the scanner emits
|
|
30
|
+
* 1,892 `imports` edges of which zero reach the graph, because their target is a module path
|
|
31
|
+
* rather than a symbol and the builder resolves symbol-to-symbol relations only. Everything
|
|
32
|
+
* downstream — impact, localization, the pre-edit gate — queries the graph, so the graph column is
|
|
33
|
+
* the one a coverage claim has to be made in.
|
|
34
|
+
*/
|
|
35
|
+
graphResolved: ExtractableRelation[];
|
|
36
|
+
}
|
|
37
|
+
export declare const LANGUAGE_CAPABILITY: Record<string, LanguageCapability>;
|
|
38
|
+
export declare function capabilityFor(ext: string): LanguageCapability | undefined;
|
|
39
|
+
export interface LanguageGap {
|
|
40
|
+
ext: string;
|
|
41
|
+
/** Relation kinds this language's extractor does NOT recover at all. */
|
|
42
|
+
missing: ExtractableRelation[];
|
|
43
|
+
/**
|
|
44
|
+
* @implements A-SPEC-286
|
|
45
|
+
* Recovered by the scanner but never resolved into a graph edge. A distinct fact from `missing`:
|
|
46
|
+
* the information exists and is still unusable to anything that queries the graph.
|
|
47
|
+
*/
|
|
48
|
+
extractedButUnresolved: ExtractableRelation[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The capability gaps for the languages ACTUALLY present in a scan.
|
|
52
|
+
*
|
|
53
|
+
* Only languages that appear are listed: enumerating every language the tool could theoretically
|
|
54
|
+
* meet would bury the ones this answer actually rests on. A language with nothing missing drops out
|
|
55
|
+
* entirely, so the report shrinks to nothing as extraction catches up rather than becoming noise.
|
|
56
|
+
*/
|
|
57
|
+
export declare function capabilityGapsFor(sourcePaths: readonly string[]): LanguageGap[];
|
|
58
|
+
/** Every advertised extension must have an entry; a blank would be a silent overclaim. */
|
|
59
|
+
export declare const ADVERTISED_EXTENSIONS: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* An unknown extension gets a family of its own rather than joining a shared bucket: an unfamiliar
|
|
62
|
+
* language must not be able to take another language's definitions. Narrowing is the safe direction
|
|
63
|
+
* — it costs recall, which is disclosed, instead of asserting something false.
|
|
64
|
+
*/
|
|
65
|
+
export declare function languageFamilyOf(ext: string): string;
|
|
@@ -0,0 +1,145 @@
|
|
|
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.ADVERTISED_EXTENSIONS = exports.LANGUAGE_CAPABILITY = exports.EXTRACTABLE_RELATIONS = void 0;
|
|
37
|
+
exports.capabilityFor = capabilityFor;
|
|
38
|
+
exports.capabilityGapsFor = capabilityGapsFor;
|
|
39
|
+
exports.languageFamilyOf = languageFamilyOf;
|
|
40
|
+
// @implements A-SPEC-289
|
|
41
|
+
// @implements A-SPEC-288
|
|
42
|
+
// @implements A-SPEC-287
|
|
43
|
+
// @implements A-SPEC-286
|
|
44
|
+
// @implements A-SPEC-285
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const cpg_scanner_1 = require("./cpg-scanner");
|
|
47
|
+
/**
|
|
48
|
+
* What the extractor actually recovers, per language.
|
|
49
|
+
*
|
|
50
|
+
* Goal Phase 4 requires that per-language coverage and its limits always travel with the result.
|
|
51
|
+
* They did not, and the capability is genuinely uneven — measured with fixtures 2026-08-28, symbols
|
|
52
|
+
* come out of every advertised language, `imports` only out of Python and TypeScript-family files,
|
|
53
|
+
* and `inherits` out of NONE of them, not even where the declaration is completely unambiguous
|
|
54
|
+
* (`class Child(Base)`, `extends Base`).
|
|
55
|
+
*
|
|
56
|
+
* The problem was never the unevenness; it was the silence. Coverage reported only extensions
|
|
57
|
+
* OUTSIDE the scannable set, so an impact answer over a Java file looked exactly as trustworthy as
|
|
58
|
+
* one over Python while resting on strictly less evidence, and every downstream answer inherited
|
|
59
|
+
* that asymmetry without anyone being told.
|
|
60
|
+
*
|
|
61
|
+
* This table is pinned against the real scanner by fixtures in the sibling test. If it drifts from
|
|
62
|
+
* reality in either direction that test fails — a capability table that cannot fail guarantees
|
|
63
|
+
* nothing.
|
|
64
|
+
*/
|
|
65
|
+
exports.EXTRACTABLE_RELATIONS = ['calls', 'imports', 'inherits'];
|
|
66
|
+
// @implements A-SPEC-288 — `extends` joins Python's inheritance; `implements` stays a stated gap.
|
|
67
|
+
// @implements A-SPEC-289 — imports now resolve to FILE nodes when the specifier is relative and
|
|
68
|
+
// names a scanned file; bare specifiers stay external and unresolved by design.
|
|
69
|
+
const TS_FAMILY = { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] };
|
|
70
|
+
/** Symbols and calls only — no import edges, no inheritance. */
|
|
71
|
+
const CALLS_ONLY = { symbols: true, relations: ['calls'], graphResolved: ['calls'] };
|
|
72
|
+
exports.LANGUAGE_CAPABILITY = {
|
|
73
|
+
'.ts': TS_FAMILY, '.mts': TS_FAMILY, '.cts': TS_FAMILY, '.tsx': TS_FAMILY,
|
|
74
|
+
'.js': TS_FAMILY, '.mjs': TS_FAMILY, '.jsx': TS_FAMILY,
|
|
75
|
+
// CommonJS `require()` is not recovered as an import, unlike ESM `import` — measured.
|
|
76
|
+
'.cjs': CALLS_ONLY,
|
|
77
|
+
// @implements A-SPEC-286 — the first language whose inheritance is both extracted AND resolved.
|
|
78
|
+
'.py': { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] },
|
|
79
|
+
'.java': CALLS_ONLY, '.cs': CALLS_ONLY, '.go': CALLS_ONLY, '.rs': CALLS_ONLY,
|
|
80
|
+
// @implements A-SPEC-287 — C++ used to extract calls that never became graph edges, because its
|
|
81
|
+
// edge scope omitted the class node the symbol walk uses. Fixed; it now resolves like the others.
|
|
82
|
+
'.cpp': CALLS_ONLY, '.cc': CALLS_ONLY, '.cxx': CALLS_ONLY,
|
|
83
|
+
'.hpp': CALLS_ONLY, '.hh': CALLS_ONLY, '.h': CALLS_ONLY,
|
|
84
|
+
};
|
|
85
|
+
function capabilityFor(ext) {
|
|
86
|
+
return exports.LANGUAGE_CAPABILITY[ext.toLowerCase()];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The capability gaps for the languages ACTUALLY present in a scan.
|
|
90
|
+
*
|
|
91
|
+
* Only languages that appear are listed: enumerating every language the tool could theoretically
|
|
92
|
+
* meet would bury the ones this answer actually rests on. A language with nothing missing drops out
|
|
93
|
+
* entirely, so the report shrinks to nothing as extraction catches up rather than becoming noise.
|
|
94
|
+
*/
|
|
95
|
+
function capabilityGapsFor(sourcePaths) {
|
|
96
|
+
const present = new Set();
|
|
97
|
+
for (const p of sourcePaths) {
|
|
98
|
+
const ext = path.extname(p).toLowerCase();
|
|
99
|
+
if (capabilityFor(ext))
|
|
100
|
+
present.add(ext);
|
|
101
|
+
}
|
|
102
|
+
return [...present].sort()
|
|
103
|
+
.map((ext) => {
|
|
104
|
+
const cap = capabilityFor(ext);
|
|
105
|
+
return {
|
|
106
|
+
ext,
|
|
107
|
+
missing: exports.EXTRACTABLE_RELATIONS.filter((r) => !cap.relations.includes(r)),
|
|
108
|
+
extractedButUnresolved: cap.relations.filter((r) => !cap.graphResolved.includes(r)),
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
.filter((gap) => gap.missing.length > 0 || gap.extractedButUnresolved.length > 0);
|
|
112
|
+
}
|
|
113
|
+
/** Every advertised extension must have an entry; a blank would be a silent overclaim. */
|
|
114
|
+
exports.ADVERTISED_EXTENSIONS = cpg_scanner_1.SCANNABLE_EXTENSIONS;
|
|
115
|
+
// @implements A-SPEC-300
|
|
116
|
+
/**
|
|
117
|
+
* Extensions that can actually call each other, grouped.
|
|
118
|
+
*
|
|
119
|
+
* Name resolution links a call to a definition when the name is defined in exactly one file. That
|
|
120
|
+
* rule had no notion of language, and measured on a five-language fixture (2026-08-28) it asserted
|
|
121
|
+
* CODE:Child.run@Child.java --calls--> CODE:helper@lib.rs
|
|
122
|
+
* A Java method calling a Rust function. Worse, the correct target sat in the SAME file: it was
|
|
123
|
+
* missed on qualification, leaving the Rust definition as the only candidate, so the precision rule
|
|
124
|
+
* actively confirmed a wrong answer. A missing edge widens scope; a wrong edge narrows it onto the
|
|
125
|
+
* wrong place.
|
|
126
|
+
*
|
|
127
|
+
* The groups follow real interop, not file-name similarity: TypeScript and JavaScript genuinely call
|
|
128
|
+
* each other, and a C++ implementation genuinely calls its header's declarations.
|
|
129
|
+
*/
|
|
130
|
+
const LANGUAGE_FAMILIES = {
|
|
131
|
+
'.ts': 'ts-js', '.mts': 'ts-js', '.cts': 'ts-js', '.tsx': 'ts-js',
|
|
132
|
+
'.js': 'ts-js', '.mjs': 'ts-js', '.jsx': 'ts-js', '.cjs': 'ts-js',
|
|
133
|
+
'.py': 'python',
|
|
134
|
+
'.java': 'java', '.cs': 'csharp', '.go': 'go', '.rs': 'rust',
|
|
135
|
+
'.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp', '.h': 'cpp',
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* An unknown extension gets a family of its own rather than joining a shared bucket: an unfamiliar
|
|
139
|
+
* language must not be able to take another language's definitions. Narrowing is the safe direction
|
|
140
|
+
* — it costs recall, which is disclosed, instead of asserting something false.
|
|
141
|
+
*/
|
|
142
|
+
function languageFamilyOf(ext) {
|
|
143
|
+
const lower = ext.toLowerCase();
|
|
144
|
+
return LANGUAGE_FAMILIES[lower] ?? `ext:${lower}`;
|
|
145
|
+
}
|
|
@@ -43,6 +43,32 @@ const LANG_CONFIG = {
|
|
|
43
43
|
funcTypes: ['function_declaration'],
|
|
44
44
|
methodTypes: ['method_definition'],
|
|
45
45
|
funcIsMethodInClass: false,
|
|
46
|
+
// @implements A-SPEC-301
|
|
47
|
+
// Contracts are deliberately NOT emitted, and this is a measured decision rather than an
|
|
48
|
+
// oversight. This repository has 192 interfaces and 54 type aliases that the graph does not
|
|
49
|
+
// know about — about a quarter of its top-level declarations — and in TypeScript the interface
|
|
50
|
+
// IS the contract, so Phase 5's contract-level impact has no object to reason about.
|
|
51
|
+
//
|
|
52
|
+
// Emitting them was implemented and measured on the point-in-time replay corpus (2026-08-28):
|
|
53
|
+
//
|
|
54
|
+
// hit rate recall
|
|
55
|
+
// Top-1 0.250 -> 0.333 0.194 -> 0.278 better
|
|
56
|
+
// Top-3 0.500 -> 0.500 0.331 -> 0.331 unchanged
|
|
57
|
+
// Top-5 0.583 -> 0.583 0.470 -> 0.470 unchanged
|
|
58
|
+
// Top-10 0.833 -> 0.750 0.690 -> 0.607 WORSE
|
|
59
|
+
//
|
|
60
|
+
// 246 extra candidates sharpen the top of the ranking and crowd true files out of the tail. The
|
|
61
|
+
// A-SPEC-297 floor on Top-10 recall (0.62) caught it. Operating principle 11 is explicit that a
|
|
62
|
+
// graph extension must show an improvement, and this one shows a trade, so it was reverted.
|
|
63
|
+
// A-SPEC-302 tried the obvious next design — contracts as nodes, excluded from localization
|
|
64
|
+
// scoring — and measured it: every replay metric came back EXACTLY at baseline
|
|
65
|
+
// (Top-1 0.250/0.194, Top-10 0.833/0.690). So S-301's Top-1 gain came entirely from scoring
|
|
66
|
+
// contract names, and removing them from scoring removes the gain with it. What remains is 246
|
|
67
|
+
// nodes and 4 edges with no measured downstream benefit, because nothing consumes contract nodes
|
|
68
|
+
// yet. Operating principle 11 forbids exactly that, so this was declined too.
|
|
69
|
+
//
|
|
70
|
+
// What would justify it: building the CONSUMER and the nodes in one slice, so the benefit is
|
|
71
|
+
// measurable in the same experiment. Nodes first, benefit later, is the shape that keeps failing.
|
|
46
72
|
},
|
|
47
73
|
python: {
|
|
48
74
|
classTypes: ['class_definition'],
|
|
@@ -84,9 +110,22 @@ LANG_CONFIG.tsx = LANG_CONFIG.typescript;
|
|
|
84
110
|
// only when lexically inside a class, and recursing into ANY def resets
|
|
85
111
|
// enclosingClass to null for its children, so a def nested inside a def is
|
|
86
112
|
// never mistaken for a method of the outer class).
|
|
113
|
+
// @implements A-SPEC-291
|
|
114
|
+
// Symbols are qualified by the FULL path of named scopes, exactly as `namedScopeOf` qualifies an
|
|
115
|
+
// edge's caller. The two used to disagree: this walk qualified only by an enclosing CLASS, so a
|
|
116
|
+
// function nested in a function came out bare (`inner`) and an object-literal method was not emitted
|
|
117
|
+
// at all, while the edge walk named the caller `outer.inner` / `makeHandlers.doThing`. The builder
|
|
118
|
+
// requires a caller to be a real node in its file, so every such edge was dropped — measured
|
|
119
|
+
// 2026-08-28, 1,307 relations in this repository (591 in TypeScript, 9.4%), including every call
|
|
120
|
+
// made by the MCP handler surface, which lives in methods of an object a function returns.
|
|
121
|
+
//
|
|
122
|
+
// `scope` carries whether each segment is a class, because `kind` still distinguishes a method of a
|
|
123
|
+
// class from a plain nested function. An unnamed scope (a function EXPRESSION) contributes no
|
|
124
|
+
// segment — the edge walk does not count it either, so the two stay aligned.
|
|
87
125
|
function walkSymbols(tree, config) {
|
|
88
126
|
const out = [];
|
|
89
|
-
const
|
|
127
|
+
const pathOf = (scope, name) => [...scope.map((s) => s.name), name].join('.');
|
|
128
|
+
const visit = (node, scope) => {
|
|
90
129
|
const type = node.type;
|
|
91
130
|
if (config.classTypes.includes(type)) {
|
|
92
131
|
const nameNode = node.childForFieldName('name');
|
|
@@ -95,65 +134,68 @@ function walkSymbols(tree, config) {
|
|
|
95
134
|
out.push({
|
|
96
135
|
name,
|
|
97
136
|
kind: 'class',
|
|
98
|
-
qualifiedName: name,
|
|
137
|
+
qualifiedName: pathOf(scope, name),
|
|
99
138
|
startLine: node.startPosition.row + 1,
|
|
100
139
|
endLine: node.endPosition.row + 1,
|
|
101
140
|
});
|
|
102
141
|
}
|
|
103
|
-
const
|
|
142
|
+
const childScope = name ? [...scope, { name, isClass: true }] : scope;
|
|
104
143
|
for (let i = 0; i < node.childCount; i++)
|
|
105
|
-
visit(node.child(i),
|
|
144
|
+
visit(node.child(i), childScope);
|
|
106
145
|
return;
|
|
107
146
|
}
|
|
108
147
|
if (config.funcTypes.includes(type)) {
|
|
109
148
|
const nameNode = node.childForFieldName('name');
|
|
110
149
|
const name = nameNode ? nameNode.text : undefined;
|
|
150
|
+
let childScope = scope;
|
|
111
151
|
if (name) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
// A method only when the IMMEDIATE enclosing scope is a class — the same question the old
|
|
153
|
+
// `enclosingClass` flag answered, now asked of the path's last segment. Python's nested-def
|
|
154
|
+
// reset rule falls out of this for free: a def inside a def has a function as its immediate
|
|
155
|
+
// scope, so it is a function, and its qualified name states the nesting rather than hiding it.
|
|
156
|
+
const enclosing = scope[scope.length - 1];
|
|
157
|
+
const isMethod = config.funcIsMethodInClass && !!enclosing && enclosing.isClass;
|
|
115
158
|
out.push({
|
|
116
159
|
name,
|
|
117
|
-
kind,
|
|
118
|
-
qualifiedName,
|
|
160
|
+
kind: isMethod ? 'method' : 'function',
|
|
161
|
+
qualifiedName: pathOf(scope, name),
|
|
119
162
|
startLine: node.startPosition.row + 1,
|
|
120
163
|
endLine: node.endPosition.row + 1,
|
|
121
164
|
});
|
|
165
|
+
childScope = [...scope, { name, isClass: false }];
|
|
122
166
|
}
|
|
123
|
-
// Nested-def reset rule (see funcIsMethodInClass doc above): only
|
|
124
|
-
// context-sensitive languages (Python) reset enclosingClass to null
|
|
125
|
-
// here; languages with a dedicated methodTypes node (TypeScript) carry
|
|
126
|
-
// enclosingClass through unchanged, matching original behavior.
|
|
127
|
-
const childEnclosing = config.funcIsMethodInClass ? null : enclosingClass;
|
|
128
167
|
for (let i = 0; i < node.childCount; i++)
|
|
129
|
-
visit(node.child(i),
|
|
168
|
+
visit(node.child(i), childScope);
|
|
130
169
|
return;
|
|
131
170
|
}
|
|
132
171
|
if (config.methodTypes.includes(type)) {
|
|
133
172
|
const nameNode = node.childForFieldName('name');
|
|
134
|
-
|
|
173
|
+
let childScope = scope;
|
|
174
|
+
// An object-literal method is emitted only when the scope path can QUALIFY it. Nested inside
|
|
175
|
+
// a named function it is nameable — `makeHandlers.doThing` — and the edge walk already called
|
|
176
|
+
// it that, so emitting it is what makes the two agree. At the top level there is nothing to
|
|
177
|
+
// qualify it with, and a bare `foo` symbol would collide with any real function of that name
|
|
178
|
+
// and corrupt resolution for both: that is the spurious-unqualified-method case the previous
|
|
179
|
+
// rule existed to prevent, and it still holds.
|
|
180
|
+
if (nameNode && scope.length > 0) {
|
|
135
181
|
const name = nameNode.text;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
startLine: node.startPosition.row + 1,
|
|
145
|
-
endLine: node.endPosition.row + 1,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
182
|
+
out.push({
|
|
183
|
+
name,
|
|
184
|
+
kind: 'method',
|
|
185
|
+
qualifiedName: pathOf(scope, name),
|
|
186
|
+
startLine: node.startPosition.row + 1,
|
|
187
|
+
endLine: node.endPosition.row + 1,
|
|
188
|
+
});
|
|
189
|
+
childScope = [...scope, { name, isClass: false }];
|
|
148
190
|
}
|
|
149
191
|
for (let i = 0; i < node.childCount; i++)
|
|
150
|
-
visit(node.child(i),
|
|
192
|
+
visit(node.child(i), childScope);
|
|
151
193
|
return;
|
|
152
194
|
}
|
|
153
195
|
for (let i = 0; i < node.childCount; i++)
|
|
154
|
-
visit(node.child(i),
|
|
196
|
+
visit(node.child(i), scope);
|
|
155
197
|
};
|
|
156
|
-
visit(tree.rootNode,
|
|
198
|
+
visit(tree.rootNode, []);
|
|
157
199
|
return out;
|
|
158
200
|
}
|
|
159
201
|
// @implements A-SPEC-121.6
|
|
@@ -488,11 +530,26 @@ function extractSymbolsFromTree(tree, lang = 'typescript') {
|
|
|
488
530
|
// identifier segment); `scopeTypes` are the class/function nodes whose `name` builds the enclosing
|
|
489
531
|
// qualified path (so a method's edges resolve to `Class.method`, matching the symbol walk).
|
|
490
532
|
const EDGE_CONFIG = {
|
|
533
|
+
// @implements A-SPEC-300
|
|
534
|
+
// Go had the same defect, found by measuring rather than by assuming it was Rust-only: the symbol
|
|
535
|
+
// walk qualifies a method by its RECEIVER type (`Store.Load`) while the edge walk used the bare
|
|
536
|
+
// `name` field, so Go method calls never became edges either.
|
|
491
537
|
go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'] },
|
|
492
|
-
|
|
538
|
+
// @implements A-SPEC-300
|
|
539
|
+
// `impl_item`/`trait_item` must open a qualifying scope, exactly as the symbol walk does: a method
|
|
540
|
+
// in `impl Greet for En` is the symbol `En.hello`, but its edges came out qualified as bare
|
|
541
|
+
// `hello`, so the caller matched no node and EVERY Rust method call was dropped (measured:
|
|
542
|
+
// callerNotNamed 1 of 1). Same defect C++ had under A-SPEC-287.
|
|
543
|
+
rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item', 'impl_item', 'trait_item'] },
|
|
493
544
|
java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'] },
|
|
494
545
|
csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'] },
|
|
495
|
-
|
|
546
|
+
// @implements A-SPEC-287
|
|
547
|
+
// C++ listed only `function_definition`, so a member function's edges came out qualified as bare
|
|
548
|
+
// `run` while the symbol walk (which treats class_specifier/struct_specifier as scopes) emitted
|
|
549
|
+
// `Child.run`. The builder requires the caller to be a real node in that file, so EVERY C++ edge
|
|
550
|
+
// was dropped and the language contributed symbols and no edges at all. This is not a new rule —
|
|
551
|
+
// it is the rule this table's own comment states, which java and csharp already follow.
|
|
552
|
+
cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['class_specifier', 'struct_specifier', 'function_definition'] },
|
|
496
553
|
};
|
|
497
554
|
// The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
|
|
498
555
|
// (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
|
|
@@ -515,6 +572,23 @@ function calleeNameOf(node) {
|
|
|
515
572
|
// The shared table-driven edge walk. `from` is the enclosing definition's QUALIFIED name (so it
|
|
516
573
|
// matches the symbol node id and can be resolved), `to` is the bare callee name. A call the config
|
|
517
574
|
// cannot read emits nothing — never a guessed edge.
|
|
575
|
+
// @implements A-SPEC-300
|
|
576
|
+
// The receiver's type name for a Go method, descending through pointer and generic forms —
|
|
577
|
+
// `(s Store)`, `(s *Store)`, `(s *Store[T])` all yield `Store`. Mirrors the symbol walk so edge
|
|
578
|
+
// qualification and symbol qualification cannot drift apart.
|
|
579
|
+
function goReceiverTypeName(methodDeclNode) {
|
|
580
|
+
const receiver = methodDeclNode.childForFieldName('receiver');
|
|
581
|
+
if (!receiver)
|
|
582
|
+
return null;
|
|
583
|
+
const paramDecl = receiver.namedChild(0);
|
|
584
|
+
let t = paramDecl && paramDecl.childForFieldName('type');
|
|
585
|
+
for (let hops = 0; t && hops < 5; hops++) {
|
|
586
|
+
if (t.type === 'type_identifier')
|
|
587
|
+
return t.text;
|
|
588
|
+
t = t.childForFieldName('type') ?? t.namedChild(0);
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
518
592
|
function walkEdges(tree, cfg) {
|
|
519
593
|
const out = [];
|
|
520
594
|
const scope = new Set(cfg.scopeTypes);
|
|
@@ -524,6 +598,21 @@ function walkEdges(tree, cfg) {
|
|
|
524
598
|
// `declarator` fields to the first identifier — otherwise a C++ function's edges would qualify to
|
|
525
599
|
// `<module>` and never resolve to its bare symbol name.
|
|
526
600
|
const scopeName = (n) => {
|
|
601
|
+
// @implements A-SPEC-300
|
|
602
|
+
// Nodes whose qualifying name is NOT in a `name` field. `impl_item` has no name at all — the
|
|
603
|
+
// type it implements for lives in `type` — and a Go `method_declaration` has a `name`, but the
|
|
604
|
+
// symbol walk qualifies it by the receiver type, so using `name` here would disagree with the
|
|
605
|
+
// node ids that actually exist.
|
|
606
|
+
if (n.type === 'impl_item') {
|
|
607
|
+
const t = n.childForFieldName('type');
|
|
608
|
+
return t ? (t.childForFieldName('name')?.text ?? calleeNameOf(t) ?? t.text) : null;
|
|
609
|
+
}
|
|
610
|
+
if (n.type === 'method_declaration' && n.childForFieldName('receiver')) {
|
|
611
|
+
const recv = goReceiverTypeName(n);
|
|
612
|
+
const nm2 = n.childForFieldName('name');
|
|
613
|
+
if (recv && nm2)
|
|
614
|
+
return `${recv}.${nm2.text}`;
|
|
615
|
+
}
|
|
527
616
|
const nm = n.childForFieldName('name');
|
|
528
617
|
if (nm)
|
|
529
618
|
return nm.text;
|
|
@@ -636,6 +725,26 @@ function extractEdgesFromTree(tree, lang = 'typescript') {
|
|
|
636
725
|
return nm ? nm.text : null;
|
|
637
726
|
};
|
|
638
727
|
const visit = (node, scope) => {
|
|
728
|
+
if (node.type === 'class_declaration' || node.type === 'interface_declaration') {
|
|
729
|
+
// @implements A-SPEC-288
|
|
730
|
+
// `class Child extends Base` and `interface A extends B` are syntactically unambiguous, so
|
|
731
|
+
// this costs no precision. Only a plain identifier base is taken: `extends mixin(Base)` is
|
|
732
|
+
// computed and cannot be read statically, and `implements` is a different relation that this
|
|
733
|
+
// slice deliberately leaves out. Both stay gaps rather than guesses.
|
|
734
|
+
const nm = node.childForFieldName('name');
|
|
735
|
+
const heritage = (node.namedChildren ?? []).find((c) => c.type === 'class_heritage' || c.type === 'extends_type_clause');
|
|
736
|
+
if (nm && heritage) {
|
|
737
|
+
for (const clause of [heritage, ...(heritage.namedChildren ?? [])]) {
|
|
738
|
+
if (clause.type === 'implements_clause')
|
|
739
|
+
continue;
|
|
740
|
+
for (const c of clause.namedChildren ?? []) {
|
|
741
|
+
if (c.type === 'identifier' || c.type === 'type_identifier') {
|
|
742
|
+
out.push({ from: nm.text, to: c.text, rel: 'inherits' });
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
639
748
|
if (node.type === 'import_statement') {
|
|
640
749
|
const src = node.childForFieldName('source') ?? (node.namedChildren && node.namedChildren.find((c) => c.type === 'string'));
|
|
641
750
|
if (src)
|
|
@@ -673,6 +782,8 @@ function extractEdgesFromTree(tree, lang = 'typescript') {
|
|
|
673
782
|
}
|
|
674
783
|
const named = namedScopeOf(node);
|
|
675
784
|
const childScope = named ? scope.concat(named) : scope;
|
|
785
|
+
// Type parameters are scoped to their declaration; collect them before descending so a use of
|
|
786
|
+
// `C` inside `function identity<C>(v: C)` is never mistaken for the contract `C`.
|
|
676
787
|
for (let i = 0; i < node.childCount; i++)
|
|
677
788
|
visit(node.child(i), childScope);
|
|
678
789
|
};
|
|
@@ -719,7 +830,41 @@ function walkPythonEdges(tree) {
|
|
|
719
830
|
return n.text;
|
|
720
831
|
};
|
|
721
832
|
const visit = (node) => {
|
|
722
|
-
if (node.type === '
|
|
833
|
+
if (node.type === 'class_definition') {
|
|
834
|
+
// @implements A-SPEC-286
|
|
835
|
+
// `class Child(Base)` is syntactically unambiguous, so this costs no precision. Excluded:
|
|
836
|
+
// keyword arguments (`metaclass=Meta` is not a base) and starred/dynamic forms, which produce
|
|
837
|
+
// NOTHING rather than a guess — a wrong edge silently corrupts every impact answer that walks
|
|
838
|
+
// it, while a missing one shows up as a gap.
|
|
839
|
+
const name = node.childForFieldName('name');
|
|
840
|
+
const supers = node.childForFieldName('superclasses');
|
|
841
|
+
if (name && supers) {
|
|
842
|
+
for (const arg of supers.namedChildren ?? []) {
|
|
843
|
+
if (arg.type === 'keyword_argument')
|
|
844
|
+
continue;
|
|
845
|
+
let base = null;
|
|
846
|
+
if (arg.type === 'identifier')
|
|
847
|
+
base = arg.text;
|
|
848
|
+
// `mod.Base` -> `Base`, and `Generic[T]` -> `Generic`: same convention the call walk uses.
|
|
849
|
+
else if (arg.type === 'attribute') {
|
|
850
|
+
const a = arg.childForFieldName('attribute');
|
|
851
|
+
base = a ? a.text : null;
|
|
852
|
+
}
|
|
853
|
+
else if (arg.type === 'subscript') {
|
|
854
|
+
const v = arg.childForFieldName('value');
|
|
855
|
+
if (v && v.type === 'identifier')
|
|
856
|
+
base = v.text;
|
|
857
|
+
else if (v && v.type === 'attribute') {
|
|
858
|
+
const a = v.childForFieldName('attribute');
|
|
859
|
+
base = a ? a.text : null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (base)
|
|
863
|
+
out.push({ from: enclosing(name), to: base, rel: 'inherits' });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else if (node.type === 'import_statement') {
|
|
723
868
|
for (const c of node.namedChildren ?? []) {
|
|
724
869
|
const m = moduleName(c);
|
|
725
870
|
if (m)
|
|
@@ -26,7 +26,7 @@ export interface CodeSymbol {
|
|
|
26
26
|
export interface CodeEdge {
|
|
27
27
|
from: string;
|
|
28
28
|
to: string;
|
|
29
|
-
rel: 'calls' | 'imports';
|
|
29
|
+
rel: 'calls' | 'imports' | 'inherits';
|
|
30
30
|
}
|
|
31
31
|
export type TsLang = 'typescript' | 'tsx' | 'python' | 'csharp' | 'java' | 'go' | 'rust' | 'cpp';
|
|
32
32
|
/**
|
|
@@ -22,4 +22,16 @@ export type RechainResult = {
|
|
|
22
22
|
} | {
|
|
23
23
|
refused: string;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* @implements A-SPEC-455
|
|
27
|
+
* Is the COMMITTED mainline a hash+seq prefix of the working copy?
|
|
28
|
+
*
|
|
29
|
+
* Exported so the turn boundary asks the same question the rechain CLI asks. It was inline here,
|
|
30
|
+
* and inline meant it ran only when an operator invoked a command — the fourth instance in one day
|
|
31
|
+
* of a judgement that is right and enforced in exactly one place.
|
|
32
|
+
*
|
|
33
|
+
* Duplicating it instead would be the mistake A-SPEC-449 measured: the config rule knew the
|
|
34
|
+
* copy/move family and the code rule did not, because the vocabulary lived in two copies.
|
|
35
|
+
*/
|
|
36
|
+
export declare function mainlineIsPrefix(mainline: ProvenanceEvent[], working: ProvenanceEvent[]): boolean;
|
|
25
37
|
export declare function rechainLedger(mainline: ProvenanceEvent[], working: ProvenanceEvent[], key?: string): RechainResult;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @implements A-SPEC-256.1
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.acceptsAsMainline = acceptsAsMainline;
|
|
5
|
+
exports.mainlineIsPrefix = mainlineIsPrefix;
|
|
5
6
|
exports.rechainLedger = rechainLedger;
|
|
6
7
|
/**
|
|
7
8
|
* Re-chain a forked ledger against an EXPLICIT mainline — never a greedy one.
|
|
@@ -36,14 +37,28 @@ const bodyOf = (e) => {
|
|
|
36
37
|
const { seq: _s, prevHash: _p, hash: _h, ...body } = e;
|
|
37
38
|
return body;
|
|
38
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* @implements A-SPEC-455
|
|
42
|
+
* Is the COMMITTED mainline a hash+seq prefix of the working copy?
|
|
43
|
+
*
|
|
44
|
+
* Exported so the turn boundary asks the same question the rechain CLI asks. It was inline here,
|
|
45
|
+
* and inline meant it ran only when an operator invoked a command — the fourth instance in one day
|
|
46
|
+
* of a judgement that is right and enforced in exactly one place.
|
|
47
|
+
*
|
|
48
|
+
* Duplicating it instead would be the mistake A-SPEC-449 measured: the config rule knew the
|
|
49
|
+
* copy/move family and the code rule did not, because the vocabulary lived in two copies.
|
|
50
|
+
*/
|
|
51
|
+
function mainlineIsPrefix(mainline, working) {
|
|
52
|
+
return mainline.length <= working.length
|
|
53
|
+
&& mainline.every((e, i) => working[i] !== undefined && working[i].hash === e.hash && working[i].seq === e.seq);
|
|
54
|
+
}
|
|
39
55
|
function rechainLedger(mainline, working, key) {
|
|
40
56
|
// The working file is ALREADY-VERIFIED only when it verifies under the current key AND carries
|
|
41
57
|
// the mainline as a hash-prefix (round-4): the adoption arm belongs to COMMITTED mainline
|
|
42
58
|
// candidates only — applied to the working copy it certified a keyless re-signed replacement as
|
|
43
59
|
// "no change" (reproduced forgery), and a rolled-back verifying prefix as "no change" (leaving
|
|
44
60
|
// committed history unrestored — the working copy shorter than the committed mainline).
|
|
45
|
-
const mainlineIsPrefixOfWorking = mainline
|
|
46
|
-
&& mainline.every((e, i) => working[i] !== undefined && working[i].hash === e.hash && working[i].seq === e.seq);
|
|
61
|
+
const mainlineIsPrefixOfWorking = mainlineIsPrefix(mainline, working);
|
|
47
62
|
if ((0, provenance_chain_1.verifyChain)(working, key).ok && mainlineIsPrefixOfWorking) {
|
|
48
63
|
return { events: working, orphanCount: 0, alreadyVerified: true };
|
|
49
64
|
}
|
|
@@ -183,6 +183,27 @@ class ProvenanceLedger {
|
|
|
183
183
|
if (!r.ok)
|
|
184
184
|
broken.push({ replicaId: c.replicaId, file: c.file, detail: r.detail, brokenAt: r.brokenAt });
|
|
185
185
|
}
|
|
186
|
+
// @implements A-SPEC-458
|
|
187
|
+
// Cross-replica double-spend. consumeNonceExclusively's lock is per-file, so two machines can
|
|
188
|
+
// each spend the same single-use nonce offline and the merge is chain-valid — each chain
|
|
189
|
+
// verifies. The comment there promised this is "DETECTED afterwards in the merged ledger";
|
|
190
|
+
// measured 2026-08-30, nothing did. A single-use nonce appearing in two `nonce-consumed`
|
|
191
|
+
// records is always a violation (REQ-141): there is no legitimate re-spend. Counted by the raw
|
|
192
|
+
// input value, the same rule isNonceConsumed matches on — one consume path writes one form, so
|
|
193
|
+
// a real double-spend records the same value twice.
|
|
194
|
+
const counts = new Map();
|
|
195
|
+
for (const e of this.loadAll()) {
|
|
196
|
+
if (e.kind !== 'nonce-consumed')
|
|
197
|
+
continue;
|
|
198
|
+
for (const inp of e.inputs ?? [])
|
|
199
|
+
counts.set(inp, (counts.get(inp) ?? 0) + 1);
|
|
200
|
+
}
|
|
201
|
+
for (const [nonce, n] of counts) {
|
|
202
|
+
if (n > 1) {
|
|
203
|
+
broken.push({ replicaId: null, file: '(cross-replica)',
|
|
204
|
+
detail: `single-use nonce ${nonce.slice(0, 16)}… spent ${n} times across replicas — REQ-141 double-spend` });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
186
207
|
return { ok: broken.length === 0, broken };
|
|
187
208
|
}
|
|
188
209
|
/**
|
|
@@ -34,5 +34,16 @@ function blindSpotSummary() {
|
|
|
34
34
|
// shell route to either state is denied by this gate; what remains is deletion from outside the
|
|
35
35
|
// session, and saying so is the only honest option left.
|
|
36
36
|
+ ' `.ax` 전체가 세션 밖에서 삭제되면(에디터·Finder·세션 밖 git) 원장도 함께 사라져'
|
|
37
|
-
+ ' 거버넌스를 켠 적 없는 프로젝트와 구별되지 않습니다 — `.ax/specs`만 사라진 경우는 탐지됩니다(REQ-175).'
|
|
37
|
+
+ ' 거버넌스를 켠 적 없는 프로젝트와 구별되지 않습니다 — `.ax/specs`만 사라진 경우는 탐지됩니다(REQ-175).'
|
|
38
|
+
// @implements A-SPEC-421
|
|
39
|
+
// Measured against the INSTALLED build, which is the only place it shows: `tool_name: "Bash"`
|
|
40
|
+
// with `rm -rf /` is denied and `tool_name: "mcp__shell__run"` with the same command is allowed.
|
|
41
|
+
// The gate dispatches on the tool's NAME, so a name it does not know carries whatever it likes.
|
|
42
|
+
// Every MCP tool is such a name, and this repository itself runs several MCP servers — so this
|
|
43
|
+
// is the shape of the gap, not a hypothetical. Denying unknown names instead was considered and
|
|
44
|
+
// rejected: it would refuse every harness's Read, Grep and search tools and leave the guardrail
|
|
45
|
+
// unusable. What is fixable here is the map, and an undeclared gap reads as coverage.
|
|
46
|
+
+ ' 게이트는 **도구 이름**으로 판정하므로 이름을 모르는 도구는 검사하지 않습니다 —'
|
|
47
|
+
+ ' MCP 서버가 제공하는 도구(`mcp__<서버>__<도구>`)는 셸을 실행하더라도 이 게이트를 거치지'
|
|
48
|
+
+ ' 않습니다. MCP 서버는 각자의 승인 경로로 통제하십시오.';
|
|
38
49
|
}
|