@clear-capabilities/agentic-security-scanner 0.183.1 → 0.185.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/bin/agentic-security.js +88 -0
- package/dist/{2a8456e0800aeee871b6.json → 0b301d45b231f144a1b4.json} +27 -9
- package/dist/1128.index.js +908 -0
- package/dist/1336.index.js +261 -112
- package/dist/1665.index.js +828 -0
- package/dist/2923.index.js +17 -9
- package/dist/9382.index.js +23 -2
- package/dist/9579.index.js +11 -4
- package/dist/agentic-security.mjs +15 -15
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/frontend/src/shell.js +12 -2
- package/package.json +27 -9
- package/src/context-intelligence/controls.js +15 -0
- package/src/context-intelligence/reachability.js +8 -2
- package/src/dataflow/CLAUDE.md +43 -1
- package/src/dataflow/backward.js +42 -7
- package/src/dataflow/cross-service-taint.js +83 -1
- package/src/dataflow/demand-query.js +929 -0
- package/src/dataflow/dep-fingerprint.js +718 -0
- package/src/dataflow/engine.js +48 -6
- package/src/dataflow/ifds-precise.js +22 -0
- package/src/dataflow/incremental-replay.js +118 -0
- package/src/dataflow/incremental-session.js +257 -0
- package/src/dataflow/incremental.js +237 -35
- package/src/dataflow/index.js +25 -94
- package/src/dataflow/summaries.js +72 -6
- package/src/dataflow/tabulation.js +80 -24
- package/src/discovery/hunter.js +6 -2
- package/src/discovery/index.js +5 -2
- package/src/engine.js +63 -3
- package/src/graph/CLAUDE.md +31 -0
- package/src/graph/adapters/artifact.js +81 -0
- package/src/graph/adapters/crossrepo.js +75 -0
- package/src/graph/adapters/dataflow.js +76 -0
- package/src/graph/adapters/provenance.js +92 -0
- package/src/graph/adapters/semantic.js +89 -0
- package/src/graph/crosswalk-builders.js +152 -0
- package/src/graph/crosswalk.js +146 -0
- package/src/graph/index.js +15 -0
- package/src/graph/observation.js +57 -0
- package/src/graph/refs.js +105 -0
- package/src/graph/registry.js +129 -0
- package/src/graph/relations.js +81 -0
- package/src/graph/scc.js +241 -0
- package/src/graph/view.js +254 -0
- package/src/language/haskell-disclosure.js +13 -1
- package/src/lineage/CLAUDE.md +71 -8
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +1 -1
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +6 -0
- package/src/lineage/boundary-contract.js +273 -0
- package/src/lineage/completion.js +205 -0
- package/src/lineage/continuation.js +140 -0
- package/src/lineage/control-credit.js +313 -0
- package/src/lineage/correlation.js +199 -0
- package/src/lineage/coverage.js +8 -1
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/cut-planner.js +548 -0
- package/src/lineage/driver.js +219 -66
- package/src/lineage/engine.js +166 -21
- package/src/lineage/export-json.js +5 -0
- package/src/lineage/graph-builder.js +84 -7
- package/src/lineage/haskell-view.js +59 -0
- package/src/lineage/index.js +3 -0
- package/src/lineage/nix-view.js +35 -0
- package/src/lineage/observation-adapters.js +17 -9
- package/src/lineage/owned-build.js +77 -0
- package/src/lineage/owned-worker-entry.js +22 -0
- package/src/lineage/owned-worker.js +95 -0
- package/src/lineage/path-evidence.js +123 -0
- package/src/lineage/path-predicates.js +529 -0
- package/src/lineage/path-query.js +189 -20
- package/src/lineage/path-store.js +191 -17
- package/src/lineage/scc-solver.js +186 -0
- package/src/lineage/smt-orchestrator.js +138 -0
- package/src/lineage/source-seeding.js +36 -2
- package/src/lineage/summaries.js +66 -4
- package/src/lineage/value-versions.js +130 -0
- package/src/lineage/witness-selection.js +95 -0
- package/src/pipeline/scan-health.js +19 -0
- package/src/posture/CLAUDE.md +13 -0
- package/src/posture/evidence-bundle.js +1 -1
- package/src/posture/witness-bundle.js +573 -0
- package/src/posture/witness-runtime.js +276 -0
- package/src/report/index.js +2 -0
|
@@ -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
|
+
}
|
package/src/graph/scc.js
ADDED
|
@@ -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
|
+
}
|