@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
@@ -0,0 +1,129 @@
1
+ //
2
+ // registry.js: the set of graph views a query may span (DAG-06 R06.6).
3
+ //
4
+ // What registration rejects, each with a typed GraphViewError:
5
+ // INCOMPATIBLE_VERSION an adapter whose major version this build cannot read
6
+ // ID_COLLISION the same (graph kind, repository, digest) registered
7
+ // twice with different content: a digest names ONE
8
+ // immutable graph
9
+ // BAD_REF a malformed view
10
+ //
11
+ // What lookups reject:
12
+ // DANGLING_REF the graph is not registered, or the entity is not in it
13
+ // VERSION_INCOMPATIBLE a reference written by a newer reference format
14
+ // AMBIGUOUS_LOCAL_ID a bare local id that exists in more than one graph; a
15
+ // local id is only meaningful inside its graph, so the
16
+ // registry will not pick one
17
+ //
18
+ // A reference never resolves across repositories: the repository is part of the
19
+ // reference, so `pnode:abc` in repository A and `pnode:abc` in repository B are
20
+ // two different entities.
21
+
22
+ import { GraphViewError, graphIdentityKey, validateRef, REF_VERSION } from './refs.js';
23
+ import { PROVENANCE_ADAPTER_VERSION } from './adapters/provenance.js';
24
+ import { DATAFLOW_ADAPTER_VERSION } from './adapters/dataflow.js';
25
+ import { SEMANTIC_ADAPTER_VERSION } from './adapters/semantic.js';
26
+ import { ARTIFACT_ADAPTER_VERSION } from './adapters/artifact.js';
27
+ import { CROSSREPO_ADAPTER_VERSION, FINDINGS_ADAPTER_VERSION } from './adapters/crossrepo.js';
28
+
29
+ const major = (v) => Number(String(v).split('.')[0]);
30
+
31
+ /** Adapter majors this build understands, per graph kind. */
32
+ export const SUPPORTED_ADAPTER_MAJORS = Object.freeze({
33
+ provenance: [major(PROVENANCE_ADAPTER_VERSION)],
34
+ dataflow: [major(DATAFLOW_ADAPTER_VERSION)],
35
+ semantic: [major(SEMANTIC_ADAPTER_VERSION)],
36
+ artifact: [major(ARTIFACT_ADAPTER_VERSION)],
37
+ 'cross-repo-links': [major(CROSSREPO_ADAPTER_VERSION)],
38
+ findings: [major(FINDINGS_ADAPTER_VERSION)],
39
+ });
40
+
41
+ export function createGraphRegistry() {
42
+ const views = new Map(); // identity key -> view
43
+
44
+ function viewFor(ref) {
45
+ const bad = validateRef(ref);
46
+ if (bad) {
47
+ throw new GraphViewError(ref && ref.v !== undefined && ref.v !== REF_VERSION ? 'VERSION_INCOMPATIBLE' : 'BAD_REF', bad, { ref });
48
+ }
49
+ const v = views.get(graphIdentityKey(ref));
50
+ if (!v) throw new GraphViewError('DANGLING_REF', `no registered ${ref.graphKind} graph for repository ${ref.repository} with digest ${ref.graphDigest.slice(0, 12)}`, { ref });
51
+ return v;
52
+ }
53
+
54
+ const foreignViews = () => [...views.values()].filter((v) => v._allowForeign);
55
+
56
+ return {
57
+ register(view) {
58
+ const kinds = SUPPORTED_ADAPTER_MAJORS[view?.graphKind];
59
+ if (!kinds) throw new GraphViewError('BAD_REF', `unknown graph kind ${view?.graphKind}`);
60
+ if (!kinds.includes(major(view.adapterVersion))) {
61
+ throw new GraphViewError('INCOMPATIBLE_VERSION', `${view.graphKind} adapter ${view.adapterVersion} is not readable by this build (supported majors: ${kinds.join(',')})`);
62
+ }
63
+ const key = graphIdentityKey(view);
64
+ const prev = views.get(key);
65
+ if (prev) {
66
+ if (prev.fingerprint !== view.fingerprint) {
67
+ throw new GraphViewError('ID_COLLISION', `${view.graphKind}/${view.repository} digest ${view.graphDigest.slice(0, 12)} is already registered with different content`, { key });
68
+ }
69
+ return prev;
70
+ }
71
+ views.set(key, view);
72
+ return view;
73
+ },
74
+
75
+ views() { return [...views.values()]; },
76
+ viewFor,
77
+
78
+ has(ref) {
79
+ const v = views.get(graphIdentityKey(ref));
80
+ return !!v && v.has(ref);
81
+ },
82
+
83
+ node(ref) { return viewFor(ref).node(ref); },
84
+
85
+ outgoing(ref, filter) {
86
+ const out = viewFor(ref).outgoing(ref, filter);
87
+ for (const fv of foreignViews()) if (fv.graphKind !== ref.graphKind || fv.repository !== ref.repository || fv.graphDigest !== ref.graphDigest) out.push(...fv.outgoing(ref, filter));
88
+ return out;
89
+ },
90
+
91
+ incoming(ref, filter) {
92
+ const inn = viewFor(ref).incoming(ref, filter);
93
+ for (const fv of foreignViews()) if (fv.graphKind !== ref.graphKind || fv.repository !== ref.repository || fv.graphDigest !== ref.graphDigest) inn.push(...fv.incoming(ref, filter));
94
+ return inn;
95
+ },
96
+
97
+ /** A bare local id is ambiguous when more than one registered graph has it. */
98
+ resolveLocal(entityKind, localId, { graphKind } = {}) {
99
+ const hits = [];
100
+ for (const v of views.values()) {
101
+ if (graphKind && v.graphKind !== graphKind) continue;
102
+ try { hits.push(v.ref(entityKind, localId)); } catch { /* not in this graph */ }
103
+ }
104
+ if (hits.length === 0) throw new GraphViewError('DANGLING_REF', `no ${entityKind} ${localId} in any registered graph`);
105
+ if (hits.length > 1) throw new GraphViewError('AMBIGUOUS_LOCAL_ID', `${entityKind} ${localId} exists in ${hits.length} registered graphs; use a full reference`, { candidates: hits });
106
+ return hits[0];
107
+ },
108
+
109
+ /**
110
+ * Classify a foreign endpoint (for example, a cross-repository link end):
111
+ * resolved registered, and the entity exists
112
+ * unresolved no registered graph with that identity (not an error:
113
+ * the remote graph simply is not loaded)
114
+ * dangling the graph is registered but the entity is not in it
115
+ * repository-mismatch a graph with that digest is registered, but under a
116
+ * different repository than the reference claims
117
+ */
118
+ classify(ref) {
119
+ const bad = validateRef(ref);
120
+ if (bad) throw new GraphViewError('BAD_REF', bad, { ref });
121
+ const v = views.get(graphIdentityKey(ref));
122
+ if (v) return v.has(ref) ? 'resolved' : 'dangling';
123
+ for (const o of views.values()) {
124
+ if (o.graphKind === ref.graphKind && o.graphDigest === ref.graphDigest && o.repository !== ref.repository) return 'repository-mismatch';
125
+ }
126
+ return 'unresolved';
127
+ },
128
+ };
129
+ }
@@ -0,0 +1,81 @@
1
+ //
2
+ // relations.js: relation kinds and the traversal filter (DAG-06 R06.3, R06.4).
3
+ //
4
+ // Two independent axes, never collapsed into one:
5
+ // - RELATION: what the edge means (data flow, calls, ...).
6
+ // - ORIGIN: how the edge came to exist (derived from code, declared by an
7
+ // operator, observed at runtime, ...).
8
+ //
9
+ // A query chooses the relations it may traverse AND the origins it trusts.
10
+ // The default filter traverses code-derived and derived edges only, so an
11
+ // operator declaration or a runtime observation is never silently walked as
12
+ // if it were a code-derived data flow (R06.4).
13
+
14
+ import { GraphViewError } from './refs.js';
15
+
16
+ export const RELATION_KINDS = Object.freeze([
17
+ 'data_flow', // a value moves from one place to another
18
+ 'control_dependence', // execution of B depends on a decision at A
19
+ 'calls', // a call site invokes a callee
20
+ 'returns', // a callee result comes back to a caller
21
+ 'dependency', // A needs B (package, module, component)
22
+ 'declaration', // A declares/contains B (symbol -> call site, origin)
23
+ 'observation', // a runtime observation attached to an entity
24
+ 'artifact_derivation', // A was built into / produced B
25
+ ]);
26
+
27
+ export const EDGE_ORIGINS = Object.freeze([
28
+ 'code', // read directly from analyzed source
29
+ 'derived', // computed from code-derived facts by a projection or index
30
+ 'declared', // asserted by an operator (cross-repo links, deployments)
31
+ 'observed', // seen at runtime (telemetry)
32
+ 'schema', // taken from a schema or manifest
33
+ ]);
34
+
35
+ /** Origins a default query trusts as "the program's own structure". */
36
+ export const DEFAULT_TRUSTED_ORIGINS = Object.freeze(['code', 'derived', 'schema']);
37
+
38
+ /**
39
+ * Normalize a relation filter. Accepted forms:
40
+ * undefined / null -> every relation, trusted origins only
41
+ * 'calls' | ['calls', ...] -> those relations, trusted origins only
42
+ * { relations, origins } -> explicit on both axes
43
+ * Unknown relation or origin names throw, so a typo never silently widens
44
+ * or empties a query.
45
+ */
46
+ export function normalizeFilter(filter) {
47
+ let relations = null;
48
+ let origins = DEFAULT_TRUSTED_ORIGINS;
49
+ if (filter === undefined || filter === null) {
50
+ // defaults
51
+ } else if (typeof filter === 'string') {
52
+ relations = [filter];
53
+ } else if (Array.isArray(filter)) {
54
+ relations = filter;
55
+ } else if (typeof filter === 'object') {
56
+ if (filter.relations !== undefined && filter.relations !== null) {
57
+ relations = typeof filter.relations === 'string' ? [filter.relations] : filter.relations;
58
+ }
59
+ if (filter.origins !== undefined && filter.origins !== null) {
60
+ origins = typeof filter.origins === 'string' ? [filter.origins] : filter.origins;
61
+ }
62
+ } else {
63
+ throw new GraphViewError('BAD_FILTER', 'a relation filter is a name, a list of names or { relations, origins }');
64
+ }
65
+ if (relations !== null) {
66
+ if (!Array.isArray(relations)) throw new GraphViewError('BAD_FILTER', 'relations must be a list');
67
+ for (const r of relations) {
68
+ if (!RELATION_KINDS.includes(r)) throw new GraphViewError('BAD_FILTER', `unknown relation kind ${JSON.stringify(r)}`);
69
+ }
70
+ }
71
+ if (!Array.isArray(origins)) throw new GraphViewError('BAD_FILTER', 'origins must be a list');
72
+ for (const o of origins) {
73
+ if (!EDGE_ORIGINS.includes(o)) throw new GraphViewError('BAD_FILTER', `unknown edge origin ${JSON.stringify(o)}`);
74
+ }
75
+ return { relations: relations ? new Set(relations) : null, origins: new Set(origins) };
76
+ }
77
+
78
+ export function edgeMatches(edge, nf) {
79
+ if (nf.relations && !nf.relations.has(edge.relation)) return false;
80
+ return nf.origins.has(edge.origin);
81
+ }
@@ -0,0 +1,241 @@
1
+ // Strongly connected components and condensation for typed directed graphs.
2
+ //
3
+ // Pure and iterative: no recursion anywhere (a 100,000-node chain must not
4
+ // overflow the JavaScript stack), no I/O, no dependence on any analysis
5
+ // engine. The raw graph is never modified; the condensation is a derived
6
+ // view. It is a DAG, the raw graph is a provenance graph that may be cyclic.
7
+ //
8
+ // Input: `{ nodes: string[], edges: Array<[from, to] | {from, to}> }`.
9
+ // Node ids are strings. Duplicate nodes and duplicate edges are collapsed.
10
+ // An edge whose endpoint is not in `nodes` throws a TypeError rather than
11
+ // being silently dropped (the caller decides how to filter).
12
+ //
13
+ // Output is independent of the order of `nodes` and `edges`:
14
+ // components Array<{ id, members, internalEdges, recursive }>
15
+ // ids are 0..n-1 assigned by ascending smallest member, so
16
+ // they are stable. `members` is sorted. `internalEdges` is the
17
+ // sorted, de-duplicated list of [from, to] inside the
18
+ // component (self-loops included). `recursive` is true when
19
+ // the component has more than one member or a self-loop.
20
+ // componentOf Map<node, componentId>
21
+ // condensationEdges sorted unique [fromId, toId] across components
22
+ // order component ids in a deterministic topological order: for
23
+ // every cross-component edge u -> v, comp(u) appears before
24
+ // comp(v). Ties are broken by smallest component id.
25
+ //
26
+ // To schedule callees before callers, pass edges as callee -> caller (or
27
+ // reverse `order`). SCC membership says nodes can reach each other; it is
28
+ // not evidence that every pair has a feasible data path in any context.
29
+
30
+ function cmp(a, b) { return a < b ? -1 : a > b ? 1 : 0; }
31
+
32
+ function edgeEnds(e) {
33
+ if (Array.isArray(e)) return [e[0], e[1]];
34
+ return [e.from, e.to];
35
+ }
36
+
37
+ // Minimal binary min-heap of non-negative integers.
38
+ class IntHeap {
39
+ constructor() { this.a = []; }
40
+ get size() { return this.a.length; }
41
+ push(v) {
42
+ const a = this.a;
43
+ let i = a.length;
44
+ a.push(v);
45
+ while (i > 0) {
46
+ const p = (i - 1) >> 1;
47
+ if (a[p] <= v) break;
48
+ a[i] = a[p];
49
+ i = p;
50
+ }
51
+ a[i] = v;
52
+ }
53
+ pop() {
54
+ const a = this.a;
55
+ const top = a[0];
56
+ const last = a.pop();
57
+ if (a.length > 0) {
58
+ let i = 0;
59
+ const n = a.length;
60
+ for (;;) {
61
+ let c = 2 * i + 1;
62
+ if (c >= n) break;
63
+ if (c + 1 < n && a[c + 1] < a[c]) c++;
64
+ if (a[c] >= last) break;
65
+ a[i] = a[c];
66
+ i = c;
67
+ }
68
+ a[i] = last;
69
+ }
70
+ return top;
71
+ }
72
+ }
73
+
74
+ export function computeScc(graph) {
75
+ const rawNodes = (graph && graph.nodes) || [];
76
+ const rawEdges = (graph && graph.edges) || [];
77
+ const names = [...new Set(rawNodes)].sort(cmp);
78
+ const n = names.length;
79
+ const index = new Map();
80
+ for (let i = 0; i < n; i++) index.set(names[i], i);
81
+
82
+ // Adjacency over integer indexes, de-duplicated and sorted.
83
+ const adjSets = new Array(n).fill(null);
84
+ for (const e of rawEdges) {
85
+ const [f, t] = edgeEnds(e);
86
+ const fi = index.get(f);
87
+ const ti = index.get(t);
88
+ if (fi === undefined || ti === undefined) {
89
+ throw new TypeError(`scc: edge endpoint not in nodes: ${String(f)} -> ${String(t)}`);
90
+ }
91
+ (adjSets[fi] ||= new Set()).add(ti);
92
+ }
93
+ const adj = adjSets.map(s => (s ? [...s].sort((a, b) => a - b) : []));
94
+
95
+ // Iterative Tarjan with explicit call stack.
96
+ const idx = new Int32Array(n).fill(-1);
97
+ const low = new Int32Array(n);
98
+ const onStack = new Uint8Array(n);
99
+ const comp = new Int32Array(n).fill(-1);
100
+ const sccStack = [];
101
+ const callNode = [];
102
+ const callPos = [];
103
+ let counter = 0;
104
+ const rawComps = [];
105
+ for (let root = 0; root < n; root++) {
106
+ if (idx[root] !== -1) continue;
107
+ callNode.push(root); callPos.push(0);
108
+ idx[root] = low[root] = counter++;
109
+ sccStack.push(root); onStack[root] = 1;
110
+ while (callNode.length > 0) {
111
+ const top = callNode.length - 1;
112
+ const v = callNode[top];
113
+ const pos = callPos[top];
114
+ const out = adj[v];
115
+ if (pos < out.length) {
116
+ callPos[top] = pos + 1;
117
+ const w = out[pos];
118
+ if (idx[w] === -1) {
119
+ idx[w] = low[w] = counter++;
120
+ sccStack.push(w); onStack[w] = 1;
121
+ callNode.push(w); callPos.push(0);
122
+ } else if (onStack[w] && idx[w] < low[v]) {
123
+ low[v] = idx[w];
124
+ }
125
+ } else {
126
+ if (low[v] === idx[v]) {
127
+ const members = [];
128
+ let w;
129
+ do {
130
+ w = sccStack.pop();
131
+ onStack[w] = 0;
132
+ members.push(w);
133
+ } while (w !== v);
134
+ rawComps.push(members);
135
+ }
136
+ callNode.pop(); callPos.pop();
137
+ if (callNode.length > 0) {
138
+ const parent = callNode[callNode.length - 1];
139
+ if (low[v] < low[parent]) low[parent] = low[v];
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ // Canonical component ids: ascending smallest member index (indexes follow
146
+ // the sorted names, so this is order-independent).
147
+ for (const m of rawComps) m.sort((a, b) => a - b);
148
+ rawComps.sort((a, b) => a[0] - b[0]);
149
+ for (let c = 0; c < rawComps.length; c++) for (const v of rawComps[c]) comp[v] = c;
150
+
151
+ const k = rawComps.length;
152
+ const internal = Array.from({ length: k }, () => []);
153
+ const condSets = new Array(k).fill(null);
154
+ for (let v = 0; v < n; v++) {
155
+ const cv = comp[v];
156
+ for (const w of adj[v]) {
157
+ const cw = comp[w];
158
+ if (cv === cw) internal[cv].push([v, w]);
159
+ else (condSets[cv] ||= new Set()).add(cw);
160
+ }
161
+ }
162
+ const condAdj = condSets.map(s => (s ? [...s].sort((a, b) => a - b) : []));
163
+
164
+ // Kahn's algorithm with a min-heap on component id.
165
+ const indeg = new Int32Array(k);
166
+ for (let c = 0; c < k; c++) for (const d of condAdj[c]) indeg[d]++;
167
+ const heap = new IntHeap();
168
+ for (let c = 0; c < k; c++) if (indeg[c] === 0) heap.push(c);
169
+ const order = [];
170
+ while (heap.size > 0) {
171
+ const c = heap.pop();
172
+ order.push(c);
173
+ for (const d of condAdj[c]) if (--indeg[d] === 0) heap.push(d);
174
+ }
175
+
176
+ const components = new Array(k);
177
+ for (let c = 0; c < k; c++) {
178
+ const members = rawComps[c].map(i => names[i]);
179
+ const internalEdges = internal[c].map(([a, b]) => [names[a], names[b]]);
180
+ internalEdges.sort((x, y) => cmp(x[0], y[0]) || cmp(x[1], y[1]));
181
+ components[c] = {
182
+ id: c,
183
+ members,
184
+ internalEdges,
185
+ recursive: members.length > 1 || internalEdges.length > 0,
186
+ };
187
+ }
188
+ const componentOf = new Map();
189
+ for (let v = 0; v < n; v++) componentOf.set(names[v], comp[v]);
190
+ const condensationEdges = [];
191
+ for (let c = 0; c < k; c++) for (const d of condAdj[c]) condensationEdges.push([c, d]);
192
+
193
+ return { components, componentOf, condensationEdges, order };
194
+ }
195
+
196
+ // Checks a result against its input graph. Returns `{ ok, problems }`; every
197
+ // problem is a human-readable string. Verifies: each node is in exactly one
198
+ // component, members are sorted, every edge is either internal to a
199
+ // component or goes forward in `order`, internal edges match, and the
200
+ // condensation edges are exactly the cross-component edges.
201
+ export function verifyScc(graph, result) {
202
+ const problems = [];
203
+ const nodes = [...new Set((graph && graph.nodes) || [])];
204
+ const pos = new Map();
205
+ result.order.forEach((c, i) => pos.set(c, i));
206
+ if (result.order.length !== result.components.length || pos.size !== result.components.length) {
207
+ problems.push('order is not a permutation of the components');
208
+ }
209
+ const seen = new Set();
210
+ for (const c of result.components) {
211
+ for (let i = 0; i < c.members.length; i++) {
212
+ if (i > 0 && !(c.members[i - 1] < c.members[i])) problems.push(`component ${c.id} members not strictly sorted`);
213
+ if (seen.has(c.members[i])) problems.push(`node ${c.members[i]} in more than one component`);
214
+ seen.add(c.members[i]);
215
+ if (result.componentOf.get(c.members[i]) !== c.id) problems.push(`componentOf disagrees for ${c.members[i]}`);
216
+ }
217
+ }
218
+ for (const nd of nodes) if (!seen.has(nd)) problems.push(`node ${nd} missing from components`);
219
+ const cross = new Set();
220
+ const internalCount = new Map();
221
+ const edgeSeen = new Set();
222
+ for (const e of (graph && graph.edges) || []) {
223
+ const [f, t] = edgeEnds(e);
224
+ const key = `${f}\u0000${t}`;
225
+ if (edgeSeen.has(key)) continue;
226
+ edgeSeen.add(key);
227
+ const cf = result.componentOf.get(f);
228
+ const ct = result.componentOf.get(t);
229
+ if (cf === undefined || ct === undefined) { problems.push(`edge ${f} -> ${t} has an unknown endpoint`); continue; }
230
+ if (cf === ct) { internalCount.set(cf, (internalCount.get(cf) || 0) + 1); continue; }
231
+ cross.add(`${cf}\u0000${ct}`);
232
+ if (!(pos.get(cf) < pos.get(ct))) problems.push(`cross edge ${f} -> ${t} violates topological order`);
233
+ }
234
+ for (const c of result.components) {
235
+ if (c.internalEdges.length !== (internalCount.get(c.id) || 0)) problems.push(`component ${c.id} internal edge count mismatch`);
236
+ if (c.recursive !== (c.members.length > 1 || c.internalEdges.length > 0)) problems.push(`component ${c.id} recursive flag wrong`);
237
+ }
238
+ const condKeys = new Set(result.condensationEdges.map(([a, b]) => `${a}\u0000${b}`));
239
+ if (condKeys.size !== cross.size || [...cross].some(x => !condKeys.has(x))) problems.push('condensation edges differ from the cross-component edges');
240
+ return { ok: problems.length === 0, problems };
241
+ }