@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,654 @@
|
|
|
1
|
+
import { assertFoundationCatalog, cloneFoundationCatalog, cloneFoundationDefinition, digestFoundationDefinition, findFoundationRecord, findLatestFoundationRecord, foundationCatalogOrEmpty, foundationKey, foundationRef, nextFoundationRevision } from './foundation-catalog.js';
|
|
2
|
+
import { validateFoundationDefinition, validateFoundationRelation } from './ontology-foundation.js';
|
|
3
|
+
import { loadPersonalOs, mutatePersonalOs } from './ssot.js';
|
|
4
|
+
export class FoundationStoreError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
currentRevision;
|
|
7
|
+
constructor(code, message, options = {}) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'FoundationStoreError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.currentRevision = options.currentRevision;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Canonical foundation persistence over the existing Graph SSOT aggregate.
|
|
16
|
+
* There is intentionally no separate objectives/world-model file or writer.
|
|
17
|
+
*/
|
|
18
|
+
export class GraphFoundationRevisionStore {
|
|
19
|
+
dataDir;
|
|
20
|
+
policy;
|
|
21
|
+
endpointResolver;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
if (!options.dataDir)
|
|
24
|
+
throw new FoundationStoreError('invalid_input', 'dataDir is required');
|
|
25
|
+
this.dataDir = options.dataDir;
|
|
26
|
+
this.policy = options.policy;
|
|
27
|
+
this.endpointResolver = options.endpointResolver;
|
|
28
|
+
}
|
|
29
|
+
async create(definition, context) {
|
|
30
|
+
const candidate = prepareDefinition(definition, 'create');
|
|
31
|
+
if (candidate.revision !== '1') {
|
|
32
|
+
throw new FoundationStoreError('invalid_input', 'A new foundation definition must start at revision 1');
|
|
33
|
+
}
|
|
34
|
+
validateForDraft(candidate);
|
|
35
|
+
assertContext(context);
|
|
36
|
+
await this.authorize({ action: 'create', context, next: candidate });
|
|
37
|
+
const ref = await mutatePersonalOs(this.dataDir, async (current) => {
|
|
38
|
+
const catalog = readCatalog(current);
|
|
39
|
+
if (catalog.records.some((record) => (record.definition.id === candidate.id && record.definition.type === candidate.type))) {
|
|
40
|
+
throw new FoundationStoreError('revision_conflict', `Foundation resource ${candidate.type}/${candidate.id} already exists`);
|
|
41
|
+
}
|
|
42
|
+
const record = makeRecord(candidate);
|
|
43
|
+
await this.authorize({
|
|
44
|
+
action: 'read',
|
|
45
|
+
context,
|
|
46
|
+
reference: foundationRef(record),
|
|
47
|
+
current: candidate
|
|
48
|
+
});
|
|
49
|
+
const nextCatalog = appendRecord(catalog, record);
|
|
50
|
+
return withFoundationCatalog(current, nextCatalog);
|
|
51
|
+
}).then(() => foundationRef(makeRecord(candidate)));
|
|
52
|
+
return this.readback(ref, context);
|
|
53
|
+
}
|
|
54
|
+
async read(reference, context) {
|
|
55
|
+
assertReferenceInput(reference);
|
|
56
|
+
assertContext(context);
|
|
57
|
+
const os = await this.load();
|
|
58
|
+
const catalog = readCatalog(os);
|
|
59
|
+
const record = findFoundationRecord(catalog, reference);
|
|
60
|
+
if (!record)
|
|
61
|
+
return null;
|
|
62
|
+
const latest = findLatestFoundationRecord(catalog, reference.type, reference.id);
|
|
63
|
+
if (!latest) {
|
|
64
|
+
throw new FoundationStoreError('corrupt_catalog', `Latest pointer is missing for ${reference.type}/${reference.id}`);
|
|
65
|
+
}
|
|
66
|
+
// Historical content is immutable, but access is evaluated against the
|
|
67
|
+
// current revision. A principal removed from the current ACL must not
|
|
68
|
+
// regain access merely by requesting an older revision.
|
|
69
|
+
await this.authorize({ action: 'read', context, reference, current: latest.definition });
|
|
70
|
+
return cloneRecord(record);
|
|
71
|
+
}
|
|
72
|
+
async readLatest(type, id, context) {
|
|
73
|
+
if (!isFoundationType(type) || !id) {
|
|
74
|
+
throw new FoundationStoreError('invalid_input', 'type and id are required');
|
|
75
|
+
}
|
|
76
|
+
assertContext(context);
|
|
77
|
+
const os = await this.load();
|
|
78
|
+
const catalog = readCatalog(os);
|
|
79
|
+
const record = findLatestFoundationRecord(catalog, type, id);
|
|
80
|
+
if (!record)
|
|
81
|
+
return null;
|
|
82
|
+
await this.authorize({ action: 'read', context, reference: foundationRef(record), current: record.definition });
|
|
83
|
+
return cloneRecord(record);
|
|
84
|
+
}
|
|
85
|
+
async update(input, context) {
|
|
86
|
+
assertContext(context);
|
|
87
|
+
assertReferenceInput(input.reference);
|
|
88
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== input.reference.revision) {
|
|
89
|
+
throw new FoundationStoreError('revision_conflict', 'expectedRevision must match reference.revision', {
|
|
90
|
+
currentRevision: input.reference.revision
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const reference = { ...input.reference };
|
|
94
|
+
const nextInput = cloneFoundationDefinition(input.next);
|
|
95
|
+
if (nextInput.id !== reference.id || nextInput.type !== reference.type) {
|
|
96
|
+
throw new FoundationStoreError('invalid_input', 'An update cannot change the logical id or type');
|
|
97
|
+
}
|
|
98
|
+
const candidate = {
|
|
99
|
+
...nextInput,
|
|
100
|
+
revision: nextFoundationRevision(reference.revision)
|
|
101
|
+
};
|
|
102
|
+
validateForDraft(candidate);
|
|
103
|
+
let savedRef;
|
|
104
|
+
await mutatePersonalOs(this.dataDir, async (current) => {
|
|
105
|
+
const catalog = readCatalog(current);
|
|
106
|
+
const latest = findLatestFoundationRecord(catalog, reference.type, reference.id);
|
|
107
|
+
if (!latest)
|
|
108
|
+
throw new FoundationStoreError('not_found', `Foundation resource ${reference.type}/${reference.id} was not found`);
|
|
109
|
+
if (latest.definition.revision !== reference.revision) {
|
|
110
|
+
throw new FoundationStoreError('revision_conflict', `Expected ${reference.type}/${reference.id}@${reference.revision}, current is @${latest.definition.revision}`, {
|
|
111
|
+
currentRevision: latest.definition.revision
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
await this.authorize({ action: 'update', context, reference, current: latest.definition, next: candidate });
|
|
115
|
+
const record = makeRecord(candidate);
|
|
116
|
+
await this.authorize({
|
|
117
|
+
action: 'read',
|
|
118
|
+
context,
|
|
119
|
+
reference: foundationRef(record),
|
|
120
|
+
current: candidate
|
|
121
|
+
});
|
|
122
|
+
const nextCatalog = appendRecord(catalog, record);
|
|
123
|
+
savedRef = foundationRef(record);
|
|
124
|
+
return withFoundationCatalog(current, nextCatalog);
|
|
125
|
+
});
|
|
126
|
+
if (!savedRef)
|
|
127
|
+
throw new FoundationStoreError('readback_mismatch', 'Foundation update did not produce a revision reference');
|
|
128
|
+
return this.readback(savedRef, context);
|
|
129
|
+
}
|
|
130
|
+
async list(type, context) {
|
|
131
|
+
assertContext(context);
|
|
132
|
+
if (type !== undefined && !isFoundationType(type)) {
|
|
133
|
+
throw new FoundationStoreError('invalid_input', `Unknown foundation type ${String(type)}`);
|
|
134
|
+
}
|
|
135
|
+
const os = await this.load();
|
|
136
|
+
const catalog = readCatalog(os);
|
|
137
|
+
const records = catalog.records.filter((record) => record.definition.revision === catalog.latest[foundationKey(record.definition)]?.revision)
|
|
138
|
+
.filter((record) => type === undefined || record.definition.type === type);
|
|
139
|
+
const visible = [];
|
|
140
|
+
for (const record of records) {
|
|
141
|
+
await this.authorize({ action: 'list', context, reference: foundationRef(record), current: record.definition });
|
|
142
|
+
visible.push(cloneRecord(record));
|
|
143
|
+
}
|
|
144
|
+
return visible;
|
|
145
|
+
}
|
|
146
|
+
async addRelation(relation, context) {
|
|
147
|
+
assertContext(context);
|
|
148
|
+
validateRelationInput(relation);
|
|
149
|
+
await mutatePersonalOs(this.dataDir, async (current) => {
|
|
150
|
+
const catalog = readCatalog(current);
|
|
151
|
+
const resolved = await resolveFoundationRelationResources({
|
|
152
|
+
catalog,
|
|
153
|
+
relation,
|
|
154
|
+
context,
|
|
155
|
+
current,
|
|
156
|
+
endpointResolver: this.endpointResolver
|
|
157
|
+
});
|
|
158
|
+
// A trusted context scope is a store-level boundary. The injected
|
|
159
|
+
// policy may add organization-specific role checks, but it cannot turn
|
|
160
|
+
// a resolved cross-scope endpoint into an in-scope one.
|
|
161
|
+
for (const resource of resolved.resources)
|
|
162
|
+
assertScopeAccess(resource, context);
|
|
163
|
+
await this.authorize({ action: 'link', context, relation: resolved.relation, resources: resolved.resources });
|
|
164
|
+
const serialized = JSON.stringify(resolved.relation);
|
|
165
|
+
if (catalog.relations.some((existing) => JSON.stringify(existing) === serialized))
|
|
166
|
+
return current;
|
|
167
|
+
return withFoundationCatalog(current, {
|
|
168
|
+
...cloneFoundationCatalog(catalog),
|
|
169
|
+
relations: [...catalog.relations, JSON.parse(serialized)]
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async load() {
|
|
174
|
+
try {
|
|
175
|
+
return await loadPersonalOs(this.dataDir);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
throw normalizeStorageError(error);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async authorize(request) {
|
|
182
|
+
if (this.policy) {
|
|
183
|
+
try {
|
|
184
|
+
const allowed = await this.policy.authorize({
|
|
185
|
+
...request,
|
|
186
|
+
...(request.current ? { current: cloneFoundationDefinition(request.current) } : {}),
|
|
187
|
+
...(request.next ? { next: cloneFoundationDefinition(request.next) } : {}),
|
|
188
|
+
...(request.resources ? { resources: request.resources.map(cloneAuthorizationResource) } : {})
|
|
189
|
+
});
|
|
190
|
+
if (allowed === false)
|
|
191
|
+
throw new FoundationStoreError('authorization_denied', 'Foundation policy denied the operation');
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof FoundationStoreError)
|
|
195
|
+
throw error;
|
|
196
|
+
throw new FoundationStoreError('authorization_denied', formatError(error));
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (request.action === 'create') {
|
|
201
|
+
if (!request.next)
|
|
202
|
+
throw new FoundationStoreError('invalid_input', 'Create authorization requires a definition');
|
|
203
|
+
assertScopeAccess(request.next, request.context);
|
|
204
|
+
assertAclWrite(request.next, request.context.principal);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (request.current) {
|
|
208
|
+
assertScopeAccess(request.current, request.context);
|
|
209
|
+
if (request.action === 'read' || request.action === 'list') {
|
|
210
|
+
assertAclRead(request.current, request.context.principal);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
assertAclWrite(request.current, request.context.principal);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (request.action === 'link') {
|
|
218
|
+
if (!request.resources || request.resources.length === 0) {
|
|
219
|
+
throw new FoundationStoreError('authorization_denied', 'A foundation relation requires resolved endpoint resources');
|
|
220
|
+
}
|
|
221
|
+
// The relation itself is stored in the shared catalog. Without an
|
|
222
|
+
// injected tenant policy, require write access to every local foundation
|
|
223
|
+
// endpoint so a caller cannot link an object it cannot modify.
|
|
224
|
+
for (const resource of request.resources) {
|
|
225
|
+
assertScopeAccess(resource, request.context);
|
|
226
|
+
assertAclWrite(resource, request.context.principal);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async readback(reference, context) {
|
|
231
|
+
const saved = await this.read(reference, context);
|
|
232
|
+
if (!saved || saved.digest !== reference.digest || saved.definition.revision !== reference.revision) {
|
|
233
|
+
throw new FoundationStoreError('readback_mismatch', `Foundation readback failed for ${reference.type}/${reference.id}@${reference.revision}`);
|
|
234
|
+
}
|
|
235
|
+
return foundationRef(saved);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
export function createFoundationRevisionStore(options) {
|
|
239
|
+
return new GraphFoundationRevisionStore(options);
|
|
240
|
+
}
|
|
241
|
+
export const createFoundationStore = createFoundationRevisionStore;
|
|
242
|
+
/**
|
|
243
|
+
* Creates a concrete, in-memory Story endpoint adapter for OSS consumers.
|
|
244
|
+
* Only immutable revision/security metadata is supplied; no Story body is
|
|
245
|
+
* duplicated into the foundation catalog. Current ACL and scope are taken
|
|
246
|
+
* from the greatest registered revision while the requested revision is only
|
|
247
|
+
* checked for existence and retained in the relation.
|
|
248
|
+
*/
|
|
249
|
+
export function createLocalStoryResolver(records) {
|
|
250
|
+
const grouped = new Map();
|
|
251
|
+
for (const record of records) {
|
|
252
|
+
validateLocalStoryRevision(record);
|
|
253
|
+
const revisions = grouped.get(record.id) ?? [];
|
|
254
|
+
if (revisions.some((candidate) => candidate.revision === record.revision)) {
|
|
255
|
+
throw new TypeError(`Duplicate Story revision ${record.id}@${record.revision}`);
|
|
256
|
+
}
|
|
257
|
+
revisions.push(cloneLocalStoryRevision(record));
|
|
258
|
+
grouped.set(record.id, revisions);
|
|
259
|
+
}
|
|
260
|
+
for (const revisions of grouped.values()) {
|
|
261
|
+
revisions.sort((left, right) => compareRevisions(left.revision, right.revision));
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
resolve({ endpoint }) {
|
|
265
|
+
if (endpoint.type !== 'story')
|
|
266
|
+
return null;
|
|
267
|
+
const revisions = grouped.get(endpoint.id);
|
|
268
|
+
if (!revisions)
|
|
269
|
+
return null;
|
|
270
|
+
const current = revisions[revisions.length - 1];
|
|
271
|
+
const requested = endpoint.revision === undefined
|
|
272
|
+
? current
|
|
273
|
+
: revisions.find((candidate) => candidate.revision === endpoint.revision);
|
|
274
|
+
if (!requested)
|
|
275
|
+
return null;
|
|
276
|
+
return {
|
|
277
|
+
id: endpoint.id,
|
|
278
|
+
type: 'story',
|
|
279
|
+
revision: requested.revision,
|
|
280
|
+
currentRevision: current.revision,
|
|
281
|
+
acl: cloneAcl(current.acl),
|
|
282
|
+
scope: cloneScope(current.scope)
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Descriptive alias for consumers that use the generic endpoint terminology. */
|
|
288
|
+
export const createStoryEndpointResolver = createLocalStoryResolver;
|
|
289
|
+
function prepareDefinition(definition, operation) {
|
|
290
|
+
if (!definition || typeof definition !== 'object') {
|
|
291
|
+
throw new FoundationStoreError('invalid_input', `${operation} requires a foundation definition`);
|
|
292
|
+
}
|
|
293
|
+
const clone = cloneFoundationDefinition(definition);
|
|
294
|
+
if (!clone.id || !isFoundationType(clone.type) || !clone.revision) {
|
|
295
|
+
throw new FoundationStoreError('invalid_input', `${operation} requires id, type, and revision`);
|
|
296
|
+
}
|
|
297
|
+
return clone;
|
|
298
|
+
}
|
|
299
|
+
function validateForDraft(definition) {
|
|
300
|
+
try {
|
|
301
|
+
const result = validateFoundationDefinition(definition, { use: 'draft' });
|
|
302
|
+
if (!result.valid) {
|
|
303
|
+
throw new FoundationStoreError('invalid_input', result.issues.map((issue) => `${issue.path}: ${issue.message}`).join('; '));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error instanceof FoundationStoreError)
|
|
308
|
+
throw error;
|
|
309
|
+
throw new FoundationStoreError('invalid_input', formatError(error));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function validateRelationInput(relation) {
|
|
313
|
+
try {
|
|
314
|
+
const result = validateFoundationRelation(relation);
|
|
315
|
+
if (!result.valid)
|
|
316
|
+
throw new FoundationStoreError('invalid_input', result.issues.map((issue) => issue.message).join('; '));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (error instanceof FoundationStoreError)
|
|
320
|
+
throw error;
|
|
321
|
+
throw new FoundationStoreError('invalid_input', formatError(error));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function makeRecord(definition) {
|
|
325
|
+
return { definition: cloneFoundationDefinition(definition), digest: digestFoundationDefinition(definition) };
|
|
326
|
+
}
|
|
327
|
+
function cloneRecord(record) {
|
|
328
|
+
return { definition: cloneFoundationDefinition(record.definition), digest: record.digest };
|
|
329
|
+
}
|
|
330
|
+
function appendRecord(catalog, record) {
|
|
331
|
+
const next = cloneFoundationCatalog(catalog);
|
|
332
|
+
next.records.push(cloneRecord(record));
|
|
333
|
+
next.latest[foundationKey(record.definition)] = {
|
|
334
|
+
id: record.definition.id,
|
|
335
|
+
type: record.definition.type,
|
|
336
|
+
revision: record.definition.revision
|
|
337
|
+
};
|
|
338
|
+
return next;
|
|
339
|
+
}
|
|
340
|
+
function readCatalog(os) {
|
|
341
|
+
if (os.graph.version !== 2) {
|
|
342
|
+
throw new FoundationStoreError('unsupported_graph', 'Foundation catalog writes require canonical Graph v2');
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
assertFoundationCatalog(os.graph.foundation);
|
|
346
|
+
return foundationCatalogOrEmpty(os.graph.foundation);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
throw new FoundationStoreError('corrupt_catalog', formatError(error));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function resolveFoundationRelationResources(input) {
|
|
353
|
+
const resources = [];
|
|
354
|
+
const seen = new Set();
|
|
355
|
+
const endpoints = ['source', 'target'];
|
|
356
|
+
const resolvedEndpoints = {};
|
|
357
|
+
for (const position of endpoints) {
|
|
358
|
+
const endpoint = input.relation[position];
|
|
359
|
+
if (isFoundationType(endpoint.type)) {
|
|
360
|
+
if (!endpoint.revision) {
|
|
361
|
+
throw new FoundationStoreError('invalid_input', `Foundation relation endpoint ${endpoint.type}/${endpoint.id} requires a revision`);
|
|
362
|
+
}
|
|
363
|
+
const requested = findFoundationRecord(input.catalog, {
|
|
364
|
+
id: endpoint.id,
|
|
365
|
+
type: endpoint.type,
|
|
366
|
+
revision: endpoint.revision
|
|
367
|
+
});
|
|
368
|
+
if (!requested) {
|
|
369
|
+
throw new FoundationStoreError('not_found', `Foundation relation endpoint ${endpoint.type}/${endpoint.id}@${endpoint.revision} was not found`);
|
|
370
|
+
}
|
|
371
|
+
const latest = findLatestFoundationRecord(input.catalog, endpoint.type, endpoint.id);
|
|
372
|
+
if (!latest) {
|
|
373
|
+
throw new FoundationStoreError('corrupt_catalog', `Latest pointer is missing for ${endpoint.type}/${endpoint.id}`);
|
|
374
|
+
}
|
|
375
|
+
const key = foundationKey(latest.definition);
|
|
376
|
+
if (!seen.has(key)) {
|
|
377
|
+
seen.add(key);
|
|
378
|
+
resources.push(cloneFoundationDefinition(latest.definition));
|
|
379
|
+
}
|
|
380
|
+
resolvedEndpoints[position] = { ...endpoint };
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (!input.endpointResolver) {
|
|
384
|
+
// Do not authorize an external endpoint from an ID alone. Returning a
|
|
385
|
+
// generic denial also avoids turning an absent resolver into a lookup
|
|
386
|
+
// oracle for private Story/project identifiers.
|
|
387
|
+
throw new FoundationStoreError('authorization_denied', `No trusted endpoint resolver is configured for ${endpoint.type}/${endpoint.id}`);
|
|
388
|
+
}
|
|
389
|
+
let resource;
|
|
390
|
+
try {
|
|
391
|
+
resource = await input.endpointResolver.resolve({
|
|
392
|
+
endpoint: {
|
|
393
|
+
id: endpoint.id,
|
|
394
|
+
type: endpoint.type,
|
|
395
|
+
...(endpoint.revision === undefined ? {} : { revision: endpoint.revision })
|
|
396
|
+
},
|
|
397
|
+
context: input.context,
|
|
398
|
+
// This callback executes within mutatePersonalOs's canonical lock.
|
|
399
|
+
current: input.current
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
if (error instanceof FoundationStoreError)
|
|
404
|
+
throw error;
|
|
405
|
+
throw new FoundationStoreError('authorization_denied', 'The trusted endpoint resolver failed closed');
|
|
406
|
+
}
|
|
407
|
+
if (!resource) {
|
|
408
|
+
throw new FoundationStoreError('not_found', `Relation endpoint ${endpoint.type}/${endpoint.id} was not found`);
|
|
409
|
+
}
|
|
410
|
+
validateEndpointResource(resource, endpoint);
|
|
411
|
+
const key = endpointResourceKey(resource);
|
|
412
|
+
if (!seen.has(key)) {
|
|
413
|
+
seen.add(key);
|
|
414
|
+
resources.push(cloneEndpointResource(resource));
|
|
415
|
+
}
|
|
416
|
+
// Persist a concrete revision even when the caller referred to the
|
|
417
|
+
// current Story by ID only. This makes the link historically reproducible.
|
|
418
|
+
resolvedEndpoints[position] = {
|
|
419
|
+
id: resource.id,
|
|
420
|
+
type: resource.type,
|
|
421
|
+
revision: resource.revision
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
if (resources.length === 0) {
|
|
425
|
+
throw new FoundationStoreError('authorization_denied', 'A foundation relation requires resolved endpoint resources');
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
relation: {
|
|
429
|
+
...input.relation,
|
|
430
|
+
source: resolvedEndpoints.source ?? input.relation.source,
|
|
431
|
+
target: resolvedEndpoints.target ?? input.relation.target
|
|
432
|
+
},
|
|
433
|
+
resources
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function withFoundationCatalog(os, foundation) {
|
|
437
|
+
if (os.graph.version !== 2) {
|
|
438
|
+
throw new FoundationStoreError('unsupported_graph', 'Foundation catalog writes require canonical Graph v2');
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
...os,
|
|
442
|
+
graph: {
|
|
443
|
+
...os.graph,
|
|
444
|
+
foundation
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function assertReferenceInput(reference) {
|
|
449
|
+
if (!reference || typeof reference !== 'object' || !reference.id || !isFoundationType(reference.type) || !/^[1-9]\d*$/.test(reference.revision)) {
|
|
450
|
+
throw new FoundationStoreError('invalid_input', 'A revision reference requires id, type, and a positive revision');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function assertContext(context) {
|
|
454
|
+
if (!context || typeof context.principal !== 'string' || context.principal.length === 0) {
|
|
455
|
+
throw new FoundationStoreError('authorization_denied', 'A trusted principal is required');
|
|
456
|
+
}
|
|
457
|
+
if (context.scope !== undefined && !isValidScope(context.scope)) {
|
|
458
|
+
throw new FoundationStoreError('authorization_denied', 'A trusted context scope is invalid');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function assertAclRead(definition, principal) {
|
|
462
|
+
const acl = definition.acl;
|
|
463
|
+
if (acl.visibility === 'public' || acl.ownerId === principal || acl.readerIds.includes(principal) || acl.writerIds.includes(principal))
|
|
464
|
+
return;
|
|
465
|
+
throw new FoundationStoreError('authorization_denied', `Principal ${principal} cannot read ${definition.type}/${definition.id}`);
|
|
466
|
+
}
|
|
467
|
+
function assertAclWrite(resource, principal) {
|
|
468
|
+
const acl = resource.acl;
|
|
469
|
+
if (acl.ownerId === principal || acl.writerIds.includes(principal))
|
|
470
|
+
return;
|
|
471
|
+
throw new FoundationStoreError('authorization_denied', `Principal ${principal} cannot write ${resource.type}/${resource.id}`);
|
|
472
|
+
}
|
|
473
|
+
function assertScopeAccess(resource, context) {
|
|
474
|
+
if (!context.scope)
|
|
475
|
+
return;
|
|
476
|
+
const allowed = new Set(context.scope.subjectIds);
|
|
477
|
+
if (resource.scope.subjectIds.some((subjectId) => allowed.has(subjectId)))
|
|
478
|
+
return;
|
|
479
|
+
throw new FoundationStoreError('scope_violation', `Principal ${context.principal} cannot access ${resource.type}/${resource.id} outside the trusted scope`);
|
|
480
|
+
}
|
|
481
|
+
function cloneAuthorizationResource(resource) {
|
|
482
|
+
return isFoundationDefinitionResource(resource)
|
|
483
|
+
? cloneFoundationDefinition(resource)
|
|
484
|
+
: cloneEndpointResource(resource);
|
|
485
|
+
}
|
|
486
|
+
function isFoundationDefinitionResource(resource) {
|
|
487
|
+
return isFoundationType(resource.type) && 'adoptionState' in resource;
|
|
488
|
+
}
|
|
489
|
+
function cloneEndpointResource(resource) {
|
|
490
|
+
return {
|
|
491
|
+
id: resource.id,
|
|
492
|
+
type: resource.type,
|
|
493
|
+
revision: resource.revision,
|
|
494
|
+
currentRevision: resource.currentRevision,
|
|
495
|
+
acl: cloneAcl(resource.acl),
|
|
496
|
+
scope: cloneScope(resource.scope)
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
function endpointResourceKey(resource) {
|
|
500
|
+
return JSON.stringify([resource.type, resource.id]);
|
|
501
|
+
}
|
|
502
|
+
function validateEndpointResource(resource, endpoint) {
|
|
503
|
+
if (resource.id !== endpoint.id || resource.type !== endpoint.type) {
|
|
504
|
+
throw new FoundationStoreError('authorization_denied', 'The endpoint resolver returned a mismatched resource');
|
|
505
|
+
}
|
|
506
|
+
if (isFoundationType(resource.type)) {
|
|
507
|
+
throw new FoundationStoreError('authorization_denied', 'Foundation endpoints must be resolved by the canonical catalog');
|
|
508
|
+
}
|
|
509
|
+
if (!isPositiveRevision(resource.revision) || !isPositiveRevision(resource.currentRevision)) {
|
|
510
|
+
throw new FoundationStoreError('authorization_denied', 'The endpoint resolver returned an invalid revision');
|
|
511
|
+
}
|
|
512
|
+
if (endpoint.revision !== undefined && endpoint.revision !== resource.revision) {
|
|
513
|
+
throw new FoundationStoreError('authorization_denied', 'The endpoint resolver did not resolve the requested revision');
|
|
514
|
+
}
|
|
515
|
+
if (endpoint.revision === undefined && resource.revision !== resource.currentRevision) {
|
|
516
|
+
throw new FoundationStoreError('authorization_denied', 'An ID-only endpoint must resolve to its current revision');
|
|
517
|
+
}
|
|
518
|
+
if (compareRevisions(resource.currentRevision, resource.revision) < 0) {
|
|
519
|
+
throw new FoundationStoreError('authorization_denied', 'The endpoint resolver returned a revision newer than current');
|
|
520
|
+
}
|
|
521
|
+
if (!isValidAcl(resource.acl) || !isValidScope(resource.scope)) {
|
|
522
|
+
throw new FoundationStoreError('authorization_denied', 'The endpoint resolver returned invalid authorization metadata');
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function validateLocalStoryRevision(record) {
|
|
526
|
+
if (!record || typeof record !== 'object' || !record.id || !isPositiveRevision(record.revision)) {
|
|
527
|
+
throw new TypeError('A local Story revision requires a non-empty id and positive revision');
|
|
528
|
+
}
|
|
529
|
+
if (!isValidAcl(record.acl) || !isValidScope(record.scope)) {
|
|
530
|
+
throw new TypeError(`Local Story ${record.id}@${record.revision} has invalid ACL or scope metadata`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function cloneLocalStoryRevision(record) {
|
|
534
|
+
return {
|
|
535
|
+
id: record.id,
|
|
536
|
+
revision: record.revision,
|
|
537
|
+
acl: cloneAcl(record.acl),
|
|
538
|
+
scope: cloneScope(record.scope)
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function cloneAcl(acl) {
|
|
542
|
+
return {
|
|
543
|
+
ownerId: acl.ownerId,
|
|
544
|
+
visibility: acl.visibility,
|
|
545
|
+
readerIds: [...acl.readerIds],
|
|
546
|
+
writerIds: [...acl.writerIds]
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function cloneScope(scope) {
|
|
550
|
+
return {
|
|
551
|
+
subjectIds: [...scope.subjectIds],
|
|
552
|
+
validFrom: scope.validFrom,
|
|
553
|
+
...(scope.validUntil === undefined ? {} : { validUntil: scope.validUntil })
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
function isValidAcl(value) {
|
|
557
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
558
|
+
return false;
|
|
559
|
+
const candidate = value;
|
|
560
|
+
return typeof candidate.ownerId === 'string'
|
|
561
|
+
&& candidate.ownerId.length > 0
|
|
562
|
+
&& (candidate.visibility === 'private'
|
|
563
|
+
|| candidate.visibility === 'project'
|
|
564
|
+
|| candidate.visibility === 'organization'
|
|
565
|
+
|| candidate.visibility === 'public')
|
|
566
|
+
&& Array.isArray(candidate.readerIds)
|
|
567
|
+
&& candidate.readerIds.every((id) => typeof id === 'string' && id.length > 0)
|
|
568
|
+
&& Array.isArray(candidate.writerIds)
|
|
569
|
+
&& candidate.writerIds.every((id) => typeof id === 'string' && id.length > 0);
|
|
570
|
+
}
|
|
571
|
+
function isValidScope(value) {
|
|
572
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
573
|
+
return false;
|
|
574
|
+
const candidate = value;
|
|
575
|
+
if (!Array.isArray(candidate.subjectIds))
|
|
576
|
+
return false;
|
|
577
|
+
if (candidate.subjectIds.length === 0 || !candidate.subjectIds.every((id) => typeof id === 'string' && id.length > 0)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (typeof candidate.validFrom !== 'string' || parseStrictRfc3339(candidate.validFrom) === undefined) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
if (candidate.validUntil === undefined)
|
|
584
|
+
return true;
|
|
585
|
+
if (typeof candidate.validUntil !== 'string')
|
|
586
|
+
return false;
|
|
587
|
+
const from = parseStrictRfc3339(candidate.validFrom);
|
|
588
|
+
const until = parseStrictRfc3339(candidate.validUntil);
|
|
589
|
+
return from !== undefined && until !== undefined && until > from;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Keep resolver metadata validation aligned with the foundation ontology.
|
|
593
|
+
* Date.parse accepts malformed calendar values by normalizing them (for
|
|
594
|
+
* example, February 30), so endpoint authorization must use strict RFC3339
|
|
595
|
+
* parsing before comparing scope boundaries.
|
|
596
|
+
*/
|
|
597
|
+
function parseStrictRfc3339(value) {
|
|
598
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/.exec(value);
|
|
599
|
+
if (!match)
|
|
600
|
+
return undefined;
|
|
601
|
+
const year = Number(match[1]);
|
|
602
|
+
const month = Number(match[2]);
|
|
603
|
+
const day = Number(match[3]);
|
|
604
|
+
const hour = Number(match[4]);
|
|
605
|
+
const minute = Number(match[5]);
|
|
606
|
+
const second = Number(match[6]);
|
|
607
|
+
const fraction = match[7] ?? '';
|
|
608
|
+
const zone = match[8];
|
|
609
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
|
|
610
|
+
return undefined;
|
|
611
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
612
|
+
return undefined;
|
|
613
|
+
let offsetMinutes = 0;
|
|
614
|
+
if (zone !== 'Z') {
|
|
615
|
+
const sign = zone[0] === '-' ? -1 : 1;
|
|
616
|
+
const offsetHour = Number(zone.slice(1, 3));
|
|
617
|
+
const offsetMinute = Number(zone.slice(4, 6));
|
|
618
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
619
|
+
return undefined;
|
|
620
|
+
offsetMinutes = sign * (offsetHour * 60 + offsetMinute);
|
|
621
|
+
}
|
|
622
|
+
const milliseconds = Number((fraction + '000').slice(0, 3));
|
|
623
|
+
const timestamp = Date.UTC(year, month - 1, day, hour, minute, second, milliseconds)
|
|
624
|
+
- offsetMinutes * 60_000;
|
|
625
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
626
|
+
}
|
|
627
|
+
function daysInMonth(year, month) {
|
|
628
|
+
if (month === 2) {
|
|
629
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
630
|
+
}
|
|
631
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
632
|
+
}
|
|
633
|
+
function isPositiveRevision(value) {
|
|
634
|
+
return typeof value === 'string' && /^[1-9]\d*$/.test(value);
|
|
635
|
+
}
|
|
636
|
+
function compareRevisions(left, right) {
|
|
637
|
+
const leftValue = BigInt(left);
|
|
638
|
+
const rightValue = BigInt(right);
|
|
639
|
+
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
640
|
+
}
|
|
641
|
+
function isFoundationType(value) {
|
|
642
|
+
return value === 'objective' || value === 'variable' || value === 'model' || value === 'constraint';
|
|
643
|
+
}
|
|
644
|
+
function normalizeStorageError(error) {
|
|
645
|
+
if (error instanceof FoundationStoreError)
|
|
646
|
+
return error;
|
|
647
|
+
const message = formatError(error);
|
|
648
|
+
if (message.includes('FOUNDATION-CATALOG-CORRUPT'))
|
|
649
|
+
return new FoundationStoreError('corrupt_catalog', message);
|
|
650
|
+
return new FoundationStoreError('corrupt_catalog', message);
|
|
651
|
+
}
|
|
652
|
+
function formatError(error) {
|
|
653
|
+
return error instanceof Error ? error.message : String(error);
|
|
654
|
+
}
|