@unson/brainbase-mcp 0.6.0 → 0.7.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/dist/canonical-task-contract.d.ts +9 -0
- package/dist/canonical-task-contract.js +39 -0
- package/dist/canonical-task-principal.d.ts +16 -0
- package/dist/canonical-task-principal.js +36 -0
- package/dist/canonical-task-service.d.ts +251 -0
- package/dist/canonical-task-service.js +809 -0
- package/dist/company-os-evaluation.d.ts +193 -0
- package/dist/company-os-evaluation.js +1150 -0
- package/dist/company-os-impact-review.d.ts +248 -0
- package/dist/company-os-impact-review.js +663 -0
- package/dist/company-os-learning-adoption.d.ts +288 -0
- package/dist/company-os-learning-adoption.js +944 -0
- package/dist/company-os-objectives.d.ts +52 -0
- package/dist/company-os-objectives.js +197 -0
- package/dist/company-os-receipt-adapter.d.ts +109 -0
- package/dist/company-os-receipt-adapter.js +636 -0
- package/dist/constraint-exception-store.d.ts +23 -0
- package/dist/constraint-exception-store.js +108 -0
- package/dist/constraint-resolution.d.ts +175 -0
- package/dist/constraint-resolution.js +922 -0
- package/dist/decision-adapter.d.ts +145 -0
- package/dist/decision-adapter.js +586 -0
- package/dist/durable-wait-http.d.ts +51 -0
- package/dist/durable-wait-http.js +497 -0
- package/dist/durable-waits.d.ts +234 -0
- package/dist/durable-waits.js +944 -0
- package/dist/execution-authority-http.d.ts +28 -0
- package/dist/execution-authority-http.js +359 -0
- package/dist/execution-authority.d.ts +201 -0
- package/dist/execution-authority.js +634 -0
- package/dist/foundation-catalog.d.ts +26 -0
- package/dist/foundation-catalog.js +206 -0
- package/dist/foundation-constraint-store.d.ts +50 -0
- package/dist/foundation-constraint-store.js +215 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/judgment-dag-composition.d.ts +224 -0
- package/dist/judgment-dag-composition.js +731 -0
- package/dist/judgment-dag.d.ts +2 -0
- package/dist/judgment-dag.js +1 -0
- package/dist/judgment-problem-snapshot.d.ts +185 -0
- package/dist/judgment-problem-snapshot.js +777 -0
- package/dist/judgment-view-http.d.ts +22 -0
- package/dist/judgment-view-http.js +128 -0
- package/dist/judgment-view.d.ts +222 -0
- package/dist/judgment-view.js +627 -0
- package/dist/knowledge-adapter.d.ts +112 -0
- package/dist/knowledge-adapter.js +691 -0
- package/dist/ontology-foundation.d.ts +209 -0
- package/dist/ontology-foundation.js +1000 -0
- package/dist/ontology.d.ts +13 -0
- package/dist/ontology.js +14 -0
- package/dist/organization-connection.d.ts +164 -0
- package/dist/organization-connection.js +428 -0
- package/dist/personal-knowledge.d.ts +120 -0
- package/dist/personal-knowledge.js +750 -0
- package/dist/problem-candidates.d.ts +149 -0
- package/dist/problem-candidates.js +898 -0
- package/dist/problem-selection.d.ts +238 -0
- package/dist/problem-selection.js +995 -0
- package/dist/resource-reservation-http.d.ts +31 -0
- package/dist/resource-reservation-http.js +371 -0
- package/dist/resource-reservations.d.ts +221 -0
- package/dist/resource-reservations.js +577 -0
- package/dist/server.d.ts +97 -0
- package/dist/server.js +180 -1
- package/dist/ssot.d.ts +6 -1
- package/dist/ssot.js +63 -5
- package/dist/streamable-http.d.ts +42 -0
- package/dist/streamable-http.js +288 -0
- package/dist/types.d.ts +20 -0
- package/dist/world-model.d.ts +181 -0
- package/dist/world-model.js +873 -0
- package/package.json +139 -3
- package/ui/judgment-view.css +238 -0
- package/ui/judgment-view.js +508 -0
- package/ui/objective-editor.css +180 -0
- package/ui/objective-editor.js +1139 -0
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, open, lstat, mkdir, readdir, readFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { validateFoundationDefinition } from './ontology-foundation.js';
|
|
5
|
+
/**
|
|
6
|
+
* An immutable input bundle for one judgment.
|
|
7
|
+
*
|
|
8
|
+
* The bundle deliberately stores references to canonical objects instead of
|
|
9
|
+
* copying Objective, Model, or Constraint definitions into a second SSOT.
|
|
10
|
+
* Providers validate those references at the boundary of save/load.
|
|
11
|
+
*/
|
|
12
|
+
export const JUDGMENT_PROBLEM_SNAPSHOT_VERSION = 'judgment-problem-snapshot.v1';
|
|
13
|
+
export class JudgmentProblemSnapshotError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
reference;
|
|
16
|
+
constructor(code, message, options = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'JudgmentProblemSnapshotError';
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.reference = options.reference;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const SNAPSHOT_ID_PATTERN = /^sha256:([0-9a-f]{64})$/u;
|
|
24
|
+
const POSITIVE_REVISION_PATTERN = /^[1-9]\d*$/u;
|
|
25
|
+
const SCOPE_TYPES = ['personal', 'project', 'organization'];
|
|
26
|
+
const REFERENCE_KINDS = [
|
|
27
|
+
'objective',
|
|
28
|
+
'criterion',
|
|
29
|
+
'variable',
|
|
30
|
+
'observation',
|
|
31
|
+
'model',
|
|
32
|
+
'constraint',
|
|
33
|
+
'authority',
|
|
34
|
+
'resource',
|
|
35
|
+
'deadline',
|
|
36
|
+
'dag',
|
|
37
|
+
'evidence'
|
|
38
|
+
];
|
|
39
|
+
const SNAPSHOT_KEYS = [
|
|
40
|
+
'created_at',
|
|
41
|
+
'execution_permission',
|
|
42
|
+
'owner_scope',
|
|
43
|
+
'problem_id',
|
|
44
|
+
'question',
|
|
45
|
+
'read_policy',
|
|
46
|
+
'references',
|
|
47
|
+
'revision',
|
|
48
|
+
'snapshot_version'
|
|
49
|
+
];
|
|
50
|
+
const ENVELOPE_KEYS = ['snapshot', 'snapshot_id', 'snapshot_version'];
|
|
51
|
+
const REFERENCE_KEYS = ['digest', 'evidence', 'id', 'kind', 'revision', 'scope', 'valid_from', 'valid_to'];
|
|
52
|
+
const EVIDENCE_KEYS = ['access', 'content', 'digest', 'mode', 'reference_id', 'reference_revision'];
|
|
53
|
+
const SCOPE_KEYS = ['id', 'type'];
|
|
54
|
+
const ACL_KEYS = ['ownerId', 'readerIds', 'visibility', 'writerIds'];
|
|
55
|
+
const LOCATOR_DIRECTORY = 'judgment-problem-snapshots';
|
|
56
|
+
const REVISION_DIRECTORY = 'revisions';
|
|
57
|
+
function fail(code, message, reference) {
|
|
58
|
+
throw new JudgmentProblemSnapshotError(code, message, reference ? { reference } : undefined);
|
|
59
|
+
}
|
|
60
|
+
function isPlainRecord(value) {
|
|
61
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
62
|
+
return false;
|
|
63
|
+
const prototype = Object.getPrototypeOf(value);
|
|
64
|
+
return prototype === Object.prototype || prototype === null;
|
|
65
|
+
}
|
|
66
|
+
function hasExactKeys(value, expected) {
|
|
67
|
+
const keys = Object.keys(value).sort();
|
|
68
|
+
const sorted = [...expected].sort();
|
|
69
|
+
return keys.length === sorted.length && keys.every((key, index) => key === sorted[index]);
|
|
70
|
+
}
|
|
71
|
+
function hasOnlyKeys(value, allowed) {
|
|
72
|
+
const allowedSet = new Set(allowed);
|
|
73
|
+
return Object.keys(value).every((key) => allowedSet.has(key));
|
|
74
|
+
}
|
|
75
|
+
function requireString(value, label) {
|
|
76
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.includes('\0')) {
|
|
77
|
+
fail('invalid_request', `${label} must be a non-empty string`);
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function requireRevision(value, label) {
|
|
82
|
+
const revision = requireString(value, label);
|
|
83
|
+
if (!POSITIVE_REVISION_PATTERN.test(revision)) {
|
|
84
|
+
fail('invalid_request', `${label} must be a positive canonical decimal revision`);
|
|
85
|
+
}
|
|
86
|
+
return revision;
|
|
87
|
+
}
|
|
88
|
+
function requireSnapshotId(value, label) {
|
|
89
|
+
if (typeof value !== 'string' || !SNAPSHOT_ID_PATTERN.test(value)) {
|
|
90
|
+
fail('invalid_request', `${label} must be sha256:<64 lowercase hex>`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
function requireDate(value, label) {
|
|
95
|
+
const date = requireString(value, label);
|
|
96
|
+
if (Number.isNaN(Date.parse(date)))
|
|
97
|
+
fail('invalid_request', `${label} must be an ISO date`);
|
|
98
|
+
return date;
|
|
99
|
+
}
|
|
100
|
+
function assertScope(value, label) {
|
|
101
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, SCOPE_KEYS) ||
|
|
102
|
+
!SCOPE_TYPES.includes(value.type) ||
|
|
103
|
+
typeof value.id !== 'string' || value.id.trim().length === 0 || value.id.includes('\0')) {
|
|
104
|
+
fail('invalid_request', `${label} is invalid`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function assertAcl(value, label) {
|
|
108
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ACL_KEYS) ||
|
|
109
|
+
typeof value.ownerId !== 'string' || value.ownerId.trim().length === 0 || value.ownerId.includes('\0') ||
|
|
110
|
+
!Array.isArray(value.readerIds) || !Array.isArray(value.writerIds) ||
|
|
111
|
+
!['private', 'project', 'organization', 'public'].includes(String(value.visibility))) {
|
|
112
|
+
fail('invalid_request', `${label} is invalid`);
|
|
113
|
+
}
|
|
114
|
+
if (value.readerIds.some((id) => typeof id !== 'string' || id.trim().length === 0) ||
|
|
115
|
+
value.writerIds.some((id) => typeof id !== 'string' || id.trim().length === 0)) {
|
|
116
|
+
fail('invalid_request', `${label}.readerIds and writerIds must contain non-empty strings`);
|
|
117
|
+
}
|
|
118
|
+
const readerIds = value.readerIds;
|
|
119
|
+
const writerIds = value.writerIds;
|
|
120
|
+
if (new Set(readerIds).size !== readerIds.length || new Set(writerIds).size !== writerIds.length) {
|
|
121
|
+
fail('invalid_request', `${label}.readerIds and writerIds must not contain duplicates`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function compareUnicodeCodePoints(left, right) {
|
|
125
|
+
const leftPoints = Array.from(left, (character) => character.codePointAt(0));
|
|
126
|
+
const rightPoints = Array.from(right, (character) => character.codePointAt(0));
|
|
127
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
128
|
+
for (let index = 0; index < length; index += 1) {
|
|
129
|
+
if (leftPoints[index] !== rightPoints[index])
|
|
130
|
+
return leftPoints[index] - rightPoints[index];
|
|
131
|
+
}
|
|
132
|
+
return leftPoints.length - rightPoints.length;
|
|
133
|
+
}
|
|
134
|
+
function canonicalValue(value, active = new Set()) {
|
|
135
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
136
|
+
return value;
|
|
137
|
+
if (typeof value === 'number') {
|
|
138
|
+
if (!Number.isFinite(value))
|
|
139
|
+
fail('invalid_request', 'snapshot values must be finite JSON numbers');
|
|
140
|
+
return Object.is(value, -0) ? 0 : value;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value !== 'object')
|
|
143
|
+
fail('invalid_request', 'snapshot values must be JSON-compatible');
|
|
144
|
+
if (active.has(value))
|
|
145
|
+
fail('invalid_request', 'snapshot values must not be cyclic');
|
|
146
|
+
active.add(value);
|
|
147
|
+
try {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
const result = [];
|
|
150
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
151
|
+
if (!(index in value))
|
|
152
|
+
fail('invalid_request', 'snapshot arrays must not be sparse');
|
|
153
|
+
result.push(canonicalValue(value[index], active));
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
if (!isPlainRecord(value))
|
|
158
|
+
fail('invalid_request', 'snapshot objects must be plain records');
|
|
159
|
+
const result = Object.create(null);
|
|
160
|
+
for (const key of Object.keys(value).sort(compareUnicodeCodePoints)) {
|
|
161
|
+
result[key] = canonicalValue(value[key], active);
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
active.delete(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function canonicalJson(value) {
|
|
170
|
+
const json = JSON.stringify(canonicalValue(value));
|
|
171
|
+
if (json === undefined)
|
|
172
|
+
fail('invalid_request', 'snapshot is not JSON serializable');
|
|
173
|
+
return json;
|
|
174
|
+
}
|
|
175
|
+
function deepFreeze(value) {
|
|
176
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
177
|
+
Object.freeze(value);
|
|
178
|
+
for (const child of Object.values(value))
|
|
179
|
+
deepFreeze(child);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function contentDigest(value) {
|
|
184
|
+
return `sha256:${createHash('sha256').update(canonicalJson(value), 'utf8').digest('hex')}`;
|
|
185
|
+
}
|
|
186
|
+
function payloadFor(snapshot) {
|
|
187
|
+
return { snapshot_version: JUDGMENT_PROBLEM_SNAPSHOT_VERSION, snapshot };
|
|
188
|
+
}
|
|
189
|
+
function validateEvidence(value, label) {
|
|
190
|
+
if (!isPlainRecord(value) || !hasOnlyKeys(value, EVIDENCE_KEYS) ||
|
|
191
|
+
!Object.prototype.hasOwnProperty.call(value, 'mode') ||
|
|
192
|
+
!Object.prototype.hasOwnProperty.call(value, 'digest') ||
|
|
193
|
+
!Object.prototype.hasOwnProperty.call(value, 'access') ||
|
|
194
|
+
(value.mode !== 'embedded_content' && value.mode !== 'immutable_reference') ||
|
|
195
|
+
typeof value.digest !== 'string' || !SNAPSHOT_ID_PATTERN.test(value.digest)) {
|
|
196
|
+
fail('invalid_request', `${label} is invalid`);
|
|
197
|
+
}
|
|
198
|
+
assertAcl(value.access, `${label}.access`);
|
|
199
|
+
const hasContent = Object.prototype.hasOwnProperty.call(value, 'content') && value.content !== undefined;
|
|
200
|
+
const hasReference = typeof value.reference_id === 'string' && value.reference_id.length > 0 &&
|
|
201
|
+
typeof value.reference_revision === 'string' && value.reference_revision.length > 0;
|
|
202
|
+
if (value.mode === 'embedded_content') {
|
|
203
|
+
if (!hasContent || hasReference)
|
|
204
|
+
fail('invalid_request', `${label} embedded content binding is invalid`);
|
|
205
|
+
const content = canonicalValue(value.content);
|
|
206
|
+
if (contentDigest(content) !== value.digest)
|
|
207
|
+
fail('integrity_mismatch', `${label}.content does not match digest`);
|
|
208
|
+
return deepFreeze({
|
|
209
|
+
mode: 'embedded_content',
|
|
210
|
+
digest: value.digest,
|
|
211
|
+
content,
|
|
212
|
+
access: normalizeAcl(value.access)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (hasContent || !hasReference)
|
|
216
|
+
fail('invalid_request', `${label} immutable reference binding is invalid`);
|
|
217
|
+
requireRevision(value.reference_revision, `${label}.reference_revision`);
|
|
218
|
+
return deepFreeze({
|
|
219
|
+
mode: 'immutable_reference',
|
|
220
|
+
digest: value.digest,
|
|
221
|
+
reference_id: value.reference_id,
|
|
222
|
+
reference_revision: value.reference_revision,
|
|
223
|
+
access: normalizeAcl(value.access)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function normalizeAcl(value) {
|
|
227
|
+
return {
|
|
228
|
+
ownerId: value.ownerId,
|
|
229
|
+
visibility: value.visibility,
|
|
230
|
+
readerIds: [...value.readerIds],
|
|
231
|
+
writerIds: [...value.writerIds]
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function validateReference(value, index) {
|
|
235
|
+
const label = `references[${index}]`;
|
|
236
|
+
if (!isPlainRecord(value) || !hasOnlyKeys(value, REFERENCE_KEYS) ||
|
|
237
|
+
!Object.prototype.hasOwnProperty.call(value, 'kind') ||
|
|
238
|
+
!Object.prototype.hasOwnProperty.call(value, 'id') ||
|
|
239
|
+
!Object.prototype.hasOwnProperty.call(value, 'revision') ||
|
|
240
|
+
!Object.prototype.hasOwnProperty.call(value, 'digest') ||
|
|
241
|
+
!Object.prototype.hasOwnProperty.call(value, 'scope') ||
|
|
242
|
+
!Object.prototype.hasOwnProperty.call(value, 'valid_from') ||
|
|
243
|
+
!REFERENCE_KINDS.includes(value.kind)) {
|
|
244
|
+
fail('invalid_request', `${label} is invalid`);
|
|
245
|
+
}
|
|
246
|
+
const id = requireString(value.id, `${label}.id`);
|
|
247
|
+
const revision = requireRevision(value.revision, `${label}.revision`);
|
|
248
|
+
const digest = requireSnapshotId(value.digest, `${label}.digest`);
|
|
249
|
+
assertScope(value.scope, `${label}.scope`);
|
|
250
|
+
const validFrom = requireDate(value.valid_from, `${label}.valid_from`);
|
|
251
|
+
const validTo = value.valid_to === undefined || value.valid_to === null
|
|
252
|
+
? undefined
|
|
253
|
+
: requireDate(value.valid_to, `${label}.valid_to`);
|
|
254
|
+
if (validTo !== undefined && Date.parse(validTo) < Date.parse(validFrom)) {
|
|
255
|
+
fail('invalid_request', `${label}.valid_to precedes valid_from`);
|
|
256
|
+
}
|
|
257
|
+
const evidence = value.evidence === undefined ? undefined : validateEvidence(value.evidence, `${label}.evidence`);
|
|
258
|
+
return deepFreeze({
|
|
259
|
+
kind: value.kind,
|
|
260
|
+
id,
|
|
261
|
+
revision,
|
|
262
|
+
digest,
|
|
263
|
+
scope: { type: value.scope.type, id: value.scope.id },
|
|
264
|
+
valid_from: validFrom,
|
|
265
|
+
...(validTo === undefined ? {} : { valid_to: validTo }),
|
|
266
|
+
...(evidence === undefined ? {} : { evidence })
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function validateSnapshot(value) {
|
|
270
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, SNAPSHOT_KEYS) ||
|
|
271
|
+
value.snapshot_version !== JUDGMENT_PROBLEM_SNAPSHOT_VERSION ||
|
|
272
|
+
value.execution_permission !== 'none') {
|
|
273
|
+
fail('invalid_request', 'snapshot shape or version is invalid');
|
|
274
|
+
}
|
|
275
|
+
const problemId = requireString(value.problem_id, 'snapshot.problem_id');
|
|
276
|
+
const revision = requireRevision(value.revision, 'snapshot.revision');
|
|
277
|
+
const question = requireString(value.question, 'snapshot.question');
|
|
278
|
+
assertScope(value.owner_scope, 'snapshot.owner_scope');
|
|
279
|
+
assertAcl(value.read_policy, 'snapshot.read_policy');
|
|
280
|
+
if (!Array.isArray(value.references) || value.references.length === 0) {
|
|
281
|
+
fail('missing_reference', 'snapshot.references must contain required references');
|
|
282
|
+
}
|
|
283
|
+
const references = value.references.map((reference, index) => validateReference(reference, index));
|
|
284
|
+
const createdAt = requireDate(value.created_at, 'snapshot.created_at');
|
|
285
|
+
return deepFreeze({
|
|
286
|
+
snapshot_version: JUDGMENT_PROBLEM_SNAPSHOT_VERSION,
|
|
287
|
+
problem_id: problemId,
|
|
288
|
+
revision,
|
|
289
|
+
question,
|
|
290
|
+
owner_scope: { type: value.owner_scope.type, id: value.owner_scope.id },
|
|
291
|
+
references,
|
|
292
|
+
read_policy: normalizeAcl(value.read_policy),
|
|
293
|
+
execution_permission: 'none',
|
|
294
|
+
created_at: createdAt
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function requiredKinds(references) {
|
|
298
|
+
return new Set(references.map((reference) => reference.kind));
|
|
299
|
+
}
|
|
300
|
+
function validateRequiredReferences(snapshot) {
|
|
301
|
+
const kinds = requiredKinds(snapshot.references);
|
|
302
|
+
const required = [
|
|
303
|
+
'objective',
|
|
304
|
+
'criterion',
|
|
305
|
+
'observation',
|
|
306
|
+
'model',
|
|
307
|
+
'constraint',
|
|
308
|
+
'authority',
|
|
309
|
+
'resource',
|
|
310
|
+
'deadline'
|
|
311
|
+
];
|
|
312
|
+
const missing = required.filter((kind) => !kinds.has(kind));
|
|
313
|
+
if (missing.length > 0)
|
|
314
|
+
fail('missing_reference', `snapshot is missing required references: ${missing.join(', ')}`);
|
|
315
|
+
}
|
|
316
|
+
function requireRoot(value) {
|
|
317
|
+
if (typeof value !== 'string' || value.trim().length === 0 || value.includes('\0')) {
|
|
318
|
+
fail('invalid_request', 'artifact root must be a non-empty filesystem path');
|
|
319
|
+
}
|
|
320
|
+
return path.resolve(value);
|
|
321
|
+
}
|
|
322
|
+
function locatorFor(root, problemId, revision) {
|
|
323
|
+
const keyDigest = contentDigest({ problem_id: problemId, revision });
|
|
324
|
+
const hex = keyDigest.slice('sha256:'.length);
|
|
325
|
+
return {
|
|
326
|
+
key_digest: keyDigest,
|
|
327
|
+
file: path.join(root, LOCATOR_DIRECTORY, REVISION_DIRECTORY, `${hex}.json`)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function providerResultError(reference, result) {
|
|
331
|
+
if (result.status === 'missing') {
|
|
332
|
+
return new JudgmentProblemSnapshotError('missing_reference', result.message ?? `Reference ${reference.kind}/${reference.id}@${reference.revision} is missing`, { reference });
|
|
333
|
+
}
|
|
334
|
+
if (result.status === 'not_applicable') {
|
|
335
|
+
return new JudgmentProblemSnapshotError('not_applicable', result.message ?? `Reference ${reference.kind}/${reference.id}@${reference.revision} is not applicable`, { reference });
|
|
336
|
+
}
|
|
337
|
+
if (result.status === 'unauthorized') {
|
|
338
|
+
return new JudgmentProblemSnapshotError('unauthorized', result.message ?? `Reference ${reference.kind}/${reference.id}@${reference.revision} is not readable`, { reference });
|
|
339
|
+
}
|
|
340
|
+
return new JudgmentProblemSnapshotError('unresolved_constraint', result.message ?? `Reference ${reference.kind}/${reference.id}@${reference.revision} is unresolved`, { reference });
|
|
341
|
+
}
|
|
342
|
+
function evidenceReference(owner, evidence) {
|
|
343
|
+
if (evidence.mode !== 'immutable_reference' || evidence.reference_id === undefined || evidence.reference_revision === undefined) {
|
|
344
|
+
fail('invalid_request', 'immutable evidence reference is incomplete', owner);
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
kind: 'evidence',
|
|
348
|
+
id: evidence.reference_id,
|
|
349
|
+
revision: evidence.reference_revision,
|
|
350
|
+
digest: evidence.digest,
|
|
351
|
+
scope: owner.scope,
|
|
352
|
+
valid_from: owner.valid_from,
|
|
353
|
+
...(owner.valid_to === undefined ? {} : { valid_to: owner.valid_to })
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
async function resolveOneReference(reference, phase, context, provider, errorReference = reference) {
|
|
357
|
+
let result;
|
|
358
|
+
try {
|
|
359
|
+
result = await provider.resolve({ reference, phase, context });
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
if (error instanceof JudgmentProblemSnapshotError)
|
|
363
|
+
throw error;
|
|
364
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
365
|
+
? error.code
|
|
366
|
+
: undefined;
|
|
367
|
+
if (code === 'authorization_denied' || code === 'unauthorized') {
|
|
368
|
+
fail('unauthorized', `Reference provider denied ${reference.kind}/${reference.id}@${reference.revision}: ${error instanceof Error ? error.message : String(error)}`, errorReference);
|
|
369
|
+
}
|
|
370
|
+
fail('unresolved_constraint', `Reference provider failed for ${reference.kind}/${reference.id}@${reference.revision}: ${error instanceof Error ? error.message : String(error)}`, errorReference);
|
|
371
|
+
}
|
|
372
|
+
if (!result || !['resolved', 'missing', 'not_applicable', 'unauthorized', 'unresolved'].includes(result.status)) {
|
|
373
|
+
fail('invalid_request', `Reference provider returned an invalid result for ${reference.kind}/${reference.id}@${reference.revision}`, errorReference);
|
|
374
|
+
}
|
|
375
|
+
if (result.status !== 'resolved')
|
|
376
|
+
throw providerResultError(errorReference, result);
|
|
377
|
+
if (result.digest === undefined) {
|
|
378
|
+
fail('integrity_mismatch', `Reference provider did not return the exact revision digest for ${reference.kind}/${reference.id}@${reference.revision}`, errorReference);
|
|
379
|
+
}
|
|
380
|
+
if (result.digest !== reference.digest) {
|
|
381
|
+
fail('integrity_mismatch', `Reference provider digest does not match ${reference.kind}/${reference.id}@${reference.revision}`, errorReference);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function resolveReferences(snapshot, phase, context, provider) {
|
|
385
|
+
for (const reference of snapshot.references) {
|
|
386
|
+
await resolveOneReference(reference, phase, context, provider);
|
|
387
|
+
if (reference.evidence?.mode === 'immutable_reference') {
|
|
388
|
+
await resolveOneReference(evidenceReference(reference, reference.evidence), phase, context, provider, reference);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function authorize(action, snapshot, context, provider) {
|
|
393
|
+
try {
|
|
394
|
+
const result = await provider.authorize({
|
|
395
|
+
action,
|
|
396
|
+
context,
|
|
397
|
+
acl: snapshot.read_policy,
|
|
398
|
+
snapshot
|
|
399
|
+
});
|
|
400
|
+
if (result === false)
|
|
401
|
+
throw new Error('provider denied access');
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (error instanceof JudgmentProblemSnapshotError)
|
|
405
|
+
throw error;
|
|
406
|
+
fail('unauthorized', `${action} access was denied: ${error instanceof Error ? error.message : String(error)}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
export const defaultJudgmentProblemSnapshotAccessProvider = {
|
|
410
|
+
authorize({ action, context, acl }) {
|
|
411
|
+
const principal = context.principal;
|
|
412
|
+
if (action === 'write') {
|
|
413
|
+
return acl.ownerId === principal || acl.writerIds.includes(principal);
|
|
414
|
+
}
|
|
415
|
+
return acl.visibility === 'public' || acl.ownerId === principal ||
|
|
416
|
+
acl.readerIds.includes(principal) || acl.writerIds.includes(principal);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
async function assertRevisionDirectory(root, create) {
|
|
420
|
+
let rootDetails;
|
|
421
|
+
try {
|
|
422
|
+
rootDetails = await lstat(root);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
fail('invalid_request', 'artifact root must be an existing regular directory');
|
|
426
|
+
}
|
|
427
|
+
if (!rootDetails.isDirectory() || rootDetails.isSymbolicLink()) {
|
|
428
|
+
fail('invalid_request', 'artifact root must be an existing regular directory');
|
|
429
|
+
}
|
|
430
|
+
const base = path.join(root, LOCATOR_DIRECTORY);
|
|
431
|
+
const revisions = path.join(base, REVISION_DIRECTORY);
|
|
432
|
+
if (!create) {
|
|
433
|
+
const details = await lstat(revisions).catch(() => undefined);
|
|
434
|
+
if (details === undefined)
|
|
435
|
+
fail('not_found', 'judgment problem snapshot was not found');
|
|
436
|
+
if (!details.isDirectory() || details.isSymbolicLink())
|
|
437
|
+
fail('integrity_mismatch', 'snapshot revision directory is invalid');
|
|
438
|
+
return revisions;
|
|
439
|
+
}
|
|
440
|
+
for (const directory of [base, revisions]) {
|
|
441
|
+
let details = await lstat(directory).catch(() => undefined);
|
|
442
|
+
if (details === undefined) {
|
|
443
|
+
try {
|
|
444
|
+
await mkdir(directory, { mode: 0o700 });
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (error.code !== 'EEXIST')
|
|
448
|
+
fail('storage_io_error', 'snapshot directory could not be prepared');
|
|
449
|
+
}
|
|
450
|
+
details = await lstat(directory).catch(() => undefined);
|
|
451
|
+
}
|
|
452
|
+
if (!details || !details.isDirectory() || details.isSymbolicLink()) {
|
|
453
|
+
fail('integrity_mismatch', 'snapshot directory must be a regular directory');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return revisions;
|
|
457
|
+
}
|
|
458
|
+
async function readRegular(file) {
|
|
459
|
+
const details = await lstat(file).catch((error) => {
|
|
460
|
+
if (error.code === 'ENOENT')
|
|
461
|
+
fail('not_found', 'judgment problem snapshot was not found');
|
|
462
|
+
fail('storage_io_error', 'snapshot metadata could not be read');
|
|
463
|
+
});
|
|
464
|
+
if (!details.isFile() || details.isSymbolicLink())
|
|
465
|
+
fail('integrity_mismatch', 'snapshot must be a regular file');
|
|
466
|
+
try {
|
|
467
|
+
return await readFile(file, 'utf8');
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
fail('storage_io_error', 'snapshot bytes could not be read');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function envelopeBytes(envelope) {
|
|
474
|
+
return `${canonicalJson(envelope)}\n`;
|
|
475
|
+
}
|
|
476
|
+
function parseEnvelope(bytes) {
|
|
477
|
+
if (!bytes.endsWith('\n'))
|
|
478
|
+
fail('integrity_mismatch', 'stored snapshot bytes are not canonical');
|
|
479
|
+
let parsed;
|
|
480
|
+
try {
|
|
481
|
+
parsed = JSON.parse(bytes.slice(0, -1));
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
fail('integrity_mismatch', 'stored snapshot bytes are not complete JSON');
|
|
485
|
+
}
|
|
486
|
+
if (!isPlainRecord(parsed) || !hasExactKeys(parsed, ENVELOPE_KEYS) ||
|
|
487
|
+
parsed.snapshot_version !== JUDGMENT_PROBLEM_SNAPSHOT_VERSION ||
|
|
488
|
+
typeof parsed.snapshot_id !== 'string') {
|
|
489
|
+
fail('integrity_mismatch', 'stored snapshot envelope is invalid');
|
|
490
|
+
}
|
|
491
|
+
const snapshot = validateSnapshot(parsed.snapshot);
|
|
492
|
+
validateRequiredReferences(snapshot);
|
|
493
|
+
const snapshotId = requireSnapshotId(parsed.snapshot_id, 'stored snapshot_id');
|
|
494
|
+
const expectedId = computeJudgmentProblemSnapshotId(snapshot);
|
|
495
|
+
if (snapshotId !== expectedId)
|
|
496
|
+
fail('integrity_mismatch', 'stored snapshot identity does not match its content');
|
|
497
|
+
const canonicalBytes = envelopeBytes({
|
|
498
|
+
snapshot_id: expectedId,
|
|
499
|
+
snapshot_version: JUDGMENT_PROBLEM_SNAPSHOT_VERSION,
|
|
500
|
+
snapshot
|
|
501
|
+
});
|
|
502
|
+
if (bytes !== canonicalBytes)
|
|
503
|
+
fail('integrity_mismatch', 'stored snapshot bytes are not canonical');
|
|
504
|
+
return {
|
|
505
|
+
envelope: deepFreeze({
|
|
506
|
+
snapshot_id: expectedId,
|
|
507
|
+
snapshot_version: JUDGMENT_PROBLEM_SNAPSHOT_VERSION,
|
|
508
|
+
snapshot
|
|
509
|
+
}),
|
|
510
|
+
bytes
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
async function findById(root, snapshotId) {
|
|
514
|
+
const directory = await assertRevisionDirectory(root, false);
|
|
515
|
+
const direct = path.join(directory, `${snapshotId.slice('sha256:'.length)}.json`);
|
|
516
|
+
const directBytes = await readRegular(direct).catch((error) => {
|
|
517
|
+
if (error instanceof JudgmentProblemSnapshotError && error.code === 'not_found')
|
|
518
|
+
return undefined;
|
|
519
|
+
throw error;
|
|
520
|
+
});
|
|
521
|
+
if (directBytes !== undefined)
|
|
522
|
+
return parseEnvelope(directBytes);
|
|
523
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => {
|
|
524
|
+
fail('storage_io_error', 'snapshot revision directory could not be listed');
|
|
525
|
+
});
|
|
526
|
+
for (const entry of entries) {
|
|
527
|
+
if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith('.json'))
|
|
528
|
+
continue;
|
|
529
|
+
const bytes = await readRegular(path.join(directory, entry.name));
|
|
530
|
+
const parsed = parseEnvelope(bytes);
|
|
531
|
+
if (parsed.envelope.snapshot_id === snapshotId)
|
|
532
|
+
return parsed;
|
|
533
|
+
}
|
|
534
|
+
fail('not_found', 'judgment problem snapshot was not found');
|
|
535
|
+
}
|
|
536
|
+
function receipt(snapshot, snapshotId, status) {
|
|
537
|
+
return deepFreeze({
|
|
538
|
+
snapshot_id: snapshotId,
|
|
539
|
+
snapshot_version: JUDGMENT_PROBLEM_SNAPSHOT_VERSION,
|
|
540
|
+
problem_id: snapshot.problem_id,
|
|
541
|
+
revision: snapshot.revision,
|
|
542
|
+
status
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Derive the content address without writing. This validates only the
|
|
547
|
+
* snapshot shape; canonical reference resolution belongs to providers.
|
|
548
|
+
*/
|
|
549
|
+
export function computeJudgmentProblemSnapshotId(value) {
|
|
550
|
+
return contentDigest(payloadFor(validateSnapshot(value)));
|
|
551
|
+
}
|
|
552
|
+
export async function saveJudgmentProblemSnapshot(request) {
|
|
553
|
+
if (!isPlainRecord(request))
|
|
554
|
+
fail('invalid_request', 'save request must be a plain object');
|
|
555
|
+
const root = requireRoot(request.root);
|
|
556
|
+
const snapshot = validateSnapshot(request.snapshot);
|
|
557
|
+
validateRequiredReferences(snapshot);
|
|
558
|
+
if (!request.referenceProvider || typeof request.referenceProvider.resolve !== 'function') {
|
|
559
|
+
fail('invalid_request', 'referenceProvider.resolve is required');
|
|
560
|
+
}
|
|
561
|
+
const context = validateAccessContext(request.access);
|
|
562
|
+
const accessProvider = request.accessProvider ?? defaultJudgmentProblemSnapshotAccessProvider;
|
|
563
|
+
await authorize('write', snapshot, context, accessProvider);
|
|
564
|
+
for (const reference of snapshot.references) {
|
|
565
|
+
if (reference.evidence === undefined)
|
|
566
|
+
continue;
|
|
567
|
+
try {
|
|
568
|
+
const allowed = await accessProvider.authorize({
|
|
569
|
+
action: 'read',
|
|
570
|
+
context,
|
|
571
|
+
acl: reference.evidence.access,
|
|
572
|
+
snapshot,
|
|
573
|
+
reference
|
|
574
|
+
});
|
|
575
|
+
if (allowed === false) {
|
|
576
|
+
fail('unauthorized', `evidence read access was denied for ${reference.kind}/${reference.id}@${reference.revision}`, reference);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch (error) {
|
|
580
|
+
if (error instanceof JudgmentProblemSnapshotError)
|
|
581
|
+
throw error;
|
|
582
|
+
fail('unauthorized', `evidence read access was denied for ${reference.kind}/${reference.id}@${reference.revision}`, reference);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
await resolveReferences(snapshot, 'save', context, request.referenceProvider);
|
|
586
|
+
const snapshotId = computeJudgmentProblemSnapshotId(snapshot);
|
|
587
|
+
const envelope = {
|
|
588
|
+
snapshot_id: snapshotId,
|
|
589
|
+
snapshot_version: JUDGMENT_PROBLEM_SNAPSHOT_VERSION,
|
|
590
|
+
snapshot
|
|
591
|
+
};
|
|
592
|
+
const bytes = envelopeBytes(envelope);
|
|
593
|
+
const directory = await assertRevisionDirectory(root, true);
|
|
594
|
+
const file = locatorFor(root, snapshot.problem_id, snapshot.revision).file;
|
|
595
|
+
const temporary = path.join(directory, `.tmp-${process.pid}-${randomUUID()}`);
|
|
596
|
+
let handle;
|
|
597
|
+
try {
|
|
598
|
+
handle = await open(temporary, 'wx', 0o600);
|
|
599
|
+
await handle.writeFile(bytes, 'utf8');
|
|
600
|
+
await handle.sync();
|
|
601
|
+
await handle.close();
|
|
602
|
+
handle = undefined;
|
|
603
|
+
try {
|
|
604
|
+
await link(temporary, file);
|
|
605
|
+
return receipt(snapshot, snapshotId, 'created');
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
if (error.code !== 'EEXIST')
|
|
609
|
+
throw error;
|
|
610
|
+
const existingBytes = await readRegular(file);
|
|
611
|
+
const existing = parseEnvelope(existingBytes).envelope;
|
|
612
|
+
if (existing.snapshot.problem_id !== snapshot.problem_id || existing.snapshot.revision !== snapshot.revision) {
|
|
613
|
+
fail('integrity_mismatch', 'snapshot revision locator points to a different problem revision');
|
|
614
|
+
}
|
|
615
|
+
if (existing.snapshot_id !== snapshotId || existingBytes !== bytes) {
|
|
616
|
+
fail('conflict', `Problem ${snapshot.problem_id}@${snapshot.revision} already has a different snapshot`);
|
|
617
|
+
}
|
|
618
|
+
return receipt(snapshot, snapshotId, 'existing');
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
if (error instanceof JudgmentProblemSnapshotError)
|
|
623
|
+
throw error;
|
|
624
|
+
throw new JudgmentProblemSnapshotError('storage_io_error', 'judgment problem snapshot could not be saved');
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
if (handle !== undefined)
|
|
628
|
+
await handle.close().catch(() => undefined);
|
|
629
|
+
await unlink(temporary).catch(() => undefined);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
export async function loadJudgmentProblemSnapshot(request) {
|
|
633
|
+
if (!isPlainRecord(request))
|
|
634
|
+
fail('invalid_request', 'load request must be a plain object');
|
|
635
|
+
const root = requireRoot(request.root);
|
|
636
|
+
const context = validateAccessContext(request.access);
|
|
637
|
+
const referenceResolution = request.reference_resolution ?? 'current';
|
|
638
|
+
if (referenceResolution !== 'current' && referenceResolution !== 'historical') {
|
|
639
|
+
fail('invalid_request', 'reference_resolution must be current or historical');
|
|
640
|
+
}
|
|
641
|
+
if (request.snapshot_id === undefined && (request.problem_id === undefined || request.revision === undefined)) {
|
|
642
|
+
fail('invalid_request', 'load requires snapshot_id or problem_id and revision');
|
|
643
|
+
}
|
|
644
|
+
const suppliedId = request.snapshot_id === undefined ? undefined : requireSnapshotId(request.snapshot_id, 'snapshot_id');
|
|
645
|
+
const suppliedProblemId = request.problem_id === undefined ? undefined : requireString(request.problem_id, 'problem_id');
|
|
646
|
+
const suppliedRevision = request.revision === undefined ? undefined : requireRevision(request.revision, 'revision');
|
|
647
|
+
const parsed = suppliedProblemId !== undefined && suppliedRevision !== undefined
|
|
648
|
+
? parseEnvelope(await readRegular(locatorFor(root, suppliedProblemId, suppliedRevision).file))
|
|
649
|
+
: await findById(root, suppliedId);
|
|
650
|
+
const { envelope } = parsed;
|
|
651
|
+
if (suppliedId !== undefined && envelope.snapshot_id !== suppliedId)
|
|
652
|
+
fail('integrity_mismatch', 'requested snapshot_id does not match stored content');
|
|
653
|
+
if (suppliedProblemId !== undefined && envelope.snapshot.problem_id !== suppliedProblemId)
|
|
654
|
+
fail('integrity_mismatch', 'requested problem_id does not match stored content');
|
|
655
|
+
if (suppliedRevision !== undefined && envelope.snapshot.revision !== suppliedRevision)
|
|
656
|
+
fail('integrity_mismatch', 'requested revision does not match stored content');
|
|
657
|
+
const accessProvider = request.accessProvider ?? defaultJudgmentProblemSnapshotAccessProvider;
|
|
658
|
+
await authorize('read', envelope.snapshot, context, accessProvider);
|
|
659
|
+
for (const reference of envelope.snapshot.references) {
|
|
660
|
+
if (reference.evidence !== undefined) {
|
|
661
|
+
try {
|
|
662
|
+
const allowed = await accessProvider.authorize({
|
|
663
|
+
action: 'read',
|
|
664
|
+
context,
|
|
665
|
+
acl: reference.evidence.access,
|
|
666
|
+
snapshot: envelope.snapshot,
|
|
667
|
+
reference
|
|
668
|
+
});
|
|
669
|
+
if (allowed === false)
|
|
670
|
+
fail('unauthorized', `evidence access was denied for ${reference.kind}/${reference.id}@${reference.revision}`, reference);
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
if (error instanceof JudgmentProblemSnapshotError)
|
|
674
|
+
throw error;
|
|
675
|
+
fail('unauthorized', `evidence access was denied for ${reference.kind}/${reference.id}@${reference.revision}`, reference);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (!request.referenceProvider || typeof request.referenceProvider.resolve !== 'function') {
|
|
680
|
+
fail('invalid_request', `referenceProvider.resolve is required for ${referenceResolution} reference resolution`);
|
|
681
|
+
}
|
|
682
|
+
await resolveReferences(envelope.snapshot, referenceResolution === 'current' ? 'read' : 'historical_read', context, request.referenceProvider);
|
|
683
|
+
return envelope.snapshot;
|
|
684
|
+
}
|
|
685
|
+
function validateAccessContext(value) {
|
|
686
|
+
if (!isPlainRecord(value) || typeof value.principal !== 'string' || value.principal.trim().length === 0 || value.principal.includes('\0')) {
|
|
687
|
+
fail('unauthorized', 'a trusted principal is required');
|
|
688
|
+
}
|
|
689
|
+
return { principal: value.principal };
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Adapt the generic foundation store to the snapshot resolver. The store
|
|
693
|
+
* read and exact digest check always run before an optional caller validator.
|
|
694
|
+
* `save`/`read` additionally validate the definition for judgment use and
|
|
695
|
+
* reference scope. `historical_read` deliberately skips those past-use and
|
|
696
|
+
* applicability checks, but the trusted store must still enforce its current
|
|
697
|
+
* read ACL; an old revision cannot be read through a revoked current ACL.
|
|
698
|
+
* The optional validator can add organization-specific policy, but cannot
|
|
699
|
+
* bypass the canonical store or digest check.
|
|
700
|
+
*/
|
|
701
|
+
export function createJudgmentProblemFoundationReferenceProvider(options) {
|
|
702
|
+
return {
|
|
703
|
+
async resolve(input) {
|
|
704
|
+
if (!['objective', 'variable', 'model', 'constraint'].includes(input.reference.kind)) {
|
|
705
|
+
if (options.resolveOther)
|
|
706
|
+
return options.resolveOther(input);
|
|
707
|
+
return { status: 'unresolved', message: `No provider is registered for ${input.reference.kind}` };
|
|
708
|
+
}
|
|
709
|
+
const resolved = await options.store.read({
|
|
710
|
+
id: input.reference.id,
|
|
711
|
+
type: input.reference.kind,
|
|
712
|
+
revision: input.reference.revision
|
|
713
|
+
}, { principal: input.context.principal });
|
|
714
|
+
if (!resolved)
|
|
715
|
+
return { status: 'missing', message: 'Foundation revision was not found' };
|
|
716
|
+
if (resolved.digest !== input.reference.digest)
|
|
717
|
+
return { status: 'unresolved', message: 'Foundation digest changed' };
|
|
718
|
+
if (!isPlainRecord(resolved.definition) || resolved.definition.type !== input.reference.kind) {
|
|
719
|
+
return { status: 'unresolved', message: 'Foundation revision did not return a matching definition' };
|
|
720
|
+
}
|
|
721
|
+
const definition = resolved.definition;
|
|
722
|
+
if (input.phase !== 'historical_read') {
|
|
723
|
+
const validation = validateFoundationDefinition(definition, { use: 'judgment' });
|
|
724
|
+
if (!validation.valid) {
|
|
725
|
+
const notApplicable = validation.issues.some((issue) => (issue.code === 'UNAUTHORIZED_USE' || issue.code === 'UNVERIFIED_MODEL'));
|
|
726
|
+
return {
|
|
727
|
+
status: notApplicable ? 'not_applicable' : 'unresolved',
|
|
728
|
+
message: validation.issues.map((issue) => issue.message).join('; ')
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
const applicability = validateFoundationReferenceApplicability(input.reference, definition);
|
|
732
|
+
if (!applicability.valid) {
|
|
733
|
+
return { status: 'not_applicable', message: applicability.message };
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (options.validate) {
|
|
737
|
+
return options.validate({
|
|
738
|
+
...input,
|
|
739
|
+
record: { definition, digest: resolved.digest }
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return { status: 'resolved', digest: resolved.digest };
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function validateFoundationReferenceApplicability(reference, definition) {
|
|
747
|
+
const scopes = definition.type === 'model'
|
|
748
|
+
? [definition.scope, definition.applicability]
|
|
749
|
+
: [definition.scope];
|
|
750
|
+
for (const scope of scopes) {
|
|
751
|
+
if (!scopeContainsReference(scope, reference)) {
|
|
752
|
+
return {
|
|
753
|
+
valid: false,
|
|
754
|
+
message: `Foundation ${definition.type}/${definition.id}@${definition.revision} is outside the reference scope or validity period`
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return { valid: true };
|
|
759
|
+
}
|
|
760
|
+
function scopeContainsReference(scope, reference) {
|
|
761
|
+
if (!scope.subjectIds.includes(reference.scope.id))
|
|
762
|
+
return false;
|
|
763
|
+
const sourceFrom = Date.parse(scope.validFrom);
|
|
764
|
+
const sourceUntil = scope.validUntil === undefined ? Number.POSITIVE_INFINITY : Date.parse(scope.validUntil);
|
|
765
|
+
const referenceFrom = Date.parse(reference.valid_from);
|
|
766
|
+
const referenceUntil = reference.valid_to == null ? Number.POSITIVE_INFINITY : Date.parse(reference.valid_to);
|
|
767
|
+
// Open-ended source/reference periods are represented by +Infinity. Only
|
|
768
|
+
// supplied timestamps must be finite; rejecting Infinity here would make
|
|
769
|
+
// every valid open-ended Foundation scope look inapplicable.
|
|
770
|
+
if (!Number.isFinite(sourceFrom) || !Number.isFinite(referenceFrom))
|
|
771
|
+
return false;
|
|
772
|
+
if (scope.validUntil !== undefined && !Number.isFinite(sourceUntil))
|
|
773
|
+
return false;
|
|
774
|
+
if (reference.valid_to != null && !Number.isFinite(referenceUntil))
|
|
775
|
+
return false;
|
|
776
|
+
return sourceFrom <= referenceFrom && sourceUntil >= referenceUntil;
|
|
777
|
+
}
|