gitnexus 1.6.8-rc.11 → 1.6.8-rc.13

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.
Files changed (54) hide show
  1. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  2. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  3. package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
  4. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  5. package/dist/cli/analyze-config.js +1 -0
  6. package/dist/cli/analyze.d.ts +6 -0
  7. package/dist/cli/analyze.js +2 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/core/ingestion/cfg/cfg-builder.d.ts +51 -0
  10. package/dist/core/ingestion/cfg/cfg-builder.js +100 -0
  11. package/dist/core/ingestion/cfg/collect.d.ts +30 -0
  12. package/dist/core/ingestion/cfg/collect.js +34 -0
  13. package/dist/core/ingestion/cfg/control-flow-context.d.ts +27 -0
  14. package/dist/core/ingestion/cfg/control-flow-context.js +49 -0
  15. package/dist/core/ingestion/cfg/emit.d.ts +64 -0
  16. package/dist/core/ingestion/cfg/emit.js +95 -0
  17. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  18. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  19. package/dist/core/ingestion/cfg/types.d.ts +66 -0
  20. package/dist/core/ingestion/cfg/types.js +13 -0
  21. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +49 -0
  22. package/dist/core/ingestion/cfg/visitors/typescript.js +492 -0
  23. package/dist/core/ingestion/language-provider.d.ts +11 -0
  24. package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
  25. package/dist/core/ingestion/languages/cpp/captures.js +24 -1
  26. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  27. package/dist/core/ingestion/languages/typescript.js +5 -0
  28. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  29. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  30. package/dist/core/ingestion/method-types.d.ts +2 -0
  31. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  32. package/dist/core/ingestion/model/symbol-table.js +1 -0
  33. package/dist/core/ingestion/parsing-processor.js +1 -0
  34. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  35. package/dist/core/ingestion/pipeline.d.ts +23 -0
  36. package/dist/core/ingestion/scope-extractor.js +3 -0
  37. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
  38. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
  39. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -0
  40. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  41. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
  42. package/dist/core/ingestion/scope-resolution/pipeline/run.js +62 -0
  43. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  44. package/dist/core/ingestion/utils/method-props.js +1 -0
  45. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
  46. package/dist/core/ingestion/workers/parse-worker.js +43 -1
  47. package/dist/core/ingestion/workers/worker-pool.d.ts +9 -0
  48. package/dist/core/ingestion/workers/worker-pool.js +5 -2
  49. package/dist/core/run-analyze.d.ts +32 -0
  50. package/dist/core/run-analyze.js +75 -1
  51. package/dist/storage/parse-cache.d.ts +22 -1
  52. package/dist/storage/parse-cache.js +20 -10
  53. package/dist/storage/repo-manager.d.ts +31 -7
  54. package/package.json +1 -1
@@ -96,5 +96,26 @@ export interface ParsedFile {
96
96
  * side effects — the contract default) leave this undefined.
97
97
  */
98
98
  readonly captureSideChannel?: unknown;
99
+ /**
100
+ * Per-function control-flow graphs for this file (#2081 M1, PDG/taint
101
+ * substrate). A DISTINCT field from {@link captureSideChannel} — different
102
+ * producer, consumer, and lifecycle: the worker builds it from the
103
+ * tree-sitter AST via `LanguageProvider.cfgVisitor` (only on a `--pdg` run),
104
+ * and scope-resolution emits BasicBlock nodes + CFG edges from it while the
105
+ * disk-backed ParsedFile store is still live (it is NOT a capture-time
106
+ * marker the resolver restores into module maps). Kept separate so a future
107
+ * change to either channel's shape invalidates independently.
108
+ *
109
+ * Shared / ingestion code treats this as opaque (`unknown`) per AGENTS.md.
110
+ * Concretely it is a `readonly FunctionCfg[]` (see
111
+ * `core/ingestion/cfg/types.ts`) — plain JSON-serializable data (no AST
112
+ * refs, no class instances) so it round-trips through the parse cache and
113
+ * the `parsedfile-store` (whose interning reviver keys on `nodeId`, which
114
+ * these blocks/edges deliberately lack).
115
+ *
116
+ * Optional: `undefined` on non-`--pdg` runs and for languages with no
117
+ * `cfgVisitor` — the default for every run today.
118
+ */
119
+ readonly cfgSideChannel?: unknown;
99
120
  }
100
121
  //# sourceMappingURL=parsed-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parsed-file.d.ts","sourceRoot":"","sources":["../../src/scope-resolution/parsed-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,cAAc,EAAE,SAAS,aAAa,EAAE,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CACvC"}
1
+ {"version":3,"file":"parsed-file.d.ts","sourceRoot":"","sources":["../../src/scope-resolution/parsed-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,cAAc,EAAE,SAAS,aAAa,EAAE,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAEtC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC"}
@@ -54,6 +54,10 @@ export interface SymbolDefinition {
54
54
  * Currently used by C++ overload ranking to exclude explicit constructors
55
55
  * from implicit user-defined conversion candidates. */
56
56
  isExplicit?: boolean;
57
+ /** True when the callable is declared unavailable (for example C++ `= delete`).
58
+ * Unavailable callables still participate in overload selection, but a
59
+ * selected unavailable target must suppress edge emission. */
60
+ isDeleted?: boolean;
57
61
  /** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
58
62
  ownerId?: string;
59
63
  /** #1982/#1993: bridge-held enclosing-namespace path (e.g. `NS1`, `Outer.Inner`)
@@ -1 +1 @@
1
- {"version":3,"file":"symbol-definition.d.ts","sourceRoot":"","sources":["../../src/scope-resolution/symbol-definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACjE,4CAA4C;IAC5C,WAAW,EAAE,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;IAC3E,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB;;+FAE2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;iHAC6G;IAC7G,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;uFACmF;IACnF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;4FACwF;IACxF,oBAAoB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC5C,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;yEAKqE;IACrE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;4DAEwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;iEAO6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B"}
1
+ {"version":3,"file":"symbol-definition.d.ts","sourceRoot":"","sources":["../../src/scope-resolution/symbol-definition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,EAAE,EAAE,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACjE,4CAA4C;IAC5C,WAAW,EAAE,OAAO,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;IAC3E,4EAA4E;IAC5E,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB;;+FAE2F;IAC3F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;iHAC6G;IAC7G,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;uFACmF;IACnF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;4FACwF;IACxF,oBAAoB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC5C,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gGAAgG;IAChG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B;;;;;yEAKqE;IACrE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;4DAEwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;mEAE+D;IAC/D,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;iEAO6D;IAC7D,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B"}
@@ -63,6 +63,7 @@ const KEY_SPECS = {
63
63
  skipContextFiles: { target: 'skipAgentsMd', kind: 'boolean' },
64
64
  skipAiContext: { target: 'skipAgentsMd', kind: 'boolean' },
65
65
  skipSkills: { target: 'skipSkills', kind: 'boolean' },
66
+ pdg: { target: 'pdg', kind: 'boolean' },
66
67
  indexOnly: { target: 'indexOnly', kind: 'boolean' },
67
68
  stats: { target: 'stats', kind: 'boolean' },
68
69
  noStats: { target: 'stats', kind: 'boolean-negate' },
@@ -37,6 +37,12 @@ export interface AnalyzeOptions {
37
37
  verbose?: boolean;
38
38
  /** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */
39
39
  skipAgentsMd?: boolean;
40
+ /**
41
+ * Build the control-flow-graph / PDG substrate (#2081 M1). Opt-in; off by
42
+ * default. Threaded to both the worker (CFG build) and scope-resolution
43
+ * (BasicBlock/CFG emit).
44
+ */
45
+ pdg?: boolean;
40
46
  /**
41
47
  * Stats inclusion in AGENTS.md and CLAUDE.md.
42
48
  *
@@ -841,6 +841,8 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
841
841
  skipGit: options.skipGit,
842
842
  skipAgentsMd,
843
843
  skipSkills,
844
+ // CFG/PDG substrate opt-in (#2081 M1) — threaded to both sinks downstream.
845
+ pdg: options.pdg === true,
844
846
  // Resolved default branch (CLI > .gitnexusrc > auto-detect > "main")
845
847
  // threaded into the generated regression-compare example (#243).
846
848
  defaultBranch: resolvedDefaultBranch,
package/dist/cli/index.js CHANGED
@@ -32,6 +32,8 @@ program
32
32
  .option('--skills', 'Generate repo-specific skill files from detected communities ' +
33
33
  '(no-op when --index-only is also set).')
34
34
  .option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
35
+ .option('--pdg', 'Build the control-flow-graph / PDG substrate (BasicBlock nodes + CFG edges) ' +
36
+ 'for supported languages. Opt-in; off by default. (#2081 M1)')
35
37
  .option('--default-branch <branch>', 'Default branch used in the generated regression-compare example (base_ref). ' +
36
38
  'Falls back to .gitnexusrc, then auto-detected origin/HEAD, then "main".')
37
39
  .option('--branch <name>', 'Index the working tree under a specific branch slot (multi-branch indexing). ' +
@@ -0,0 +1,51 @@
1
+ /**
2
+ * CfgBuilder (issue #2081, M1) — the language-agnostic accumulator.
3
+ *
4
+ * A per-language `CfgVisitor` drives this: it creates blocks as it walks
5
+ * statements, wires edges (including back-edges and break/continue/return/throw
6
+ * targets resolved via {@link ControlFlowContext}), and calls {@link finish} to
7
+ * produce the serializable {@link FunctionCfg}. The builder owns the synthetic
8
+ * ENTRY (index 0) and EXIT blocks and de-duplicates identical edges so repeated
9
+ * `connect` calls (common when wiring a set of dangling exits) stay idempotent.
10
+ *
11
+ * It has no knowledge of any AST — it is exercised directly in unit tests with
12
+ * hand-built block sequences, which is how the classic CFG hazards are pinned
13
+ * before the tree-sitter visitor (U2) drives it.
14
+ */
15
+ import type { BasicBlockData, CfgEdgeKind, FunctionCfg } from './types.js';
16
+ export declare class CfgBuilder {
17
+ private readonly filePath;
18
+ private readonly functionStartLine;
19
+ private readonly functionEndLine;
20
+ /** Start column of the owning function — disambiguates same-line functions
21
+ * in the BasicBlock ids (see {@link FunctionCfg.functionStartColumn}).
22
+ * Defaults to 0 for hand-built test CFGs that don't model columns. */
23
+ private readonly functionStartColumn;
24
+ private readonly blocks;
25
+ private readonly edges;
26
+ private readonly edgeKeys;
27
+ readonly entryIndex: number;
28
+ readonly exitIndex: number;
29
+ constructor(filePath: string, functionStartLine: number, functionEndLine: number,
30
+ /** Start column of the owning function — disambiguates same-line functions
31
+ * in the BasicBlock ids (see {@link FunctionCfg.functionStartColumn}).
32
+ * Defaults to 0 for hand-built test CFGs that don't model columns. */
33
+ functionStartColumn?: number);
34
+ /** Create a block and return its index. */
35
+ newBlock(startLine: number, endLine: number, text: string, kind?: BasicBlockData['kind']): number;
36
+ /** Add a single edge (idempotent on from+to+kind). */
37
+ edge(from: number, to: number, kind: CfgEdgeKind): void;
38
+ /** Wire a set of dangling exits to a single target block with one kind. */
39
+ connect(exits: readonly number[], to: number, kind?: CfgEdgeKind): void;
40
+ /** Extend a block's end line as more statements accrue to it. */
41
+ extendBlock(index: number, endLine: number, appendText?: string): void;
42
+ get blockCount(): number;
43
+ /** Produce the serializable CFG. Caller is responsible for having wired the
44
+ * function's dangling exits to {@link exitIndex} before calling. */
45
+ finish(): FunctionCfg;
46
+ }
47
+ /**
48
+ * Block indices reachable from `entryIndex` by following edges. Backs the
49
+ * reachability property tests (R9) over hand-built and visitor-produced CFGs.
50
+ */
51
+ export declare const reachableBlocks: (cfg: FunctionCfg) => Set<number>;
@@ -0,0 +1,100 @@
1
+ export class CfgBuilder {
2
+ filePath;
3
+ functionStartLine;
4
+ functionEndLine;
5
+ functionStartColumn;
6
+ blocks = [];
7
+ edges = [];
8
+ edgeKeys = new Set();
9
+ entryIndex;
10
+ exitIndex;
11
+ constructor(filePath, functionStartLine, functionEndLine,
12
+ /** Start column of the owning function — disambiguates same-line functions
13
+ * in the BasicBlock ids (see {@link FunctionCfg.functionStartColumn}).
14
+ * Defaults to 0 for hand-built test CFGs that don't model columns. */
15
+ functionStartColumn = 0) {
16
+ this.filePath = filePath;
17
+ this.functionStartLine = functionStartLine;
18
+ this.functionEndLine = functionEndLine;
19
+ this.functionStartColumn = functionStartColumn;
20
+ this.entryIndex = this.newBlock(functionStartLine, functionStartLine, '', 'entry');
21
+ this.exitIndex = this.newBlock(functionEndLine, functionEndLine, '', 'exit');
22
+ }
23
+ /** Create a block and return its index. */
24
+ newBlock(startLine, endLine, text, kind = 'normal') {
25
+ this.blocks.push({ startLine, endLine, textParts: text ? [text] : [], kind });
26
+ return this.blocks.length - 1;
27
+ }
28
+ /** Add a single edge (idempotent on from+to+kind). */
29
+ edge(from, to, kind) {
30
+ const key = `${from}->${to}:${kind}`;
31
+ if (this.edgeKeys.has(key))
32
+ return;
33
+ this.edgeKeys.add(key);
34
+ this.edges.push({ from, to, kind });
35
+ }
36
+ /** Wire a set of dangling exits to a single target block with one kind. */
37
+ connect(exits, to, kind = 'seq') {
38
+ for (const from of exits)
39
+ this.edge(from, to, kind);
40
+ }
41
+ /** Extend a block's end line as more statements accrue to it. */
42
+ extendBlock(index, endLine, appendText) {
43
+ const b = this.blocks[index];
44
+ if (!b)
45
+ return;
46
+ if (endLine > b.endLine)
47
+ b.endLine = endLine;
48
+ if (appendText)
49
+ b.textParts.push(appendText);
50
+ }
51
+ get blockCount() {
52
+ return this.blocks.length;
53
+ }
54
+ /** Produce the serializable CFG. Caller is responsible for having wired the
55
+ * function's dangling exits to {@link exitIndex} before calling. */
56
+ finish() {
57
+ return {
58
+ filePath: this.filePath,
59
+ functionStartLine: this.functionStartLine,
60
+ functionEndLine: this.functionEndLine,
61
+ functionStartColumn: this.functionStartColumn,
62
+ entryIndex: this.entryIndex,
63
+ exitIndex: this.exitIndex,
64
+ blocks: this.blocks.map((b, index) => ({
65
+ index,
66
+ startLine: b.startLine,
67
+ endLine: b.endLine,
68
+ text: b.textParts.join('\n'),
69
+ kind: b.kind,
70
+ })),
71
+ edges: [...this.edges],
72
+ };
73
+ }
74
+ }
75
+ /**
76
+ * Block indices reachable from `entryIndex` by following edges. Backs the
77
+ * reachability property tests (R9) over hand-built and visitor-produced CFGs.
78
+ */
79
+ export const reachableBlocks = (cfg) => {
80
+ const adj = new Map();
81
+ for (const e of cfg.edges) {
82
+ const list = adj.get(e.from);
83
+ if (list)
84
+ list.push(e.to);
85
+ else
86
+ adj.set(e.from, [e.to]);
87
+ }
88
+ const seen = new Set([cfg.entryIndex]);
89
+ const stack = [cfg.entryIndex];
90
+ while (stack.length) {
91
+ const n = stack.pop();
92
+ for (const next of adj.get(n) ?? []) {
93
+ if (!seen.has(next)) {
94
+ seen.add(next);
95
+ stack.push(next);
96
+ }
97
+ }
98
+ }
99
+ return seen;
100
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * collectFunctionCfgs (issue #2081, M1).
3
+ *
4
+ * Walks a parsed file's tree-sitter tree and builds one {@link FunctionCfg} per
5
+ * CFG-bearing function via the language's {@link CfgVisitor}. Runs IN THE PARSE
6
+ * WORKER (where the AST lives — KTD1/KTD7); the result rides on
7
+ * `ParsedFile.cfgSideChannel` across the worker→main boundary.
8
+ *
9
+ * Nested functions are enumerated independently — each gets its own CFG, and
10
+ * appears as an opaque straight-line block in its enclosing function's CFG (the
11
+ * visitor does not descend into nested function bodies). `maxFunctionLines`
12
+ * bounds per-function cost: a function whose source span exceeds the cap is
13
+ * skipped (and counted) rather than walked, so a pathological mega-function
14
+ * cannot blow up worker time/memory. A cap of `0` means no limit.
15
+ */
16
+ import type { SyntaxNode } from '../utils/ast-helpers.js';
17
+ import type { CfgVisitor, FunctionCfg } from './types.js';
18
+ /**
19
+ * Default per-function source-line cap used by the worker when the `--pdg` run
20
+ * does not specify `pdgMaxFunctionLines`. A function longer than this (almost
21
+ * always minified/generated code) is skipped rather than walked — its CFG is
22
+ * both expensive and low-value. Overridable via `PipelineOptions.pdgMaxFunctionLines`.
23
+ */
24
+ export declare const DEFAULT_PDG_MAX_FUNCTION_LINES = 2000;
25
+ export interface CollectedCfgs {
26
+ readonly cfgs: readonly FunctionCfg[];
27
+ /** Functions skipped for exceeding `maxFunctionLines` (0 ⇒ none skipped). */
28
+ readonly skipped: number;
29
+ }
30
+ export declare function collectFunctionCfgs(root: SyntaxNode, visitor: CfgVisitor<SyntaxNode>, filePath: string, maxFunctionLines?: number): CollectedCfgs;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Default per-function source-line cap used by the worker when the `--pdg` run
3
+ * does not specify `pdgMaxFunctionLines`. A function longer than this (almost
4
+ * always minified/generated code) is skipped rather than walked — its CFG is
5
+ * both expensive and low-value. Overridable via `PipelineOptions.pdgMaxFunctionLines`.
6
+ */
7
+ export const DEFAULT_PDG_MAX_FUNCTION_LINES = 2000;
8
+ export function collectFunctionCfgs(root, visitor, filePath, maxFunctionLines = 0) {
9
+ const cfgs = [];
10
+ let skipped = 0;
11
+ const stack = [root];
12
+ while (stack.length) {
13
+ const node = stack.pop();
14
+ if (visitor.isFunction(node)) {
15
+ const lines = node.endPosition.row - node.startPosition.row + 1;
16
+ if (maxFunctionLines > 0 && lines > maxFunctionLines) {
17
+ skipped++;
18
+ }
19
+ else {
20
+ const cfg = visitor.buildFunctionCfg(node, filePath);
21
+ if (cfg)
22
+ cfgs.push(cfg);
23
+ }
24
+ }
25
+ // Descend regardless (a skipped mega-function may still contain small
26
+ // nested functions that are worth a CFG of their own).
27
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
28
+ const child = node.namedChild(i);
29
+ if (child)
30
+ stack.push(child);
31
+ }
32
+ }
33
+ return { cfgs, skipped };
34
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ControlFlowContext (issue #2081, M1).
3
+ *
4
+ * Resolves the targets of `break`/`continue` (plain and labeled) as the visitor
5
+ * descends through loops and switches. Loops and switches push a target frame
6
+ * on entry and pop it on exit; a labeled statement attaches its label to the
7
+ * frame of the construct it labels, so `break outer` / `continue outer` resolve
8
+ * against the right enclosing loop/switch rather than the nearest one.
9
+ */
10
+ export declare class ControlFlowContext {
11
+ private readonly stack;
12
+ pushLoop(continueTo: number, breakTo: number, label?: string): void;
13
+ pushSwitch(breakTo: number, label?: string): void;
14
+ pop(): void;
15
+ /**
16
+ * Target block for a `break`. With a label, the nearest enclosing frame
17
+ * carrying that label (loop or switch); without, the nearest frame of any
18
+ * kind. Returns `undefined` if there is no valid target (malformed input).
19
+ */
20
+ breakTarget(label?: string): number | undefined;
21
+ /**
22
+ * Target block for a `continue`. With a label, the nearest enclosing **loop**
23
+ * carrying that label; without, the nearest loop (switches are skipped — you
24
+ * cannot `continue` a switch). Returns `undefined` if there is no valid loop.
25
+ */
26
+ continueTarget(label?: string): number | undefined;
27
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * ControlFlowContext (issue #2081, M1).
3
+ *
4
+ * Resolves the targets of `break`/`continue` (plain and labeled) as the visitor
5
+ * descends through loops and switches. Loops and switches push a target frame
6
+ * on entry and pop it on exit; a labeled statement attaches its label to the
7
+ * frame of the construct it labels, so `break outer` / `continue outer` resolve
8
+ * against the right enclosing loop/switch rather than the nearest one.
9
+ */
10
+ export class ControlFlowContext {
11
+ stack = [];
12
+ pushLoop(continueTo, breakTo, label) {
13
+ this.stack.push({ kind: 'loop', continueTo, breakTo, label });
14
+ }
15
+ pushSwitch(breakTo, label) {
16
+ this.stack.push({ kind: 'switch', breakTo, label });
17
+ }
18
+ pop() {
19
+ this.stack.pop();
20
+ }
21
+ /**
22
+ * Target block for a `break`. With a label, the nearest enclosing frame
23
+ * carrying that label (loop or switch); without, the nearest frame of any
24
+ * kind. Returns `undefined` if there is no valid target (malformed input).
25
+ */
26
+ breakTarget(label) {
27
+ for (let i = this.stack.length - 1; i >= 0; i--) {
28
+ const f = this.stack[i];
29
+ if (label === undefined || f.label === label)
30
+ return f.breakTo;
31
+ }
32
+ return undefined;
33
+ }
34
+ /**
35
+ * Target block for a `continue`. With a label, the nearest enclosing **loop**
36
+ * carrying that label; without, the nearest loop (switches are skipped — you
37
+ * cannot `continue` a switch). Returns `undefined` if there is no valid loop.
38
+ */
39
+ continueTarget(label) {
40
+ for (let i = this.stack.length - 1; i >= 0; i--) {
41
+ const f = this.stack[i];
42
+ if (f.kind !== 'loop')
43
+ continue;
44
+ if (label === undefined || f.label === label)
45
+ return f.continueTo;
46
+ }
47
+ return undefined;
48
+ }
49
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * cfg/emit.ts (issue #2081, M1) — serialized side-channel → graph.
3
+ *
4
+ * Pure helper: given a file's per-function CFGs (off `ParsedFile.cfgSideChannel`,
5
+ * produced by the worker in U3), emit one persisted `BasicBlock` node per block
6
+ * and one `CFG` edge per edge into the {@link KnowledgeGraph}. Invoked from
7
+ * scope-resolution (run.ts Phase 4) while the disk-backed ParsedFile store is
8
+ * still live — the only window where the worker-built CFGs are loaded (KTD1/
9
+ * KTD5). Default (`--pdg` off) runs never call this, so the emitted graph stays
10
+ * byte-identical to a pre-#2081 run.
11
+ *
12
+ * BasicBlock id: `BasicBlock:<filePath>:<functionStartLine>:<functionStartColumn>:<blockIndex>`
13
+ * (KTD3). The function start line+column segments disambiguate blocks across
14
+ * multiple functions in one file — including same-line functions — since each
15
+ * function's block indices restart at 0; blocks carry no `name` (the
16
+ * BasicBlock table has no such column). The edge KIND
17
+ * (`seq`/`cond-true`/…) rides in the relationship `reason` — CFG edges are
18
+ * values of the single `CodeRelation` table's `type` column (`'CFG'`), so the
19
+ * kind cannot be its own edge type and is queried via `reason`.
20
+ */
21
+ import type { KnowledgeGraph } from '../../graph/types.js';
22
+ import type { FunctionCfg } from './types.js';
23
+ /**
24
+ * Default per-function CFG edge cap. A pathological generated function could
25
+ * otherwise emit an unbounded edge set; the cap bounds graph growth and is
26
+ * overridable via `--pdg` options. `0` (in options) means no cap (unlimited
27
+ * — see the `cap` mapping in {@link emitFileCfgs}); `undefined` means this
28
+ * default.
29
+ */
30
+ export declare const DEFAULT_MAX_CFG_EDGES_PER_FUNCTION = 5000;
31
+ export interface CfgEmitResult {
32
+ blocks: number;
33
+ edges: number;
34
+ /** Edges dropped because a function's edge count exceeded the cap. */
35
+ droppedEdges: number;
36
+ /** Number of functions that hit the cap. */
37
+ cappedFunctions: number;
38
+ }
39
+ /**
40
+ * Whether an untrusted `cfgSideChannel` element is safe to feed to
41
+ * {@link emitFileCfgs}. Deliberately NOT full FunctionCfg validation — it
42
+ * checks exactly the fields whose corruption is SILENT given emit's
43
+ * mechanics: {@link basicBlockId} string-templates every id-anchor value
44
+ * (filePath, function start line/column, block index, edge endpoints) and
45
+ * the graph's addNode/addRelationship are no-throw Map inserts. Unchecked,
46
+ * a missing anchor field cross-wires same-`undefined`-id blocks across
47
+ * functions (addNode is first-writer-wins), and an edge endpoint that
48
+ * matches no block index becomes a dangling `BasicBlock:…:<n>` edge that
49
+ * detonates much later at DB bulk-load instead of throwing here — so
50
+ * endpoints are checked for MEMBERSHIP in the block-index set, not just
51
+ * integer-ness. Lives in this module so the guard evolves with the id
52
+ * templating it defends (#2099 F4; M2 fields that join the id path must
53
+ * join this check).
54
+ */
55
+ export declare const isEmitSafeCfg: (cfg: FunctionCfg | undefined | null) => cfg is FunctionCfg;
56
+ /**
57
+ * Emit BasicBlock nodes + CFG edges for every function CFG in `cfgs`.
58
+ *
59
+ * `maxEdgesPerFunction` caps edges per function. On overflow we stop emitting
60
+ * that function's remaining edges and call `onWarn` naming the dropped count —
61
+ * no silent truncation (KTD6/R6). Block nodes are always fully emitted (their
62
+ * count is bounded by the function's statement count); only edges are capped.
63
+ */
64
+ export declare function emitFileCfgs(graph: KnowledgeGraph, cfgs: readonly FunctionCfg[], maxEdgesPerFunction?: number, onWarn?: (message: string) => void): CfgEmitResult;
@@ -0,0 +1,95 @@
1
+ import { generateId } from '../../../lib/utils.js';
2
+ /**
3
+ * Default per-function CFG edge cap. A pathological generated function could
4
+ * otherwise emit an unbounded edge set; the cap bounds graph growth and is
5
+ * overridable via `--pdg` options. `0` (in options) means no cap (unlimited
6
+ * — see the `cap` mapping in {@link emitFileCfgs}); `undefined` means this
7
+ * default.
8
+ */
9
+ export const DEFAULT_MAX_CFG_EDGES_PER_FUNCTION = 5000;
10
+ const basicBlockId = (filePath, functionStartLine, functionStartColumn, blockIndex) => `BasicBlock:${filePath}:${functionStartLine}:${functionStartColumn}:${blockIndex}`;
11
+ /**
12
+ * Whether an untrusted `cfgSideChannel` element is safe to feed to
13
+ * {@link emitFileCfgs}. Deliberately NOT full FunctionCfg validation — it
14
+ * checks exactly the fields whose corruption is SILENT given emit's
15
+ * mechanics: {@link basicBlockId} string-templates every id-anchor value
16
+ * (filePath, function start line/column, block index, edge endpoints) and
17
+ * the graph's addNode/addRelationship are no-throw Map inserts. Unchecked,
18
+ * a missing anchor field cross-wires same-`undefined`-id blocks across
19
+ * functions (addNode is first-writer-wins), and an edge endpoint that
20
+ * matches no block index becomes a dangling `BasicBlock:…:<n>` edge that
21
+ * detonates much later at DB bulk-load instead of throwing here — so
22
+ * endpoints are checked for MEMBERSHIP in the block-index set, not just
23
+ * integer-ness. Lives in this module so the guard evolves with the id
24
+ * templating it defends (#2099 F4; M2 fields that join the id path must
25
+ * join this check).
26
+ */
27
+ export const isEmitSafeCfg = (cfg) => {
28
+ if (typeof cfg?.filePath !== 'string' ||
29
+ !Number.isInteger(cfg.functionStartLine) ||
30
+ !Number.isInteger(cfg.functionStartColumn) ||
31
+ !Array.isArray(cfg.blocks) ||
32
+ !Array.isArray(cfg.edges)) {
33
+ return false;
34
+ }
35
+ const blockIndices = new Set();
36
+ for (const b of cfg.blocks) {
37
+ if (!Number.isInteger(b?.index))
38
+ return false;
39
+ blockIndices.add(b.index);
40
+ }
41
+ return cfg.edges.every((e) => blockIndices.has(e?.from) && blockIndices.has(e?.to));
42
+ };
43
+ /**
44
+ * Emit BasicBlock nodes + CFG edges for every function CFG in `cfgs`.
45
+ *
46
+ * `maxEdgesPerFunction` caps edges per function. On overflow we stop emitting
47
+ * that function's remaining edges and call `onWarn` naming the dropped count —
48
+ * no silent truncation (KTD6/R6). Block nodes are always fully emitted (their
49
+ * count is bounded by the function's statement count); only edges are capped.
50
+ */
51
+ export function emitFileCfgs(graph, cfgs, maxEdgesPerFunction = DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, onWarn) {
52
+ const result = { blocks: 0, edges: 0, droppedEdges: 0, cappedFunctions: 0 };
53
+ const cap = maxEdgesPerFunction > 0 ? maxEdgesPerFunction : Infinity;
54
+ for (const cfg of cfgs) {
55
+ const { filePath, functionStartLine, functionStartColumn } = cfg;
56
+ for (const b of cfg.blocks) {
57
+ graph.addNode({
58
+ id: basicBlockId(filePath, functionStartLine, functionStartColumn, b.index),
59
+ label: 'BasicBlock',
60
+ properties: {
61
+ name: '', // BasicBlock has no name column; identified by id + span
62
+ filePath,
63
+ startLine: b.startLine,
64
+ endLine: b.endLine,
65
+ text: b.text,
66
+ },
67
+ });
68
+ result.blocks++;
69
+ }
70
+ let emittedForFn = 0;
71
+ for (const e of cfg.edges) {
72
+ if (emittedForFn >= cap) {
73
+ const dropped = cfg.edges.length - emittedForFn;
74
+ result.droppedEdges += dropped;
75
+ result.cappedFunctions++;
76
+ onWarn?.(`[cfg] ${filePath}:${functionStartLine}: per-function CFG edge cap ` +
77
+ `(${maxEdgesPerFunction}) reached — dropped ${dropped} of ${cfg.edges.length} edges`);
78
+ break;
79
+ }
80
+ const sourceId = basicBlockId(filePath, functionStartLine, functionStartColumn, e.from);
81
+ const targetId = basicBlockId(filePath, functionStartLine, functionStartColumn, e.to);
82
+ graph.addRelationship({
83
+ id: generateId('CFG', `${sourceId}->${targetId}:${e.kind}`),
84
+ type: 'CFG',
85
+ sourceId,
86
+ targetId,
87
+ confidence: 1.0,
88
+ reason: e.kind, // CfgEdgeKind (seq/cond-true/loop-back/…) — queryable
89
+ });
90
+ result.edges++;
91
+ emittedForFn++;
92
+ }
93
+ }
94
+ return result;
95
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TraversalResult (issue #2081, M1).
3
+ *
4
+ * Visiting a statement (or a statement sequence) returns the block its control
5
+ * flow ENTERS through, plus the set of blocks whose **normal** control flows
6
+ * out the bottom (the "dangling exits") — to be wired to the entry of whatever
7
+ * comes next. Abnormal exits (return/break/continue/throw) are wired directly
8
+ * to their targets during the walk and are NOT part of `exits`.
9
+ *
10
+ * A statement that cannot fall through (e.g. ends in `return`/`throw`, or both
11
+ * branches of an `if` return) yields an empty `exits` array.
12
+ */
13
+ export interface TraversalResult {
14
+ /** Block index control enters this statement/sequence through. */
15
+ readonly entry: number;
16
+ /** Block indices whose normal control falls out the bottom (may be empty). */
17
+ readonly exits: readonly number[];
18
+ }
19
+ /** A sequence of statements that produced no blocks (e.g. an empty body). */
20
+ export declare const emptyTraversal: (entry: number) => TraversalResult;
@@ -0,0 +1,2 @@
1
+ /** A sequence of statements that produced no blocks (e.g. an empty body). */
2
+ export const emptyTraversal = (entry) => ({ entry, exits: [entry] });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * CFG data model — plain, JSON-serializable types (issue #2081, M1).
3
+ *
4
+ * These cross the worker→main boundary and the disk-backed/durable ParsedFile
5
+ * store, so they must contain NO tree-sitter AST references, class instances,
6
+ * or anything that does not survive `JSON.stringify` → `JSON.parse`. Block and
7
+ * edge endpoints are referenced by integer index within a function's CFG.
8
+ *
9
+ * The per-language `CfgVisitor` (built in the parse worker, where the AST
10
+ * lives — see the M1 plan KTD1/KTD7) produces a `FunctionCfg` per function; the
11
+ * array of them is what rides on `ParsedFile.cfgSideChannel`.
12
+ */
13
+ /** A basic block: a maximal straight-line run of statements between leaders. */
14
+ export interface BasicBlockData {
15
+ /** Block index within its function. The synthetic ENTRY is always 0. */
16
+ readonly index: number;
17
+ readonly startLine: number;
18
+ readonly endLine: number;
19
+ /** Source snippet for the block (empty for synthetic ENTRY/EXIT). */
20
+ readonly text: string;
21
+ readonly kind: 'entry' | 'exit' | 'normal';
22
+ }
23
+ /** Why one block flows to another — drives the `reason` on the emitted CFG edge. */
24
+ export type CfgEdgeKind = 'seq' | 'cond-true' | 'cond-false' | 'loop-back' | 'break' | 'continue' | 'return' | 'throw' | 'switch-case' | 'fallthrough';
25
+ export interface CfgEdgeData {
26
+ readonly from: number;
27
+ readonly to: number;
28
+ readonly kind: CfgEdgeKind;
29
+ }
30
+ /** One function's control-flow graph. `cfgSideChannel` is `readonly FunctionCfg[]`. */
31
+ export interface FunctionCfg {
32
+ readonly filePath: string;
33
+ /** Source span of the owning function — anchors the BasicBlock node ids. */
34
+ readonly functionStartLine: number;
35
+ readonly functionEndLine: number;
36
+ /**
37
+ * Start COLUMN of the owning function. Combined with `functionStartLine` it
38
+ * disambiguates the BasicBlock node ids when two functions share a start line
39
+ * — e.g. `{ a: () => x(), b: () => y() }`, where both arrows begin on the same
40
+ * line and each restarts its block indices at 0. Without the column the ids
41
+ * collide and the graph's first-writer-wins `addNode` silently drops the
42
+ * second function's blocks and cross-wires its edges.
43
+ */
44
+ readonly functionStartColumn: number;
45
+ readonly entryIndex: number;
46
+ readonly exitIndex: number;
47
+ readonly blocks: readonly BasicBlockData[];
48
+ readonly edges: readonly CfgEdgeData[];
49
+ }
50
+ /**
51
+ * Per-language CFG strategy. Invoked **in the parse worker** for each function
52
+ * node. `TNode` is the language's AST node type (tree-sitter `SyntaxNode` for
53
+ * TS/JS) — kept generic so this module stays AST-library-agnostic. Returns
54
+ * `undefined` when the node is not a CFG-bearing function (the caller skips it).
55
+ */
56
+ export interface CfgVisitor<TNode = unknown> {
57
+ buildFunctionCfg(fnNode: TNode, filePath: string): FunctionCfg | undefined;
58
+ /**
59
+ * Whether `node` is a CFG-bearing function this visitor handles. Lets the
60
+ * worker enumerate functions (and apply the per-function line budget) by a
61
+ * cheap node-type test, instead of attempting to build a CFG for every AST
62
+ * node. `buildFunctionCfg` still re-checks, so this is purely an optimization
63
+ * + the seam the line-budget hooks into.
64
+ */
65
+ isFunction(node: TNode): boolean;
66
+ }