@quolu/lattice 0.52.0 → 0.52.2
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/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1502
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -162
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1167
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3141 -3126
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -625
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
package/src/dag-chain.mjs
CHANGED
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
export class DagChainError extends Error {
|
|
2
|
-
constructor(code, message) {
|
|
3
|
-
super(message);
|
|
4
|
-
this.name = 'DagChainError';
|
|
5
|
-
this.code = code;
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class DagCycleError extends DagChainError {
|
|
10
|
-
constructor(message = 'directed graph contains a cycle') {
|
|
11
|
-
super('DAG_CYCLE', message);
|
|
12
|
-
this.name = 'DagCycleError';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function defaultCompare(left, right) {
|
|
17
|
-
const leftText = String(left);
|
|
18
|
-
const rightText = String(right);
|
|
19
|
-
if (leftText < rightText) return -1;
|
|
20
|
-
if (leftText > rightText) return 1;
|
|
21
|
-
return 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function assertOptions(countCap, representativeLimit) {
|
|
25
|
-
if (!Number.isSafeInteger(countCap) || countCap < 1 || countCap >= Number.MAX_SAFE_INTEGER) {
|
|
26
|
-
throw new DagChainError(
|
|
27
|
-
'DAG_INVALID_COUNT_CAP',
|
|
28
|
-
'countCap must be a positive safe integer below Number.MAX_SAFE_INTEGER',
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
if (
|
|
32
|
-
!Number.isSafeInteger(representativeLimit)
|
|
33
|
-
|| representativeLimit < 0
|
|
34
|
-
|| representativeLimit > 8
|
|
35
|
-
) {
|
|
36
|
-
throw new DagChainError(
|
|
37
|
-
'DAG_INVALID_REPRESENTATIVE_LIMIT',
|
|
38
|
-
'representativeLimit must be a safe integer from 0 through 8',
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function normalizeGraph(nodes, edges) {
|
|
44
|
-
if (!Array.isArray(nodes) || !Array.isArray(edges)) {
|
|
45
|
-
throw new DagChainError('DAG_INVALID_GRAPH', 'nodes and edges must be arrays');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const nodeSet = new Set();
|
|
49
|
-
for (const node of nodes) {
|
|
50
|
-
if (nodeSet.has(node)) {
|
|
51
|
-
throw new DagChainError('DAG_DUPLICATE_NODE', 'nodes must be unique');
|
|
52
|
-
}
|
|
53
|
-
nodeSet.add(node);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const successors = new Map(nodes.map((node) => [node, []]));
|
|
57
|
-
const predecessors = new Map(nodes.map((node) => [node, []]));
|
|
58
|
-
const normalizedEdges = [];
|
|
59
|
-
const seenBySource = new Map();
|
|
60
|
-
|
|
61
|
-
for (const edge of edges) {
|
|
62
|
-
if (!Array.isArray(edge) || edge.length !== 2) {
|
|
63
|
-
throw new DagChainError('DAG_INVALID_EDGE', 'each edge must be a [from, to] pair');
|
|
64
|
-
}
|
|
65
|
-
const [from, to] = edge;
|
|
66
|
-
if (!nodeSet.has(from) || !nodeSet.has(to)) {
|
|
67
|
-
throw new DagChainError('DAG_DANGLING_EDGE', 'edge endpoint is absent from nodes');
|
|
68
|
-
}
|
|
69
|
-
let targets = seenBySource.get(from);
|
|
70
|
-
if (targets === undefined) {
|
|
71
|
-
targets = new Set();
|
|
72
|
-
seenBySource.set(from, targets);
|
|
73
|
-
}
|
|
74
|
-
if (targets.has(to)) continue;
|
|
75
|
-
targets.add(to);
|
|
76
|
-
successors.get(from).push(to);
|
|
77
|
-
predecessors.get(to).push(from);
|
|
78
|
-
normalizedEdges.push([from, to]);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return { successors, predecessors, edges: normalizedEdges };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function topologicalOrder(nodes, successors, predecessors) {
|
|
85
|
-
const indegree = new Map(nodes.map((node) => [node, predecessors.get(node).length]));
|
|
86
|
-
const ready = [];
|
|
87
|
-
for (const node of nodes) {
|
|
88
|
-
if (indegree.get(node) === 0) ready.push(node);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const order = [];
|
|
92
|
-
let cursor = 0;
|
|
93
|
-
while (cursor < ready.length) {
|
|
94
|
-
const node = ready[cursor];
|
|
95
|
-
cursor += 1;
|
|
96
|
-
order.push(node);
|
|
97
|
-
for (const successor of successors.get(node)) {
|
|
98
|
-
const remaining = indegree.get(successor) - 1;
|
|
99
|
-
indegree.set(successor, remaining);
|
|
100
|
-
if (remaining === 0) ready.push(successor);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (order.length !== nodes.length) throw new DagCycleError();
|
|
105
|
-
return order;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function saturatedAdd(left, right, saturationPoint) {
|
|
109
|
-
if (left >= saturationPoint || right >= saturationPoint) return saturationPoint;
|
|
110
|
-
if (left > saturationPoint - right) return saturationPoint;
|
|
111
|
-
return left + right;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function enumerateRepresentatives({
|
|
115
|
-
orderedNodes,
|
|
116
|
-
successors,
|
|
117
|
-
down,
|
|
118
|
-
up,
|
|
119
|
-
maximumDepth,
|
|
120
|
-
representativeLimit,
|
|
121
|
-
}) {
|
|
122
|
-
if (maximumDepth === 0 || representativeLimit === 0) return [];
|
|
123
|
-
|
|
124
|
-
const eligibleSuccessors = new Map();
|
|
125
|
-
for (const node of orderedNodes) {
|
|
126
|
-
eligibleSuccessors.set(
|
|
127
|
-
node,
|
|
128
|
-
successors.get(node).filter((successor) => (
|
|
129
|
-
up.get(successor) + 1 === up.get(node)
|
|
130
|
-
)),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const starts = orderedNodes.filter((node) => down.get(node) === 1 && up.get(node) === maximumDepth);
|
|
135
|
-
const representatives = [];
|
|
136
|
-
|
|
137
|
-
for (const start of starts) {
|
|
138
|
-
const path = [start];
|
|
139
|
-
const frames = [{ node: start, successorIndex: 0 }];
|
|
140
|
-
|
|
141
|
-
while (frames.length > 0 && representatives.length < representativeLimit) {
|
|
142
|
-
const frame = frames[frames.length - 1];
|
|
143
|
-
if (up.get(frame.node) === 1) {
|
|
144
|
-
representatives.push([...path]);
|
|
145
|
-
frames.pop();
|
|
146
|
-
path.pop();
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const choices = eligibleSuccessors.get(frame.node);
|
|
151
|
-
if (frame.successorIndex >= choices.length) {
|
|
152
|
-
frames.pop();
|
|
153
|
-
path.pop();
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const successor = choices[frame.successorIndex];
|
|
158
|
-
frame.successorIndex += 1;
|
|
159
|
-
path.push(successor);
|
|
160
|
-
frames.push({ node: successor, successorIndex: 0 });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (representatives.length >= representativeLimit) break;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return representatives;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function analyzeDagChains(
|
|
170
|
-
nodes,
|
|
171
|
-
edges,
|
|
172
|
-
{
|
|
173
|
-
countCap = 1_000_000,
|
|
174
|
-
representativeLimit = 8,
|
|
175
|
-
compare = defaultCompare,
|
|
176
|
-
} = {},
|
|
177
|
-
) {
|
|
178
|
-
assertOptions(countCap, representativeLimit);
|
|
179
|
-
if (typeof compare !== 'function') {
|
|
180
|
-
throw new DagChainError('DAG_INVALID_COMPARATOR', 'compare must be a function');
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const graph = normalizeGraph(nodes, edges);
|
|
184
|
-
const order = topologicalOrder(nodes, graph.successors, graph.predecessors);
|
|
185
|
-
const saturationPoint = countCap + 1;
|
|
186
|
-
const down = new Map();
|
|
187
|
-
const waysDown = new Map();
|
|
188
|
-
|
|
189
|
-
for (const node of order) {
|
|
190
|
-
let depth = 1;
|
|
191
|
-
let ways = 1;
|
|
192
|
-
for (const predecessor of graph.predecessors.get(node)) {
|
|
193
|
-
const candidateDepth = down.get(predecessor) + 1;
|
|
194
|
-
if (candidateDepth > depth) {
|
|
195
|
-
depth = candidateDepth;
|
|
196
|
-
ways = waysDown.get(predecessor);
|
|
197
|
-
} else if (candidateDepth === depth) {
|
|
198
|
-
ways = saturatedAdd(ways, waysDown.get(predecessor), saturationPoint);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
down.set(node, depth);
|
|
202
|
-
waysDown.set(node, ways);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const up = new Map();
|
|
206
|
-
const waysUp = new Map();
|
|
207
|
-
for (let index = order.length - 1; index >= 0; index -= 1) {
|
|
208
|
-
const node = order[index];
|
|
209
|
-
let depth = 1;
|
|
210
|
-
let ways = 1;
|
|
211
|
-
for (const successor of graph.successors.get(node)) {
|
|
212
|
-
const candidateDepth = up.get(successor) + 1;
|
|
213
|
-
if (candidateDepth > depth) {
|
|
214
|
-
depth = candidateDepth;
|
|
215
|
-
ways = waysUp.get(successor);
|
|
216
|
-
} else if (candidateDepth === depth) {
|
|
217
|
-
ways = saturatedAdd(ways, waysUp.get(successor), saturationPoint);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
up.set(node, depth);
|
|
221
|
-
waysUp.set(node, ways);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
let maximumDepth = 0;
|
|
225
|
-
for (const node of nodes) maximumDepth = Math.max(maximumDepth, down.get(node) ?? 0);
|
|
226
|
-
|
|
227
|
-
let saturatedCount = 0;
|
|
228
|
-
for (const node of nodes) {
|
|
229
|
-
if (down.get(node) === maximumDepth) {
|
|
230
|
-
saturatedCount = saturatedAdd(saturatedCount, waysDown.get(node), saturationPoint);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const orderedNodes = [...nodes].sort(compare);
|
|
235
|
-
for (const values of graph.successors.values()) values.sort(compare);
|
|
236
|
-
const longestChainNodes = orderedNodes.filter((node) => (
|
|
237
|
-
down.get(node) + up.get(node) - 1 === maximumDepth
|
|
238
|
-
));
|
|
239
|
-
const longestChainEdges = graph.edges
|
|
240
|
-
.filter(([from, to]) => down.get(from) + up.get(to) === maximumDepth)
|
|
241
|
-
.sort((left, right) => compare(left[0], right[0]) || compare(left[1], right[1]));
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
maximumDepth,
|
|
245
|
-
longestChainNodes,
|
|
246
|
-
longestChainEdges,
|
|
247
|
-
longestChainCount: {
|
|
248
|
-
count: Math.min(saturatedCount, countCap),
|
|
249
|
-
overflow: saturatedCount === saturationPoint,
|
|
250
|
-
},
|
|
251
|
-
representativeChains: enumerateRepresentatives({
|
|
252
|
-
orderedNodes,
|
|
253
|
-
successors: graph.successors,
|
|
254
|
-
down,
|
|
255
|
-
up,
|
|
256
|
-
maximumDepth,
|
|
257
|
-
representativeLimit,
|
|
258
|
-
}),
|
|
259
|
-
topologicalOrder: order,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
1
|
+
export class DagChainError extends Error {
|
|
2
|
+
constructor(code, message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'DagChainError';
|
|
5
|
+
this.code = code;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class DagCycleError extends DagChainError {
|
|
10
|
+
constructor(message = 'directed graph contains a cycle') {
|
|
11
|
+
super('DAG_CYCLE', message);
|
|
12
|
+
this.name = 'DagCycleError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function defaultCompare(left, right) {
|
|
17
|
+
const leftText = String(left);
|
|
18
|
+
const rightText = String(right);
|
|
19
|
+
if (leftText < rightText) return -1;
|
|
20
|
+
if (leftText > rightText) return 1;
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertOptions(countCap, representativeLimit) {
|
|
25
|
+
if (!Number.isSafeInteger(countCap) || countCap < 1 || countCap >= Number.MAX_SAFE_INTEGER) {
|
|
26
|
+
throw new DagChainError(
|
|
27
|
+
'DAG_INVALID_COUNT_CAP',
|
|
28
|
+
'countCap must be a positive safe integer below Number.MAX_SAFE_INTEGER',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (
|
|
32
|
+
!Number.isSafeInteger(representativeLimit)
|
|
33
|
+
|| representativeLimit < 0
|
|
34
|
+
|| representativeLimit > 8
|
|
35
|
+
) {
|
|
36
|
+
throw new DagChainError(
|
|
37
|
+
'DAG_INVALID_REPRESENTATIVE_LIMIT',
|
|
38
|
+
'representativeLimit must be a safe integer from 0 through 8',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeGraph(nodes, edges) {
|
|
44
|
+
if (!Array.isArray(nodes) || !Array.isArray(edges)) {
|
|
45
|
+
throw new DagChainError('DAG_INVALID_GRAPH', 'nodes and edges must be arrays');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nodeSet = new Set();
|
|
49
|
+
for (const node of nodes) {
|
|
50
|
+
if (nodeSet.has(node)) {
|
|
51
|
+
throw new DagChainError('DAG_DUPLICATE_NODE', 'nodes must be unique');
|
|
52
|
+
}
|
|
53
|
+
nodeSet.add(node);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const successors = new Map(nodes.map((node) => [node, []]));
|
|
57
|
+
const predecessors = new Map(nodes.map((node) => [node, []]));
|
|
58
|
+
const normalizedEdges = [];
|
|
59
|
+
const seenBySource = new Map();
|
|
60
|
+
|
|
61
|
+
for (const edge of edges) {
|
|
62
|
+
if (!Array.isArray(edge) || edge.length !== 2) {
|
|
63
|
+
throw new DagChainError('DAG_INVALID_EDGE', 'each edge must be a [from, to] pair');
|
|
64
|
+
}
|
|
65
|
+
const [from, to] = edge;
|
|
66
|
+
if (!nodeSet.has(from) || !nodeSet.has(to)) {
|
|
67
|
+
throw new DagChainError('DAG_DANGLING_EDGE', 'edge endpoint is absent from nodes');
|
|
68
|
+
}
|
|
69
|
+
let targets = seenBySource.get(from);
|
|
70
|
+
if (targets === undefined) {
|
|
71
|
+
targets = new Set();
|
|
72
|
+
seenBySource.set(from, targets);
|
|
73
|
+
}
|
|
74
|
+
if (targets.has(to)) continue;
|
|
75
|
+
targets.add(to);
|
|
76
|
+
successors.get(from).push(to);
|
|
77
|
+
predecessors.get(to).push(from);
|
|
78
|
+
normalizedEdges.push([from, to]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { successors, predecessors, edges: normalizedEdges };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function topologicalOrder(nodes, successors, predecessors) {
|
|
85
|
+
const indegree = new Map(nodes.map((node) => [node, predecessors.get(node).length]));
|
|
86
|
+
const ready = [];
|
|
87
|
+
for (const node of nodes) {
|
|
88
|
+
if (indegree.get(node) === 0) ready.push(node);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const order = [];
|
|
92
|
+
let cursor = 0;
|
|
93
|
+
while (cursor < ready.length) {
|
|
94
|
+
const node = ready[cursor];
|
|
95
|
+
cursor += 1;
|
|
96
|
+
order.push(node);
|
|
97
|
+
for (const successor of successors.get(node)) {
|
|
98
|
+
const remaining = indegree.get(successor) - 1;
|
|
99
|
+
indegree.set(successor, remaining);
|
|
100
|
+
if (remaining === 0) ready.push(successor);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (order.length !== nodes.length) throw new DagCycleError();
|
|
105
|
+
return order;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function saturatedAdd(left, right, saturationPoint) {
|
|
109
|
+
if (left >= saturationPoint || right >= saturationPoint) return saturationPoint;
|
|
110
|
+
if (left > saturationPoint - right) return saturationPoint;
|
|
111
|
+
return left + right;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function enumerateRepresentatives({
|
|
115
|
+
orderedNodes,
|
|
116
|
+
successors,
|
|
117
|
+
down,
|
|
118
|
+
up,
|
|
119
|
+
maximumDepth,
|
|
120
|
+
representativeLimit,
|
|
121
|
+
}) {
|
|
122
|
+
if (maximumDepth === 0 || representativeLimit === 0) return [];
|
|
123
|
+
|
|
124
|
+
const eligibleSuccessors = new Map();
|
|
125
|
+
for (const node of orderedNodes) {
|
|
126
|
+
eligibleSuccessors.set(
|
|
127
|
+
node,
|
|
128
|
+
successors.get(node).filter((successor) => (
|
|
129
|
+
up.get(successor) + 1 === up.get(node)
|
|
130
|
+
)),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const starts = orderedNodes.filter((node) => down.get(node) === 1 && up.get(node) === maximumDepth);
|
|
135
|
+
const representatives = [];
|
|
136
|
+
|
|
137
|
+
for (const start of starts) {
|
|
138
|
+
const path = [start];
|
|
139
|
+
const frames = [{ node: start, successorIndex: 0 }];
|
|
140
|
+
|
|
141
|
+
while (frames.length > 0 && representatives.length < representativeLimit) {
|
|
142
|
+
const frame = frames[frames.length - 1];
|
|
143
|
+
if (up.get(frame.node) === 1) {
|
|
144
|
+
representatives.push([...path]);
|
|
145
|
+
frames.pop();
|
|
146
|
+
path.pop();
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const choices = eligibleSuccessors.get(frame.node);
|
|
151
|
+
if (frame.successorIndex >= choices.length) {
|
|
152
|
+
frames.pop();
|
|
153
|
+
path.pop();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const successor = choices[frame.successorIndex];
|
|
158
|
+
frame.successorIndex += 1;
|
|
159
|
+
path.push(successor);
|
|
160
|
+
frames.push({ node: successor, successorIndex: 0 });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (representatives.length >= representativeLimit) break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return representatives;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function analyzeDagChains(
|
|
170
|
+
nodes,
|
|
171
|
+
edges,
|
|
172
|
+
{
|
|
173
|
+
countCap = 1_000_000,
|
|
174
|
+
representativeLimit = 8,
|
|
175
|
+
compare = defaultCompare,
|
|
176
|
+
} = {},
|
|
177
|
+
) {
|
|
178
|
+
assertOptions(countCap, representativeLimit);
|
|
179
|
+
if (typeof compare !== 'function') {
|
|
180
|
+
throw new DagChainError('DAG_INVALID_COMPARATOR', 'compare must be a function');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const graph = normalizeGraph(nodes, edges);
|
|
184
|
+
const order = topologicalOrder(nodes, graph.successors, graph.predecessors);
|
|
185
|
+
const saturationPoint = countCap + 1;
|
|
186
|
+
const down = new Map();
|
|
187
|
+
const waysDown = new Map();
|
|
188
|
+
|
|
189
|
+
for (const node of order) {
|
|
190
|
+
let depth = 1;
|
|
191
|
+
let ways = 1;
|
|
192
|
+
for (const predecessor of graph.predecessors.get(node)) {
|
|
193
|
+
const candidateDepth = down.get(predecessor) + 1;
|
|
194
|
+
if (candidateDepth > depth) {
|
|
195
|
+
depth = candidateDepth;
|
|
196
|
+
ways = waysDown.get(predecessor);
|
|
197
|
+
} else if (candidateDepth === depth) {
|
|
198
|
+
ways = saturatedAdd(ways, waysDown.get(predecessor), saturationPoint);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
down.set(node, depth);
|
|
202
|
+
waysDown.set(node, ways);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const up = new Map();
|
|
206
|
+
const waysUp = new Map();
|
|
207
|
+
for (let index = order.length - 1; index >= 0; index -= 1) {
|
|
208
|
+
const node = order[index];
|
|
209
|
+
let depth = 1;
|
|
210
|
+
let ways = 1;
|
|
211
|
+
for (const successor of graph.successors.get(node)) {
|
|
212
|
+
const candidateDepth = up.get(successor) + 1;
|
|
213
|
+
if (candidateDepth > depth) {
|
|
214
|
+
depth = candidateDepth;
|
|
215
|
+
ways = waysUp.get(successor);
|
|
216
|
+
} else if (candidateDepth === depth) {
|
|
217
|
+
ways = saturatedAdd(ways, waysUp.get(successor), saturationPoint);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
up.set(node, depth);
|
|
221
|
+
waysUp.set(node, ways);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let maximumDepth = 0;
|
|
225
|
+
for (const node of nodes) maximumDepth = Math.max(maximumDepth, down.get(node) ?? 0);
|
|
226
|
+
|
|
227
|
+
let saturatedCount = 0;
|
|
228
|
+
for (const node of nodes) {
|
|
229
|
+
if (down.get(node) === maximumDepth) {
|
|
230
|
+
saturatedCount = saturatedAdd(saturatedCount, waysDown.get(node), saturationPoint);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const orderedNodes = [...nodes].sort(compare);
|
|
235
|
+
for (const values of graph.successors.values()) values.sort(compare);
|
|
236
|
+
const longestChainNodes = orderedNodes.filter((node) => (
|
|
237
|
+
down.get(node) + up.get(node) - 1 === maximumDepth
|
|
238
|
+
));
|
|
239
|
+
const longestChainEdges = graph.edges
|
|
240
|
+
.filter(([from, to]) => down.get(from) + up.get(to) === maximumDepth)
|
|
241
|
+
.sort((left, right) => compare(left[0], right[0]) || compare(left[1], right[1]));
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
maximumDepth,
|
|
245
|
+
longestChainNodes,
|
|
246
|
+
longestChainEdges,
|
|
247
|
+
longestChainCount: {
|
|
248
|
+
count: Math.min(saturatedCount, countCap),
|
|
249
|
+
overflow: saturatedCount === saturationPoint,
|
|
250
|
+
},
|
|
251
|
+
representativeChains: enumerateRepresentatives({
|
|
252
|
+
orderedNodes,
|
|
253
|
+
successors: graph.successors,
|
|
254
|
+
down,
|
|
255
|
+
up,
|
|
256
|
+
maximumDepth,
|
|
257
|
+
representativeLimit,
|
|
258
|
+
}),
|
|
259
|
+
topologicalOrder: order,
|
|
260
|
+
};
|
|
261
|
+
}
|