@holmes-lab/holmes-kit 0.5.0 → 0.6.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 CHANGED
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ <!-- @implements A-SPEC-209 -->
8
+ ## [0.6.0] - 2026-09-02
9
+
10
+ Three more languages earn corpus-validated control- and data-flow: Java, C#, and Rust join
11
+ TypeScript, Python and Go. Six of seven official languages now build CFG/DDG/CDG with zero
12
+ invariant violations over real open-source corpora. Every grade in the matrix is a measurement.
13
+
14
+ ### Added
15
+
16
+ - **Java CFG/DDG/CDG** (REQ-520): rides the TS chassis (same probed shapes for if/while/for/do
17
+ and the try clauses) plus the four seams where Java's semantics live: ONE switch node carrying
18
+ two styles (old groups fall through, arrow rules never touch), try-with-resources with resource
19
+ defs, catch parameters inside catch_formal_parameter, and field/method names excluded from
20
+ uses (`this.x = x` reads x once). **Measured over gson+junit4: 8,376/8,376 functions, zero
21
+ refusals, zero violations, 19,818 REACHING_DEF + 7,622 CDG edges.** The corpus caught three
22
+ would-have-shipped defects — named comment nodes, empty bodies (`while (c()) ;` — latent for
23
+ TS too), and Java's wrapperless else that lowered an else-if CONDITION as a statement.
24
+ - **C# CFG/DDG/CDG** (REQ-521): same face as Java, opposite semantics where it hurts — switch
25
+ fallthrough is a compile error (sections isolate), `yield` is the iterator protocol
26
+ (yield return continues, yield break exits), and `#if/#else` arms are ALTERNATIVES lowered as
27
+ branches. **Measured over Newtonsoft.Json+RestSharp: 9,184 functions, zero violations; the
28
+ only refusals are designed (preproc_elif 3, goto 1); 51 files (4.4%) refuse honestly at L1
29
+ where conditional compilation crosses syntax.** One sealed design was corrected mid-slice and
30
+ re-sealed: a declaration pattern binds at the switch HEADER, because a def on the point that
31
+ uses it can never reach.
32
+ - **Rust CFG/DDG/CDG** (REQ-522): control flow is an EXPRESSION in Rust (measured: if 1,170 /
33
+ match 468 / for 307 all inside expression_statement), which creates a new failure mode — a
34
+ zero-violation but HOLLOW graph. The corpus gate therefore demands branch edges exist, not
35
+ just that invariants hold. Match is exhaustive (no phantom escape — rustc enforces it), `?` is
36
+ an implicit early return (both paths leave the point), bare `loop` is infinite, let-else's
37
+ else arm diverges, and pattern bindings follow the lowercase-initial convention (documented
38
+ heuristic). **Measured over ripgrep+serde: 5,247/5,247 functions, zero refusals, zero
39
+ violations, 4,786 branch edges (avg 5.6 blocks/function), 13,647 REACHING_DEF + 6,808 CDG
40
+ edges.**
41
+
7
42
  <!-- @implements A-SPEC-209 -->
8
43
  ## [0.5.0] - 2026-09-02
9
44
 
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 8f00a9a-mtjc55io
1
+ 814d021-mtjewubg
@@ -12,6 +12,14 @@ exports.FUNCTION_TYPES = new Set([
12
12
  'function_definition', 'lambda',
13
13
  // @implements A-SPEC-519.1 — Go: methods and function literals (closures/goroutine bodies).
14
14
  'method_declaration', 'func_literal',
15
+ // @implements A-SPEC-520.1 — Java: constructors, lambdas and initializer blocks.
16
+ 'constructor_declaration', 'lambda_expression', 'static_initializer',
17
+ 'instance_initializer', 'compact_constructor_declaration',
18
+ // @implements A-SPEC-521.1 — C#: local functions, anonymous methods, operators, accessors.
19
+ 'local_function_statement', 'anonymous_method_expression', 'destructor_declaration',
20
+ 'operator_declaration', 'conversion_operator_declaration', 'accessor_declaration',
21
+ // @implements A-SPEC-522.1 — Rust: named functions and closures.
22
+ 'function_item', 'closure_expression',
15
23
  ]);
16
24
  const TS_SIMPLE = new Set([
17
25
  'expression_statement', 'lexical_declaration', 'variable_declaration', 'debugger_statement',
@@ -69,8 +77,83 @@ const GO_RULES = {
69
77
  ]),
70
78
  block: 'block',
71
79
  };
80
+ // @implements A-SPEC-520.1 — Java, classified from the census over gson+junit4 (8,376
81
+ // functions, 20 statement types). Java rides the TS chassis: if/while/for/do/labeled/break/
82
+ // continue/return/throw and the try-clause names are the SAME probed shapes. What is Java's own:
83
+ // one switch node carrying two styles, a try body spelled `block`, try-with-resources, and
84
+ // synchronized as a body wrapper.
85
+ const JAVA_SIMPLE = new Set([
86
+ 'expression_statement', 'local_variable_declaration', 'class_declaration',
87
+ 'record_declaration', 'assert_statement', 'empty_statement',
88
+ ]);
89
+ const JAVA_RULES = {
90
+ simple: JAVA_SIMPLE,
91
+ handled: new Set([
92
+ ...JAVA_SIMPLE,
93
+ 'block', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
94
+ 'enhanced_for_statement', 'switch_expression', 'try_statement',
95
+ 'try_with_resources_statement', 'synchronized_statement', 'labeled_statement',
96
+ 'break_statement', 'continue_statement', 'return_statement', 'throw_statement',
97
+ 'yield_statement',
98
+ ]),
99
+ block: 'block',
100
+ };
101
+ // @implements A-SPEC-521.1 — C#, classified from the census over newtonsoft+restsharp (13,642
102
+ // functions, 27 statement types). Same face as Java, opposite semantics in three places: switch
103
+ // fallthrough is ILLEGAL (sections isolate), `yield` is the iterator protocol (return passes
104
+ // through, break exits), and the preprocessor puts ALTERNATIVES in statement position. `goto`
105
+ // refuses (one corpus occurrence). Files where conditional compilation crosses syntax refuse at
106
+ // L1 (51 of 1,149 — the honest limit, recorded).
107
+ const CSHARP_SIMPLE = new Set([
108
+ 'expression_statement', 'local_declaration_statement', 'local_function_statement',
109
+ 'empty_statement', 'preproc_region', 'preproc_endregion', 'preproc_pragma',
110
+ ]);
111
+ const CSHARP_RULES = {
112
+ simple: CSHARP_SIMPLE,
113
+ handled: new Set([
114
+ ...CSHARP_SIMPLE,
115
+ 'block', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
116
+ 'foreach_statement', 'switch_statement', 'try_statement', 'using_statement',
117
+ 'lock_statement', 'checked_statement', 'preproc_if',
118
+ 'break_statement', 'continue_statement', 'return_statement', 'throw_statement',
119
+ 'yield_statement',
120
+ ]),
121
+ block: 'block',
122
+ };
123
+ // @implements A-SPEC-522.1 — Rust, where control flow is an EXPRESSION. The simple set
124
+ // enumerates the value expressions and declarations the corpus census actually put in statement
125
+ // or trailing position (36 kinds over ripgrep+serde); anything new refuses, keeping fail-closed
126
+ // meaningful. Control expressions are lowered as control wherever they stand as a statement or a
127
+ // trailing value — flattening them would produce the zero-violation HOLLOW graph this slice's
128
+ // corpus gate (branch edges > 0) exists to catch.
129
+ const RUST_SIMPLE = new Set([
130
+ 'let_declaration', 'call_expression', 'macro_invocation', 'identifier', 'self',
131
+ 'scoped_identifier', 'field_expression', 'reference_expression', 'binary_expression',
132
+ 'unary_expression', 'assignment_expression', 'compound_assignment_expr', 'struct_expression',
133
+ 'tuple_expression', 'array_expression', 'index_expression', 'await_expression',
134
+ 'type_cast_expression', 'range_expression', 'closure_expression', 'parenthesized_expression',
135
+ 'integer_literal', 'float_literal', 'string_literal', 'raw_string_literal', 'char_literal',
136
+ 'boolean_literal', 'unit_expression', 'try_expression', 'method_call_expression',
137
+ 'use_declaration', 'struct_item', 'enum_item', 'impl_item', 'function_item', 'const_item',
138
+ 'static_item', 'type_item', 'mod_item', 'trait_item', 'macro_definition', 'attribute_item',
139
+ 'inner_attribute_item', 'empty_statement', 'foreign_mod_item', 'extern_crate_declaration',
140
+ // expression_statement lands here when its inner is NOT a control expression (the unwrap
141
+ // check runs first), so a plain `g();` is one point.
142
+ 'expression_statement',
143
+ ]);
144
+ const RUST_RULES = {
145
+ simple: RUST_SIMPLE,
146
+ handled: new Set([
147
+ ...RUST_SIMPLE,
148
+ 'block', 'expression_statement', 'if_expression', 'match_expression', 'for_expression',
149
+ 'while_expression', 'loop_expression', 'return_expression', 'break_expression',
150
+ 'continue_expression',
151
+ ]),
152
+ block: 'block',
153
+ };
72
154
  const LANG_RULES = {
73
- typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES,
155
+ typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES, java: JAVA_RULES,
156
+ csharp: CSHARP_RULES, rust: RUST_RULES,
74
157
  };
75
158
  // @implements A-SPEC-510.6
76
159
  /**
@@ -115,6 +198,7 @@ function cfgOf(ast, fn, source) {
115
198
  const rules = LANG_RULES[ast.lang];
116
199
  const isPython = ast.lang === 'python';
117
200
  const isGo = ast.lang === 'go';
201
+ const isRust = ast.lang === 'rust';
118
202
  const kids = childrenIndex(ast);
119
203
  // Points: one per lowered statement (block ids after coalescing). 0=ENTRY, 1=EXIT.
120
204
  const stmtsOf = [[], []];
@@ -155,7 +239,9 @@ function cfgOf(ast, fn, source) {
155
239
  }
156
240
  return { entry, exits };
157
241
  };
158
- const stmtChildren = (i) => kids.of(i).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'comment');
242
+ const stmtChildren = (i) =>
243
+ // `comment` (TS/PY/Go) and Java's line_comment/block_comment are all named nodes.
244
+ kids.of(i).filter((k) => ast.nodes[k].named && !ast.nodes[k].type.includes('comment'));
159
245
  const wireException = (first, targetProvider) => {
160
246
  // Conservative: every point created inside [first, now) can throw into the handler.
161
247
  const target = targetProvider();
@@ -168,8 +254,47 @@ function cfgOf(ast, fn, source) {
168
254
  throw new Unsupported(t);
169
255
  const label = ctx.pendingLabel;
170
256
  ctx.pendingLabel = null;
257
+ // @implements A-SPEC-522.1 — Rust control lives in expressions. A statement whose sole
258
+ // named child is a control expression lowers as that control (flattening it makes the
259
+ // hollow graph); a `?` anywhere in a point's subtree adds the implicit early-return edge;
260
+ // a let-else is a branch whose false arm is the (compiler-enforced) diverging else block.
261
+ const RUST_CONTROL = new Set(['if_expression', 'match_expression', 'for_expression', 'while_expression', 'loop_expression', 'return_expression', 'break_expression', 'continue_expression']);
262
+ if (isRust && t === 'expression_statement') {
263
+ const inner = stmtChildren(i);
264
+ if (inner.length === 1 && RUST_CONTROL.has(ast.nodes[inner[0]].type))
265
+ return lowerStmt(inner[0], ctx);
266
+ }
267
+ const rustQuestion = (p, root) => {
268
+ if (!isRust)
269
+ return;
270
+ const stack = [root];
271
+ while (stack.length) {
272
+ const n = stack.pop();
273
+ if (n !== root && exports.FUNCTION_TYPES.has(ast.nodes[n].type))
274
+ continue; // closures own their `?`
275
+ if (ast.nodes[n].type === 'try_expression') {
276
+ link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
277
+ return;
278
+ }
279
+ for (const c of kids.of(n))
280
+ stack.push(c);
281
+ }
282
+ };
283
+ if (isRust && t === 'let_declaration') {
284
+ const elseBlock = kids.of(i).some((k) => !ast.nodes[k].named && source !== undefined
285
+ && source.slice(ast.nodes[k].start, ast.nodes[k].end) === 'else')
286
+ ? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(-1) : undefined;
287
+ if (elseBlock !== undefined) {
288
+ const p = newPoint(i);
289
+ rustQuestion(p, i);
290
+ const ef = lowerStmt(elseBlock, ctx);
291
+ link(p, ef.entry, 'false');
292
+ return { entry: p, exits: [{ from: p, kind: 'true' }, ...ef.exits] };
293
+ }
294
+ }
171
295
  if (rules.simple.has(t)) {
172
296
  const p = newPoint(i);
297
+ rustQuestion(p, i);
173
298
  return { entry: p, exits: [{ from: p, kind: 'seq' }] };
174
299
  }
175
300
  switch (t) {
@@ -209,9 +334,29 @@ function cfgOf(ast, fn, source) {
209
334
  : stmtChildren(clause).find((k) => ast.nodes[k].type === 'block');
210
335
  }
211
336
  }
337
+ else if (ast.lang === 'java' || ast.lang === 'csharp') {
338
+ // @implements A-SPEC-520.1 / A-SPEC-521.1 — Java and C# have NO else_clause
339
+ // wrapper: named[2] IS the
340
+ // alternative (a block, or the next if_statement of an else-if chain). Unwrapping it
341
+ // like TS lowered an else-if's CONDITION as a statement — the corpus caught it.
342
+ alt = named[2];
343
+ }
212
344
  else {
213
345
  alt = named[2] !== undefined ? stmtChildren(named[2])[0] : undefined;
214
346
  }
347
+ if (cons === undefined || !rules.handled.has(ast.nodes[cons].type)) {
348
+ // `if (c()) ;` — an empty then-arm: both edges leave the header directly.
349
+ const exits0 = [{ from: p, kind: 'true' }];
350
+ if (alt !== undefined && rules.handled.has(ast.nodes[alt].type)) {
351
+ const af0 = lowerStmt(alt, ctx);
352
+ link(p, af0.entry, 'false');
353
+ exits0.push(...af0.exits);
354
+ }
355
+ else {
356
+ exits0.push({ from: p, kind: 'false' });
357
+ }
358
+ return { entry: p, exits: exits0 };
359
+ }
215
360
  const cf = lowerStmt(cons, ctx);
216
361
  link(p, cf.entry, 'true');
217
362
  const exits = [...cf.exits];
@@ -240,6 +385,17 @@ function cfgOf(ast, fn, source) {
240
385
  const bodyIdx = (isPython || isGo)
241
386
  ? kids.of(i).filter((k) => ast.nodes[k].type === 'block').at(0)
242
387
  : stmtChildren(i).at(-1);
388
+ // Found by the Java corpus, latent for TS too: `for (; ; ) ;` has NO named children —
389
+ // empty clauses and an empty-statement body. The loop is a bare spin: header with a
390
+ // self loop-back and only the (over-approximate) exhaustion exit.
391
+ if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
392
+ // Latent for TS, found by the Java corpus: an EMPTY body (`while (c()) ;`,
393
+ // `for (; ; ) ;`) leaves no statement child, so `.at(-1)` lands on the condition
394
+ // itself. The loop is a bare spin: header with a self loop-back.
395
+ const header = newPoint(i);
396
+ link(header, header, 'loop-back');
397
+ return { entry: header, exits: [{ from: header, kind: 'false' }] };
398
+ }
243
399
  // @implements A-SPEC-519.1 — a bare Go `for {}` (no named non-block child) is INFINITE:
244
400
  // no exhaustion edge. `for { select {…} }` is the daemon idiom, and a phantom false edge
245
401
  // would hang spurious control dependence on everything after the loop. A for_clause with
@@ -273,6 +429,10 @@ function cfgOf(ast, fn, source) {
273
429
  case 'do_statement': {
274
430
  const header = newPoint(i); // condition point (the `do` node)
275
431
  const bodyIdx = stmtChildren(i)[0];
432
+ if (bodyIdx === undefined || !rules.handled.has(ast.nodes[bodyIdx].type)) {
433
+ link(header, header, 'loop-back');
434
+ return { entry: header, exits: [{ from: header, kind: 'false' }] };
435
+ }
276
436
  const myBreak = { label, target: -1 };
277
437
  ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
278
438
  ctx.contT.push({ label, target: header }, { label: null, target: header });
@@ -368,6 +528,236 @@ function cfgOf(ast, fn, source) {
368
528
  exits.push({ from: b.point, kind: 'break' });
369
529
  return { entry: p, exits };
370
530
  }
531
+ // @implements A-SPEC-520.1 — Java's enhanced for: the header owns the binding; body
532
+ // loops back; exhaustion exits false — same skeleton as Go's range.
533
+ case 'enhanced_for_statement':
534
+ case 'foreach_statement': {
535
+ const header = newPoint(i);
536
+ const bodyIdx = kids.of(i).find((k) => ast.nodes[k].type === 'block')
537
+ ?? stmtChildren(i).at(-1);
538
+ const myBreak = { label, target: -1 };
539
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
540
+ ctx.contT.push({ label, target: header }, { label: null, target: header });
541
+ const bf = lowerStmt(bodyIdx, ctx);
542
+ ctx.breakT.splice(-2);
543
+ ctx.contT.splice(-2);
544
+ link(header, bf.entry, 'true');
545
+ for (const e of bf.exits)
546
+ link(e.from, header, 'loop-back');
547
+ const exits = [{ from: header, kind: 'false' }];
548
+ for (const b of claimBreaks(myBreak))
549
+ exits.push({ from: b.point, kind: 'break' });
550
+ return { entry: header, exits };
551
+ }
552
+ // @implements A-SPEC-520.1 — synchronized: a body wrapper for control flow (the lock is
553
+ // a use the DDG reads off the header point); lock semantics are declared out of scope.
554
+ case 'synchronized_statement':
555
+ case 'lock_statement':
556
+ case 'checked_statement': {
557
+ const p = newPoint(i);
558
+ const body = kids.of(i).find((k) => ast.nodes[k].type === 'block');
559
+ if (body === undefined)
560
+ throw new Unsupported(t + ' without a block');
561
+ const bf = lowerStmt(body, ctx);
562
+ link(p, bf.entry, 'seq');
563
+ return { entry: p, exits: bf.exits };
564
+ }
565
+ // @implements A-SPEC-521.1 — the preprocessor puts ALTERNATIVES in statement position.
566
+ // Sequencing the arms would fabricate flow no build configuration ever runs; each arm is
567
+ // a branch and both rejoin after #endif (a may-analysis over every configuration).
568
+ case 'preproc_if': {
569
+ const p = newPoint(i);
570
+ const named = stmtChildren(i);
571
+ const elseArm = named.find((k) => ['preproc_else', 'preproc_elif'].includes(ast.nodes[k].type));
572
+ const ifStmts = named.filter((k) => rules.handled.has(ast.nodes[k].type));
573
+ const exits = [];
574
+ const f1 = lowerSeq(ifStmts, ctx);
575
+ link(p, f1.entry, 'true');
576
+ exits.push(...f1.exits);
577
+ if (elseArm !== undefined) {
578
+ if (ast.nodes[elseArm].type === 'preproc_elif')
579
+ throw new Unsupported('preproc_elif');
580
+ const elseStmts = stmtChildren(elseArm).filter((k) => rules.handled.has(ast.nodes[k].type));
581
+ const f2 = lowerSeq(elseStmts, ctx);
582
+ link(p, f2.entry, 'false');
583
+ exits.push(...f2.exits);
584
+ }
585
+ else {
586
+ exits.push({ from: p, kind: 'false' });
587
+ }
588
+ return { entry: p, exits };
589
+ }
590
+ // @implements A-SPEC-521.1 — using: a wrapper whose header owns the resource defs
591
+ // (variable_declarator does the DDG work); the implicit Dispose is covered by the
592
+ // enclosing try wiring, same posture as Java's try-with-resources.
593
+ case 'using_statement': {
594
+ const p = newPoint(i);
595
+ const body = kids.of(i).find((k) => ast.nodes[k].type === 'block')
596
+ ?? stmtChildren(i).filter((k) => rules.handled.has(ast.nodes[k].type)).at(-1);
597
+ if (body === undefined)
598
+ return { entry: p, exits: [{ from: p, kind: 'seq' }] };
599
+ const bf = lowerStmt(body, ctx);
600
+ link(p, bf.entry, 'seq');
601
+ return { entry: p, exits: bf.exits };
602
+ }
603
+ // @implements A-SPEC-520.1 — ONE node, TWO styles. Old-style groups fall through into
604
+ // each other (TS switch semantics); arrow rules never touch (Go case semantics). A
605
+ // `yield` leaves the switch like a break-with-value. Statement position only — an
606
+ // expression-position switch stays inside its statement's point, like a ternary.
607
+ case 'switch_expression': {
608
+ const p = newPoint(i);
609
+ const myBreak = { label, target: -1 };
610
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
611
+ ctx.yieldT.push(myBreak);
612
+ const swBlock = kids.of(i).find((k) => ast.nodes[k].type === 'switch_block');
613
+ const members = swBlock === undefined ? [] : kids.of(swBlock).filter((k) => ['switch_block_statement_group', 'switch_rule'].includes(ast.nodes[k].type));
614
+ let sawDefault = false;
615
+ let prevFall = [];
616
+ const exits = [];
617
+ for (const m of members) {
618
+ const labelNode = kids.of(m).find((k) => ast.nodes[k].type === 'switch_label');
619
+ if (labelNode !== undefined && source !== undefined
620
+ && source.slice(ast.nodes[labelNode].start, ast.nodes[labelNode].end).trim().startsWith('default'))
621
+ sawDefault = true;
622
+ const stmts = stmtChildren(m).filter((k) => ast.nodes[k].type !== 'switch_label');
623
+ const cf = lowerSeq(stmts, ctx);
624
+ link(p, cf.entry, 'case');
625
+ if (ast.nodes[m].type === 'switch_block_statement_group') {
626
+ for (const e of prevFall)
627
+ link(e.from, cf.entry, 'seq'); // implicit fallthrough
628
+ prevFall = cf.exits;
629
+ }
630
+ else {
631
+ exits.push(...cf.exits); // arrow rule: isolated
632
+ }
633
+ }
634
+ exits.push(...prevFall);
635
+ if (!sawDefault)
636
+ exits.push({ from: p, kind: 'default' });
637
+ ctx.breakT.splice(-2);
638
+ ctx.yieldT.pop();
639
+ for (const b of claimBreaks(myBreak))
640
+ exits.push({ from: b.point, kind: 'break' });
641
+ return { entry: p, exits };
642
+ }
643
+ case 'yield_statement': {
644
+ const p = newPoint(i);
645
+ // @implements A-SPEC-521.1 — the SAME node name, two protocols. Java: a value-carrying
646
+ // switch escape. C#: the iterator protocol — `yield return v` (has a named child)
647
+ // CONTINUES executing; `yield break` (bare) leaves the function.
648
+ if (ast.lang === 'csharp') {
649
+ if (stmtChildren(i).length > 0)
650
+ return { entry: p, exits: [{ from: p, kind: 'seq' }] };
651
+ link(p, EXIT, 'return');
652
+ return { entry: p, exits: [] };
653
+ }
654
+ const owner = ctx.yieldT.at(-1);
655
+ if (owner === undefined)
656
+ throw new Unsupported('yield outside switch');
657
+ pendingBreaks.push({ point: p, owner });
658
+ return { entry: p, exits: [] };
659
+ }
660
+ // @implements A-SPEC-522.1 — Rust's if EXPRESSION: [cond, block, else_clause?[block|if]].
661
+ case 'if_expression': {
662
+ const p = newPoint(i);
663
+ rustQuestion(p, i === undefined ? i : kids.of(i)[0] ?? i);
664
+ const consR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
665
+ const elseClause = kids.of(i).find((k) => ast.nodes[k].type === 'else_clause');
666
+ const altR = elseClause === undefined ? undefined
667
+ : kids.of(elseClause).find((k) => ['block', 'if_expression'].includes(ast.nodes[k].type));
668
+ const cf = lowerStmt(consR, ctx);
669
+ link(p, cf.entry, 'true');
670
+ const exits = [...cf.exits];
671
+ if (altR !== undefined) {
672
+ const af = lowerStmt(altR, ctx);
673
+ link(p, af.entry, 'false');
674
+ exits.push(...af.exits);
675
+ }
676
+ else {
677
+ exits.push({ from: p, kind: 'false' });
678
+ }
679
+ return { entry: p, exits };
680
+ }
681
+ // @implements A-SPEC-522.1 — match: arms isolated, EXHAUSTIVE (rustc enforces it, so a
682
+ // "no arm matched" escape would be flow no execution takes — the opposite of Python's
683
+ // match, deliberately). Arm values may be blocks or bare expressions.
684
+ case 'match_expression': {
685
+ const p = newPoint(i);
686
+ const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
687
+ const exits = [];
688
+ for (const arm of mb === undefined ? [] : kids.of(mb)) {
689
+ if (ast.nodes[arm].type !== 'match_arm')
690
+ continue;
691
+ const val = kids.of(arm).filter((k) => ast.nodes[k].named && ast.nodes[k].type !== 'match_pattern').at(-1);
692
+ if (val === undefined)
693
+ continue;
694
+ const vf = rules.handled.has(ast.nodes[val].type)
695
+ ? lowerStmt(val, ctx)
696
+ : (() => { const vp = newPoint(val); rustQuestion(vp, val); return { entry: vp, exits: [{ from: vp, kind: 'seq' }] }; })();
697
+ link(p, vf.entry, 'case');
698
+ exits.push(...vf.exits);
699
+ }
700
+ return { entry: p, exits };
701
+ }
702
+ // @implements A-SPEC-522.1 — for/while/loop expressions ride the loop skeleton; a bare
703
+ // `loop` is INFINITE (no exhaustion edge — Go's bare-for call, same reasoning).
704
+ case 'for_expression':
705
+ case 'while_expression':
706
+ case 'loop_expression': {
707
+ const header = newPoint(i);
708
+ rustQuestion(header, i);
709
+ const bodyR = kids.of(i).find((k) => ast.nodes[k].type === 'block');
710
+ const myBreak = { label, target: -1 };
711
+ const loopLabel = (() => {
712
+ const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
713
+ return ln !== undefined && source !== undefined
714
+ ? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : label;
715
+ })();
716
+ const owner = { label: loopLabel, target: -1 };
717
+ ctx.breakT.push({ label: loopLabel, owner }, { label: null, owner });
718
+ ctx.contT.push({ label: loopLabel, target: header }, { label: null, target: header });
719
+ const bf = lowerStmt(bodyR, ctx);
720
+ ctx.breakT.splice(-2);
721
+ ctx.contT.splice(-2);
722
+ link(header, bf.entry, 'true');
723
+ for (const e of bf.exits)
724
+ link(e.from, header, 'loop-back');
725
+ const exits = [];
726
+ if (t !== 'loop_expression')
727
+ exits.push({ from: header, kind: 'false' });
728
+ for (const b of claimBreaks(owner))
729
+ exits.push({ from: b.point, kind: 'break' });
730
+ void myBreak;
731
+ return { entry: header, exits };
732
+ }
733
+ case 'return_expression': {
734
+ const p = newPoint(i);
735
+ rustQuestion(p, i);
736
+ link(p, EXIT, 'return');
737
+ return { entry: p, exits: [] };
738
+ }
739
+ case 'break_expression': {
740
+ const p = newPoint(i);
741
+ const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
742
+ const lbl = ln !== undefined && source !== undefined
743
+ ? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
744
+ const bentry = nearestBreak(ctx.breakT, lbl);
745
+ if (!bentry)
746
+ throw new Unsupported('break outside breakable');
747
+ pendingBreaks.push({ point: p, owner: bentry.owner });
748
+ return { entry: p, exits: [] };
749
+ }
750
+ case 'continue_expression': {
751
+ const p = newPoint(i);
752
+ const ln = kids.of(i).find((k) => ast.nodes[k].type === 'label');
753
+ const lbl = ln !== undefined && source !== undefined
754
+ ? source.slice(ast.nodes[ln].start, ast.nodes[ln].end) : null;
755
+ const centry = nearestCont(ctx.contT, lbl);
756
+ if (!centry || centry.target < 0)
757
+ throw new Unsupported('continue outside loop');
758
+ link(p, centry.target, 'continue');
759
+ return { entry: p, exits: [] };
760
+ }
371
761
  // @implements A-SPEC-519.1 — select: each communication case is a branch off the header;
372
762
  // WITHOUT a default the statement BLOCKS until some case fires, so there is no "no arm
373
763
  // matched" escape (the opposite of Python's match).
@@ -420,6 +810,36 @@ function cfgOf(ast, fn, source) {
420
810
  return { entry: p, exits: [] };
421
811
  }
422
812
  case 'switch_statement': {
813
+ // @implements A-SPEC-521.1 — C# shares the node NAME with TS and inverts the
814
+ // semantics: implicit fallthrough is a compile error, so sections are ISOLATED.
815
+ // (The suite caught the first attempt — a duplicate case label silently shadowed the
816
+ // TS lowering and broke its fallthrough pin.)
817
+ if (ast.lang === 'csharp') {
818
+ const p = newPoint(i);
819
+ const myBreak = { label, target: -1 };
820
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
821
+ const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'switch_body');
822
+ let sawDefault = false;
823
+ const exits = [];
824
+ for (const sec of swBody === undefined ? [] : kids.of(swBody)) {
825
+ if (ast.nodes[sec].type !== 'switch_section')
826
+ continue;
827
+ const children = stmtChildren(sec);
828
+ const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
829
+ const hasPattern = children.some((k) => ast.nodes[k].type.includes('pattern'));
830
+ if (!hasPattern)
831
+ sawDefault = true; // a default section carries no pattern
832
+ const cf = lowerSeq(stmts, ctx);
833
+ link(p, cf.entry, hasPattern ? 'case' : 'default');
834
+ exits.push(...cf.exits); // isolated: nothing flows to a sibling
835
+ }
836
+ if (!sawDefault)
837
+ exits.push({ from: p, kind: 'default' });
838
+ ctx.breakT.splice(-2);
839
+ for (const b of claimBreaks(myBreak))
840
+ exits.push({ from: b.point, kind: 'break' });
841
+ return { entry: p, exits };
842
+ }
423
843
  const p = newPoint(i);
424
844
  const body = stmtChildren(i).at(-1); // switch_body
425
845
  const myBreak = { label, target: -1 };
@@ -478,13 +898,17 @@ function cfgOf(ast, fn, source) {
478
898
  link(p, ctx.catchT.at(-1) ?? EXIT, 'throw');
479
899
  return { entry: p, exits: [] };
480
900
  }
901
+ // @implements A-SPEC-520.1 — try-with-resources joins the try case: same clause wiring,
902
+ // plus a point for the resource_specification so its defs (`var r = open()`) have a home.
903
+ case 'try_with_resources_statement':
481
904
  case 'try_statement': {
482
905
  const named = stmtChildren(i);
483
906
  // @implements A-SPEC-510.7 — Python spells the same shape with `block` / `except_clause`
484
907
  // / `finally_clause`, PLUS an `else_clause` that runs ONLY when no exception was raised.
908
+ // @implements A-SPEC-520.1 — Java spells the try body `block`; TS `statement_block`.
485
909
  const body = isPython
486
910
  ? kids.of(i).find((k) => ast.nodes[k].type === 'block')
487
- : named.find((k) => ast.nodes[k].type === 'statement_block');
911
+ : named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
488
912
  const handler = isPython
489
913
  ? kids.of(i).find((k) => ast.nodes[k].type === 'except_clause')
490
914
  : named.find((k) => ast.nodes[k].type === 'catch_clause');
@@ -538,6 +962,12 @@ function cfgOf(ast, fn, source) {
538
962
  else {
539
963
  exits.push(...after);
540
964
  }
965
+ const resSpec = kids.of(i).find((k) => ast.nodes[k].type === 'resource_specification');
966
+ if (resSpec !== undefined) {
967
+ const rp = newPoint(resSpec);
968
+ link(rp, bf.entry, 'seq');
969
+ return { entry: rp, exits };
970
+ }
541
971
  return { entry: bf.entry, exits };
542
972
  }
543
973
  default:
@@ -569,7 +999,7 @@ function cfgOf(ast, fn, source) {
569
999
  }
570
1000
  return undefined;
571
1001
  };
572
- const ctx = { breakT: [], contT: [], catchT: [], pendingLabel: null };
1002
+ const ctx = { breakT: [], contT: [], catchT: [], yieldT: [], pendingLabel: null };
573
1003
  const bodyNode = ast.nodes[fn.bodyIndex];
574
1004
  const flow = bodyNode.type === rules.block
575
1005
  ? lowerSeq(stmtChildren(fn.bodyIndex), ctx)
@@ -17,7 +17,9 @@ function defUseOf(ast, cfg, fn, source) {
17
17
  const isPython = ast.lang === 'python';
18
18
  const isGo = ast.lang === 'go';
19
19
  const params = new Map();
20
- const bareArrow = ast.nodes[fn.nodeIndex].type === 'arrow_function'
20
+ const bareArrow = (ast.nodes[fn.nodeIndex].type === 'arrow_function'
21
+ // @implements A-SPEC-520.1 — Java's single-parameter lambda (`z -> …`) has the same shape.
22
+ || ast.nodes[fn.nodeIndex].type === 'lambda_expression')
21
23
  && !kids.of(fn.nodeIndex).some((k) => ast.nodes[k].type === 'formal_parameters');
22
24
  for (const k of kids.of(fn.nodeIndex)) {
23
25
  // Only formal_parameters — a declaration's NAME identifier is not a parameter (measured: the
@@ -27,7 +29,9 @@ function defUseOf(ast, cfg, fn, source) {
27
29
  // times on one declaration: receiver, parameters, NAMED RESULTS. All of them bind names at
28
30
  // ENTRY, so Go must not break after the first (the probe pinned the second-list shape).
29
31
  const isParamList = ast.nodes[k].type === 'formal_parameters' || ast.nodes[k].type === 'parameters'
30
- || ast.nodes[k].type === 'parameter_list';
32
+ || ast.nodes[k].type === 'parameter_list'
33
+ // @implements A-SPEC-522.1 — Rust: fn parameters and closure |x| parameters.
34
+ || ast.nodes[k].type === 'parameters' || ast.nodes[k].type === 'closure_parameters';
31
35
  if (!isParamList && !(bareArrow && ast.nodes[k].type === 'identifier'))
32
36
  continue;
33
37
  const collect = (n) => {
@@ -55,6 +59,21 @@ function defUseOf(ast, cfg, fn, source) {
55
59
  for (const b of cfg.blocks)
56
60
  for (const s of b.stmts)
57
61
  stmtSet.add(s);
62
+ // @implements A-SPEC-522.1 — walk a Rust PATTERN: lowercase-initial identifiers bind (defs),
63
+ // capitalized ones are constructors/types (skipped). Everything else recurses.
64
+ const walkRustPattern = (n, stmt) => {
65
+ const ty = ast.nodes[n].type;
66
+ if (ty === 'identifier') {
67
+ const t = text(n);
68
+ if (/^[a-z_]/.test(t) && t !== '_')
69
+ add(defs, stmt, t);
70
+ return;
71
+ }
72
+ if (ty.includes('literal') || ty === 'type_identifier')
73
+ return;
74
+ for (const c of kids.of(n))
75
+ walkRustPattern(c, stmt);
76
+ };
58
77
  const classify = (stmt) => {
59
78
  const walk = (n, defCtx) => {
60
79
  const ty = ast.nodes[n].type;
@@ -62,7 +81,7 @@ function defUseOf(ast, cfg, fn, source) {
62
81
  return; // nested function boundary
63
82
  if (n !== stmt && stmtSet.has(n))
64
83
  return; // nested statements are their own rows
65
- if (ty === 'type_annotation' || ty === 'type_arguments' || ty === 'comment')
84
+ if (ty === 'type_annotation' || ty === 'type_arguments' || ty.includes('comment'))
66
85
  return;
67
86
  if (ty === 'identifier' || ty === 'shorthand_property_identifier_pattern') {
68
87
  if (defCtx)
@@ -78,6 +97,105 @@ function defUseOf(ast, cfg, fn, source) {
78
97
  if (ty === 'field_identifier' || ty === 'package_identifier' || ty === 'label_name')
79
98
  return;
80
99
  switch (ty) {
100
+ // @implements A-SPEC-522.1 — Rust's binding shapes. Pattern identifiers bind by the
101
+ // LOWERCASE-INITIAL convention (rustc warns on violations): `Some(x)` binds x, skips the
102
+ // constructor Some — a documented heuristic, not a guess about arbitrary code.
103
+ case 'let_declaration': {
104
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
105
+ if (named[0] !== undefined)
106
+ walkRustPattern(named[0], stmt);
107
+ for (const r of named.slice(1))
108
+ walk(r, false);
109
+ return;
110
+ }
111
+ case 'compound_assignment_expr': {
112
+ const [lhs, ...rest] = kids.of(n).filter((c) => ast.nodes[c].named);
113
+ if (lhs !== undefined && ast.nodes[lhs].type === 'identifier') {
114
+ add(defs, stmt, text(lhs));
115
+ add(uses, stmt, text(lhs));
116
+ }
117
+ else if (lhs !== undefined)
118
+ walk(lhs, false);
119
+ for (const r of rest)
120
+ walk(r, false);
121
+ return;
122
+ }
123
+ case 'for_expression': {
124
+ // `for PAT in ITER { .. }` — the pattern binds fresh; iterable is a use.
125
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
126
+ if (named[0] !== undefined)
127
+ walkRustPattern(named[0], stmt);
128
+ for (const r of named.slice(1))
129
+ walk(r, false);
130
+ return;
131
+ }
132
+ case 'let_condition': {
133
+ // `while let Some(x) = e` / `if let ...` — pattern binds at the header point.
134
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
135
+ if (named[0] !== undefined)
136
+ walkRustPattern(named[0], stmt);
137
+ for (const r of named.slice(1))
138
+ walk(r, false);
139
+ return;
140
+ }
141
+ case 'scoped_identifier': {
142
+ // a::b::c — path segments are module/type names, not local reads.
143
+ return;
144
+ }
145
+ // @implements A-SPEC-520.1 — Java's shapes. A field's name and a method's name are plain
146
+ // `identifier` nodes in this grammar, so an unguarded walk would read `this.x = x` as TWO
147
+ // uses of x and every call as a use of its method name — with a same-named local, that is
148
+ // a fabricated reaching edge, not just noise.
149
+ // @implements A-SPEC-521.1 — C#: the LAST named child of a member access is the member's
150
+ // NAME (`this` is unnamed, so `this.x` has exactly one — the name). Walking it would read
151
+ // `this.x = x` as two uses of x. Delegates held in FIELDS lose their use through this
152
+ // (declared); a bare local delegate call (`a()`) keeps its use — the callee is walked.
153
+ case 'member_access_expression': {
154
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
155
+ for (let ix = 0; ix < named.length - 1; ix++)
156
+ walk(named[ix], false);
157
+ return;
158
+ }
159
+ case 'field_access': {
160
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
161
+ if (named.length > 0)
162
+ walk(named[0], false); // the object; the field name is skipped
163
+ return;
164
+ }
165
+ case 'method_invocation': {
166
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
167
+ const argsAt = named.findIndex((c) => ast.nodes[c].type === 'argument_list');
168
+ named.forEach((c, ix) => {
169
+ if (ix === (argsAt > 0 ? argsAt - 1 : -1) && ast.nodes[c].type === 'identifier')
170
+ return; // the name
171
+ if (ast.nodes[c].type === 'type_arguments')
172
+ return;
173
+ walk(c, false);
174
+ });
175
+ return;
176
+ }
177
+ case 'enhanced_for_statement':
178
+ case 'foreach_statement': {
179
+ // `for (T s : xs)` — the binding is a def; the iterable and body-side reads are uses.
180
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
181
+ for (const c of named) {
182
+ if (ast.nodes[c].type === 'identifier')
183
+ add(defs, stmt, text(c));
184
+ else
185
+ walk(c, false);
186
+ }
187
+ return;
188
+ }
189
+ case 'resource': {
190
+ // try-with-resources: `var r = open()` — r defs at the resource point, the init is a use.
191
+ for (const c of kids.of(n)) {
192
+ if (ast.nodes[c].type === 'identifier')
193
+ add(defs, stmt, text(c));
194
+ else
195
+ walk(c, false);
196
+ }
197
+ return;
198
+ }
81
199
  // @implements A-SPEC-519.1 — Go's binding shapes, written from the probed grammar.
82
200
  case 'short_var_declaration': {
83
201
  // `a, b := e1, e2` — every LHS identifier is a def; the RHS list is uses.
@@ -285,13 +403,69 @@ function defUseOf(ast, cfg, fn, source) {
285
403
  };
286
404
  for (const s of stmtSet)
287
405
  classify(s);
406
+ // @implements A-SPEC-522.1 — Rust match patterns bind at the MATCH HEADER point (the same
407
+ // posture as C#'s declaration pattern, for the same measured reason: a def on the point that
408
+ // uses it can never reach). Guards (`n if g(n)`) read at the header too.
409
+ if (ast.lang === 'rust') {
410
+ ast.nodes.forEach((n, i) => {
411
+ if (n.type !== 'match_expression' || !stmtSet.has(i))
412
+ return;
413
+ const mb = kids.of(i).find((k) => ast.nodes[k].type === 'match_block');
414
+ for (const arm of mb === undefined ? [] : kids.of(mb)) {
415
+ if (ast.nodes[arm].type !== 'match_arm')
416
+ continue;
417
+ const pat = kids.of(arm).find((k) => ast.nodes[k].type === 'match_pattern');
418
+ if (pat !== undefined) {
419
+ walkRustPattern(pat, i);
420
+ // a guard inside the pattern (`n if g(n)`) reads its names at dispatch time
421
+ for (const g of kids.of(pat)) {
422
+ if (ast.nodes[g].type !== 'match_pattern' && ast.nodes[g].named) {
423
+ const collect = (m) => {
424
+ if (ast.nodes[m].type === 'identifier' && /^[a-z_]/.test(text(m)))
425
+ add(uses, i, text(m));
426
+ for (const c of kids.of(m))
427
+ collect(c);
428
+ };
429
+ collect(g);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ });
435
+ }
436
+ // @implements A-SPEC-521.1 — a C# declaration pattern (`case string f:`) binds at the SWITCH
437
+ // HEADER point, the same posture as Go's type-switch binding. Attaching it to the section's
438
+ // first statement was the first design and measurably wrong: reaching definitions read a use
439
+ // against the statement's IN set, so a def on the same point can never reach `case string f:
440
+ // Use(f);` — and that first statement usually IS the use. (Design corrected mid-slice, re-sealed.)
441
+ ast.nodes.forEach((n, i) => {
442
+ if (n.type !== 'switch_section')
443
+ return;
444
+ const pat = kids.of(i).find((k) => ast.nodes[k].type === 'declaration_pattern');
445
+ if (pat === undefined)
446
+ return;
447
+ const bind = kids.of(pat).filter((k) => ast.nodes[k].named).at(-1);
448
+ if (bind === undefined || ast.nodes[bind].type !== 'identifier')
449
+ return;
450
+ let sw = ast.nodes[i].parent; // switch_body
451
+ while (sw >= 0 && ast.nodes[sw].type !== 'switch_statement')
452
+ sw = ast.nodes[sw].parent;
453
+ if (sw >= 0 && stmtSet.has(sw))
454
+ add(defs, sw, text(bind));
455
+ });
288
456
  // catch parameters: a def at the head of the handler body's first statement.
289
457
  ast.nodes.forEach((n, i) => {
290
458
  if (n.type !== 'catch_clause')
291
459
  return;
292
460
  const named = kids.of(i).filter((k) => ast.nodes[k].named);
293
- const param = named.find((k) => ast.nodes[k].type === 'identifier');
294
- const block = named.find((k) => ast.nodes[k].type === 'statement_block');
461
+ // @implements A-SPEC-520.1 Java wraps the parameter in catch_formal_parameter; TS puts the
462
+ // identifier directly under the clause. One lookup serves both.
463
+ const formal = named.find((k) => ast.nodes[k].type === 'catch_formal_parameter');
464
+ const param = named.find((k) => ast.nodes[k].type === 'identifier')
465
+ ?? (formal !== undefined
466
+ ? kids.of(formal).find((k) => ast.nodes[k].type === 'identifier')
467
+ : undefined);
468
+ const block = named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
295
469
  if (param === undefined || block === undefined)
296
470
  return;
297
471
  const first = kids.of(block).find((k) => stmtSet.has(k));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",