@unson/brainbase-mcp 0.4.1 → 0.6.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/README.md +13 -5
- package/contracts/judgment-dag/digest.json +4 -4
- package/contracts/judgment-dag/source-lock.json +65 -1
- package/contracts/judgment-value-proof/fixture.json +54 -0
- package/contracts/judgment-value-proof/schema.json +163 -0
- package/dist/cli.js +17 -0
- package/dist/embedding-provider.d.ts +72 -0
- package/dist/embedding-provider.js +391 -0
- package/dist/graph-retrieval.d.ts +94 -0
- package/dist/graph-retrieval.js +443 -0
- package/dist/judgment-dag-artifact-store.d.ts +29 -0
- package/dist/judgment-dag-artifact-store.js +416 -0
- package/dist/judgment-dag-replay-evaluation.d.ts +108 -0
- package/dist/judgment-dag-replay-evaluation.js +424 -0
- package/dist/judgment-dag.d.ts +4 -0
- package/dist/judgment-dag.js +2 -0
- package/dist/judgment-value-proof.d.ts +92 -0
- package/dist/judgment-value-proof.js +229 -0
- package/dist/organization-graph.d.ts +76 -0
- package/dist/organization-graph.js +331 -0
- package/dist/portable-graph.d.ts +39 -0
- package/dist/portable-graph.js +240 -0
- package/dist/server.d.ts +42 -3
- package/dist/server.js +52 -7
- package/docs/management/judgment-dag-milestones.md +31 -1
- package/package.json +22 -6
- package/ui/icons/mana/LICENSE.tabler.txt +21 -0
- package/ui/icons/mana/affiliate.svg +20 -0
- package/ui/icons/mana/brand-github.svg +15 -0
- package/ui/icons/mana/brand-google-drive.svg +17 -0
- package/ui/icons/mana/brand-slack.svg +18 -0
- package/ui/icons/mana/cpu.svg +24 -0
- package/ui/outcome-knowledge.css +365 -0
- package/ui/outcome-knowledge.js +1570 -0
- package/ui/outcome-mana.css +255 -0
- package/ui/outcome-mana.js +2325 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, lstat, mkdir, open, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { validateJudgmentDAG } from './judgment-dag-core.js';
|
|
5
|
+
export const JUDGMENT_DAG_RUN_ARTIFACT_VERSION = 'judgment-dag-run-artifact.v1';
|
|
6
|
+
export class JudgmentDAGArtifactError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'JudgmentDAGArtifactError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const ARTIFACT_ID_PATTERN = /^sha256:([0-9a-f]{64})$/u;
|
|
15
|
+
const RUN_RECORD_KEYS = ['dag', 'execution_order', 'input', 'nodes', 'run_id', 'runner_versions'];
|
|
16
|
+
const RUNNER_VERSION_KEYS = ['runner_type', 'version'];
|
|
17
|
+
const NODE_RECORD_KEYS = [
|
|
18
|
+
'dependency_outputs', 'input', 'input_contract', 'node_id', 'output',
|
|
19
|
+
'output_contract', 'runner_type', 'runner_version'
|
|
20
|
+
];
|
|
21
|
+
const DEPENDENCY_OUTPUT_KEYS = ['node_id', 'output'];
|
|
22
|
+
const ENVELOPE_KEYS = ['artifact_id', 'artifact_version', 'record'];
|
|
23
|
+
function compareUnicodeCodePoints(left, right) {
|
|
24
|
+
const leftPoints = Array.from(left, (character) => character.codePointAt(0));
|
|
25
|
+
const rightPoints = Array.from(right, (character) => character.codePointAt(0));
|
|
26
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
27
|
+
for (let index = 0; index < length; index += 1) {
|
|
28
|
+
if (leftPoints[index] !== rightPoints[index])
|
|
29
|
+
return leftPoints[index] - rightPoints[index];
|
|
30
|
+
}
|
|
31
|
+
return leftPoints.length - rightPoints.length;
|
|
32
|
+
}
|
|
33
|
+
function artifactError(code, message) {
|
|
34
|
+
return new JudgmentDAGArtifactError(code, message);
|
|
35
|
+
}
|
|
36
|
+
function isPlainRecord(value) {
|
|
37
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
38
|
+
return false;
|
|
39
|
+
const prototype = Object.getPrototypeOf(value);
|
|
40
|
+
return prototype === Object.prototype || prototype === null;
|
|
41
|
+
}
|
|
42
|
+
function hasExactKeys(value, expected) {
|
|
43
|
+
const keys = Object.keys(value).sort();
|
|
44
|
+
return keys.length === expected.length && keys.every((key, index) => key === expected[index]);
|
|
45
|
+
}
|
|
46
|
+
function canonicalSnapshot(value, active = new Set()) {
|
|
47
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
48
|
+
return value;
|
|
49
|
+
if (typeof value === 'number') {
|
|
50
|
+
if (!Number.isFinite(value))
|
|
51
|
+
throw artifactError('invalid_request', 'artifact values must be finite JSON numbers');
|
|
52
|
+
return Object.is(value, -0) ? 0 : value;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value !== 'object') {
|
|
55
|
+
throw artifactError('invalid_request', 'artifact values must be JSON-compatible');
|
|
56
|
+
}
|
|
57
|
+
if (active.has(value))
|
|
58
|
+
throw artifactError('invalid_request', 'artifact values must not be cyclic');
|
|
59
|
+
active.add(value);
|
|
60
|
+
try {
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
const result = [];
|
|
63
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
64
|
+
if (!(index in value))
|
|
65
|
+
throw artifactError('invalid_request', 'artifact arrays must not be sparse');
|
|
66
|
+
result.push(canonicalSnapshot(value[index], active));
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
if (!isPlainRecord(value))
|
|
71
|
+
throw artifactError('invalid_request', 'artifact objects must be plain records');
|
|
72
|
+
const result = Object.create(null);
|
|
73
|
+
for (const key of Object.keys(value).sort(compareUnicodeCodePoints)) {
|
|
74
|
+
result[key] = canonicalSnapshot(value[key], active);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
active.delete(value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function canonicalJSON(value) {
|
|
83
|
+
return JSON.stringify(canonicalSnapshot(value));
|
|
84
|
+
}
|
|
85
|
+
function deepFreeze(value) {
|
|
86
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
87
|
+
Object.freeze(value);
|
|
88
|
+
for (const child of Object.values(value))
|
|
89
|
+
deepFreeze(child);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function jsonEqual(left, right) {
|
|
94
|
+
return canonicalJSON(left) === canonicalJSON(right);
|
|
95
|
+
}
|
|
96
|
+
function requireString(value) {
|
|
97
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
98
|
+
}
|
|
99
|
+
function validateRunnerVersions(value) {
|
|
100
|
+
if (!Array.isArray(value))
|
|
101
|
+
throw artifactError('invalid_request', 'record.runner_versions must be an array');
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const result = value.map((candidate) => {
|
|
104
|
+
if (!isPlainRecord(candidate) || !hasExactKeys(candidate, RUNNER_VERSION_KEYS) ||
|
|
105
|
+
!requireString(candidate.runner_type) || !requireString(candidate.version) ||
|
|
106
|
+
seen.has(candidate.runner_type)) {
|
|
107
|
+
throw artifactError('invalid_request', 'record.runner_versions is invalid');
|
|
108
|
+
}
|
|
109
|
+
seen.add(candidate.runner_type);
|
|
110
|
+
return {
|
|
111
|
+
runner_type: candidate.runner_type,
|
|
112
|
+
version: candidate.version
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const sorted = [...result].sort((left, right) => left.runner_type.localeCompare(right.runner_type));
|
|
116
|
+
if (!jsonEqual(result, sorted))
|
|
117
|
+
throw artifactError('invalid_request', 'record.runner_versions must be sorted');
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
function validateDependencyOutputs(value) {
|
|
121
|
+
if (!Array.isArray(value))
|
|
122
|
+
throw artifactError('invalid_request', 'node dependency outputs must be an array');
|
|
123
|
+
return value.map((candidate) => {
|
|
124
|
+
if (!isPlainRecord(candidate) || !hasExactKeys(candidate, DEPENDENCY_OUTPUT_KEYS) ||
|
|
125
|
+
!requireString(candidate.node_id)) {
|
|
126
|
+
throw artifactError('invalid_request', 'node dependency output is invalid');
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
node_id: candidate.node_id,
|
|
130
|
+
output: canonicalSnapshot(candidate.output)
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function validateRunRecord(value) {
|
|
135
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, RUN_RECORD_KEYS) || !requireString(value.run_id)) {
|
|
136
|
+
throw artifactError('invalid_request', 'run artifact record shape is invalid');
|
|
137
|
+
}
|
|
138
|
+
const dag = canonicalSnapshot(value.dag);
|
|
139
|
+
let validation;
|
|
140
|
+
try {
|
|
141
|
+
validation = validateJudgmentDAG(dag);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw artifactError('invalid_request', 'run artifact DAG is invalid');
|
|
145
|
+
}
|
|
146
|
+
const input = canonicalSnapshot(value.input);
|
|
147
|
+
if (!Array.isArray(value.execution_order) ||
|
|
148
|
+
value.execution_order.some((nodeId) => !requireString(nodeId)) ||
|
|
149
|
+
!jsonEqual(value.execution_order, validation.execution_order)) {
|
|
150
|
+
throw artifactError('invalid_request', 'record.execution_order does not match the DAG');
|
|
151
|
+
}
|
|
152
|
+
const runnerVersions = validateRunnerVersions(value.runner_versions);
|
|
153
|
+
const expectedRunnerTypes = [...new Set(dag.nodes.map((node) => node.runner_type))]
|
|
154
|
+
.sort((left, right) => left.localeCompare(right));
|
|
155
|
+
if (!jsonEqual(runnerVersions.map((entry) => entry.runner_type), expectedRunnerTypes)) {
|
|
156
|
+
throw artifactError('invalid_request', 'record.runner_versions must exactly match the DAG runner types');
|
|
157
|
+
}
|
|
158
|
+
const versionByRunner = new Map(runnerVersions.map((entry) => [entry.runner_type, entry.version]));
|
|
159
|
+
if (!Array.isArray(value.nodes) || value.nodes.length !== validation.execution_order.length) {
|
|
160
|
+
throw artifactError('invalid_request', 'record.nodes does not match the execution order');
|
|
161
|
+
}
|
|
162
|
+
const dagNodeById = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
163
|
+
const outputByNode = new Map();
|
|
164
|
+
const nodes = [];
|
|
165
|
+
for (let index = 0; index < value.nodes.length; index += 1) {
|
|
166
|
+
const candidate = value.nodes[index];
|
|
167
|
+
const expectedId = validation.execution_order[index];
|
|
168
|
+
const dagNode = dagNodeById.get(expectedId);
|
|
169
|
+
if (!isPlainRecord(candidate) || !hasExactKeys(candidate, NODE_RECORD_KEYS) || dagNode === undefined ||
|
|
170
|
+
candidate.node_id !== expectedId || candidate.runner_type !== dagNode.runner_type ||
|
|
171
|
+
candidate.input_contract !== dagNode.input_contract || candidate.output_contract !== dagNode.output_contract ||
|
|
172
|
+
candidate.runner_version !== versionByRunner.get(dagNode.runner_type) ||
|
|
173
|
+
!jsonEqual(candidate.input, input)) {
|
|
174
|
+
throw artifactError('invalid_request', 'record node does not match the DAG and runner versions');
|
|
175
|
+
}
|
|
176
|
+
const dependencyOutputs = validateDependencyOutputs(candidate.dependency_outputs);
|
|
177
|
+
const expectedDependencies = dagNode.depends_on.slice().sort().map((nodeId) => ({
|
|
178
|
+
node_id: nodeId,
|
|
179
|
+
output: outputByNode.get(nodeId)
|
|
180
|
+
}));
|
|
181
|
+
if (expectedDependencies.some((entry) => entry.output === undefined) ||
|
|
182
|
+
!jsonEqual(dependencyOutputs, expectedDependencies)) {
|
|
183
|
+
throw artifactError('invalid_request', 'record dependency outputs do not match prior node outputs');
|
|
184
|
+
}
|
|
185
|
+
const output = canonicalSnapshot(candidate.output);
|
|
186
|
+
outputByNode.set(expectedId, output);
|
|
187
|
+
nodes.push({
|
|
188
|
+
node_id: expectedId,
|
|
189
|
+
runner_type: dagNode.runner_type,
|
|
190
|
+
runner_version: candidate.runner_version,
|
|
191
|
+
input_contract: dagNode.input_contract,
|
|
192
|
+
output_contract: dagNode.output_contract,
|
|
193
|
+
input,
|
|
194
|
+
dependency_outputs: dependencyOutputs,
|
|
195
|
+
output
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return deepFreeze({
|
|
199
|
+
run_id: value.run_id,
|
|
200
|
+
dag,
|
|
201
|
+
input,
|
|
202
|
+
execution_order: [...validation.execution_order],
|
|
203
|
+
runner_versions: runnerVersions,
|
|
204
|
+
nodes
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function requireRoot(value) {
|
|
208
|
+
if (!requireString(value) || value.includes('\0')) {
|
|
209
|
+
throw artifactError('invalid_request', 'artifact root must be a non-empty filesystem path');
|
|
210
|
+
}
|
|
211
|
+
return path.resolve(value);
|
|
212
|
+
}
|
|
213
|
+
function payloadFor(record) {
|
|
214
|
+
return { artifact_version: JUDGMENT_DAG_RUN_ARTIFACT_VERSION, record };
|
|
215
|
+
}
|
|
216
|
+
function artifactIdFor(payload) {
|
|
217
|
+
return `sha256:${createHash('sha256').update(canonicalJSON(payload), 'utf8').digest('hex')}`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Derive the exact J0 content address for a run record without writing it.
|
|
221
|
+
* The record is validated and normalized with the same contract used by save/load.
|
|
222
|
+
*/
|
|
223
|
+
export function computeJudgmentDAGRunArtifactId(value) {
|
|
224
|
+
return artifactIdFor(payloadFor(validateRunRecord(value)));
|
|
225
|
+
}
|
|
226
|
+
function locator(root, artifactId) {
|
|
227
|
+
if (typeof artifactId !== 'string') {
|
|
228
|
+
throw artifactError('invalid_artifact_id', 'artifact_id must be sha256:<64 lowercase hex>');
|
|
229
|
+
}
|
|
230
|
+
const match = ARTIFACT_ID_PATTERN.exec(artifactId);
|
|
231
|
+
if (match === null)
|
|
232
|
+
throw artifactError('invalid_artifact_id', 'artifact_id must be sha256:<64 lowercase hex>');
|
|
233
|
+
return path.join(root, 'artifacts', `${match[1]}.json`);
|
|
234
|
+
}
|
|
235
|
+
async function assertArtifactDirectory(root) {
|
|
236
|
+
const directory = path.join(root, 'artifacts');
|
|
237
|
+
let rootDetails;
|
|
238
|
+
try {
|
|
239
|
+
rootDetails = await lstat(root);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
throw artifactError('invalid_request', 'artifact root must be an existing regular directory');
|
|
243
|
+
}
|
|
244
|
+
if (!rootDetails.isDirectory() || rootDetails.isSymbolicLink()) {
|
|
245
|
+
throw artifactError('invalid_request', 'artifact root must be an existing regular directory');
|
|
246
|
+
}
|
|
247
|
+
let details;
|
|
248
|
+
try {
|
|
249
|
+
details = await lstat(directory);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error.code !== 'ENOENT') {
|
|
253
|
+
throw artifactError('storage_io_error', 'artifact directory metadata could not be read');
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await mkdir(directory, { mode: 0o700 });
|
|
257
|
+
}
|
|
258
|
+
catch (mkdirError) {
|
|
259
|
+
if (mkdirError.code !== 'EEXIST') {
|
|
260
|
+
throw artifactError('storage_io_error', 'artifact directory could not be prepared');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
details = await lstat(directory);
|
|
264
|
+
}
|
|
265
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
266
|
+
throw artifactError('invalid_artifact', 'artifact directory must be a regular directory');
|
|
267
|
+
}
|
|
268
|
+
return directory;
|
|
269
|
+
}
|
|
270
|
+
async function requireArtifactDirectory(root) {
|
|
271
|
+
const rootDetails = await lstat(root).catch(() => undefined);
|
|
272
|
+
if (rootDetails === undefined || !rootDetails.isDirectory() || rootDetails.isSymbolicLink()) {
|
|
273
|
+
throw artifactError('invalid_request', 'artifact root must be an existing regular directory');
|
|
274
|
+
}
|
|
275
|
+
const details = await lstat(path.join(root, 'artifacts')).catch(() => undefined);
|
|
276
|
+
if (details === undefined)
|
|
277
|
+
throw artifactError('not_found', 'run artifact was not found');
|
|
278
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
279
|
+
throw artifactError('invalid_artifact', 'artifact directory must be a regular directory');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function readRegularArtifact(file) {
|
|
283
|
+
let details;
|
|
284
|
+
try {
|
|
285
|
+
details = await lstat(file);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error.code === 'ENOENT')
|
|
289
|
+
throw artifactError('not_found', 'run artifact was not found');
|
|
290
|
+
throw artifactError('storage_io_error', 'run artifact metadata could not be read');
|
|
291
|
+
}
|
|
292
|
+
if (!details.isFile() || details.isSymbolicLink()) {
|
|
293
|
+
throw artifactError('invalid_artifact', 'run artifact must be a regular file');
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
return await readFile(file, 'utf8');
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
throw artifactError('storage_io_error', 'run artifact bytes could not be read');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function receipt(artifactId, record, status) {
|
|
303
|
+
return deepFreeze({
|
|
304
|
+
artifact_id: artifactId,
|
|
305
|
+
artifact_version: JUDGMENT_DAG_RUN_ARTIFACT_VERSION,
|
|
306
|
+
run_id: record.run_id,
|
|
307
|
+
status
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
export async function saveJudgmentDAGRunArtifact(request) {
|
|
311
|
+
if (!isPlainRecord(request))
|
|
312
|
+
throw artifactError('invalid_request', 'save request must be a plain object');
|
|
313
|
+
const root = requireRoot(request.root);
|
|
314
|
+
const record = validateRunRecord(request.record);
|
|
315
|
+
const payload = payloadFor(record);
|
|
316
|
+
const artifactId = computeJudgmentDAGRunArtifactId(record);
|
|
317
|
+
const envelope = {
|
|
318
|
+
artifact_id: artifactId,
|
|
319
|
+
artifact_version: JUDGMENT_DAG_RUN_ARTIFACT_VERSION,
|
|
320
|
+
record
|
|
321
|
+
};
|
|
322
|
+
const bytes = `${canonicalJSON(envelope)}\n`;
|
|
323
|
+
let directory;
|
|
324
|
+
try {
|
|
325
|
+
directory = await assertArtifactDirectory(root);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
if (error instanceof JudgmentDAGArtifactError)
|
|
329
|
+
throw error;
|
|
330
|
+
throw artifactError('storage_io_error', 'artifact directory could not be prepared');
|
|
331
|
+
}
|
|
332
|
+
const file = locator(root, artifactId);
|
|
333
|
+
try {
|
|
334
|
+
const existing = await readRegularArtifact(file);
|
|
335
|
+
if (existing !== bytes)
|
|
336
|
+
throw artifactError('integrity_mismatch', 'existing artifact bytes do not match their content address');
|
|
337
|
+
return receipt(artifactId, record, 'existing');
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
if (!(error instanceof JudgmentDAGArtifactError) || error.code !== 'not_found')
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
const temporary = path.join(directory, `.tmp-${process.pid}-${randomUUID()}`);
|
|
344
|
+
let handle;
|
|
345
|
+
try {
|
|
346
|
+
handle = await open(temporary, 'wx', 0o600);
|
|
347
|
+
await handle.writeFile(bytes, 'utf8');
|
|
348
|
+
await handle.sync();
|
|
349
|
+
await handle.close();
|
|
350
|
+
handle = undefined;
|
|
351
|
+
try {
|
|
352
|
+
await link(temporary, file);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error.code !== 'EEXIST')
|
|
356
|
+
throw error;
|
|
357
|
+
const existing = await readRegularArtifact(file);
|
|
358
|
+
if (existing !== bytes)
|
|
359
|
+
throw artifactError('integrity_mismatch', 'existing artifact bytes do not match their content address');
|
|
360
|
+
return receipt(artifactId, record, 'existing');
|
|
361
|
+
}
|
|
362
|
+
return receipt(artifactId, record, 'created');
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (error instanceof JudgmentDAGArtifactError)
|
|
366
|
+
throw error;
|
|
367
|
+
throw artifactError('storage_io_error', 'run artifact could not be saved');
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
if (handle !== undefined)
|
|
371
|
+
await handle.close().catch(() => undefined);
|
|
372
|
+
await unlink(temporary).catch(() => undefined);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
export async function loadJudgmentDAGRunArtifact(request) {
|
|
376
|
+
if (!isPlainRecord(request))
|
|
377
|
+
throw artifactError('invalid_request', 'load request must be a plain object');
|
|
378
|
+
const root = requireRoot(request.root);
|
|
379
|
+
const file = locator(root, request.artifact_id);
|
|
380
|
+
await requireArtifactDirectory(root);
|
|
381
|
+
const bytes = await readRegularArtifact(file);
|
|
382
|
+
if (!bytes.endsWith('\n'))
|
|
383
|
+
throw artifactError('integrity_mismatch', 'stored artifact bytes are not canonical');
|
|
384
|
+
let parsed;
|
|
385
|
+
try {
|
|
386
|
+
parsed = JSON.parse(bytes.slice(0, -1));
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
throw artifactError('integrity_mismatch', 'stored artifact bytes are not complete JSON');
|
|
390
|
+
}
|
|
391
|
+
if (!isPlainRecord(parsed) || !hasExactKeys(parsed, ENVELOPE_KEYS) ||
|
|
392
|
+
parsed.artifact_version !== JUDGMENT_DAG_RUN_ARTIFACT_VERSION ||
|
|
393
|
+
typeof parsed.artifact_id !== 'string') {
|
|
394
|
+
throw artifactError('invalid_artifact', 'stored artifact envelope is invalid');
|
|
395
|
+
}
|
|
396
|
+
let record;
|
|
397
|
+
try {
|
|
398
|
+
record = validateRunRecord(parsed.record);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
throw artifactError('invalid_artifact', 'stored run record is invalid');
|
|
402
|
+
}
|
|
403
|
+
const payload = payloadFor(record);
|
|
404
|
+
const computedId = artifactIdFor(payload);
|
|
405
|
+
if (computedId !== request.artifact_id || parsed.artifact_id !== request.artifact_id) {
|
|
406
|
+
throw artifactError('integrity_mismatch', 'stored artifact identity does not match its content');
|
|
407
|
+
}
|
|
408
|
+
const canonicalBytes = `${canonicalJSON({
|
|
409
|
+
artifact_id: computedId,
|
|
410
|
+
artifact_version: JUDGMENT_DAG_RUN_ARTIFACT_VERSION,
|
|
411
|
+
record
|
|
412
|
+
})}\n`;
|
|
413
|
+
if (bytes !== canonicalBytes)
|
|
414
|
+
throw artifactError('integrity_mismatch', 'stored artifact bytes are not canonical');
|
|
415
|
+
return record;
|
|
416
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { JudgmentDAG } from './judgment-dag-core.js';
|
|
2
|
+
import { type JudgmentDAGRunRecord, type JudgmentDAGRunRequest } from './judgment-dag-runner.js';
|
|
3
|
+
import { type JudgmentDAGRunArtifactId } from './judgment-dag-artifact-store.js';
|
|
4
|
+
export declare const JUDGMENT_DAG_OUTCOME_ATTACHMENT_VERSION: "judgment-dag-outcome.v1";
|
|
5
|
+
export declare const JUDGMENT_DAG_EVALUATION_EVENT_SET_VERSION: "judgment-dag-evaluation-event-set.v1";
|
|
6
|
+
export type JudgmentDAGReplayEvaluationErrorCode = 'invalid_request' | 'runner_version_mismatch' | 'artifact_mismatch' | 'context_mismatch' | 'outcome_mismatch' | 'invalid_score';
|
|
7
|
+
export declare class JudgmentDAGReplayEvaluationError extends Error {
|
|
8
|
+
readonly code: JudgmentDAGReplayEvaluationErrorCode;
|
|
9
|
+
constructor(code: JudgmentDAGReplayEvaluationErrorCode, message: string);
|
|
10
|
+
}
|
|
11
|
+
export interface JudgmentDAGRunArtifactReference {
|
|
12
|
+
readonly artifact_id: JudgmentDAGRunArtifactId;
|
|
13
|
+
readonly record: JudgmentDAGRunRecord;
|
|
14
|
+
}
|
|
15
|
+
export interface ReplayJudgmentDAGRunRequest {
|
|
16
|
+
readonly source: JudgmentDAGRunArtifactReference;
|
|
17
|
+
readonly replay_run_id: string;
|
|
18
|
+
readonly mode: 'historical' | 'candidate';
|
|
19
|
+
readonly candidate_dag?: JudgmentDAG;
|
|
20
|
+
readonly runners: JudgmentDAGRunRequest['runners'];
|
|
21
|
+
}
|
|
22
|
+
export interface JudgmentDAGReplayResult {
|
|
23
|
+
readonly source_artifact_id: JudgmentDAGRunArtifactId;
|
|
24
|
+
readonly source_run_id: string;
|
|
25
|
+
readonly mode: 'historical' | 'candidate';
|
|
26
|
+
readonly record: JudgmentDAGRunRecord;
|
|
27
|
+
}
|
|
28
|
+
export interface JudgmentDAGOutcomeObservation {
|
|
29
|
+
readonly metric_id: string;
|
|
30
|
+
readonly scope: 'run' | 'node';
|
|
31
|
+
readonly node_id?: string;
|
|
32
|
+
readonly value: boolean | number;
|
|
33
|
+
}
|
|
34
|
+
export interface CreateJudgmentDAGOutcomeAttachmentRequest {
|
|
35
|
+
readonly run_artifact_id: JudgmentDAGRunArtifactId;
|
|
36
|
+
readonly record: JudgmentDAGRunRecord;
|
|
37
|
+
readonly observations: readonly JudgmentDAGOutcomeObservation[];
|
|
38
|
+
}
|
|
39
|
+
export interface JudgmentDAGOutcomeAttachment {
|
|
40
|
+
readonly attachment_version: typeof JUDGMENT_DAG_OUTCOME_ATTACHMENT_VERSION;
|
|
41
|
+
readonly attachment_id: JudgmentDAGRunArtifactId;
|
|
42
|
+
readonly run_artifact_id: JudgmentDAGRunArtifactId;
|
|
43
|
+
readonly run_id: string;
|
|
44
|
+
readonly observations: readonly JudgmentDAGOutcomeObservation[];
|
|
45
|
+
}
|
|
46
|
+
export interface JudgmentDAGEvaluationRun {
|
|
47
|
+
readonly artifact_id: JudgmentDAGRunArtifactId;
|
|
48
|
+
readonly record: JudgmentDAGRunRecord;
|
|
49
|
+
readonly outcome: JudgmentDAGOutcomeAttachment;
|
|
50
|
+
}
|
|
51
|
+
export interface JudgmentDAGEvaluationEvent {
|
|
52
|
+
readonly event_id: string;
|
|
53
|
+
readonly baseline: JudgmentDAGEvaluationRun;
|
|
54
|
+
readonly candidate: JudgmentDAGEvaluationRun;
|
|
55
|
+
}
|
|
56
|
+
export interface CreateJudgmentDAGEvaluationEventSetRequest {
|
|
57
|
+
readonly events: readonly JudgmentDAGEvaluationEvent[];
|
|
58
|
+
}
|
|
59
|
+
export interface JudgmentDAGEvaluationEventSet {
|
|
60
|
+
readonly event_set_version: typeof JUDGMENT_DAG_EVALUATION_EVENT_SET_VERSION;
|
|
61
|
+
readonly event_set_id: JudgmentDAGRunArtifactId;
|
|
62
|
+
readonly event_ids: readonly string[];
|
|
63
|
+
readonly events: readonly JudgmentDAGEvaluationEvent[];
|
|
64
|
+
}
|
|
65
|
+
export type JudgmentDAGEvaluationScoring = {
|
|
66
|
+
readonly kind: 'pass_fail';
|
|
67
|
+
readonly operator: 'eq' | 'gte' | 'lte';
|
|
68
|
+
readonly target: boolean | number;
|
|
69
|
+
} | {
|
|
70
|
+
readonly kind: 'numeric';
|
|
71
|
+
readonly direction: 'higher_is_better' | 'lower_is_better';
|
|
72
|
+
};
|
|
73
|
+
export interface JudgmentDAGEvaluationCriterion {
|
|
74
|
+
readonly criterion_id: string;
|
|
75
|
+
readonly goal: string;
|
|
76
|
+
readonly metric_id: string;
|
|
77
|
+
readonly scoring: JudgmentDAGEvaluationScoring;
|
|
78
|
+
}
|
|
79
|
+
export interface EvaluateJudgmentDAGVersionsRequest {
|
|
80
|
+
readonly event_set: JudgmentDAGEvaluationEventSet;
|
|
81
|
+
readonly criterion: JudgmentDAGEvaluationCriterion;
|
|
82
|
+
}
|
|
83
|
+
export interface JudgmentDAGScoreSummary {
|
|
84
|
+
readonly baseline: number;
|
|
85
|
+
readonly candidate: number;
|
|
86
|
+
readonly delta: number;
|
|
87
|
+
readonly event_count: number;
|
|
88
|
+
}
|
|
89
|
+
export type JudgmentDAGNodeCalibrationStatus = 'comparable' | 'baseline_only' | 'candidate_only' | 'no_observation';
|
|
90
|
+
export interface JudgmentDAGNodeCalibration {
|
|
91
|
+
readonly node_id: string;
|
|
92
|
+
readonly status: JudgmentDAGNodeCalibrationStatus;
|
|
93
|
+
readonly baseline: number | null;
|
|
94
|
+
readonly candidate: number | null;
|
|
95
|
+
readonly delta: number | null;
|
|
96
|
+
readonly event_count: number;
|
|
97
|
+
}
|
|
98
|
+
export interface JudgmentDAGVersionComparison {
|
|
99
|
+
readonly event_set_id: JudgmentDAGRunArtifactId;
|
|
100
|
+
readonly event_ids: readonly string[];
|
|
101
|
+
readonly criterion: JudgmentDAGEvaluationCriterion;
|
|
102
|
+
readonly overall: JudgmentDAGScoreSummary;
|
|
103
|
+
readonly node_calibration: readonly JudgmentDAGNodeCalibration[];
|
|
104
|
+
}
|
|
105
|
+
export declare function replayJudgmentDAGRun(request: ReplayJudgmentDAGRunRequest): Promise<JudgmentDAGReplayResult>;
|
|
106
|
+
export declare function createJudgmentDAGOutcomeAttachment(request: CreateJudgmentDAGOutcomeAttachmentRequest): JudgmentDAGOutcomeAttachment;
|
|
107
|
+
export declare function createJudgmentDAGEvaluationEventSet(request: CreateJudgmentDAGEvaluationEventSetRequest): JudgmentDAGEvaluationEventSet;
|
|
108
|
+
export declare function evaluateJudgmentDAGVersions(request: EvaluateJudgmentDAGVersionsRequest): JudgmentDAGVersionComparison;
|