al-sem 0.0.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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/package.json +64 -0
  4. package/scripts/d40-diff.ts +44 -0
  5. package/scripts/fetch-native-parser.ts +179 -0
  6. package/scripts/precision-sample.ts +99 -0
  7. package/scripts/precision-study.ts +42 -0
  8. package/scripts/precision-tabulate.ts +52 -0
  9. package/src/cli/baseline.ts +31 -0
  10. package/src/cli/diff.ts +199 -0
  11. package/src/cli/events-chains.ts +56 -0
  12. package/src/cli/events-fanout.ts +87 -0
  13. package/src/cli/exit-code.ts +30 -0
  14. package/src/cli/fingerprint-indexes.ts +130 -0
  15. package/src/cli/fingerprint-query.ts +543 -0
  16. package/src/cli/fingerprint-witness.ts +493 -0
  17. package/src/cli/fingerprint.ts +292 -0
  18. package/src/cli/format-compact-json.ts +45 -0
  19. package/src/cli/format-events.ts +77 -0
  20. package/src/cli/format-fingerprint.ts +295 -0
  21. package/src/cli/format-html.ts +503 -0
  22. package/src/cli/format-json.ts +13 -0
  23. package/src/cli/format-policy.ts +95 -0
  24. package/src/cli/format-sarif.ts +186 -0
  25. package/src/cli/format-terminal.ts +153 -0
  26. package/src/cli/index.ts +566 -0
  27. package/src/cli/policy.ts +204 -0
  28. package/src/config/roots-config.ts +302 -0
  29. package/src/deps/cache-versions.ts +74 -0
  30. package/src/deps/canonical-json.ts +27 -0
  31. package/src/deps/dependency-artifact.ts +144 -0
  32. package/src/deps/dependency-cache.ts +262 -0
  33. package/src/deps/dependency-dag.ts +128 -0
  34. package/src/deps/dependency-package-discovery.ts +85 -0
  35. package/src/deps/dependency-pipeline.ts +483 -0
  36. package/src/deps/dependency-projection.ts +211 -0
  37. package/src/deps/dependency-resolver.ts +154 -0
  38. package/src/deps/workspace-dependencies.ts +114 -0
  39. package/src/detectors/capability-query.ts +145 -0
  40. package/src/detectors/confidence.ts +52 -0
  41. package/src/detectors/d1-db-op-in-loop.ts +457 -0
  42. package/src/detectors/d10-self-modifying-loop.ts +114 -0
  43. package/src/detectors/d11-modify-without-get.ts +129 -0
  44. package/src/detectors/d12-dead-integration-event.ts +81 -0
  45. package/src/detectors/d13-cross-app-internal-call.ts +105 -0
  46. package/src/detectors/d14-dead-routine.ts +151 -0
  47. package/src/detectors/d16-obsolete-routine-call.ts +94 -0
  48. package/src/detectors/d17-min-version-drift.ts +157 -0
  49. package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
  50. package/src/detectors/d19-unused-parameter.ts +116 -0
  51. package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
  52. package/src/detectors/d20-unreachable-after-exit.ts +92 -0
  53. package/src/detectors/d21-read-without-load.ts +128 -0
  54. package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
  55. package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
  56. package/src/detectors/d3-load-state.ts +72 -0
  57. package/src/detectors/d3-missing-setloadfields.ts +234 -0
  58. package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
  59. package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
  60. package/src/detectors/d34-commit-in-loop.ts +206 -0
  61. package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
  62. package/src/detectors/d36-late-setloadfields.ts +162 -0
  63. package/src/detectors/d37-validate-without-persist.ts +271 -0
  64. package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
  65. package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
  66. package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
  67. package/src/detectors/d40-transitive-load-missing.ts +217 -0
  68. package/src/detectors/d41-transitive-filter-loss.ts +200 -0
  69. package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
  70. package/src/detectors/d43-event-ishandled-skip.ts +257 -0
  71. package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
  72. package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
  73. package/src/detectors/d5-set-based-opportunity.ts +162 -0
  74. package/src/detectors/d7-recursive-event-expansion.ts +151 -0
  75. package/src/detectors/d8-commit-in-transaction.ts +132 -0
  76. package/src/detectors/d9-transaction-span-summary.ts +107 -0
  77. package/src/detectors/detector-context.ts +121 -0
  78. package/src/detectors/finding-grouping.ts +61 -0
  79. package/src/detectors/path-merge.ts +174 -0
  80. package/src/detectors/registry.ts +176 -0
  81. package/src/detectors/table-display.ts +42 -0
  82. package/src/diff/diff-abi.ts +195 -0
  83. package/src/diff/diff-capabilities.ts +179 -0
  84. package/src/diff/diff-engine.ts +146 -0
  85. package/src/diff/diff-events.ts +323 -0
  86. package/src/diff/diff-identity.ts +73 -0
  87. package/src/diff/diff-indexes.ts +199 -0
  88. package/src/diff/diff-permissions.ts +260 -0
  89. package/src/diff/diff-policy.ts +101 -0
  90. package/src/diff/diff-preflight.ts +66 -0
  91. package/src/diff/diff-renames.ts +104 -0
  92. package/src/diff/diff-schema.ts +232 -0
  93. package/src/diff/format-diff.ts +148 -0
  94. package/src/engine/attribute-parser.ts +50 -0
  95. package/src/engine/capability-cone.ts +531 -0
  96. package/src/engine/combined-graph.ts +357 -0
  97. package/src/engine/control-flow-walker.ts +1317 -0
  98. package/src/engine/dispatch-sites.ts +199 -0
  99. package/src/engine/effect-lattice.ts +81 -0
  100. package/src/engine/entry-points.ts +57 -0
  101. package/src/engine/event-flow.ts +524 -0
  102. package/src/engine/event-relay.ts +92 -0
  103. package/src/engine/op-classification.ts +92 -0
  104. package/src/engine/path-walker.ts +189 -0
  105. package/src/engine/reverse-call-graph.ts +23 -0
  106. package/src/engine/root-classifier-overlay.ts +194 -0
  107. package/src/engine/root-classifier.ts +135 -0
  108. package/src/engine/scc.ts +110 -0
  109. package/src/engine/source-anchor.ts +25 -0
  110. package/src/engine/summary-context.ts +104 -0
  111. package/src/engine/summary-engine.ts +296 -0
  112. package/src/engine/summary-runner.ts +560 -0
  113. package/src/engine/transaction-spans.ts +112 -0
  114. package/src/engine/uncertainty-util.ts +54 -0
  115. package/src/hash.ts +31 -0
  116. package/src/index/attribute-from-node.ts +141 -0
  117. package/src/index/callee-from-node.ts +181 -0
  118. package/src/index/capability/background.ts +90 -0
  119. package/src/index/capability/commit.ts +44 -0
  120. package/src/index/capability/dispatch.ts +164 -0
  121. package/src/index/capability/events.ts +65 -0
  122. package/src/index/capability/extractor.ts +124 -0
  123. package/src/index/capability/file-blob.ts +137 -0
  124. package/src/index/capability/http.ts +159 -0
  125. package/src/index/capability/hyperlink.ts +60 -0
  126. package/src/index/capability/isolated-storage.ts +179 -0
  127. package/src/index/capability/table.ts +113 -0
  128. package/src/index/capability/telemetry.ts +84 -0
  129. package/src/index/capability/ui.ts +55 -0
  130. package/src/index/capability/value-source.ts +202 -0
  131. package/src/index/expression-from-node.ts +117 -0
  132. package/src/index/indexer.ts +102 -0
  133. package/src/index/intraprocedural-body.ts +1467 -0
  134. package/src/index/intraprocedural-ops.ts +253 -0
  135. package/src/index/intraprocedural-refs.ts +188 -0
  136. package/src/index/object-indexer.ts +279 -0
  137. package/src/index/routine-indexer.ts +282 -0
  138. package/src/index/routine-signature.ts +46 -0
  139. package/src/index/variable-indexer.ts +134 -0
  140. package/src/index/variable-initializer-extractor.ts +155 -0
  141. package/src/index/variable-type-normalizer.ts +83 -0
  142. package/src/index.ts +267 -0
  143. package/src/mcp/server.ts +72 -0
  144. package/src/mcp/session.ts +49 -0
  145. package/src/mcp/tools/explain-path.ts +75 -0
  146. package/src/mcp/tools/get-analysis-health.ts +62 -0
  147. package/src/mcp/tools/get-finding.ts +47 -0
  148. package/src/mcp/tools/get-routine-summary.ts +126 -0
  149. package/src/mcp/tools/list-findings.ts +85 -0
  150. package/src/mcp/tools/list-hotspots.ts +78 -0
  151. package/src/mcp/tools/list-rollups.ts +103 -0
  152. package/src/mcp/tools/validators.ts +25 -0
  153. package/src/model/attributes.ts +120 -0
  154. package/src/model/callee.ts +45 -0
  155. package/src/model/capability.ts +187 -0
  156. package/src/model/coverage.ts +85 -0
  157. package/src/model/entities.ts +628 -0
  158. package/src/model/expression.ts +98 -0
  159. package/src/model/finding.ts +110 -0
  160. package/src/model/graph-edge.ts +93 -0
  161. package/src/model/graph.ts +62 -0
  162. package/src/model/identity.ts +81 -0
  163. package/src/model/ids.ts +90 -0
  164. package/src/model/index.ts +13 -0
  165. package/src/model/model.ts +51 -0
  166. package/src/model/permission.ts +76 -0
  167. package/src/model/root-classification.ts +116 -0
  168. package/src/model/stable-identity.ts +102 -0
  169. package/src/model/summary.ts +96 -0
  170. package/src/parser/ast.ts +82 -0
  171. package/src/parser/native/ffi.ts +145 -0
  172. package/src/parser/native/parse-index-pool.ts +148 -0
  173. package/src/parser/native/parse-index-worker.ts +94 -0
  174. package/src/parser/native/wrapper.ts +353 -0
  175. package/src/parser/parser-init.ts +43 -0
  176. package/src/perf/profiler.ts +66 -0
  177. package/src/policy/policy-default.yaml +83 -0
  178. package/src/policy/policy-engine.ts +339 -0
  179. package/src/policy/policy-loader.ts +257 -0
  180. package/src/policy/policy-schema.json +379 -0
  181. package/src/policy/policy-types.ts +81 -0
  182. package/src/policy/predicate-compiler.ts +151 -0
  183. package/src/policy/predicate-evaluator.ts +267 -0
  184. package/src/policy/predicate-fields.ts +439 -0
  185. package/src/projection/actionable-anchor.ts +48 -0
  186. package/src/projection/finding-filters.ts +44 -0
  187. package/src/projection/finding-fingerprint.ts +54 -0
  188. package/src/projection/finding-groups.ts +41 -0
  189. package/src/projection/finding-summary.ts +110 -0
  190. package/src/projection/rollup-findings.ts +105 -0
  191. package/src/providers/discover.ts +88 -0
  192. package/src/providers/external.ts +46 -0
  193. package/src/providers/types.ts +36 -0
  194. package/src/providers/workspace.ts +117 -0
  195. package/src/resolve/call-resolver.ts +117 -0
  196. package/src/resolve/coverage.ts +61 -0
  197. package/src/resolve/event-graph.ts +166 -0
  198. package/src/resolve/implicit-edges.ts +53 -0
  199. package/src/resolve/record-types.ts +36 -0
  200. package/src/resolve/resolver.ts +23 -0
  201. package/src/resolve/semantic-graph.ts +29 -0
  202. package/src/resolve/symbol-table.ts +69 -0
  203. package/src/snapshot/app-snapshot.ts +74 -0
  204. package/src/snapshot/compose.ts +100 -0
  205. package/src/snapshot/derive/callsite-evidence.ts +76 -0
  206. package/src/snapshot/derive/capability-facts.ts +70 -0
  207. package/src/snapshot/derive/contracts.ts +131 -0
  208. package/src/snapshot/derive/coverage.ts +35 -0
  209. package/src/snapshot/derive/event-declarations.ts +140 -0
  210. package/src/snapshot/derive/identity-table.ts +58 -0
  211. package/src/snapshot/derive/inputs.ts +91 -0
  212. package/src/snapshot/derive/operation-evidence.ts +70 -0
  213. package/src/snapshot/derive/permissions.ts +186 -0
  214. package/src/snapshot/derive/root-classifications.ts +56 -0
  215. package/src/snapshot/derive/schema.ts +130 -0
  216. package/src/snapshot/derive/typed-edges.ts +60 -0
  217. package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
  218. package/src/snapshot/deserialize.ts +40 -0
  219. package/src/snapshot/serialize-cbor-gz.ts +12 -0
  220. package/src/snapshot/serialize-cbor.ts +19 -0
  221. package/src/snapshot/serialize-json.ts +22 -0
  222. package/src/snapshot/shard.ts +134 -0
  223. package/src/snapshot/types.ts +181 -0
  224. package/src/symbols/app-manifest.ts +96 -0
  225. package/src/symbols/app-package-zip.ts +50 -0
  226. package/src/symbols/embedded-source-reader.ts +41 -0
  227. package/src/symbols/package-hash.ts +81 -0
  228. package/src/symbols/symbol-reader.ts +101 -0
  229. package/src/symbols/symbol-reference-parser.ts +378 -0
  230. package/src/symbols/symbol-reference-reader.ts +27 -0
  231. package/tsconfig.json +18 -0
@@ -0,0 +1,531 @@
1
+ import type { CapabilityFact, CapabilityVia } from "../model/capability.ts";
2
+ import type { CoverageReason, CoverageRecord, CoverageStatus } from "../model/coverage.ts";
3
+ import type { Routine } from "../model/entities.ts";
4
+ import type { Diagnostic } from "../model/finding.ts";
5
+ import type { GraphEdge, GraphEdgeKind } from "../model/graph-edge.ts";
6
+ import type { CallsiteId, EventId, RoutineId } from "../model/ids.ts";
7
+ import type { SemanticModel } from "../model/model.ts";
8
+ import type { RoutineSummary } from "../model/summary.ts";
9
+ import type { Scc, SccInputGraph } from "./scc.ts";
10
+ import { tarjanScc } from "./scc.ts";
11
+ import { compareStrings } from "./uncertainty-util.ts";
12
+
13
+ /** Dedup key for inherited capability facts — (op, resourceKind, resourceId, confidence). */
14
+ export function inheritedFactKey(f: CapabilityFact): string {
15
+ return `${f.op}|${f.resourceKind}|${f.resourceId ?? ""}|${f.confidence}`;
16
+ }
17
+
18
+ function capabilityViaForEdgeKind(kind: GraphEdgeKind): CapabilityVia {
19
+ switch (kind) {
20
+ case "direct-call":
21
+ return "call";
22
+ case "object-run-resolved":
23
+ case "object-run-unresolved":
24
+ return "object-run";
25
+ case "event-dispatch":
26
+ return "event-dispatch";
27
+ case "implicit-trigger":
28
+ return "implicit-trigger";
29
+ case "dependency-export":
30
+ return "dependency";
31
+ }
32
+ }
33
+
34
+ function callsiteIdForEdge(edge: GraphEdge): CallsiteId | undefined {
35
+ if (
36
+ edge.kind === "direct-call" ||
37
+ edge.kind === "object-run-resolved" ||
38
+ edge.kind === "object-run-unresolved" ||
39
+ edge.kind === "dependency-export"
40
+ ) {
41
+ return edge.callsiteId;
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ /** A resolved outgoing typed edge (only edges with a `to`). */
47
+ export interface TypedOutEdge {
48
+ to: RoutineId;
49
+ kind: GraphEdgeKind;
50
+ callsite: CallsiteId | undefined;
51
+ eventId: EventId | undefined;
52
+ }
53
+
54
+ export interface TypedEdgeGraph {
55
+ nodes: RoutineId[];
56
+ /** Per-routine outgoing edges, each list sorted by edgeSortKey. */
57
+ outgoing: Map<RoutineId, TypedOutEdge[]>;
58
+ /** `from` of every object-run-unresolved edge (coverage reason source). */
59
+ unresolvedSources: Set<RoutineId>;
60
+ }
61
+
62
+ /** Total-order key for an out-edge. eventId disambiguates event-dispatch (no callsite). */
63
+ function edgeSortKey(e: TypedOutEdge): string {
64
+ return `${e.kind}|${e.callsite ?? ""}|${e.eventId ?? ""}|${e.to}`;
65
+ }
66
+
67
+ export function buildTypedEdgeGraph(model: SemanticModel): TypedEdgeGraph {
68
+ const outgoing = new Map<RoutineId, TypedOutEdge[]>();
69
+ const unresolvedSources = new Set<RoutineId>();
70
+ for (const edge of model.typedEdges ?? []) {
71
+ if (edge.kind === "object-run-unresolved") {
72
+ unresolvedSources.add(edge.from);
73
+ continue;
74
+ }
75
+ if (!("to" in edge)) continue;
76
+ const out: TypedOutEdge = {
77
+ to: edge.to,
78
+ kind: edge.kind,
79
+ callsite: callsiteIdForEdge(edge),
80
+ eventId: "eventId" in edge ? edge.eventId : undefined,
81
+ };
82
+ const list = outgoing.get(edge.from);
83
+ if (list) list.push(out);
84
+ else outgoing.set(edge.from, [out]);
85
+ }
86
+ for (const list of outgoing.values()) {
87
+ list.sort((a, b) => compareStrings(edgeSortKey(a), edgeSortKey(b)));
88
+ }
89
+ const nodes = model.routines.map((r) => r.id).sort((a, b) => compareStrings(a, b));
90
+ return { nodes, outgoing, unresolvedSources };
91
+ }
92
+
93
+ /** Adapter so tarjanScc runs over the typed-edge graph. */
94
+ export function typedEdgeSccInput(g: TypedEdgeGraph): SccInputGraph {
95
+ return { nodes: g.nodes, edgesByFrom: g.outgoing };
96
+ }
97
+
98
+ /** Per-routine direct facts grouped by dedup key (canonical rep per key). */
99
+ export type RoutineDirectFacts = Map<RoutineId, Map<string, CapabilityFact>>;
100
+
101
+ export interface ConeFactEntry {
102
+ rep: CapabilityFact; // the original DIRECT fact (re-tagged by consumers)
103
+ dist: number; // min hop distance from the SCC to a node carrying this key
104
+ }
105
+ export type ConeFacts = Map<string, ConeFactEntry>;
106
+
107
+ /** Canonical representative key — deterministic, traversal-order-independent. */
108
+ function repKey(f: CapabilityFact): string {
109
+ return [
110
+ inheritedFactKey(f),
111
+ String(f.subject ?? ""),
112
+ String(f.witnessOperationId ?? ""),
113
+ String(f.witnessCallsiteId ?? ""),
114
+ JSON.stringify(f.resourceArgSource ?? null),
115
+ JSON.stringify(f.extra ?? null),
116
+ ].join("§");
117
+ }
118
+
119
+ /** Merge `entry` (already distance-shifted) into `dst` at `key`, keeping min dist; tie-break canonical rep. */
120
+ function mergeCone(dst: ConeFacts, key: string, entry: ConeFactEntry): void {
121
+ const existing = dst.get(key);
122
+ if (existing === undefined) {
123
+ dst.set(key, entry);
124
+ return;
125
+ }
126
+ if (entry.dist < existing.dist) {
127
+ dst.set(key, entry);
128
+ } else if (
129
+ entry.dist === existing.dist &&
130
+ compareStrings(repKey(entry.rep), repKey(existing.rep)) < 0
131
+ ) {
132
+ dst.set(key, entry);
133
+ }
134
+ }
135
+
136
+ /** Re-tag a representative direct fact as an inherited fact on `subject` via a first-hop edge. */
137
+ function retag(rep: CapabilityFact, subject: RoutineId, edge: TypedOutEdge): CapabilityFact {
138
+ return {
139
+ ...rep,
140
+ subject,
141
+ provenance: "inherited",
142
+ via: capabilityViaForEdgeKind(edge.kind),
143
+ witnessCallsiteId: edge.callsite,
144
+ };
145
+ }
146
+
147
+ /** Sort key for the final capabilityFactsInherited array (deterministic output order). */
148
+ function inheritedOutputSortKey(f: CapabilityFact): string {
149
+ return [
150
+ f.op,
151
+ f.resourceKind,
152
+ String(f.resourceId ?? ""),
153
+ f.confidence,
154
+ f.via,
155
+ String(f.witnessCallsiteId ?? ""),
156
+ String(f.witnessOperationId ?? ""),
157
+ ].join("|");
158
+ }
159
+
160
+ function sortInherited(facts: CapabilityFact[]): CapabilityFact[] {
161
+ return facts.sort((a, b) => compareStrings(inheritedOutputSortKey(a), inheritedOutputSortKey(b)));
162
+ }
163
+
164
+ /**
165
+ * Singleton non-recursive fast path. For each dedup key reachable through some out-edge, pick
166
+ * the out-edge minimizing (1 + downstream cone distance), tie-break by canonical edge key.
167
+ * `subject` can never appear in a successor cone (else they'd share an SCC), so its own facts
168
+ * are excluded automatically. Reads SUCCESSOR cones, which are alive when this is called from
169
+ * the fused pass (composeInheritedCones emits before freeing) and from the unit tests (which
170
+ * use the non-freeing buildFactCones below).
171
+ */
172
+ export function inheritedFactsForSingleton(
173
+ subject: RoutineId,
174
+ g: TypedEdgeGraph,
175
+ sccIdByRoutine: Map<RoutineId, number>,
176
+ cones: Map<number, ConeFacts>,
177
+ ): CapabilityFact[] {
178
+ const best = new Map<string, { rep: CapabilityFact; dist: number; edge: TypedOutEdge }>();
179
+ for (const edge of g.outgoing.get(subject) ?? []) {
180
+ const yj = sccIdByRoutine.get(edge.to);
181
+ if (yj === undefined) continue;
182
+ const ycone = cones.get(yj);
183
+ if (ycone === undefined) continue;
184
+ for (const [key, entry] of ycone) {
185
+ const cand = { rep: entry.rep, dist: entry.dist + 1, edge };
186
+ const cur = best.get(key);
187
+ if (cur === undefined || cand.dist < cur.dist) {
188
+ best.set(key, cand);
189
+ } else if (cand.dist === cur.dist) {
190
+ if (compareStrings(edgeSortKey(edge), edgeSortKey(cur.edge)) < 0) best.set(key, cand);
191
+ }
192
+ }
193
+ }
194
+ const out: CapabilityFact[] = [];
195
+ for (const { rep, edge } of best.values()) out.push(retag(rep, subject, edge));
196
+ return sortInherited(out);
197
+ }
198
+
199
+ /**
200
+ * Set-correct, deterministic path for routines in recursive SCCs (rare). BFS expands ONLY over
201
+ * intra-SCC edges to discover each sibling member's first hop from `subject`; for every edge
202
+ * that LEAVES the SCC, it pulls the target's full precomputed cone attributed to the current
203
+ * first hop (the cone already holds the entire downstream closure, so it must not traverse
204
+ * further). Self is never enqueued, so self facts are excluded. First-reached-wins in
205
+ * sorted-edge BFS order makes the winner deterministic.
206
+ */
207
+ export function inheritedFactsByBfs(
208
+ subject: RoutineId,
209
+ g: TypedEdgeGraph,
210
+ direct: RoutineDirectFacts,
211
+ sccIdByRoutine: Map<RoutineId, number>,
212
+ cones: Map<number, ConeFacts>,
213
+ ): CapabilityFact[] {
214
+ const myScc = sccIdByRoutine.get(subject);
215
+ const seen = new Set<string>();
216
+ const out: CapabilityFact[] = [];
217
+ const visited = new Set<RoutineId>([subject]);
218
+ type QI = { id: RoutineId; firstHop: TypedOutEdge };
219
+ const queue: QI[] = [];
220
+ for (const edge of g.outgoing.get(subject) ?? []) {
221
+ if (!visited.has(edge.to)) {
222
+ visited.add(edge.to);
223
+ queue.push({ id: edge.to, firstHop: edge });
224
+ }
225
+ }
226
+ while (queue.length > 0) {
227
+ const item = queue.shift();
228
+ if (item === undefined) break;
229
+ const { id, firstHop } = item;
230
+ if (sccIdByRoutine.get(id) === myScc) {
231
+ // sibling member: emit its own direct facts (attributed to firstHop), keep walking intra-SCC
232
+ for (const [key, rep] of direct.get(id) ?? []) {
233
+ if (!seen.has(key)) {
234
+ seen.add(key);
235
+ out.push(retag(rep, subject, firstHop));
236
+ }
237
+ }
238
+ for (const edge of g.outgoing.get(id) ?? []) {
239
+ if (!visited.has(edge.to)) {
240
+ visited.add(edge.to);
241
+ queue.push({ id: edge.to, firstHop });
242
+ }
243
+ }
244
+ } else {
245
+ // downstream entry: pull its whole cone (includes its own direct at dist 0), do NOT recurse
246
+ const yj = sccIdByRoutine.get(id);
247
+ const ycone = yj !== undefined ? cones.get(yj) : undefined;
248
+ if (ycone !== undefined) {
249
+ for (const [key, entry] of ycone) {
250
+ if (!seen.has(key)) {
251
+ seen.add(key);
252
+ out.push(retag(entry.rep, subject, firstHop));
253
+ }
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return sortInherited(out);
259
+ }
260
+
261
+ /** Minimal per-routine direct coverage the cone roll-up needs. */
262
+ export type RoutineDirectCoverage = Map<
263
+ RoutineId,
264
+ { directStatus: CoverageStatus; reasons: readonly CoverageReason[] }
265
+ >;
266
+
267
+ export interface CoverageCone {
268
+ complete: boolean;
269
+ reasons: CoverageReason[]; // sorted, deduped
270
+ unknownTargets: string[]; // sorted, deduped
271
+ }
272
+
273
+ /**
274
+ * Distinct successor SCCs per SCC (cross-SCC edges only). Shared by every cone build so the
275
+ * test-only builders and the fused production pass use the SAME successor enumeration.
276
+ */
277
+ function buildSuccSccs(
278
+ g: TypedEdgeGraph,
279
+ sccs: Scc[],
280
+ sccIdByRoutine: Map<RoutineId, number>,
281
+ ): Set<number>[] {
282
+ const succSccs: Set<number>[] = sccs.map(() => new Set<number>());
283
+ for (let i = 0; i < sccs.length; i++) {
284
+ const scc = sccs[i];
285
+ if (scc === undefined) continue;
286
+ for (const m of scc.members) {
287
+ for (const e of g.outgoing.get(m) ?? []) {
288
+ const yj = sccIdByRoutine.get(e.to);
289
+ if (yj !== undefined && yj !== i) succSccs[i]?.add(yj);
290
+ }
291
+ }
292
+ }
293
+ return succSccs;
294
+ }
295
+
296
+ /**
297
+ * Build one SCC's fact cone: members' direct facts at dist 0, plus each (already-built)
298
+ * successor cone shifted to dist + 1. Shared by buildFactCones and composeInheritedCones.
299
+ */
300
+ function factConeForScc(
301
+ members: readonly RoutineId[],
302
+ succIds: Set<number>,
303
+ factCones: Map<number, ConeFacts>,
304
+ direct: RoutineDirectFacts,
305
+ ): ConeFacts {
306
+ const cone: ConeFacts = new Map();
307
+ for (const m of members) {
308
+ for (const [key, f] of direct.get(m) ?? []) mergeCone(cone, key, { rep: f, dist: 0 });
309
+ }
310
+ for (const y of succIds) {
311
+ const yc = factCones.get(y);
312
+ if (yc === undefined) continue;
313
+ for (const [key, entry] of yc) mergeCone(cone, key, { rep: entry.rep, dist: entry.dist + 1 });
314
+ }
315
+ return cone;
316
+ }
317
+
318
+ /**
319
+ * Build one SCC's coverage cone (includes self; identical for every member). Mirrors
320
+ * composeCoverage: reasons are unioned ONLY from members whose directStatus is non-complete,
321
+ * plus "object-run-unresolved" when a member is an unresolved source. A member with no coverage
322
+ * entry contributes nothing and is not added to unknownTargets. Rolls up each (already-built)
323
+ * successor cone. Shared by buildCoverageCones and composeInheritedCones.
324
+ */
325
+ function coverageConeForScc(
326
+ members: readonly RoutineId[],
327
+ succIds: Set<number>,
328
+ covCones: Map<number, CoverageCone>,
329
+ cov: RoutineDirectCoverage,
330
+ unresolvedSources: Set<RoutineId>,
331
+ ): CoverageCone {
332
+ let complete = true;
333
+ const reasonSet = new Set<CoverageReason>();
334
+ const unknownSet = new Set<string>();
335
+ for (const m of members) {
336
+ if (unresolvedSources.has(m)) {
337
+ complete = false;
338
+ reasonSet.add("object-run-unresolved");
339
+ }
340
+ const c = cov.get(m);
341
+ if (c === undefined) continue;
342
+ if (c.directStatus === "partial" || c.directStatus === "unknown") {
343
+ complete = false;
344
+ unknownSet.add(m);
345
+ for (const r of c.reasons) reasonSet.add(r);
346
+ }
347
+ }
348
+ for (const y of succIds) {
349
+ const yc = covCones.get(y);
350
+ if (yc === undefined) continue;
351
+ if (!yc.complete) complete = false;
352
+ for (const r of yc.reasons) reasonSet.add(r);
353
+ for (const t of yc.unknownTargets) unknownSet.add(t);
354
+ }
355
+ return {
356
+ complete,
357
+ reasons: [...reasonSet].sort((a, b) => compareStrings(a, b)),
358
+ unknownTargets: [...unknownSet].sort((a, b) => compareStrings(a, b)),
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Per-SCC coverage cone (includes self; identical for every member). Mirrors composeCoverage:
364
+ * reasons are unioned ONLY from members whose directStatus is non-complete, plus
365
+ * "object-run-unresolved" when a member is an unresolved source. A member with no coverage
366
+ * entry contributes nothing and is not added to unknownTargets.
367
+ *
368
+ * Retains ALL cones (no refcount freeing) — this is the test-only pure recurrence; production
369
+ * memory-bounding is done by the future fused pass composeInheritedCones. Processes SCCs in
370
+ * reverse-topological order (downstream first).
371
+ */
372
+ export function buildCoverageCones(
373
+ g: TypedEdgeGraph,
374
+ sccs: Scc[],
375
+ sccIdByRoutine: Map<RoutineId, number>,
376
+ cov: RoutineDirectCoverage,
377
+ ): Map<number, CoverageCone> {
378
+ const succSccs = buildSuccSccs(g, sccs, sccIdByRoutine);
379
+ const cones = new Map<number, CoverageCone>();
380
+ for (let i = 0; i < sccs.length; i++) {
381
+ const scc = sccs[i];
382
+ if (scc === undefined) continue;
383
+ cones.set(
384
+ i,
385
+ coverageConeForScc(scc.members, succSccs[i] ?? new Set(), cones, cov, g.unresolvedSources),
386
+ );
387
+ }
388
+ return cones;
389
+ }
390
+
391
+ /**
392
+ * Per-SCC reachable fact cone (including the SCC's own members' direct facts at dist 0).
393
+ * Processed in reverse-topological order so downstream SCCs are done first.
394
+ *
395
+ * This is the test-only pure recurrence: it RETAINS every SCC's cone in the returned Map so
396
+ * callers (and the singleton/recursive helpers above) can read any SCC's successor cones.
397
+ * Production memory-bounding (predecessor-refcount freeing of consumed downstream cones) is the
398
+ * job of the future fused pass `composeInheritedCones`, NOT this function.
399
+ */
400
+ export function buildFactCones(
401
+ g: TypedEdgeGraph,
402
+ sccs: Scc[],
403
+ sccIdByRoutine: Map<RoutineId, number>,
404
+ direct: RoutineDirectFacts,
405
+ ): Map<number, ConeFacts> {
406
+ // distinct successor SCCs per SCC (needed to pull downstream cones at dist + 1)
407
+ const succSccs = buildSuccSccs(g, sccs, sccIdByRoutine);
408
+ const cones = new Map<number, ConeFacts>();
409
+ for (let i = 0; i < sccs.length; i++) {
410
+ const scc = sccs[i];
411
+ if (scc === undefined) continue;
412
+ cones.set(i, factConeForScc(scc.members, succSccs[i] ?? new Set(), cones, direct));
413
+ }
414
+ return cones;
415
+ }
416
+
417
+ export interface InheritedConeResult {
418
+ inherited: CapabilityFact[];
419
+ coverage: CoverageRecord;
420
+ }
421
+
422
+ /**
423
+ * Compute capabilityFactsInherited + coverage for every NON-leaf routine via a single fused
424
+ * bottom-up SCC-cone pass. Reads each routine's direct facts + directStatus from `final`
425
+ * (the task-17 pass already attached them; leaf routines carry authoritative summaries).
426
+ *
427
+ * One reverse-topological walk: per SCC, build its fact cone + coverage cone (pulling already-
428
+ * built successor cones), emit per-routine results for its non-leaf members (successor cones are
429
+ * still alive), then refcount-free downstream cones whose last predecessor has now been
430
+ * processed. Bounds memory without re-traversing the graph.
431
+ *
432
+ * Engine-never-throws: on internal failure, returns an empty map and pushes a diagnostic; the
433
+ * caller falls back to empty inherited facts + baseline coverage.
434
+ */
435
+ export function composeInheritedCones(
436
+ model: SemanticModel,
437
+ final: Map<RoutineId, RoutineSummary>,
438
+ isLeaf: (r: Routine) => boolean,
439
+ diagnostics?: Diagnostic[],
440
+ ): Map<RoutineId, InheritedConeResult> {
441
+ const out = new Map<RoutineId, InheritedConeResult>();
442
+ try {
443
+ const g = buildTypedEdgeGraph(model);
444
+
445
+ const routineById = new Map<RoutineId, Routine>();
446
+ for (const r of model.routines) routineById.set(r.id, r);
447
+
448
+ // direct facts (canonical rep per dedup key) + direct coverage, for ALL routines in `final`
449
+ const direct: RoutineDirectFacts = new Map();
450
+ const cov: RoutineDirectCoverage = new Map();
451
+ for (const r of model.routines) {
452
+ const s = final.get(r.id);
453
+ if (s === undefined) continue;
454
+ const byKey = new Map<string, CapabilityFact>();
455
+ for (const f of s.capabilityFactsDirect ?? []) {
456
+ const k = inheritedFactKey(f);
457
+ const cur = byKey.get(k);
458
+ if (cur === undefined || compareStrings(repKey(f), repKey(cur)) < 0) byKey.set(k, f);
459
+ }
460
+ if (byKey.size > 0) direct.set(r.id, byKey);
461
+ const c = s.coverage;
462
+ // A routine with no coverage record contributes NOTHING to the coverage cone:
463
+ // it is not forced to "unknown" and is not added to unknownTargets. The roll-up
464
+ // below skips routines absent from `cov` via its `if (c === undefined) continue;`.
465
+ if (c !== undefined) cov.set(r.id, { directStatus: c.directStatus, reasons: c.reasons });
466
+ }
467
+
468
+ const { sccs, sccIdByRoutine } = tarjanScc(typedEdgeSccInput(g));
469
+
470
+ // distinct successor SCCs per SCC + predecessor refcounts (shared by fact + coverage cones)
471
+ const succSccs = buildSuccSccs(g, sccs, sccIdByRoutine);
472
+ const remainingUses = sccs.map(() => 0);
473
+ for (let i = 0; i < sccs.length; i++) {
474
+ for (const y of succSccs[i] ?? []) remainingUses[y] = (remainingUses[y] ?? 0) + 1;
475
+ }
476
+
477
+ const factCones = new Map<number, ConeFacts>();
478
+ const covCones = new Map<number, CoverageCone>();
479
+
480
+ for (let i = 0; i < sccs.length; i++) {
481
+ const scc = sccs[i];
482
+ if (scc === undefined) continue;
483
+
484
+ const succIds = succSccs[i] ?? new Set<number>();
485
+
486
+ // --- build this SCC's fact + coverage cones (pulling already-built successor cones) ---
487
+ const fcone = factConeForScc(scc.members, succIds, factCones, direct);
488
+ factCones.set(i, fcone);
489
+ const ccone = coverageConeForScc(scc.members, succIds, covCones, cov, g.unresolvedSources);
490
+ covCones.set(i, ccone);
491
+
492
+ // --- emit per-routine results for this SCC's NON-leaf members (cones still alive) ---
493
+ const recursive = scc.recursive;
494
+ for (const m of scc.members) {
495
+ const routine = routineById.get(m);
496
+ if (routine === undefined || isLeaf(routine)) continue;
497
+ const inherited = recursive
498
+ ? inheritedFactsByBfs(m, g, direct, sccIdByRoutine, factCones)
499
+ : inheritedFactsForSingleton(m, g, sccIdByRoutine, factCones);
500
+ const dStatus = cov.get(m)?.directStatus ?? "unknown";
501
+ out.set(m, {
502
+ inherited,
503
+ coverage: {
504
+ subject: m,
505
+ directStatus: dStatus,
506
+ inheritedStatus: ccone.complete ? "complete" : "partial",
507
+ reasons: ccone.reasons,
508
+ unknownTargets: ccone.unknownTargets,
509
+ },
510
+ });
511
+ }
512
+
513
+ // --- refcount-free downstream cones whose last predecessor (this SCC) is now done ---
514
+ for (const y of succIds) {
515
+ remainingUses[y] = (remainingUses[y] ?? 0) - 1;
516
+ if ((remainingUses[y] ?? 0) <= 0) {
517
+ factCones.delete(y);
518
+ covCones.delete(y);
519
+ }
520
+ }
521
+ }
522
+ } catch (err) {
523
+ diagnostics?.push({
524
+ severity: "warning",
525
+ stage: "summarize",
526
+ message: `capability-cone composition failed: ${err instanceof Error ? err.message : String(err)}`,
527
+ });
528
+ return new Map();
529
+ }
530
+ return out;
531
+ }