@clear-capabilities/agentic-security-scanner 0.184.0 → 0.186.1

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 (228) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/agentic-security.js +111 -1
  3. package/dist/1128.index.js +912 -0
  4. package/dist/1665.index.js +828 -0
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/5752.index.js +466 -0
  8. package/dist/7405.index.js +1 -1
  9. package/dist/8158.index.js +1713 -0
  10. package/dist/8798.index.js +1 -1
  11. package/dist/9382.index.js +2 -2
  12. package/dist/9579.index.js +1 -1
  13. package/dist/agentic-security.mjs +29 -29
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/dist/{dad58d1113a12c23a142.json → ca62ee93079be4c24eee.json} +29 -12
  16. package/dist/frontend/src/shell.js +12 -2
  17. package/package.json +29 -12
  18. package/src/dataflow/CLAUDE.md +46 -1
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/backward.js +42 -7
  21. package/src/dataflow/cross-service-taint.js +83 -1
  22. package/src/dataflow/demand-query.js +1168 -0
  23. package/src/dataflow/dep-fingerprint.js +718 -0
  24. package/src/dataflow/engine.js +48 -6
  25. package/src/dataflow/exploit-prover.js +6 -2
  26. package/src/dataflow/ifds-precise.js +22 -0
  27. package/src/dataflow/incremental-replay.js +118 -0
  28. package/src/dataflow/incremental-session.js +257 -0
  29. package/src/dataflow/incremental.js +237 -35
  30. package/src/dataflow/index.js +25 -94
  31. package/src/dataflow/ir-cache.js +268 -0
  32. package/src/dataflow/privacy-taint.js +7 -0
  33. package/src/dataflow/summaries.js +72 -6
  34. package/src/dataflow/tabulation.js +80 -24
  35. package/src/engine.js +113 -8
  36. package/src/graph/CLAUDE.md +31 -0
  37. package/src/graph/adapters/artifact.js +81 -0
  38. package/src/graph/adapters/crossrepo.js +75 -0
  39. package/src/graph/adapters/dataflow.js +76 -0
  40. package/src/graph/adapters/provenance.js +92 -0
  41. package/src/graph/adapters/semantic.js +89 -0
  42. package/src/graph/crosswalk-builders.js +152 -0
  43. package/src/graph/crosswalk.js +146 -0
  44. package/src/graph/index.js +15 -0
  45. package/src/graph/observation.js +57 -0
  46. package/src/graph/refs.js +105 -0
  47. package/src/graph/registry.js +129 -0
  48. package/src/graph/relations.js +81 -0
  49. package/src/graph/scc.js +241 -0
  50. package/src/graph/view.js +254 -0
  51. package/src/ir/CLAUDE.md +4 -0
  52. package/src/ir/callgraph.js +36 -8
  53. package/src/ir/index.js +18 -4
  54. package/src/ir/parser-js.js +10 -0
  55. package/src/language/CLAUDE.md +13 -0
  56. package/src/language/nix-option-types.js +170 -0
  57. package/src/language/nix-script-taint.js +46 -3
  58. package/src/language/nix-secrets.js +20 -2
  59. package/src/lineage/CLAUDE.md +72 -8
  60. package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
  61. package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
  62. package/src/lineage/boundary-contract.js +273 -0
  63. package/src/lineage/completion.js +205 -0
  64. package/src/lineage/continuation.js +140 -0
  65. package/src/lineage/control-credit.js +313 -0
  66. package/src/lineage/correlation.js +199 -0
  67. package/src/lineage/coverage.js +8 -1
  68. package/src/lineage/cross-repo-link.js +22 -0
  69. package/src/lineage/cut-planner.js +548 -0
  70. package/src/lineage/driver.js +219 -66
  71. package/src/lineage/engine.js +166 -21
  72. package/src/lineage/export-json.js +5 -0
  73. package/src/lineage/graph-builder.js +84 -7
  74. package/src/lineage/haskell-view.js +59 -0
  75. package/src/lineage/index.js +3 -0
  76. package/src/lineage/nix-view.js +35 -0
  77. package/src/lineage/owned-build.js +77 -0
  78. package/src/lineage/owned-worker-entry.js +22 -0
  79. package/src/lineage/owned-worker.js +95 -0
  80. package/src/lineage/path-evidence.js +123 -0
  81. package/src/lineage/path-predicates.js +529 -0
  82. package/src/lineage/path-query.js +189 -20
  83. package/src/lineage/path-store.js +191 -17
  84. package/src/lineage/scc-solver.js +186 -0
  85. package/src/lineage/smt-orchestrator.js +174 -0
  86. package/src/lineage/smt-z3-backend.js +294 -0
  87. package/src/lineage/source-seeding.js +36 -2
  88. package/src/lineage/summaries.js +66 -4
  89. package/src/lineage/value-versions.js +130 -0
  90. package/src/lineage/witness-selection.js +95 -0
  91. package/src/pipeline/scan-health.js +18 -0
  92. package/src/posture/CLAUDE.md +17 -0
  93. package/src/posture/business-logic.js +16 -7
  94. package/src/posture/dag-commands.js +442 -0
  95. package/src/posture/evidence-bundle.js +1 -1
  96. package/src/posture/finding-defaults.js +19 -0
  97. package/src/posture/git-history.js +91 -1
  98. package/src/posture/witness-bundle.js +573 -0
  99. package/src/posture/witness-runtime.js +276 -0
  100. package/src/report/index.js +2 -0
  101. package/src/sast/CLAUDE.md +2 -0
  102. package/src/sast/_call-args.js +70 -0
  103. package/src/sast/init-scope.js +87 -0
  104. package/src/sast/listen-bind-address.js +49 -0
  105. package/src/sast/redirect-target.js +106 -0
  106. package/src/sast/reflected-input.js +76 -0
  107. package/src/sast/send-file-root.js +27 -0
  108. package/src/sast/sensitive-data-logging.js +38 -1
  109. package/dist/119.index.js +0 -197
  110. package/dist/128.index.js +0 -129
  111. package/dist/1293.index.js +0 -5689
  112. package/dist/1336.index.js +0 -16182
  113. package/dist/1351.index.js +0 -133
  114. package/dist/1387.index.js +0 -271
  115. package/dist/1437.index.js +0 -23
  116. package/dist/1445.index.js +0 -110
  117. package/dist/1505.index.js +0 -151
  118. package/dist/1557.index.js +0 -293
  119. package/dist/1621.index.js +0 -311
  120. package/dist/1751.index.js +0 -160
  121. package/dist/1765.index.js +0 -727
  122. package/dist/1861.index.js +0 -690
  123. package/dist/1906.index.js +0 -433
  124. package/dist/208.index.js +0 -1631
  125. package/dist/2223.index.js +0 -159
  126. package/dist/2238.index.js +0 -295
  127. package/dist/2530.index.js +0 -152
  128. package/dist/2565.index.js +0 -116
  129. package/dist/2574.index.js +0 -127
  130. package/dist/2620.index.js +0 -345
  131. package/dist/2773.index.js +0 -98
  132. package/dist/2775.index.js +0 -32
  133. package/dist/2966.index.js +0 -171
  134. package/dist/2972.index.js +0 -87
  135. package/dist/2991.index.js +0 -131
  136. package/dist/3063.index.js +0 -833
  137. package/dist/3075.index.js +0 -227
  138. package/dist/3121.index.js +0 -297
  139. package/dist/3314.index.js +0 -263
  140. package/dist/3480.index.js +0 -1822
  141. package/dist/3481.index.js +0 -24
  142. package/dist/3506.index.js +0 -268
  143. package/dist/3533.index.js +0 -591
  144. package/dist/3653.index.js +0 -450
  145. package/dist/3740.index.js +0 -722
  146. package/dist/3822.index.js +0 -131
  147. package/dist/3868.index.js +0 -292
  148. package/dist/3918.index.js +0 -192
  149. package/dist/4090.index.js +0 -288
  150. package/dist/4113.index.js +0 -1286
  151. package/dist/4121.index.js +0 -1190
  152. package/dist/4142.index.js +0 -690
  153. package/dist/4168.index.js +0 -509
  154. package/dist/4281.index.js +0 -993
  155. package/dist/4336.index.js +0 -436
  156. package/dist/4387.index.js +0 -532
  157. package/dist/4413.index.js +0 -302
  158. package/dist/4476.index.js +0 -4510
  159. package/dist/4491.index.js +0 -133
  160. package/dist/4538.index.js +0 -185
  161. package/dist/4684.index.js +0 -319
  162. package/dist/4740.index.js +0 -165
  163. package/dist/4780.index.js +0 -209
  164. package/dist/4802.index.js +0 -225
  165. package/dist/4812.index.js +0 -379
  166. package/dist/4835.index.js +0 -126
  167. package/dist/494.index.js +0 -226
  168. package/dist/4991.index.js +0 -549
  169. package/dist/5199.index.js +0 -305
  170. package/dist/5216.index.js +0 -109
  171. package/dist/5439.index.js +0 -301
  172. package/dist/5443.index.js +0 -97
  173. package/dist/5496.index.js +0 -377
  174. package/dist/5738.index.js +0 -240
  175. package/dist/5801.index.js +0 -588
  176. package/dist/5817.index.js +0 -714
  177. package/dist/5858.index.js +0 -223
  178. package/dist/5933.index.js +0 -143
  179. package/dist/601.index.js +0 -23
  180. package/dist/6173.index.js +0 -273
  181. package/dist/6291.index.js +0 -1871
  182. package/dist/6312.index.js +0 -666
  183. package/dist/634.index.js +0 -16182
  184. package/dist/644.index.js +0 -87
  185. package/dist/6508.index.js +0 -283
  186. package/dist/6560.index.js +0 -645
  187. package/dist/6576.index.js +0 -422
  188. package/dist/6579.index.js +0 -271
  189. package/dist/6655.index.js +0 -3221
  190. package/dist/6745.index.js +0 -305
  191. package/dist/6818.index.js +0 -21642
  192. package/dist/6932.index.js +0 -1190
  193. package/dist/6972.index.js +0 -167
  194. package/dist/6995.index.js +0 -163
  195. package/dist/7249.index.js +0 -191
  196. package/dist/7372.index.js +0 -1197
  197. package/dist/7546.index.js +0 -541
  198. package/dist/76.index.js +0 -448
  199. package/dist/7879.index.js +0 -163
  200. package/dist/8025.index.js +0 -130
  201. package/dist/8054.index.js +0 -248
  202. package/dist/808.index.js +0 -1330
  203. package/dist/8097.index.js +0 -201
  204. package/dist/8126.index.js +0 -191
  205. package/dist/8178.index.js +0 -78
  206. package/dist/8382.index.js +0 -93
  207. package/dist/8408.index.js +0 -261
  208. package/dist/8540.index.js +0 -174
  209. package/dist/8635.index.js +0 -802
  210. package/dist/8688.index.js +0 -410
  211. package/dist/8699.index.js +0 -779
  212. package/dist/8891.index.js +0 -457
  213. package/dist/8916.index.js +0 -885
  214. package/dist/9146.index.js +0 -170
  215. package/dist/9168.index.js +0 -841
  216. package/dist/9292.index.js +0 -369
  217. package/dist/9422.index.js +0 -866
  218. package/dist/9440.index.js +0 -453
  219. package/dist/9479.index.js +0 -152
  220. package/dist/9481.index.js +0 -220
  221. package/dist/9539.index.js +0 -100
  222. package/dist/9547.index.js +0 -103
  223. package/dist/9692.index.js +0 -90
  224. package/dist/9800.index.js +0 -462
  225. package/dist/9899.index.js +0 -138
  226. package/dist/9981.index.js +0 -206
  227. package/dist/999.index.js +0 -117
  228. package/dist/9999.index.js +0 -1698
@@ -1,4 +1,7 @@
1
- import { emptyState } from './field-identity.js';
1
+ import { emptyState, hashState } from './field-identity.js';
2
+ import { resolveCorrelationVersion } from './correlation.js';
3
+ import { computeScc } from '../graph/scc.js';
4
+ import { solveRecursiveComponent } from './scc-solver.js';
2
5
  import { analyzeFunctionFieldIdentity } from './engine.js';
3
6
  import {
4
7
  FieldIdentitySummaryCache,
@@ -32,6 +35,27 @@ import {
32
35
  // entirely on B1's existing `_stack`-based bottom-stub for safety on a
33
36
  // recursive/cyclic call graph, exactly as B1-B3 already did.
34
37
  //
38
+ // DAG-01 UPDATE (SCC scheduling). Functions are now scheduled over the
39
+ // condensation of the call graph (`graph/scc.js`), callees before callers,
40
+ // ties broken by qid. Call edges come from `callGraph.edges` (caller ->
41
+ // callee qids, as `buildCallGraph` emits) or `opts.calleesOf(fn) -> qid[]`;
42
+ // a hand-built graph with neither has no edges, so every function is its own
43
+ // component and the order is exactly the old sorted-qid order. A component
44
+ // that is a single function with no self-call runs the legacy single-pass
45
+ // step unchanged. A RECURSIVE component (two or more functions, or a
46
+ // self-call) is solved by `scc-solver.js`: re-analyze each member until a
47
+ // full round changes no member summary (see that file for the bounded fact
48
+ // domain, the join/widening policy and what "converged" does and does not
49
+ // mean). Every analysis in a round re-reads the previous round's summaries.
50
+ // The return value gains `convergence`; `results` keeps ascending-qid order.
51
+ //
52
+ // Budgets (all optional, deterministic where possible): `opts.maxRounds`,
53
+ // `opts.maxPathDepth`, `opts.maxFunctionAnalyses` (a work count),
54
+ // `opts.deadlineMs` with `opts.now()` (default Date.now), `opts.signal`.
55
+ // When any of them stops the run, `convergence.converged` is false,
56
+ // `status` is 'partial', and `remainingFrontier` lists the qids that were
57
+ // not finished. A partial result must never be read as a safety proof.
58
+ //
35
59
  // `callGraph` must be a real object from
36
60
  // `scanner/src/ir/callgraph.js#buildCallGraph` (`{functions,
37
61
  // resolveKnownCallee, ...}`) or an equivalent hand-built fixture exposing
@@ -66,78 +90,207 @@ export function runFieldIdentityAnalysis(callGraph, opts = {}) {
66
90
  const fnList = callGraph && callGraph.functions
67
91
  ? [...callGraph.functions.values()].sort((a, b) => (a.qid < b.qid ? -1 : a.qid > b.qid ? 1 : 0))
68
92
  : [];
69
- // Live progress, additive/opt-in — mirrors dataflow/engine.js's own
70
- // `runTaintEngine` onProgress precedent exactly. Unlike that engine, this
71
- // driver has only ONE pass over `fnList` (no fixed-point pre-passes), so
72
- // every function this driver analyzes is reported, not just a dominant
73
- // subset.
93
+ // Live progress, additive/opt-in. Each function is reported once, the first
94
+ // time it is analyzed (recursive re-rounds are not re-reported).
74
95
  const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : null;
75
96
  let _progressN = 0;
97
+ const reported = new Set();
98
+ const progress = (fn) => {
99
+ if (!onProgress || reported.has(fn.qid)) return;
100
+ reported.add(fn.qid);
101
+ onProgress({ current: ++_progressN, total: fnList.length });
102
+ };
76
103
 
77
104
  const results = new Map();
78
- for (const fn of fnList) {
79
- if (onProgress) onProgress({ current: ++_progressN, total: fnList.length });
105
+ const fnByQid = new Map(fnList.map(f => [f.qid, f]));
106
+
107
+ // --- Schedule: SCC condensation of the call graph, callees first. -------
108
+ const edges = [];
109
+ const edgeSeen = new Set();
110
+ const addEdge = (caller, callee) => {
111
+ if (!fnByQid.has(caller) || !fnByQid.has(callee)) return;
112
+ const k = `${caller}\u0000${callee}`;
113
+ if (edgeSeen.has(k)) return;
114
+ edgeSeen.add(k);
115
+ // Dependency direction: the callee must come before the caller.
116
+ edges.push([callee, caller]);
117
+ };
118
+ if (callGraph && Array.isArray(callGraph.edges)) {
119
+ for (const e of callGraph.edges) if (e && typeof e.callee === 'string') addEdge(e.caller, e.callee);
120
+ }
121
+ if (typeof opts.calleesOf === 'function') {
122
+ for (const fn of fnList) for (const c of (opts.calleesOf(fn) || [])) addEdge(fn.qid, c);
123
+ }
124
+ const scc = computeScc({ nodes: fnList.map(f => f.qid), edges });
125
+
126
+ // --- Budget. ------------------------------------------------------------
127
+ const now = typeof opts.now === 'function' ? opts.now : Date.now;
128
+ let work = 0;
129
+ let stopReason = null;
130
+ const shouldStop = () => {
131
+ if (stopReason) return stopReason;
132
+ if (opts.signal && opts.signal.aborted) stopReason = 'aborted';
133
+ else if (typeof opts.deadlineMs === 'number' && now() >= opts.deadlineMs) stopReason = 'deadline';
134
+ else if (Number.isFinite(opts.deadline) && Date.now() > opts.deadline) stopReason = 'deadline';
135
+ else if (typeof opts.maxFunctionAnalyses === 'number' && work >= opts.maxFunctionAnalyses) stopReason = 'work-limit';
136
+ return stopReason;
137
+ };
138
+
139
+ // Sub-project E, increment 1 (DESIGN_GRAPH_BUILDER.md section 3): the
140
+ // seeding hook. `opts.seedEntryState(fn)` returns the entry state `fn` is
141
+ // analyzed under; a falsy return means "no seed". With no hook, this IS
142
+ // `emptyState()` and every output is byte-identical to the pre-hook driver.
143
+ const entryStateOf = (fn) => (opts.seedEntryState ? (opts.seedEntryState(fn) || emptyState()) : emptyState());
144
+ const ctxFor = (fn) => {
80
145
  const lookupCallee = createCallGraphLookup(callGraph, fn.file);
81
146
  const resolveCallSummary = createCallSummaryResolver(cache, lookupCallee);
82
- // Path provenance (Sub-project C, increment 3, §13.7 item 14): thread
83
- // opts.recordHop into the per-function ctx CONDITIONALLY, so a caller
84
- // that supplies none gets a byte-identical `{ resolveCallSummary }`
85
- // object to what this function constructed before this change —
86
- // Decision 7.2's "true by construction" property, extended to the
87
- // driver.
147
+ // Path provenance (Sub-project C, increment 3, section 13.7 item 14):
148
+ // thread opts.recordHop CONDITIONALLY so a caller that supplies none gets
149
+ // a byte-identical `{ resolveCallSummary }` object (Decision 7.2).
88
150
  const ctx = opts.recordHop ? { resolveCallSummary, recordHop: opts.recordHop } : { resolveCallSummary };
89
- // Sub-project E, increment 1 (DESIGN_GRAPH_BUILDER.md §3): the seeding
90
- // hook. `opts.seedEntryState(fn)` returns the entry state this function
91
- // should be analyzed under — a `field-identity.js` state carrying the
92
- // data-element identities a source registry matched at real call sites
93
- // INSIDE `fn`. Additive and opt-in, exactly like `opts.recordHop`
94
- // above: with no hook supplied, `entryState` is `emptyState()` and both
95
- // this call and the `cache.set` below are byte-identical to what this
96
- // loop did before the hook existed (Decision 7.2's "true by
97
- // construction" property, extended once more). A hook returning a
98
- // falsy value is treated as "no seed for this function", so a caller
99
- // can seed a subset without special-casing the rest.
100
- const entryState = opts.seedEntryState ? (opts.seedEntryState(fn) || emptyState()) : emptyState();
101
- const result = analyzeFunctionFieldIdentity(fn, entryState, ctx);
102
- results.set(fn.qid, result);
103
- // Seed the cache with this function's OWN empty-entry summary directly
104
- // (not via cache.compute, which is reserved for a CALL SITE resolving
105
- // an as-yet-uncomputed callee) — this is what lets a LATER function in
106
- // fnList that calls this one reuse the driver's own result instead of
107
- // silently recomputing it a second time.
108
- //
109
- // This CAN overwrite an entry a callee-triggered lazy compute() already
110
- // wrote for the SAME (qid, emptyState()) key earlier in this loop — and
111
- // a final whole-branch review proved by direct construction that the
112
- // two computations are NOT guaranteed to agree: createCallGraphLookup's
113
- // `callerFile` is fixed to the CALLING function's file, so when fn was
114
- // first analyzed lazily (as someone else's callee), any bare-identifier
115
- // call fn itself makes was resolved preferring THAT CALLER's file, not
116
- // fn's own. This loop's own direct call above uses fn.file, the
117
- // correct scope, so the overwrite here always uses the more precise of
118
- // the two answers, never a worse one — but it is a real overwrite of a
119
- // possibly-different prior value, not merely redundant re-computation
120
- // of an identical one. Today every entry state is emptyState() (no
121
- // source registry yet), so no identity can actually differ between the
122
- // two computations and this is unobservable; it becomes load-bearing
123
- // the moment entry states carry real identities.
124
- //
125
- // Sub-project E, increment 1 CLOSED THE SECOND, WORSE HALF of that
126
- // hazard, which the paragraph above did not name: the KEY. This line
127
- // used to write the summary under `emptyState()` unconditionally, i.e.
128
- // under a key that claims "this is what `fn` does when nothing flows
129
- // in". Once a seed puts real identities into `entryState`, that claim
130
- // is FALSE — a later call site resolving `fn` with clean arguments
131
- // builds an empty callee entry state, hits this key, and is handed
132
- // return facts that exist only because the DRIVER seeded them. That is
133
- // a fabricated identity at a call site nothing tainted, and it is
134
- // measurable (see DESIGN_GRAPH_BUILDER.md §3.6: a real two-function
135
- // fixture, plus a 27% swing in accepted hops on a 33-file project).
136
- // Keying by the state actually analyzed is both the fix and a no-op
137
- // when no hook is supplied — `entryState` IS `emptyState()` then, so
138
- // this line stays byte-identical for every pre-existing caller.
139
- cache.set(fn.qid, entryState, summaryFromAnalysisResult(result));
151
+ // DAG-03: correlation version 2 labels hops with derivation, value-version
152
+ // and invocation evidence. Off unless asked for; needs a recorder.
153
+ if (opts.recordHop && resolveCorrelationVersion(opts.correlationVersion) === 2) ctx.correlationVersion = 2;
154
+ // DAG-07: optional per-function iteration ceiling and cooperative stop
155
+ // (engine.js ignores a non-positive-integer ceiling, so this cannot loosen it).
156
+ if (opts.maxIterations !== undefined) ctx.maxIterations = opts.maxIterations;
157
+ if (opts.signal || opts.deadline !== undefined || opts.deadlineMs !== undefined) ctx.shouldStop = () => { const r = shouldStop(); return r === 'aborted' || r === 'deadline'; };
158
+ return ctx;
159
+ };
160
+ const notesFrom = cache.completionNotes().length;
161
+
162
+ const convergence = {
163
+ converged: true,
164
+ status: 'complete',
165
+ rounds: 0,
166
+ changedFacts: [],
167
+ changedFactsTotal: 0,
168
+ wideningReasons: [],
169
+ remainingFrontier: [],
170
+ stoppedBy: null,
171
+ components: [],
172
+ };
173
+ const wideningReasons = new Set();
174
+ const frontier = new Set();
175
+
176
+ const orderedComponents = scc.order.map(id => scc.components[id]);
177
+ for (const comp of orderedComponents) {
178
+ if (!comp.recursive) {
179
+ const fn = fnByQid.get(comp.members[0]);
180
+ if (shouldStop()) { frontier.add(fn.qid); continue; }
181
+ work++;
182
+ progress(fn);
183
+ const entryState = entryStateOf(fn);
184
+ const result = analyzeFunctionFieldIdentity(fn, entryState, ctxFor(fn));
185
+ results.set(fn.qid, result);
186
+ // Seed the cache with this function's OWN summary, keyed by the state
187
+ // actually analyzed. This can overwrite an entry a callee-triggered
188
+ // lazy compute() wrote for the same key; the overwrite is the more
189
+ // precise answer (it resolves bare-identifier calls against fn.file).
190
+ // With the E1 key fix this is a no-op difference when no seed hook is
191
+ // supplied. See DESIGN_GRAPH_BUILDER.md section 3.6.
192
+ cache.set(fn.qid, entryState, summaryFromAnalysisResult(result));
193
+ continue;
194
+ }
195
+
196
+ // Recursive component: solve to a fixed point or report partial.
197
+ const entryStates = new Map(comp.members.map(q => [q, entryStateOf(fnByQid.get(q))]));
198
+ const keepHashes = new Map(comp.members.map(q => [q, new Set([hashState(entryStates.get(q))])]));
199
+ const solved = solveRecursiveComponent({
200
+ members: comp.members,
201
+ maxRounds: opts.maxRounds,
202
+ maxPathDepth: opts.maxPathDepth,
203
+ maxChangedFacts: opts.maxChangedFacts,
204
+ shouldStop,
205
+ beforeRound() {
206
+ // Contexts reached through call-site argument states were computed
207
+ // against an earlier round's summaries. Forget them; the seeded
208
+ // entry context stays and is overwritten by `publish`.
209
+ for (const q of comp.members) cache.dropContexts(q, keepHashes.get(q));
210
+ },
211
+ publish(q, summary) {
212
+ cache.set(q, entryStates.get(q), summary);
213
+ },
214
+ analyze(q) {
215
+ const fn = fnByQid.get(q);
216
+ work++;
217
+ progress(fn);
218
+ let result;
219
+ const raw = cache.refreshing(q, entryStates.get(q), () => cache.compute(q, entryStates.get(q), (es) => {
220
+ result = analyzeFunctionFieldIdentity(fn, es, ctxFor(fn));
221
+ return summaryFromAnalysisResult(result);
222
+ }));
223
+ results.set(q, result);
224
+ return raw;
225
+ },
226
+ });
227
+ for (const r of solved.wideningReasons) wideningReasons.add(r);
228
+ convergence.rounds = Math.max(convergence.rounds, solved.rounds);
229
+ convergence.changedFactsTotal += solved.changedFactsTotal;
230
+ for (const f of solved.changedFacts) convergence.changedFacts.push({ component: comp.id, ...f });
231
+ convergence.components.push({
232
+ id: comp.id,
233
+ members: comp.members,
234
+ rounds: solved.rounds,
235
+ converged: solved.converged,
236
+ stoppedBy: solved.stoppedBy,
237
+ changedFacts: solved.changedFactsTotal,
238
+ });
239
+ if (!solved.converged) {
240
+ for (const q of solved.remainingFrontier) frontier.add(q);
241
+ if (solved.stoppedBy && !convergence.stoppedBy) convergence.stoppedBy = solved.stoppedBy;
242
+ }
243
+ }
244
+ if (stopReason) convergence.stoppedBy = stopReason;
245
+
246
+ // Anything never reached because a budget stopped the run is frontier too.
247
+ if (stopReason) for (const fn of fnList) if (!results.has(fn.qid)) frontier.add(fn.qid);
248
+ convergence.remainingFrontier = [...frontier].sort();
249
+ convergence.wideningReasons = [...wideningReasons].sort();
250
+ if (convergence.stoppedBy || convergence.remainingFrontier.length > 0) {
251
+ convergence.converged = false;
252
+ convergence.status = 'partial';
253
+ }
254
+
255
+ // Keep `results` in ascending-qid order regardless of schedule order.
256
+ const ordered = new Map();
257
+ for (const fn of fnList) if (results.has(fn.qid)) ordered.set(fn.qid, results.get(fn.qid));
258
+
259
+ // DAG-07 completion roll-up: per-function budget stops, degraded
260
+ // contexts and cancellation, so a cut-short unit is visible even when it
261
+ // emitted nothing.
262
+ const incomplete = [];
263
+ let cancelled = false;
264
+ for (const fn of fnList) {
265
+ const r = ordered.get(fn.qid);
266
+ if (!r || !r.completion || r.completion.status === 'complete') continue;
267
+ if (r.completion.status === 'cancelled') cancelled = true;
268
+ incomplete.push({
269
+ qid: fn.qid,
270
+ context: hashState(entryStateOf(fn)),
271
+ reasons: r.completion.reasons,
272
+ iterations: r.completion.iterations,
273
+ maxIterations: r.completion.maxIterations,
274
+ unfinishedWork: r.completion.unfinishedWork,
275
+ });
140
276
  }
277
+ if (stopReason === 'aborted' || stopReason === 'deadline') cancelled = true;
278
+ const notAnalyzed = fnList.length - ordered.size;
279
+ const seen = new Set();
280
+ const degraded = cache.completionNotes().slice(notesFrom).filter((n) => {
281
+ const k = `${n.qid}:${n.reason}`;
282
+ if (seen.has(k)) return false;
283
+ seen.add(k);
284
+ return true;
285
+ });
286
+ const completion = {
287
+ status: cancelled ? 'cancelled'
288
+ : (incomplete.length === 0 && degraded.length === 0 && convergence.status === 'complete') ? 'complete' : 'partial',
289
+ functionsAnalyzed: ordered.size,
290
+ functionsNotAnalyzed: notAnalyzed,
291
+ incomplete,
292
+ degraded,
293
+ };
141
294
 
142
- return { results, cache };
295
+ return { results: ordered, cache, convergence, completion };
143
296
  }