@holmes-lab/holmes-kit 0.2.0 → 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 +35 -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 -17
- package/dist/holmes/cli/codex-toml.d.ts +26 -0
- package/dist/holmes/cli/codex-toml.js +282 -0
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +348 -18
- package/dist/holmes/cli/index.js +16 -2
- package/dist/holmes/cli/init.js +78 -0
- package/dist/holmes/cli/interactive-prompt.js +4 -4
- package/dist/holmes/cli/mcp-launcher.d.ts +2 -2
- 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 +48 -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
|
@@ -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
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { EvidenceArtifact } from '../mcp/maintenance-evidence';
|
|
2
|
+
/**
|
|
3
|
+
* Pre-edit impact evidence — the decision core (Goal Phase 2, first slice).
|
|
4
|
+
*
|
|
5
|
+
* This module DECIDES; it does not enforce. Wiring it into the hook path is a separate slice, kept
|
|
6
|
+
* separate on purpose: a gate whose judgement and whose plumbing land together cannot be shown to
|
|
7
|
+
* have a correct judgement. Everything here is pure, so the same inputs always yield the same
|
|
8
|
+
* verdict and every branch is checkable against a fixture rather than against a live repository.
|
|
9
|
+
*
|
|
10
|
+
* The bias is fail-safe throughout. Where the evidence is silent the answer is `widen`, never
|
|
11
|
+
* `allow`: "we saw no impact" and "we could not look" are opposite claims, and only the first is a
|
|
12
|
+
* reason to proceed.
|
|
13
|
+
*/
|
|
14
|
+
export type EditDecision = 'allow' | 'widen' | 'refuse';
|
|
15
|
+
export interface EditEvidenceReason {
|
|
16
|
+
code: 'no-analysis' | 'basis-drift' | 'target-out-of-scope' | 'contract-untested' | 'unproven-absence' | 'architecture-unchecked' | 'target-drift' | 'basis-unverifiable' | 'security-unchecked' | 'compatibility-unchecked';
|
|
17
|
+
detail: string;
|
|
18
|
+
}
|
|
19
|
+
export interface EditEvidenceInput {
|
|
20
|
+
/** Repo-relative path the session wants to edit. */
|
|
21
|
+
target: string;
|
|
22
|
+
/** The persisted analysis backing this edit, or null when none was produced. */
|
|
23
|
+
artifact: EvidenceArtifact | null;
|
|
24
|
+
/** HEAD as it is NOW — compared against the commit the analysis was computed on. */
|
|
25
|
+
currentHead: string;
|
|
26
|
+
/**
|
|
27
|
+
* @implements A-SPEC-277
|
|
28
|
+
* sha256 of the target AS IT IS NOW, or null when it cannot be read (deleted, or not yet created).
|
|
29
|
+
* Compared against the digest the analysis recorded, so an uncommitted edit to the analysed file
|
|
30
|
+
* is caught even while HEAD sits still.
|
|
31
|
+
*/
|
|
32
|
+
currentTargetDigest?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface EditEvidenceVerdict {
|
|
35
|
+
decision: EditDecision;
|
|
36
|
+
reasons: EditEvidenceReason[];
|
|
37
|
+
remediation: string;
|
|
38
|
+
/** True when the target qualified for the proportional low-risk path. */
|
|
39
|
+
lowRisk: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Documents are edited constantly and almost never break anything, so demanding a graph analysis for
|
|
43
|
+
* every prose change would train people to route around the gate — the proportional path exists to
|
|
44
|
+
* keep the gate credible where it matters. The exception is documents that GOVERN: an ADR or a
|
|
45
|
+
* C-SPEC is prose that decides architecture, and A-SPEC-233 already treats those as full-regression
|
|
46
|
+
* material. Same locations, read from the type table rather than spelled twice.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isLowRiskTarget(target: string): boolean;
|
|
49
|
+
export declare function evaluateEditEvidence(inputArgs: EditEvidenceInput): EditEvidenceVerdict;
|
|
50
|
+
/**
|
|
51
|
+
* @implements A-SPEC-278
|
|
52
|
+
* The enforcement mode. `off` is the default and must be a TRUE no-op: a repository that never asked
|
|
53
|
+
* for this gate has to behave exactly as it did before, and a broken config falls back here too.
|
|
54
|
+
*/
|
|
55
|
+
export type PreEditEvidenceMode = 'off' | 'warn' | 'block';
|
|
56
|
+
/**
|
|
57
|
+
* @implements A-SPEC-278
|
|
58
|
+
* Turn a verdict into a hook decision, or into silence.
|
|
59
|
+
*
|
|
60
|
+
* Silence — `null` — is deliberate for both `off` and a clean `allow`. Returning `allow` would still
|
|
61
|
+
* be an opinion: if this gate were ever consulted ahead of another, its `allow` could mask that
|
|
62
|
+
* gate's denial. A gate that has nothing to say should say nothing.
|
|
63
|
+
*/
|
|
64
|
+
export declare function preEditEvidenceDecision(verdict: EditEvidenceVerdict, mode: PreEditEvidenceMode): {
|
|
65
|
+
permissionDecision: 'allow' | 'deny';
|
|
66
|
+
permissionDecisionReason?: string;
|
|
67
|
+
} | null;
|
|
68
|
+
/**
|
|
69
|
+
* @implements A-SPEC-278
|
|
70
|
+
* The I/O adapter, kept apart from the pure core on purpose: everything above this line is decided
|
|
71
|
+
* by fixtures, and only this function touches a disk. It reads the artifact the session declared
|
|
72
|
+
* and measures the target's current content so the core can compare them.
|
|
73
|
+
*
|
|
74
|
+
* A digest that names no stored artifact yields `artifact: null` — the same shape as "no analysis",
|
|
75
|
+
* because from the gate's side an unfindable analysis and an absent one justify equally little.
|
|
76
|
+
*/
|
|
77
|
+
export declare function loadEditEvidence(projectRoot: string, target: string, digest: string | null, currentHead: string): EditEvidenceInput;
|