@unson/brainbase-mcp 0.3.1 → 0.4.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.
- package/README.md +154 -450
- package/contracts/judgment-dag/digest.json +32 -0
- package/contracts/judgment-dag/fixture.json +21 -0
- package/contracts/judgment-dag/schema.json +80 -0
- package/contracts/judgment-dag/source-lock.json +25 -0
- package/contracts/public-message-candidate.schema.json +143 -0
- package/dist/cli.js +29 -6
- package/dist/judgment-autonomy.d.ts +62 -0
- package/dist/judgment-autonomy.js +301 -0
- package/dist/judgment-dag-core.d.ts +113 -0
- package/dist/judgment-dag-core.js +439 -0
- package/dist/judgment-dag-runner.d.ts +70 -0
- package/dist/judgment-dag-runner.js +325 -0
- package/dist/judgment-dag.d.ts +10 -0
- package/dist/judgment-dag.js +8 -0
- package/dist/judgment-host.d.ts +21 -0
- package/dist/judgment-host.js +180 -0
- package/docs/architecture/judgment-dag-core.md +313 -0
- package/docs/management/judgment-dag-milestones.md +143 -0
- package/package.json +30 -4
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared semantic contract for the local Judgment DAG kernel.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately contains only the contract and preflight checks.
|
|
5
|
+
* Runners, artifact stores, replay, and public MCP/CLI adapters belong to
|
|
6
|
+
* later milestones and must not be coupled to this core surface.
|
|
7
|
+
*/
|
|
8
|
+
export const JUDGMENT_DAG_NODE_TYPES = Object.freeze([
|
|
9
|
+
'observation',
|
|
10
|
+
'judgment',
|
|
11
|
+
'decision',
|
|
12
|
+
'resource',
|
|
13
|
+
'execution',
|
|
14
|
+
'outcome',
|
|
15
|
+
'evaluation'
|
|
16
|
+
]);
|
|
17
|
+
export const JUDGMENT_DAG_LAYERS = Object.freeze([
|
|
18
|
+
'context',
|
|
19
|
+
'judgment',
|
|
20
|
+
'resource',
|
|
21
|
+
'execution',
|
|
22
|
+
'evaluation'
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* The accepted ontology has five layers. An outcome is the result produced
|
|
26
|
+
* by the Execution DAG; it is not a sixth layer of its own.
|
|
27
|
+
*/
|
|
28
|
+
export const JUDGMENT_DAG_NODE_TYPE_TO_LAYER = Object.freeze({
|
|
29
|
+
observation: 'context',
|
|
30
|
+
judgment: 'judgment',
|
|
31
|
+
decision: 'judgment',
|
|
32
|
+
resource: 'resource',
|
|
33
|
+
execution: 'execution',
|
|
34
|
+
outcome: 'execution',
|
|
35
|
+
evaluation: 'evaluation'
|
|
36
|
+
});
|
|
37
|
+
export const JUDGMENT_DAG_SCOPE_TYPES = Object.freeze([
|
|
38
|
+
'personal',
|
|
39
|
+
'project',
|
|
40
|
+
'organization'
|
|
41
|
+
]);
|
|
42
|
+
export const JUDGMENT_DAG_RUNNER_TYPES = Object.freeze([
|
|
43
|
+
'deterministic',
|
|
44
|
+
'agent',
|
|
45
|
+
'human',
|
|
46
|
+
'committee',
|
|
47
|
+
'external'
|
|
48
|
+
]);
|
|
49
|
+
export const JUDGMENT_DAG_EDGE_RELATIONS = Object.freeze([
|
|
50
|
+
'depends_on',
|
|
51
|
+
'supports',
|
|
52
|
+
'contradicts',
|
|
53
|
+
'gates',
|
|
54
|
+
'supersedes',
|
|
55
|
+
'produces',
|
|
56
|
+
'evaluated_by',
|
|
57
|
+
'triggers'
|
|
58
|
+
]);
|
|
59
|
+
/**
|
|
60
|
+
* Exact JSON object keys accepted by the runtime contract. Metadata records
|
|
61
|
+
* are intentionally not listed here because their nested keys are arbitrary.
|
|
62
|
+
*/
|
|
63
|
+
export const JUDGMENT_DAG_ALLOWED_KEYS = Object.freeze({
|
|
64
|
+
root: Object.freeze(['id', 'version', 'nodes', 'edges']),
|
|
65
|
+
node: Object.freeze([
|
|
66
|
+
'id',
|
|
67
|
+
'node_type',
|
|
68
|
+
'layer',
|
|
69
|
+
'scope',
|
|
70
|
+
'version',
|
|
71
|
+
'description',
|
|
72
|
+
'depends_on',
|
|
73
|
+
'input_contract',
|
|
74
|
+
'output_contract',
|
|
75
|
+
'runner_type',
|
|
76
|
+
'authority',
|
|
77
|
+
'confidence',
|
|
78
|
+
'valid_from',
|
|
79
|
+
'valid_to',
|
|
80
|
+
'provenance',
|
|
81
|
+
'evaluation'
|
|
82
|
+
]),
|
|
83
|
+
scope: Object.freeze(['type', 'id']),
|
|
84
|
+
edge: Object.freeze(['from', 'to', 'relation'])
|
|
85
|
+
});
|
|
86
|
+
export class JudgmentDAGValidationError extends Error {
|
|
87
|
+
code;
|
|
88
|
+
node_id;
|
|
89
|
+
dependency_id;
|
|
90
|
+
node_layer;
|
|
91
|
+
dependency_layer;
|
|
92
|
+
cycle;
|
|
93
|
+
constructor(code, message, details = {}) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = 'JudgmentDAGValidationError';
|
|
96
|
+
this.code = code;
|
|
97
|
+
this.node_id = details.node_id;
|
|
98
|
+
this.dependency_id = details.dependency_id;
|
|
99
|
+
this.node_layer = details.node_layer;
|
|
100
|
+
this.dependency_layer = details.dependency_layer;
|
|
101
|
+
this.cycle = details.cycle;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const LAYER_INDEX = {
|
|
105
|
+
context: 0,
|
|
106
|
+
judgment: 1,
|
|
107
|
+
resource: 2,
|
|
108
|
+
execution: 3,
|
|
109
|
+
evaluation: 4
|
|
110
|
+
};
|
|
111
|
+
function isRecord(value) {
|
|
112
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
function isString(value) {
|
|
115
|
+
return typeof value === 'string';
|
|
116
|
+
}
|
|
117
|
+
function isNonEmptyString(value) {
|
|
118
|
+
return isString(value) && value.trim().length > 0;
|
|
119
|
+
}
|
|
120
|
+
function isSafeIdentifier(value) {
|
|
121
|
+
return isNonEmptyString(value) && !/[\u0000-\u001F\u007F]/u.test(value);
|
|
122
|
+
}
|
|
123
|
+
function isOneOf(values, value) {
|
|
124
|
+
return isString(value) && values.includes(value);
|
|
125
|
+
}
|
|
126
|
+
function invalidContract(message) {
|
|
127
|
+
throw new JudgmentDAGValidationError('invalid_contract', message);
|
|
128
|
+
}
|
|
129
|
+
function requireRecord(value, path) {
|
|
130
|
+
if (!isRecord(value)) {
|
|
131
|
+
invalidContract(`${path} must be an object`);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
function requireExactKeys(value, allowedKeys, path) {
|
|
136
|
+
for (const key of Object.keys(value)) {
|
|
137
|
+
if (!allowedKeys.includes(key)) {
|
|
138
|
+
invalidContract(`${path}.${key} is not an allowed contract field`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function requireNonEmptyString(value, path) {
|
|
143
|
+
if (!isNonEmptyString(value)) {
|
|
144
|
+
invalidContract(`${path} must be a non-empty string`);
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
function requireIdentifier(value, path) {
|
|
149
|
+
if (!isSafeIdentifier(value)) {
|
|
150
|
+
invalidContract(`${path} must be a non-empty string without control characters`);
|
|
151
|
+
}
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function requireArray(value, path) {
|
|
155
|
+
if (!Array.isArray(value)) {
|
|
156
|
+
invalidContract(`${path} must be an array`);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
function validateMetadata(value, path, active = new Set()) {
|
|
161
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (typeof value === 'number') {
|
|
165
|
+
if (Number.isFinite(value)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
invalidContract(`${path} must contain only finite JSON numbers`);
|
|
169
|
+
}
|
|
170
|
+
if (typeof value !== 'object') {
|
|
171
|
+
invalidContract(`${path} must contain JSON-compatible metadata`);
|
|
172
|
+
}
|
|
173
|
+
const objectValue = value;
|
|
174
|
+
if (active.has(objectValue)) {
|
|
175
|
+
invalidContract(`${path} must not contain cyclic metadata`);
|
|
176
|
+
}
|
|
177
|
+
active.add(objectValue);
|
|
178
|
+
if (Array.isArray(value)) {
|
|
179
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
180
|
+
if (!(index in value)) {
|
|
181
|
+
invalidContract(`${path}[${index}] must be defined`);
|
|
182
|
+
}
|
|
183
|
+
validateMetadata(value[index], `${path}[${index}]`, active);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const prototype = Object.getPrototypeOf(value);
|
|
188
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
189
|
+
invalidContract(`${path} must contain only plain metadata objects`);
|
|
190
|
+
}
|
|
191
|
+
for (const [key, child] of Object.entries(value)) {
|
|
192
|
+
validateMetadata(child, `${path}.${key}`, active);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
active.delete(objectValue);
|
|
196
|
+
}
|
|
197
|
+
function validateNode(value, index) {
|
|
198
|
+
const path = `nodes[${index}]`;
|
|
199
|
+
const node = requireRecord(value, path);
|
|
200
|
+
requireExactKeys(node, JUDGMENT_DAG_ALLOWED_KEYS.node, path);
|
|
201
|
+
const nodeType = node.node_type;
|
|
202
|
+
const layer = node.layer;
|
|
203
|
+
const runnerType = node.runner_type;
|
|
204
|
+
const scope = requireRecord(node.scope, `${path}.scope`);
|
|
205
|
+
requireExactKeys(scope, JUDGMENT_DAG_ALLOWED_KEYS.scope, `${path}.scope`);
|
|
206
|
+
if (!isOneOf(JUDGMENT_DAG_NODE_TYPES, nodeType)) {
|
|
207
|
+
invalidContract(`${path}.node_type is not a supported node type`);
|
|
208
|
+
}
|
|
209
|
+
if (!isOneOf(JUDGMENT_DAG_LAYERS, layer)) {
|
|
210
|
+
invalidContract(`${path}.layer is not a supported layer`);
|
|
211
|
+
}
|
|
212
|
+
if (!isOneOf(JUDGMENT_DAG_RUNNER_TYPES, runnerType)) {
|
|
213
|
+
invalidContract(`${path}.runner_type is not a supported runner type`);
|
|
214
|
+
}
|
|
215
|
+
if (!isOneOf(JUDGMENT_DAG_SCOPE_TYPES, scope.type)) {
|
|
216
|
+
invalidContract(`${path}.scope.type is not a supported scope type`);
|
|
217
|
+
}
|
|
218
|
+
if (JUDGMENT_DAG_NODE_TYPE_TO_LAYER[nodeType] !== layer) {
|
|
219
|
+
invalidContract(`${path}.layer does not match ${path}.node_type`);
|
|
220
|
+
}
|
|
221
|
+
requireIdentifier(node.id, `${path}.id`);
|
|
222
|
+
requireIdentifier(scope.id, `${path}.scope.id`);
|
|
223
|
+
requireNonEmptyString(node.version, `${path}.version`);
|
|
224
|
+
requireNonEmptyString(node.description, `${path}.description`);
|
|
225
|
+
requireNonEmptyString(node.input_contract, `${path}.input_contract`);
|
|
226
|
+
requireNonEmptyString(node.output_contract, `${path}.output_contract`);
|
|
227
|
+
const dependencies = requireArray(node.depends_on, `${path}.depends_on`);
|
|
228
|
+
for (let index = 0; index < dependencies.length; index += 1) {
|
|
229
|
+
requireIdentifier(dependencies[index], `${path}.depends_on[${index}]`);
|
|
230
|
+
}
|
|
231
|
+
if (new Set(dependencies).size !== dependencies.length) {
|
|
232
|
+
invalidContract(`${path}.depends_on must not contain duplicate node IDs`);
|
|
233
|
+
}
|
|
234
|
+
if (node.confidence !== undefined &&
|
|
235
|
+
(typeof node.confidence !== 'number' || !Number.isFinite(node.confidence) ||
|
|
236
|
+
node.confidence < 0 || node.confidence > 1)) {
|
|
237
|
+
invalidContract(`${path}.confidence must be a number between 0 and 1`);
|
|
238
|
+
}
|
|
239
|
+
for (const field of ['valid_from', 'valid_to']) {
|
|
240
|
+
if (node[field] !== undefined && node[field] !== null && !isString(node[field])) {
|
|
241
|
+
invalidContract(`${path}.${field} must be a string or null`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (node.provenance !== undefined && !Array.isArray(node.provenance)) {
|
|
245
|
+
invalidContract(`${path}.provenance must be an array`);
|
|
246
|
+
}
|
|
247
|
+
if (node.authority !== undefined) {
|
|
248
|
+
validateMetadata(node.authority, `${path}.authority`);
|
|
249
|
+
}
|
|
250
|
+
if (node.provenance !== undefined) {
|
|
251
|
+
validateMetadata(node.provenance, `${path}.provenance`);
|
|
252
|
+
}
|
|
253
|
+
if (node.evaluation !== undefined) {
|
|
254
|
+
validateMetadata(node.evaluation, `${path}.evaluation`);
|
|
255
|
+
}
|
|
256
|
+
return node;
|
|
257
|
+
}
|
|
258
|
+
function validateEdge(value, index, nodeById) {
|
|
259
|
+
const path = `edges[${index}]`;
|
|
260
|
+
const edge = requireRecord(value, path);
|
|
261
|
+
requireExactKeys(edge, JUDGMENT_DAG_ALLOWED_KEYS.edge, path);
|
|
262
|
+
const from = requireIdentifier(edge.from, `${path}.from`);
|
|
263
|
+
const to = requireIdentifier(edge.to, `${path}.to`);
|
|
264
|
+
if (!isOneOf(JUDGMENT_DAG_EDGE_RELATIONS, edge.relation)) {
|
|
265
|
+
invalidContract(`${path}.relation is not a supported edge relation`);
|
|
266
|
+
}
|
|
267
|
+
if (!nodeById.has(from) || !nodeById.has(to)) {
|
|
268
|
+
throw new JudgmentDAGValidationError('missing_dependency', `${path} references a node that is not in the DAG`, { node_id: to, dependency_id: from });
|
|
269
|
+
}
|
|
270
|
+
return edge;
|
|
271
|
+
}
|
|
272
|
+
function edgeKey(from, to) {
|
|
273
|
+
// A delimiter-based key aliases pairs such as ("a\0b", "c") and
|
|
274
|
+
// ("a", "b\0c"). JSON preserves the pair structure even if an unsafe
|
|
275
|
+
// identifier reaches this internal helper, while public identifiers are
|
|
276
|
+
// rejected by the schema/runtime before this point.
|
|
277
|
+
return JSON.stringify([from, to]);
|
|
278
|
+
}
|
|
279
|
+
function edgePairLabel(pair) {
|
|
280
|
+
const [from, to] = JSON.parse(pair);
|
|
281
|
+
return `${from} -> ${to}`;
|
|
282
|
+
}
|
|
283
|
+
function findCycle(nodes, dependenciesByNode) {
|
|
284
|
+
const state = new Map();
|
|
285
|
+
const path = [];
|
|
286
|
+
function visit(nodeId) {
|
|
287
|
+
const currentState = state.get(nodeId) ?? 0;
|
|
288
|
+
if (currentState === 1) {
|
|
289
|
+
const cycleStart = path.indexOf(nodeId);
|
|
290
|
+
return [...path.slice(cycleStart), nodeId];
|
|
291
|
+
}
|
|
292
|
+
if (currentState === 2) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
state.set(nodeId, 1);
|
|
296
|
+
path.push(nodeId);
|
|
297
|
+
for (const dependencyId of [...(dependenciesByNode.get(nodeId) ?? [])].sort()) {
|
|
298
|
+
const cycle = visit(dependencyId);
|
|
299
|
+
if (cycle !== undefined) {
|
|
300
|
+
return cycle;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
path.pop();
|
|
304
|
+
state.set(nodeId, 2);
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
for (const nodeId of nodes.map((node) => node.id).sort()) {
|
|
308
|
+
const cycle = visit(nodeId);
|
|
309
|
+
if (cycle !== undefined) {
|
|
310
|
+
return cycle;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Validate a DAG before any runner can execute it.
|
|
317
|
+
*
|
|
318
|
+
* The input is never mutated. Dependencies declared on nodes and explicit
|
|
319
|
+
* `depends_on` edges are both checked. They are two required representations
|
|
320
|
+
* of the same dependency topology and must be exact mirrors; other edge
|
|
321
|
+
* relations are not part of the execution topology.
|
|
322
|
+
*/
|
|
323
|
+
export function validateJudgmentDAG(value) {
|
|
324
|
+
const dag = requireRecord(value, 'dag');
|
|
325
|
+
requireExactKeys(dag, JUDGMENT_DAG_ALLOWED_KEYS.root, 'dag');
|
|
326
|
+
const dagId = requireIdentifier(dag.id, 'dag.id');
|
|
327
|
+
const dagVersion = requireNonEmptyString(dag.version, 'dag.version');
|
|
328
|
+
const nodeValues = requireArray(dag.nodes, 'dag.nodes');
|
|
329
|
+
const edgeValues = requireArray(dag.edges, 'dag.edges');
|
|
330
|
+
const nodes = nodeValues.map(validateNode);
|
|
331
|
+
const nodeById = new Map();
|
|
332
|
+
for (const node of nodes) {
|
|
333
|
+
if (nodeById.has(node.id)) {
|
|
334
|
+
throw new JudgmentDAGValidationError('duplicate_node', `DAG contains duplicate node ID: ${node.id}`, { node_id: node.id });
|
|
335
|
+
}
|
|
336
|
+
nodeById.set(node.id, node);
|
|
337
|
+
}
|
|
338
|
+
const dependenciesByNode = new Map(nodes.map((node) => [node.id, new Set()]));
|
|
339
|
+
const dependentsByNode = new Map(nodes.map((node) => [node.id, []]));
|
|
340
|
+
const dependencyPairs = new Set();
|
|
341
|
+
const nodeDependencyPairs = new Set();
|
|
342
|
+
function addDependency(dependencyId, dependentId) {
|
|
343
|
+
const dependent = nodeById.get(dependentId);
|
|
344
|
+
const dependency = nodeById.get(dependencyId);
|
|
345
|
+
if (dependent === undefined || dependency === undefined) {
|
|
346
|
+
throw new JudgmentDAGValidationError('missing_dependency', `Node ${dependentId} depends on missing node ${dependencyId}`, { node_id: dependentId, dependency_id: dependencyId });
|
|
347
|
+
}
|
|
348
|
+
if (dependency.scope.type !== dependent.scope.type || dependency.scope.id !== dependent.scope.id) {
|
|
349
|
+
throw new JudgmentDAGValidationError('scope_boundary_violation', `Dependency ${dependencyId} -> ${dependentId} crosses an exact scope boundary`, { node_id: dependentId, dependency_id: dependencyId });
|
|
350
|
+
}
|
|
351
|
+
const pair = edgeKey(dependencyId, dependentId);
|
|
352
|
+
if (dependencyPairs.has(pair)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
dependencyPairs.add(pair);
|
|
356
|
+
dependenciesByNode.get(dependentId)?.add(dependencyId);
|
|
357
|
+
dependentsByNode.get(dependencyId)?.push(dependentId);
|
|
358
|
+
const dependencyLayerIndex = LAYER_INDEX[dependency.layer];
|
|
359
|
+
const dependentLayerIndex = LAYER_INDEX[dependent.layer];
|
|
360
|
+
if (dependencyLayerIndex > dependentLayerIndex) {
|
|
361
|
+
throw new JudgmentDAGValidationError('reverse_layer_dependency', `Node ${dependentId} in layer ${dependent.layer} depends on later layer ` +
|
|
362
|
+
`${dependency.layer} node ${dependencyId}`, {
|
|
363
|
+
node_id: dependentId,
|
|
364
|
+
dependency_id: dependencyId,
|
|
365
|
+
node_layer: dependent.layer,
|
|
366
|
+
dependency_layer: dependency.layer
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
for (const node of nodes) {
|
|
371
|
+
for (const dependencyId of node.depends_on) {
|
|
372
|
+
nodeDependencyPairs.add(edgeKey(dependencyId, node.id));
|
|
373
|
+
addDependency(dependencyId, node.id);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const seenEdges = new Set();
|
|
377
|
+
const edgeDependencyPairs = new Set();
|
|
378
|
+
const edges = edgeValues.map((value, index) => validateEdge(value, index, nodeById));
|
|
379
|
+
for (const edge of edges) {
|
|
380
|
+
const key = `${edge.relation}:${edgeKey(edge.from, edge.to)}`;
|
|
381
|
+
if (seenEdges.has(key)) {
|
|
382
|
+
invalidContract(`edges contains duplicate relation ${key}`);
|
|
383
|
+
}
|
|
384
|
+
seenEdges.add(key);
|
|
385
|
+
if (edge.relation === 'depends_on') {
|
|
386
|
+
edgeDependencyPairs.add(edgeKey(edge.from, edge.to));
|
|
387
|
+
addDependency(edge.from, edge.to);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const pair of nodeDependencyPairs) {
|
|
391
|
+
if (!edgeDependencyPairs.has(pair)) {
|
|
392
|
+
invalidContract(`depends_on edge is missing for node dependency ${edgePairLabel(pair)}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (const pair of edgeDependencyPairs) {
|
|
396
|
+
if (!nodeDependencyPairs.has(pair)) {
|
|
397
|
+
invalidContract(`node dependency is missing for depends_on edge ${edgePairLabel(pair)}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const cycle = findCycle(nodes, dependenciesByNode);
|
|
401
|
+
if (cycle !== undefined) {
|
|
402
|
+
throw new JudgmentDAGValidationError('cycle', `DAG contains a dependency cycle: ${cycle.join(' -> ')}`, { cycle });
|
|
403
|
+
}
|
|
404
|
+
const remainingDependencies = new Map([...dependenciesByNode.entries()].map(([id, dependencies]) => [id, dependencies.size]));
|
|
405
|
+
const ready = nodes
|
|
406
|
+
.filter((node) => remainingDependencies.get(node.id) === 0)
|
|
407
|
+
.map((node) => node.id)
|
|
408
|
+
.sort();
|
|
409
|
+
const executionOrder = [];
|
|
410
|
+
while (ready.length > 0) {
|
|
411
|
+
const completedNodeId = ready.shift();
|
|
412
|
+
if (completedNodeId === undefined) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
executionOrder.push(completedNodeId);
|
|
416
|
+
for (const dependentId of [...(dependentsByNode.get(completedNodeId) ?? [])].sort()) {
|
|
417
|
+
const remaining = (remainingDependencies.get(dependentId) ?? 0) - 1;
|
|
418
|
+
remainingDependencies.set(dependentId, remaining);
|
|
419
|
+
if (remaining === 0) {
|
|
420
|
+
ready.push(dependentId);
|
|
421
|
+
ready.sort();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// This is defensive after findCycle, but keeps the preflight invariant
|
|
426
|
+
// explicit if the topology implementation changes later.
|
|
427
|
+
if (executionOrder.length !== nodes.length) {
|
|
428
|
+
throw new JudgmentDAGValidationError('cycle', 'DAG does not have a complete topological order');
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
valid: true,
|
|
432
|
+
dag_id: dagId,
|
|
433
|
+
dag_version: dagVersion,
|
|
434
|
+
execution_order: executionOrder
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
export function assertValidJudgmentDAG(value) {
|
|
438
|
+
validateJudgmentDAG(value);
|
|
439
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { type JudgmentDAG, type JudgmentDAGMetadata, type JudgmentDAGNode, type JudgmentDAGRunnerType } from './judgment-dag-core.js';
|
|
2
|
+
/** A JSON-compatible value accepted at every local runner boundary. */
|
|
3
|
+
export type JudgmentDAGJSONValue = JudgmentDAGMetadata;
|
|
4
|
+
export interface JudgmentDAGRunnerInput {
|
|
5
|
+
readonly run_id: string;
|
|
6
|
+
readonly dag: JudgmentDAG;
|
|
7
|
+
readonly node: JudgmentDAGNode;
|
|
8
|
+
readonly input: JudgmentDAGJSONValue;
|
|
9
|
+
readonly dependency_outputs: readonly JudgmentDAGDependencyOutput[];
|
|
10
|
+
}
|
|
11
|
+
export interface JudgmentDAGDependencyOutput {
|
|
12
|
+
readonly node_id: string;
|
|
13
|
+
readonly output: JudgmentDAGJSONValue;
|
|
14
|
+
}
|
|
15
|
+
export interface JudgmentDAGRunnerRegistration {
|
|
16
|
+
readonly version: string;
|
|
17
|
+
readonly run: (input: JudgmentDAGRunnerInput) => JudgmentDAGJSONValue | Promise<JudgmentDAGJSONValue>;
|
|
18
|
+
}
|
|
19
|
+
export interface JudgmentDAGRunRequest {
|
|
20
|
+
readonly run_id: string;
|
|
21
|
+
readonly dag: JudgmentDAG;
|
|
22
|
+
readonly input: JudgmentDAGJSONValue;
|
|
23
|
+
readonly runners: Partial<Record<JudgmentDAGRunnerType, JudgmentDAGRunnerRegistration>>;
|
|
24
|
+
}
|
|
25
|
+
export interface JudgmentDAGRunnerVersion {
|
|
26
|
+
readonly runner_type: JudgmentDAGRunnerType;
|
|
27
|
+
readonly version: string;
|
|
28
|
+
}
|
|
29
|
+
export interface JudgmentDAGNodeRunRecord {
|
|
30
|
+
readonly node_id: string;
|
|
31
|
+
readonly runner_type: JudgmentDAGRunnerType;
|
|
32
|
+
readonly runner_version: string;
|
|
33
|
+
readonly input_contract: string;
|
|
34
|
+
readonly output_contract: string;
|
|
35
|
+
readonly input: JudgmentDAGJSONValue;
|
|
36
|
+
readonly dependency_outputs: readonly JudgmentDAGDependencyOutput[];
|
|
37
|
+
readonly output: JudgmentDAGJSONValue;
|
|
38
|
+
}
|
|
39
|
+
export interface JudgmentDAGRunRecord {
|
|
40
|
+
readonly run_id: string;
|
|
41
|
+
readonly dag: JudgmentDAG;
|
|
42
|
+
readonly input: JudgmentDAGJSONValue;
|
|
43
|
+
readonly execution_order: readonly string[];
|
|
44
|
+
readonly runner_versions: readonly JudgmentDAGRunnerVersion[];
|
|
45
|
+
readonly nodes: readonly JudgmentDAGNodeRunRecord[];
|
|
46
|
+
}
|
|
47
|
+
export type JudgmentDAGExecutionCode = 'invalid_request' | 'missing_runner' | 'invalid_runner' | 'invalid_json' | 'runner_failed';
|
|
48
|
+
export type JudgmentDAGRunnerFailureKind = 'sync_throw' | 'async_reject';
|
|
49
|
+
export interface JudgmentDAGExecutionErrorDetails {
|
|
50
|
+
readonly node_id?: string;
|
|
51
|
+
readonly node_type?: JudgmentDAGNode['node_type'];
|
|
52
|
+
readonly runner_type?: JudgmentDAGRunnerType;
|
|
53
|
+
readonly failure_kind?: JudgmentDAGRunnerFailureKind;
|
|
54
|
+
readonly cause?: unknown;
|
|
55
|
+
}
|
|
56
|
+
export declare class JudgmentDAGExecutionError extends Error {
|
|
57
|
+
readonly code: JudgmentDAGExecutionCode;
|
|
58
|
+
readonly node_id?: string;
|
|
59
|
+
readonly node_type?: JudgmentDAGNode['node_type'];
|
|
60
|
+
readonly runner_type?: JudgmentDAGRunnerType;
|
|
61
|
+
readonly failure_kind?: JudgmentDAGRunnerFailureKind;
|
|
62
|
+
readonly cause?: unknown;
|
|
63
|
+
constructor(code: JudgmentDAGExecutionCode, message: string, details?: JudgmentDAGExecutionErrorDetails);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Execute a validated local DAG sequentially using only explicit runner
|
|
67
|
+
* registrations. No clock, randomness, filesystem, network, or persistence
|
|
68
|
+
* is consulted by this function.
|
|
69
|
+
*/
|
|
70
|
+
export declare function executeJudgmentDAG(request: JudgmentDAGRunRequest): Promise<JudgmentDAGRunRecord>;
|