@quolu/lattice 0.52.2 → 0.52.4
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/package.json +1 -1
- 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-hub-protocol.mjs +198 -0
- 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/git-process.mjs +72 -0
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1057
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +699 -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 -1506
- 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 -524
- package/src/runtime-cli.mjs +4588 -4588
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -604
- 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 -166
- 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 -481
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -294
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -838
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1192
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1160
- 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 -1171
- 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 +3143 -3141
- 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 -629
- 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 +4360 -4322
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
package/src/todo-chain.mjs
CHANGED
|
@@ -1,178 +1,178 @@
|
|
|
1
|
-
import { analyzeDagChains, DagCycleError } from './dag-chain.mjs';
|
|
2
|
-
|
|
3
|
-
const ASSUMPTIONS = Object.freeze({
|
|
4
|
-
unit_duration: true,
|
|
5
|
-
capacity_ignored: true,
|
|
6
|
-
conflict_ignored: true,
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
export class TodoChainProjectionError extends Error {
|
|
10
|
-
constructor(code, message, options) {
|
|
11
|
-
super(message, options);
|
|
12
|
-
this.name = 'TodoChainProjectionError';
|
|
13
|
-
this.code = code;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class TodoChainCycleError extends TodoChainProjectionError {
|
|
18
|
-
constructor(options) {
|
|
19
|
-
super('TODO_CHAIN_CYCLE', 'merged todo topology contains a cycle', options);
|
|
20
|
-
this.name = 'TodoChainCycleError';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function fail(code, message) {
|
|
25
|
-
throw new TodoChainProjectionError(code, message);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function isPlainObject(value) {
|
|
29
|
-
return value !== null
|
|
30
|
-
&& typeof value === 'object'
|
|
31
|
-
&& !Array.isArray(value)
|
|
32
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizeNodeRef(value, context) {
|
|
36
|
-
if (!isPlainObject(value)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `${context} must be a node ref`);
|
|
37
|
-
const { project_id: projectId, plan_key: planKey, task_id: taskId } = value;
|
|
38
|
-
if (typeof projectId !== 'string' || typeof planKey !== 'string' || typeof taskId !== 'string') {
|
|
39
|
-
fail('TODO_CHAIN_INVALID_TOPOLOGY', `${context} node ref fields must be strings`);
|
|
40
|
-
}
|
|
41
|
-
return { project_id: projectId, plan_key: planKey, task_id: taskId };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function nodeKey(ref) {
|
|
45
|
-
return JSON.stringify([ref.project_id, ref.plan_key, ref.task_id]);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function compareRefs(left, right) {
|
|
49
|
-
for (const field of ['project_id', 'plan_key', 'task_id']) {
|
|
50
|
-
if (left[field] < right[field]) return -1;
|
|
51
|
-
if (left[field] > right[field]) return 1;
|
|
52
|
-
}
|
|
53
|
-
return 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function requireArray(value, context) {
|
|
57
|
-
if (!Array.isArray(value)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `${context} must be an array`);
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function normalizeOptions(options) {
|
|
62
|
-
if (options === undefined) options = {};
|
|
63
|
-
if (!isPlainObject(options)) {
|
|
64
|
-
fail('TODO_CHAIN_INVALID_OPTIONS', 'options must be an object');
|
|
65
|
-
}
|
|
66
|
-
const allowedKeys = new Set(['countCap', 'representativeLimit']);
|
|
67
|
-
for (const key of Reflect.ownKeys(options)) {
|
|
68
|
-
if (typeof key !== 'string' || !allowedKeys.has(key)) {
|
|
69
|
-
fail('TODO_CHAIN_INVALID_OPTIONS', `unknown option: ${String(key)}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const countCap = Object.hasOwn(options, 'countCap') ? options.countCap : 1_000_000;
|
|
74
|
-
const representativeLimit = Object.hasOwn(options, 'representativeLimit')
|
|
75
|
-
? options.representativeLimit
|
|
76
|
-
: 8;
|
|
77
|
-
if (!Number.isSafeInteger(countCap) || countCap < 1 || countCap >= Number.MAX_SAFE_INTEGER) {
|
|
78
|
-
fail(
|
|
79
|
-
'TODO_CHAIN_INVALID_OPTIONS',
|
|
80
|
-
'countCap must be a safe integer from 1 through Number.MAX_SAFE_INTEGER - 1',
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
if (
|
|
84
|
-
!Number.isSafeInteger(representativeLimit)
|
|
85
|
-
|| representativeLimit < 0
|
|
86
|
-
|| representativeLimit > 8
|
|
87
|
-
) {
|
|
88
|
-
fail('TODO_CHAIN_INVALID_OPTIONS', 'representativeLimit must be a safe integer from 0 through 8');
|
|
89
|
-
}
|
|
90
|
-
return { countCap, representativeLimit };
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function projectTodoChainV1(
|
|
94
|
-
mergedTodoTopology,
|
|
95
|
-
options,
|
|
96
|
-
) {
|
|
97
|
-
const { countCap, representativeLimit } = normalizeOptions(options);
|
|
98
|
-
if (!isPlainObject(mergedTodoTopology)) {
|
|
99
|
-
fail('TODO_CHAIN_INVALID_TOPOLOGY', 'mergedTodoTopology must be an object');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const nodes = requireArray(mergedTodoTopology.nodes, 'nodes');
|
|
103
|
-
const hardEdges = requireArray(mergedTodoTopology.hard_edges, 'hard_edges');
|
|
104
|
-
const joins = requireArray(mergedTodoTopology.joins, 'joins');
|
|
105
|
-
const refsByKey = new Map();
|
|
106
|
-
|
|
107
|
-
for (let index = 0; index < nodes.length; index += 1) {
|
|
108
|
-
const ref = normalizeNodeRef(nodes[index], `nodes[${index}]`);
|
|
109
|
-
const key = nodeKey(ref);
|
|
110
|
-
if (refsByKey.has(key)) fail('TODO_CHAIN_DUPLICATE_NODE', `duplicate node ref at nodes[${index}]`);
|
|
111
|
-
refsByKey.set(key, ref);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const edgeKeys = new Set();
|
|
115
|
-
const edges = [];
|
|
116
|
-
const addEdge = (fromValue, toValue, context) => {
|
|
117
|
-
const fromRef = normalizeNodeRef(fromValue, `${context}.from`);
|
|
118
|
-
const toRef = normalizeNodeRef(toValue, `${context}.to`);
|
|
119
|
-
const from = nodeKey(fromRef);
|
|
120
|
-
const to = nodeKey(toRef);
|
|
121
|
-
if (!refsByKey.has(from) || !refsByKey.has(to)) {
|
|
122
|
-
fail('TODO_CHAIN_DANGLING_EDGE', `${context} references a node absent from nodes`);
|
|
123
|
-
}
|
|
124
|
-
const edgeKey = JSON.stringify([from, to]);
|
|
125
|
-
if (edgeKeys.has(edgeKey)) return;
|
|
126
|
-
edgeKeys.add(edgeKey);
|
|
127
|
-
edges.push([from, to]);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
for (let index = 0; index < hardEdges.length; index += 1) {
|
|
131
|
-
const edge = hardEdges[index];
|
|
132
|
-
if (!isPlainObject(edge)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `hard_edges[${index}] must be an object`);
|
|
133
|
-
addEdge(edge.from, edge.to, `hard_edges[${index}]`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
for (let joinIndex = 0; joinIndex < joins.length; joinIndex += 1) {
|
|
137
|
-
const join = joins[joinIndex];
|
|
138
|
-
if (!isPlainObject(join)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `joins[${joinIndex}] must be an object`);
|
|
139
|
-
const after = requireArray(join.after, `joins[${joinIndex}].after`);
|
|
140
|
-
for (let afterIndex = 0; afterIndex < after.length; afterIndex += 1) {
|
|
141
|
-
addEdge(
|
|
142
|
-
after[afterIndex],
|
|
143
|
-
join.before,
|
|
144
|
-
`joins[${joinIndex}].after[${afterIndex}]`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let analysis;
|
|
150
|
-
try {
|
|
151
|
-
analysis = analyzeDagChains([...refsByKey.keys()], edges, {
|
|
152
|
-
countCap,
|
|
153
|
-
representativeLimit,
|
|
154
|
-
compare: (left, right) => compareRefs(refsByKey.get(left), refsByKey.get(right)),
|
|
155
|
-
});
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (error instanceof DagCycleError) throw new TodoChainCycleError({ cause: error });
|
|
158
|
-
throw error;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const toRef = (key) => ({ ...refsByKey.get(key) });
|
|
162
|
-
return {
|
|
163
|
-
schema: 'lattice.todo_chain.v1',
|
|
164
|
-
maximum_dependency_depth: analysis.maximumDepth,
|
|
165
|
-
longest_chain_node_refs: analysis.longestChainNodes.map(toRef),
|
|
166
|
-
longest_chain_edges: analysis.longestChainEdges.map(([from, to]) => ({
|
|
167
|
-
from: toRef(from),
|
|
168
|
-
to: toRef(to),
|
|
169
|
-
})),
|
|
170
|
-
longest_chain_count: analysis.longestChainCount,
|
|
171
|
-
limits: {
|
|
172
|
-
count_cap: countCap,
|
|
173
|
-
representative_limit: representativeLimit,
|
|
174
|
-
},
|
|
175
|
-
representative_chains: analysis.representativeChains.map((chain) => chain.map(toRef)),
|
|
176
|
-
assumptions: { ...ASSUMPTIONS },
|
|
177
|
-
};
|
|
178
|
-
}
|
|
1
|
+
import { analyzeDagChains, DagCycleError } from './dag-chain.mjs';
|
|
2
|
+
|
|
3
|
+
const ASSUMPTIONS = Object.freeze({
|
|
4
|
+
unit_duration: true,
|
|
5
|
+
capacity_ignored: true,
|
|
6
|
+
conflict_ignored: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export class TodoChainProjectionError extends Error {
|
|
10
|
+
constructor(code, message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = 'TodoChainProjectionError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class TodoChainCycleError extends TodoChainProjectionError {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
super('TODO_CHAIN_CYCLE', 'merged todo topology contains a cycle', options);
|
|
20
|
+
this.name = 'TodoChainCycleError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail(code, message) {
|
|
25
|
+
throw new TodoChainProjectionError(code, message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPlainObject(value) {
|
|
29
|
+
return value !== null
|
|
30
|
+
&& typeof value === 'object'
|
|
31
|
+
&& !Array.isArray(value)
|
|
32
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeNodeRef(value, context) {
|
|
36
|
+
if (!isPlainObject(value)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `${context} must be a node ref`);
|
|
37
|
+
const { project_id: projectId, plan_key: planKey, task_id: taskId } = value;
|
|
38
|
+
if (typeof projectId !== 'string' || typeof planKey !== 'string' || typeof taskId !== 'string') {
|
|
39
|
+
fail('TODO_CHAIN_INVALID_TOPOLOGY', `${context} node ref fields must be strings`);
|
|
40
|
+
}
|
|
41
|
+
return { project_id: projectId, plan_key: planKey, task_id: taskId };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function nodeKey(ref) {
|
|
45
|
+
return JSON.stringify([ref.project_id, ref.plan_key, ref.task_id]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareRefs(left, right) {
|
|
49
|
+
for (const field of ['project_id', 'plan_key', 'task_id']) {
|
|
50
|
+
if (left[field] < right[field]) return -1;
|
|
51
|
+
if (left[field] > right[field]) return 1;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requireArray(value, context) {
|
|
57
|
+
if (!Array.isArray(value)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `${context} must be an array`);
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeOptions(options) {
|
|
62
|
+
if (options === undefined) options = {};
|
|
63
|
+
if (!isPlainObject(options)) {
|
|
64
|
+
fail('TODO_CHAIN_INVALID_OPTIONS', 'options must be an object');
|
|
65
|
+
}
|
|
66
|
+
const allowedKeys = new Set(['countCap', 'representativeLimit']);
|
|
67
|
+
for (const key of Reflect.ownKeys(options)) {
|
|
68
|
+
if (typeof key !== 'string' || !allowedKeys.has(key)) {
|
|
69
|
+
fail('TODO_CHAIN_INVALID_OPTIONS', `unknown option: ${String(key)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const countCap = Object.hasOwn(options, 'countCap') ? options.countCap : 1_000_000;
|
|
74
|
+
const representativeLimit = Object.hasOwn(options, 'representativeLimit')
|
|
75
|
+
? options.representativeLimit
|
|
76
|
+
: 8;
|
|
77
|
+
if (!Number.isSafeInteger(countCap) || countCap < 1 || countCap >= Number.MAX_SAFE_INTEGER) {
|
|
78
|
+
fail(
|
|
79
|
+
'TODO_CHAIN_INVALID_OPTIONS',
|
|
80
|
+
'countCap must be a safe integer from 1 through Number.MAX_SAFE_INTEGER - 1',
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
!Number.isSafeInteger(representativeLimit)
|
|
85
|
+
|| representativeLimit < 0
|
|
86
|
+
|| representativeLimit > 8
|
|
87
|
+
) {
|
|
88
|
+
fail('TODO_CHAIN_INVALID_OPTIONS', 'representativeLimit must be a safe integer from 0 through 8');
|
|
89
|
+
}
|
|
90
|
+
return { countCap, representativeLimit };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function projectTodoChainV1(
|
|
94
|
+
mergedTodoTopology,
|
|
95
|
+
options,
|
|
96
|
+
) {
|
|
97
|
+
const { countCap, representativeLimit } = normalizeOptions(options);
|
|
98
|
+
if (!isPlainObject(mergedTodoTopology)) {
|
|
99
|
+
fail('TODO_CHAIN_INVALID_TOPOLOGY', 'mergedTodoTopology must be an object');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const nodes = requireArray(mergedTodoTopology.nodes, 'nodes');
|
|
103
|
+
const hardEdges = requireArray(mergedTodoTopology.hard_edges, 'hard_edges');
|
|
104
|
+
const joins = requireArray(mergedTodoTopology.joins, 'joins');
|
|
105
|
+
const refsByKey = new Map();
|
|
106
|
+
|
|
107
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
108
|
+
const ref = normalizeNodeRef(nodes[index], `nodes[${index}]`);
|
|
109
|
+
const key = nodeKey(ref);
|
|
110
|
+
if (refsByKey.has(key)) fail('TODO_CHAIN_DUPLICATE_NODE', `duplicate node ref at nodes[${index}]`);
|
|
111
|
+
refsByKey.set(key, ref);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const edgeKeys = new Set();
|
|
115
|
+
const edges = [];
|
|
116
|
+
const addEdge = (fromValue, toValue, context) => {
|
|
117
|
+
const fromRef = normalizeNodeRef(fromValue, `${context}.from`);
|
|
118
|
+
const toRef = normalizeNodeRef(toValue, `${context}.to`);
|
|
119
|
+
const from = nodeKey(fromRef);
|
|
120
|
+
const to = nodeKey(toRef);
|
|
121
|
+
if (!refsByKey.has(from) || !refsByKey.has(to)) {
|
|
122
|
+
fail('TODO_CHAIN_DANGLING_EDGE', `${context} references a node absent from nodes`);
|
|
123
|
+
}
|
|
124
|
+
const edgeKey = JSON.stringify([from, to]);
|
|
125
|
+
if (edgeKeys.has(edgeKey)) return;
|
|
126
|
+
edgeKeys.add(edgeKey);
|
|
127
|
+
edges.push([from, to]);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (let index = 0; index < hardEdges.length; index += 1) {
|
|
131
|
+
const edge = hardEdges[index];
|
|
132
|
+
if (!isPlainObject(edge)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `hard_edges[${index}] must be an object`);
|
|
133
|
+
addEdge(edge.from, edge.to, `hard_edges[${index}]`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (let joinIndex = 0; joinIndex < joins.length; joinIndex += 1) {
|
|
137
|
+
const join = joins[joinIndex];
|
|
138
|
+
if (!isPlainObject(join)) fail('TODO_CHAIN_INVALID_TOPOLOGY', `joins[${joinIndex}] must be an object`);
|
|
139
|
+
const after = requireArray(join.after, `joins[${joinIndex}].after`);
|
|
140
|
+
for (let afterIndex = 0; afterIndex < after.length; afterIndex += 1) {
|
|
141
|
+
addEdge(
|
|
142
|
+
after[afterIndex],
|
|
143
|
+
join.before,
|
|
144
|
+
`joins[${joinIndex}].after[${afterIndex}]`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let analysis;
|
|
150
|
+
try {
|
|
151
|
+
analysis = analyzeDagChains([...refsByKey.keys()], edges, {
|
|
152
|
+
countCap,
|
|
153
|
+
representativeLimit,
|
|
154
|
+
compare: (left, right) => compareRefs(refsByKey.get(left), refsByKey.get(right)),
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof DagCycleError) throw new TodoChainCycleError({ cause: error });
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const toRef = (key) => ({ ...refsByKey.get(key) });
|
|
162
|
+
return {
|
|
163
|
+
schema: 'lattice.todo_chain.v1',
|
|
164
|
+
maximum_dependency_depth: analysis.maximumDepth,
|
|
165
|
+
longest_chain_node_refs: analysis.longestChainNodes.map(toRef),
|
|
166
|
+
longest_chain_edges: analysis.longestChainEdges.map(([from, to]) => ({
|
|
167
|
+
from: toRef(from),
|
|
168
|
+
to: toRef(to),
|
|
169
|
+
})),
|
|
170
|
+
longest_chain_count: analysis.longestChainCount,
|
|
171
|
+
limits: {
|
|
172
|
+
count_cap: countCap,
|
|
173
|
+
representative_limit: representativeLimit,
|
|
174
|
+
},
|
|
175
|
+
representative_chains: analysis.representativeChains.map((chain) => chain.map(toRef)),
|
|
176
|
+
assumptions: { ...ASSUMPTIONS },
|
|
177
|
+
};
|
|
178
|
+
}
|