@unson/brainbase-mcp 0.6.0 → 0.8.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/contracts/foundation/graph-history.sql +634 -0
- package/contracts/judgment-value-proof/schema.json +43 -1
- 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 +820 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -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 +63 -0
- package/dist/company-os-objectives.js +234 -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-authenticated-tools.d.ts +56 -0
- package/dist/foundation-authenticated-tools.js +347 -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-graph-http.d.ts +43 -0
- package/dist/foundation-graph-http.js +243 -0
- package/dist/foundation-graph-write.d.ts +90 -0
- package/dist/foundation-graph-write.js +447 -0
- package/dist/foundation-http.d.ts +90 -0
- package/dist/foundation-http.js +439 -0
- package/dist/foundation-public-provider.d.ts +140 -0
- package/dist/foundation-public-provider.js +192 -0
- package/dist/foundation-store.d.ts +132 -0
- package/dist/foundation-store.js +654 -0
- package/dist/graph-corrections.d.ts +157 -0
- package/dist/graph-corrections.js +620 -0
- package/dist/graph-foundation-reader.d.ts +60 -0
- package/dist/graph-foundation-reader.js +494 -0
- package/dist/graph-retrieval.d.ts +6 -1
- package/dist/graph-revision-reader.d.ts +80 -0
- package/dist/graph-revision-reader.js +246 -0
- package/dist/graph-web-http.d.ts +26 -0
- package/dist/graph-web-http.js +215 -0
- package/dist/graph-web.d.ts +364 -0
- package/dist/graph-web.js +639 -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 +213 -0
- package/dist/judgment-problem-snapshot.js +1073 -0
- package/dist/judgment-value-proof-review.d.ts +166 -0
- package/dist/judgment-value-proof-review.js +437 -0
- package/dist/judgment-value-proof.d.ts +28 -1
- package/dist/judgment-value-proof.js +53 -8
- 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/knowledge-continuation.d.ts +127 -0
- package/dist/knowledge-continuation.js +446 -0
- package/dist/knowledge-lookup.d.ts +178 -0
- package/dist/knowledge-lookup.js +758 -0
- package/dist/local-web-fetch-bridge.d.ts +5 -0
- package/dist/local-web-fetch-bridge.js +37 -0
- package/dist/local-web-host.d.ts +109 -0
- package/dist/local-web-host.js +688 -0
- package/dist/local-web-security.d.ts +25 -0
- package/dist/local-web-security.js +95 -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/organization-graph.d.ts +15 -0
- package/dist/organization-graph.js +24 -4
- package/dist/personal-knowledge.d.ts +132 -0
- package/dist/personal-knowledge.js +779 -0
- package/dist/personal-memory-handover.d.ts +108 -0
- package/dist/personal-memory-handover.js +369 -0
- package/dist/philosophy-revision-reader.d.ts +85 -0
- package/dist/philosophy-revision-reader.js +241 -0
- package/dist/portable-graph.d.ts +24 -0
- package/dist/portable-graph.js +77 -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 +104 -2
- package/dist/server.js +186 -4
- 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/value-proof-review-http.d.ts +25 -0
- package/dist/value-proof-review-http.js +185 -0
- package/dist/world-model.d.ts +199 -0
- package/dist/world-model.js +955 -0
- package/package.json +230 -3
- package/ui/brainbase-tokens.css +56 -0
- package/ui/graph-own-share.js +367 -0
- package/ui/graph-projects-view.css +23 -0
- package/ui/graph-projects-view.js +730 -0
- package/ui/graph-registry-view.css +101 -0
- package/ui/graph-registry-view.js +804 -0
- package/ui/graph-view-shared.css +522 -0
- package/ui/graph-view-shared.js +1227 -0
- package/ui/judgment-view.css +312 -0
- package/ui/judgment-view.js +508 -0
- package/ui/local-web-shell.css +396 -0
- package/ui/local-web-shell.js +378 -0
- package/ui/objective-editor-http-port.js +154 -0
- package/ui/objective-editor.css +286 -0
- package/ui/objective-editor.js +1586 -0
- package/ui/outcome-knowledge.css +50 -14
- package/ui/outcome-knowledge.js +15 -1
- package/ui/outcome-mana.css +91 -14
- package/ui/outcome-mana.js +958 -61
- package/ui/value-proof-review.css +504 -0
- package/ui/value-proof-review.js +1104 -0
- package/ui/workspace-kit.css +485 -0
- package/ui/workspace-kit.js +229 -0
- package/ui/world-model-view.css +188 -0
- package/ui/world-model-view.js +486 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import { validateFoundationDefinition } from './ontology-foundation.js';
|
|
2
|
+
import { digestFoundationDefinition, findFoundationRecord, findLatestFoundationRecord, foundationCatalogOrEmpty } from './foundation-catalog.js';
|
|
3
|
+
import { loadPersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
|
|
4
|
+
/**
|
|
5
|
+
* The world-model helpers are deliberately independent of storage. Definitions
|
|
6
|
+
* belong to the shared foundation catalog; observations and adoption records are
|
|
7
|
+
* values that reference an exact definition revision. A caller can therefore
|
|
8
|
+
* pass these records through the canonical SSOT transaction without introducing
|
|
9
|
+
* a second lock or definition authority.
|
|
10
|
+
*/
|
|
11
|
+
export const WORLD_MODEL_CONTRACT_VERSION = '0.1.0';
|
|
12
|
+
export const WORLD_MODEL_RECORD_CATALOG_VERSION = 1;
|
|
13
|
+
/** Evidence records are sidecar data committed with the canonical aggregate. */
|
|
14
|
+
export const WORLD_MODEL_EVIDENCE_SIDECAR = 'evidence/world-model.json';
|
|
15
|
+
export class WorldModelStoreError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
constructor(code, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'WorldModelStoreError';
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class WorldModelValidationError extends Error {
|
|
24
|
+
issues;
|
|
25
|
+
constructor(issues) {
|
|
26
|
+
super(`World model validation failed: ${issues.map((issue) => `${issue.path} (${issue.code}): ${issue.message}`).join(', ')}`);
|
|
27
|
+
this.name = 'WorldModelValidationError';
|
|
28
|
+
this.issues = issues;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validates and clones an observation before it crosses a storage boundary.
|
|
33
|
+
* The returned value is deeply frozen, while the input remains untouched.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeWorldModelObservation(input) {
|
|
36
|
+
const issues = observationShapeIssues(input);
|
|
37
|
+
if (issues.length > 0)
|
|
38
|
+
throw new WorldModelValidationError(issues);
|
|
39
|
+
const normalized = {
|
|
40
|
+
id: input.id.trim(),
|
|
41
|
+
variableRef: freezeCloneRevision(input.variableRef),
|
|
42
|
+
subjectId: input.subjectId.trim(),
|
|
43
|
+
value: cloneWorldModelValue(input.value, 'value'),
|
|
44
|
+
occurredAt: input.occurredAt,
|
|
45
|
+
period: freezeClonePeriod(input.period),
|
|
46
|
+
recordedAt: input.recordedAt,
|
|
47
|
+
sourceRef: freezeCloneProvenance(input.sourceRef),
|
|
48
|
+
...(input.supersedes === undefined ? {} : { supersedes: input.supersedes.trim() })
|
|
49
|
+
};
|
|
50
|
+
return deepFreeze(normalized);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Checks whether an already normalized observation is compatible with the
|
|
54
|
+
* exact Variable definition it references. This does not infer a value or
|
|
55
|
+
* upgrade an observation from inferred to observed.
|
|
56
|
+
*/
|
|
57
|
+
export function validateWorldModelObservation(observation, variable) {
|
|
58
|
+
const issues = [];
|
|
59
|
+
if (observation.variableRef.type !== 'variable'
|
|
60
|
+
|| observation.variableRef.id !== variable.id
|
|
61
|
+
|| observation.variableRef.revision !== variable.revision) {
|
|
62
|
+
issues.push({
|
|
63
|
+
code: 'INVALID_VARIABLE_REFERENCE',
|
|
64
|
+
path: 'variableRef',
|
|
65
|
+
message: `Observation references ${observation.variableRef.id}@${observation.variableRef.revision}, expected ${variable.id}@${variable.revision}.`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (variable.scope.subjectIds.length > 0 && !variable.scope.subjectIds.includes(observation.subjectId)) {
|
|
69
|
+
issues.push({
|
|
70
|
+
code: 'OBSERVATION_SCOPE_MISMATCH',
|
|
71
|
+
path: 'subjectId',
|
|
72
|
+
message: `Subject ${observation.subjectId} is outside Variable ${variable.id} scope.`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!valueMatchesKind(observation.value, variable.valueKind)) {
|
|
76
|
+
issues.push({
|
|
77
|
+
code: 'VARIABLE_VALUE_KIND_MISMATCH',
|
|
78
|
+
path: 'value',
|
|
79
|
+
message: `Observation value does not match Variable valueKind ${variable.valueKind}.`
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const validFrom = parseIsoTimestamp(variable.scope.validFrom);
|
|
83
|
+
const validUntil = variable.scope.validUntil === undefined ? undefined : parseIsoTimestamp(variable.scope.validUntil);
|
|
84
|
+
const periodFrom = parseIsoTimestamp(observation.period.from);
|
|
85
|
+
const periodUntil = parseIsoTimestamp(observation.period.until);
|
|
86
|
+
if (validFrom !== undefined && periodFrom !== undefined && periodFrom < validFrom) {
|
|
87
|
+
issues.push({
|
|
88
|
+
code: 'OBSERVATION_PERIOD_MISMATCH',
|
|
89
|
+
path: 'period.from',
|
|
90
|
+
message: 'Observation period begins before the Variable scope becomes valid.'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (validUntil !== undefined && periodUntil !== undefined && periodUntil > validUntil) {
|
|
94
|
+
issues.push({
|
|
95
|
+
code: 'OBSERVATION_PERIOD_MISMATCH',
|
|
96
|
+
path: 'period.until',
|
|
97
|
+
message: 'Observation period ends after the Variable scope expires.'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return validationResult(issues);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Delegates Model validation to the shared ontology contract. The wrapper
|
|
104
|
+
* keeps world-model callers from importing ontology internals while retaining
|
|
105
|
+
* the contract's distinction between draft, judgment, evaluation, and
|
|
106
|
+
* execution use.
|
|
107
|
+
*/
|
|
108
|
+
export function validateWorldModelModel(model, use = 'judgment') {
|
|
109
|
+
return validateFoundationDefinition(model, { use });
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Creates a durable adoption record without mutating the candidate or Model.
|
|
113
|
+
* A candidate may only produce an unverified/hypothesis Model record; adopting
|
|
114
|
+
* it cannot silently turn it into a verified claim.
|
|
115
|
+
*/
|
|
116
|
+
export function retainCandidateModel(candidate, model, adoption) {
|
|
117
|
+
const issues = [];
|
|
118
|
+
if (!isNonEmptyString(adoption.adoptionId))
|
|
119
|
+
issues.push(missing('adoptionId'));
|
|
120
|
+
if (!['proposed', 'approved'].includes(String(adoption.adoptionState))) {
|
|
121
|
+
issues.push({ code: 'INVALID_FIELD', path: 'adoptionState', message: 'Adoption state must be proposed or approved.' });
|
|
122
|
+
}
|
|
123
|
+
if (!isFoundationUse(adoption.authorizedUse)) {
|
|
124
|
+
issues.push({ code: 'INVALID_FIELD', path: 'authorizedUse', message: 'Adoption authorizedUse must be a valid Foundation use.' });
|
|
125
|
+
}
|
|
126
|
+
if (!isIsoTimestamp(adoption.adoptedAt))
|
|
127
|
+
issues.push(invalidTimestamp('adoptedAt'));
|
|
128
|
+
if (!candidate.candidateId.trim())
|
|
129
|
+
issues.push(missing('candidate.candidateId'));
|
|
130
|
+
if (!candidate.hypothesis.trim())
|
|
131
|
+
issues.push(missing('candidate.hypothesis'));
|
|
132
|
+
if (!Array.isArray(candidate.evidenceIds) || candidate.evidenceIds.length === 0) {
|
|
133
|
+
issues.push({ code: 'MISSING_SOURCE', path: 'candidate.evidenceIds', message: 'Candidate adoption must retain at least one evidence reference.' });
|
|
134
|
+
}
|
|
135
|
+
else if (candidate.evidenceIds.some((evidenceId) => !isNonEmptyString(evidenceId))) {
|
|
136
|
+
issues.push({ code: 'MISSING_SOURCE', path: 'candidate.evidenceIds', message: 'Candidate evidence references must be non-empty strings.' });
|
|
137
|
+
}
|
|
138
|
+
if (!isRecord(candidate.acl) || !isNonEmptyString(candidate.acl.ownerId)) {
|
|
139
|
+
issues.push(missing('candidate.acl.ownerId'));
|
|
140
|
+
}
|
|
141
|
+
if (!['unverified', 'hypothesis'].includes(candidate.epistemicState)) {
|
|
142
|
+
issues.push({ code: 'INVALID_FIELD', path: 'candidate.epistemicState', message: 'Candidate adoption only retains unverified or hypothesis state.' });
|
|
143
|
+
}
|
|
144
|
+
if (model.type !== 'model') {
|
|
145
|
+
issues.push({ code: 'MODEL_CONTRACT_INVALID', path: 'model.type', message: 'Candidate adoption requires a Model definition.' });
|
|
146
|
+
}
|
|
147
|
+
const modelValidation = validateWorldModelModel(model, adoption.authorizedUse);
|
|
148
|
+
if (!modelValidation.valid) {
|
|
149
|
+
issues.push(...modelValidation.issues.map((issue) => ({
|
|
150
|
+
code: 'MODEL_CONTRACT_INVALID',
|
|
151
|
+
path: issue.path,
|
|
152
|
+
message: issue.message
|
|
153
|
+
})));
|
|
154
|
+
}
|
|
155
|
+
if (model.epistemicState === 'verified' || model.validationState === 'verified') {
|
|
156
|
+
issues.push({
|
|
157
|
+
code: 'INVALID_FIELD',
|
|
158
|
+
path: 'model.epistemicState',
|
|
159
|
+
message: 'A candidate adoption cannot be represented as a verified Model.'
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (adoption.modelRef !== undefined
|
|
163
|
+
&& (adoption.modelRef.type !== 'model'
|
|
164
|
+
|| adoption.modelRef.id !== model.id
|
|
165
|
+
|| adoption.modelRef.revision !== model.revision)) {
|
|
166
|
+
issues.push({
|
|
167
|
+
code: 'INVALID_VARIABLE_REFERENCE',
|
|
168
|
+
path: 'modelRef',
|
|
169
|
+
message: `Adoption modelRef must match ${model.id}@${model.revision}.`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (adoption.approvalRef !== undefined && !isDecisionRevision(adoption.approvalRef)) {
|
|
173
|
+
issues.push({
|
|
174
|
+
code: 'INVALID_APPROVAL_REFERENCE',
|
|
175
|
+
path: 'approvalRef',
|
|
176
|
+
message: 'approvalRef must contain a decision id and positive revision.'
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (adoption.adoptionState === 'approved' && !isDecisionRevision(adoption.approvalRef)) {
|
|
180
|
+
issues.push({
|
|
181
|
+
code: adoption.approvalRef === undefined ? 'MISSING_APPROVAL_REFERENCE' : 'INVALID_APPROVAL_REFERENCE',
|
|
182
|
+
path: 'approvalRef',
|
|
183
|
+
message: 'Approved adoption requires a valid approvalRef decision revision.'
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
const canonicalModelDigest = digestFoundationDefinition(model);
|
|
187
|
+
if (adoption.modelDigest !== undefined
|
|
188
|
+
&& (!isDigest(adoption.modelDigest) || adoption.modelDigest !== canonicalModelDigest)) {
|
|
189
|
+
issues.push({
|
|
190
|
+
code: 'MODEL_DIGEST_MISMATCH',
|
|
191
|
+
path: 'modelDigest',
|
|
192
|
+
message: 'modelDigest must match the canonical digest of the adopted Model definition.'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (issues.length > 0)
|
|
196
|
+
throw new WorldModelValidationError(issues);
|
|
197
|
+
const modelRef = freezeCloneRevision(adoption.modelRef ?? {
|
|
198
|
+
id: model.id,
|
|
199
|
+
type: 'model',
|
|
200
|
+
revision: model.revision
|
|
201
|
+
});
|
|
202
|
+
const retainedCandidate = {
|
|
203
|
+
candidateId: candidate.candidateId.trim(),
|
|
204
|
+
hypothesis: candidate.hypothesis,
|
|
205
|
+
evidenceIds: [...candidate.evidenceIds],
|
|
206
|
+
acl: freezeCloneAcl(candidate.acl),
|
|
207
|
+
epistemicState: candidate.epistemicState
|
|
208
|
+
};
|
|
209
|
+
const record = {
|
|
210
|
+
adoptionId: adoption.adoptionId.trim(),
|
|
211
|
+
modelRef,
|
|
212
|
+
candidate: retainedCandidate,
|
|
213
|
+
adoptionState: adoption.adoptionState,
|
|
214
|
+
authorizedUse: adoption.authorizedUse,
|
|
215
|
+
adoptedAt: adoption.adoptedAt,
|
|
216
|
+
modelDigest: canonicalModelDigest,
|
|
217
|
+
...(adoption.approvalRef === undefined ? {} : { approvalRef: freezeCloneDecisionRevision(adoption.approvalRef) })
|
|
218
|
+
};
|
|
219
|
+
return deepFreeze(record);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Creates a world-model record store over the existing canonical aggregate.
|
|
223
|
+
* Foundation definitions are delegated to the shared revision store. The
|
|
224
|
+
* observation/adoption evidence catalog is committed as a sidecar in the same
|
|
225
|
+
* SSOT transaction, so the canonical Graph remains definition-only.
|
|
226
|
+
*/
|
|
227
|
+
export function createWorldModelStore(options) {
|
|
228
|
+
if (!options || !isNonEmptyString(options.dataDir)) {
|
|
229
|
+
throw new WorldModelStoreError('invalid_input', 'dataDir is required');
|
|
230
|
+
}
|
|
231
|
+
if (!options.foundationStore || typeof options.foundationStore.read !== 'function'
|
|
232
|
+
|| typeof options.foundationStore.create !== 'function') {
|
|
233
|
+
throw new WorldModelStoreError('invalid_input', 'foundationStore with create/read is required');
|
|
234
|
+
}
|
|
235
|
+
if (options.approvalReader !== undefined && typeof options.approvalReader.isApproved !== 'function') {
|
|
236
|
+
throw new WorldModelStoreError('invalid_input', 'approvalReader.isApproved is required when approvalReader is provided');
|
|
237
|
+
}
|
|
238
|
+
return new GraphWorldModelRecordStore(options.dataDir, options.foundationStore, options.approvalReader);
|
|
239
|
+
}
|
|
240
|
+
class GraphWorldModelRecordStore {
|
|
241
|
+
dataDir;
|
|
242
|
+
foundationStore;
|
|
243
|
+
approvalReader;
|
|
244
|
+
constructor(dataDir, foundationStore, approvalReader) {
|
|
245
|
+
this.dataDir = dataDir;
|
|
246
|
+
this.foundationStore = foundationStore;
|
|
247
|
+
this.approvalReader = approvalReader;
|
|
248
|
+
}
|
|
249
|
+
async createVariable(variable, context) {
|
|
250
|
+
assertStoreContext(context);
|
|
251
|
+
assertFoundationForJudgment(variable, 'Variable');
|
|
252
|
+
return this.foundationStore.create(variable, context);
|
|
253
|
+
}
|
|
254
|
+
async createModel(model, context) {
|
|
255
|
+
assertStoreContext(context);
|
|
256
|
+
assertFoundationForJudgment(model, 'Model');
|
|
257
|
+
return this.foundationStore.create(model, context);
|
|
258
|
+
}
|
|
259
|
+
async readModel(reference, context) {
|
|
260
|
+
assertStoreContext(context);
|
|
261
|
+
assertModelReference(reference, 'reference');
|
|
262
|
+
const record = await this.foundationStore.read(reference, context);
|
|
263
|
+
if (!record)
|
|
264
|
+
return null;
|
|
265
|
+
if (record.definition.type !== 'model') {
|
|
266
|
+
throw new WorldModelStoreError('corrupt_record', `Expected Model at ${reference.id}@${reference.revision}`);
|
|
267
|
+
}
|
|
268
|
+
assertFoundationAclRead(record.definition, context.principal);
|
|
269
|
+
return cloneModel(record.definition);
|
|
270
|
+
}
|
|
271
|
+
async saveObservation(input, context) {
|
|
272
|
+
assertStoreContext(context);
|
|
273
|
+
const observation = normalizeStoreInput(input, 'observation');
|
|
274
|
+
const variableRecord = await this.foundationStore.read(observation.variableRef, context);
|
|
275
|
+
if (!variableRecord) {
|
|
276
|
+
throw new WorldModelStoreError('not_found', `Variable ${observation.variableRef.id}@${observation.variableRef.revision} was not found`);
|
|
277
|
+
}
|
|
278
|
+
if (variableRecord.definition.type !== 'variable') {
|
|
279
|
+
throw new WorldModelStoreError('corrupt_record', `Observation references non-Variable ${observation.variableRef.id}`);
|
|
280
|
+
}
|
|
281
|
+
assertFoundationAclWrite(variableRecord.definition, context.principal);
|
|
282
|
+
assertObservationCompatible(observation, variableRecord.definition);
|
|
283
|
+
await this.mutateCatalog((catalog, current) => {
|
|
284
|
+
const currentVariable = resolveFoundationAtAggregate(current, observation.variableRef, variableRecord, context.principal, 'write');
|
|
285
|
+
if (currentVariable.definition.type !== 'variable') {
|
|
286
|
+
throw new WorldModelStoreError('corrupt_record', `Variable ${observation.variableRef.id} changed type during commit`);
|
|
287
|
+
}
|
|
288
|
+
assertFoundationAclWrite(currentVariable.definition, context.principal);
|
|
289
|
+
assertObservationCompatible(observation, currentVariable.definition);
|
|
290
|
+
if (catalog.observations.some((existing) => existing.id === observation.id)) {
|
|
291
|
+
throw new WorldModelStoreError('revision_conflict', `Observation ${observation.id} already exists`);
|
|
292
|
+
}
|
|
293
|
+
if (observation.supersedes !== undefined) {
|
|
294
|
+
const superseded = catalog.observations.find((existing) => existing.id === observation.supersedes);
|
|
295
|
+
if (!superseded) {
|
|
296
|
+
throw new WorldModelStoreError('not_found', `Superseded observation ${observation.supersedes} was not found`);
|
|
297
|
+
}
|
|
298
|
+
if (superseded.variableRef.id !== observation.variableRef.id
|
|
299
|
+
|| superseded.variableRef.revision !== observation.variableRef.revision
|
|
300
|
+
|| superseded.subjectId !== observation.subjectId) {
|
|
301
|
+
throw new WorldModelStoreError('invalid_input', `Correction ${observation.id} must retain the Variable revision and subject of ${superseded.id}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
...catalog,
|
|
306
|
+
observations: [...catalog.observations, observation]
|
|
307
|
+
};
|
|
308
|
+
});
|
|
309
|
+
const readback = await this.readObservation(observation.id, context);
|
|
310
|
+
if (!readback)
|
|
311
|
+
throw new WorldModelStoreError('readback_mismatch', `Observation ${observation.id} was not readable after commit`);
|
|
312
|
+
return readback;
|
|
313
|
+
}
|
|
314
|
+
async readObservation(id, context) {
|
|
315
|
+
assertStoreContext(context);
|
|
316
|
+
if (!isNonEmptyString(id))
|
|
317
|
+
throw new WorldModelStoreError('invalid_input', 'observation id is required');
|
|
318
|
+
const catalog = await this.loadCatalog();
|
|
319
|
+
const observation = catalog.observations.find((item) => item.id === id);
|
|
320
|
+
if (!observation)
|
|
321
|
+
return null;
|
|
322
|
+
const variableRecord = await this.foundationStore.read(observation.variableRef, context);
|
|
323
|
+
if (!variableRecord) {
|
|
324
|
+
throw new WorldModelStoreError('corrupt_record', `Variable for observation ${id} is missing`);
|
|
325
|
+
}
|
|
326
|
+
if (variableRecord.definition.type !== 'variable') {
|
|
327
|
+
throw new WorldModelStoreError('corrupt_record', `Observation ${id} references a non-Variable definition`);
|
|
328
|
+
}
|
|
329
|
+
assertFoundationAclRead(variableRecord.definition, context.principal);
|
|
330
|
+
assertObservationCompatible(observation, variableRecord.definition);
|
|
331
|
+
return freezeObservation(observation);
|
|
332
|
+
}
|
|
333
|
+
async listObservations(query, context) {
|
|
334
|
+
assertStoreContext(context);
|
|
335
|
+
const catalog = await this.loadCatalog();
|
|
336
|
+
const filtered = catalog.observations.filter((observation) => matchesObservationQuery(observation, query));
|
|
337
|
+
const visible = [];
|
|
338
|
+
for (const observation of filtered) {
|
|
339
|
+
const readback = await this.readObservation(observation.id, context);
|
|
340
|
+
if (readback)
|
|
341
|
+
visible.push(readback);
|
|
342
|
+
}
|
|
343
|
+
return Object.freeze(visible);
|
|
344
|
+
}
|
|
345
|
+
async saveModelAdoption(candidate, modelRef, adoption, context) {
|
|
346
|
+
assertStoreContext(context);
|
|
347
|
+
assertModelReference(modelRef, 'modelRef');
|
|
348
|
+
const modelRecord = await this.foundationStore.read(modelRef, context);
|
|
349
|
+
if (!modelRecord) {
|
|
350
|
+
throw new WorldModelStoreError('not_found', `Model ${modelRef.id}@${modelRef.revision} was not found`);
|
|
351
|
+
}
|
|
352
|
+
if (modelRecord.definition.type !== 'model') {
|
|
353
|
+
throw new WorldModelStoreError('corrupt_record', `Adoption references non-Model ${modelRef.id}`);
|
|
354
|
+
}
|
|
355
|
+
assertFoundationAclWrite(modelRecord.definition, context.principal);
|
|
356
|
+
assertCandidateAclWrite(candidate, context.principal);
|
|
357
|
+
let record;
|
|
358
|
+
await this.mutateCatalog(async (catalog, current) => {
|
|
359
|
+
const currentModel = resolveFoundationAtAggregate(current, modelRef, modelRecord, context.principal, 'write');
|
|
360
|
+
if (currentModel.definition.type !== 'model') {
|
|
361
|
+
throw new WorldModelStoreError('corrupt_record', `Model ${modelRef.id} changed type during commit`);
|
|
362
|
+
}
|
|
363
|
+
assertFoundationAclWrite(currentModel.definition, context.principal);
|
|
364
|
+
assertCandidateAclWrite(candidate, context.principal);
|
|
365
|
+
if (catalog.adoptions.some((existing) => existing.adoptionId === adoption.adoptionId)) {
|
|
366
|
+
throw new WorldModelStoreError('revision_conflict', `Model adoption ${adoption.adoptionId} already exists`);
|
|
367
|
+
}
|
|
368
|
+
// Resolve approval while the SSOT mutation lock is held and before the
|
|
369
|
+
// sidecar is returned for commit. A post-commit approval read could
|
|
370
|
+
// fail after the approved record was already persisted.
|
|
371
|
+
await this.assertApprovalReference(modelRef, adoption, context);
|
|
372
|
+
const retained = retainCandidateModel(candidate, currentModel.definition, { ...adoption, modelRef });
|
|
373
|
+
record = retained;
|
|
374
|
+
return {
|
|
375
|
+
...catalog,
|
|
376
|
+
adoptions: [...catalog.adoptions, retained]
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
if (!record)
|
|
380
|
+
throw new WorldModelStoreError('readback_mismatch', 'Model adoption commit did not produce a record');
|
|
381
|
+
// Approval was checked before the atomic append. This structural readback
|
|
382
|
+
// deliberately avoids a second provider call that could report failure
|
|
383
|
+
// while leaving the successful commit in place.
|
|
384
|
+
const readback = await this.readModelAdoptionRecord(record.adoptionId, context, { verifyApproval: false });
|
|
385
|
+
if (!readback)
|
|
386
|
+
throw new WorldModelStoreError('readback_mismatch', `Model adoption ${record.adoptionId} was not readable after commit`);
|
|
387
|
+
return readback;
|
|
388
|
+
}
|
|
389
|
+
async readModelAdoption(adoptionId, context) {
|
|
390
|
+
return this.readModelAdoptionRecord(adoptionId, context, { verifyApproval: true });
|
|
391
|
+
}
|
|
392
|
+
async readModelAdoptionRecord(adoptionId, context, options) {
|
|
393
|
+
assertStoreContext(context);
|
|
394
|
+
if (!isNonEmptyString(adoptionId))
|
|
395
|
+
throw new WorldModelStoreError('invalid_input', 'adoptionId is required');
|
|
396
|
+
const catalog = await this.loadCatalog();
|
|
397
|
+
const record = catalog.adoptions.find((item) => item.adoptionId === adoptionId);
|
|
398
|
+
if (!record)
|
|
399
|
+
return null;
|
|
400
|
+
assertCandidateAclRead(record.candidate, context.principal);
|
|
401
|
+
const modelRecord = await this.foundationStore.read(record.modelRef, context);
|
|
402
|
+
if (!modelRecord || modelRecord.definition.type !== 'model') {
|
|
403
|
+
throw new WorldModelStoreError('corrupt_record', `Model for adoption ${adoptionId} is missing`);
|
|
404
|
+
}
|
|
405
|
+
assertFoundationAclRead(modelRecord.definition, context.principal);
|
|
406
|
+
if (options.verifyApproval)
|
|
407
|
+
await this.assertApprovalReference(record.modelRef, record, context);
|
|
408
|
+
if (record.modelDigest !== modelRecord.digest) {
|
|
409
|
+
throw new WorldModelStoreError('readback_mismatch', `Model adoption ${adoptionId} digest does not match ${record.modelRef.id}@${record.modelRef.revision}`);
|
|
410
|
+
}
|
|
411
|
+
const verified = retainCandidateModel(record.candidate, modelRecord.definition, {
|
|
412
|
+
adoptionId: record.adoptionId,
|
|
413
|
+
adoptionState: record.adoptionState,
|
|
414
|
+
authorizedUse: record.authorizedUse,
|
|
415
|
+
adoptedAt: record.adoptedAt,
|
|
416
|
+
modelRef: record.modelRef,
|
|
417
|
+
modelDigest: record.modelDigest,
|
|
418
|
+
...(record.approvalRef === undefined ? {} : { approvalRef: record.approvalRef })
|
|
419
|
+
});
|
|
420
|
+
return verified;
|
|
421
|
+
}
|
|
422
|
+
async listModelAdoptions(context) {
|
|
423
|
+
assertStoreContext(context);
|
|
424
|
+
const catalog = await this.loadCatalog();
|
|
425
|
+
const visible = [];
|
|
426
|
+
for (const record of catalog.adoptions) {
|
|
427
|
+
const readback = await this.readModelAdoption(record.adoptionId, context);
|
|
428
|
+
if (readback)
|
|
429
|
+
visible.push(readback);
|
|
430
|
+
}
|
|
431
|
+
return Object.freeze(visible);
|
|
432
|
+
}
|
|
433
|
+
async loadCatalog() {
|
|
434
|
+
try {
|
|
435
|
+
// Loading the aggregate first performs the canonical recovery/read
|
|
436
|
+
// under the SSOT lock. The sidecar is then read from the committed
|
|
437
|
+
// evidence path; writes use the same lock and transaction below.
|
|
438
|
+
const os = await loadPersonalOs(this.dataDir);
|
|
439
|
+
if (os.graph.version !== 2) {
|
|
440
|
+
throw new WorldModelStoreError('unsupported_graph', 'World-model records require canonical Graph v2');
|
|
441
|
+
}
|
|
442
|
+
return await readWorldModelCatalog(this.dataDir);
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
if (error instanceof WorldModelStoreError)
|
|
446
|
+
throw error;
|
|
447
|
+
throw new WorldModelStoreError('corrupt_record', formatError(error));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async assertApprovalReference(modelRef, adoption, context) {
|
|
451
|
+
if (adoption.adoptionState !== 'approved')
|
|
452
|
+
return;
|
|
453
|
+
if (!isDecisionRevision(adoption.approvalRef)) {
|
|
454
|
+
throw new WorldModelStoreError('approval_reference_unresolved', 'Approved model adoption requires a valid approvalRef decision revision');
|
|
455
|
+
}
|
|
456
|
+
if (!this.approvalReader) {
|
|
457
|
+
throw new WorldModelStoreError('approval_reference_unresolved', 'Approved model adoption requires an approval reader that verifies the decision binding');
|
|
458
|
+
}
|
|
459
|
+
let approved = false;
|
|
460
|
+
try {
|
|
461
|
+
approved = await this.approvalReader.isApproved({
|
|
462
|
+
approvalRef: adoption.approvalRef,
|
|
463
|
+
modelRef,
|
|
464
|
+
context
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
approved = false;
|
|
469
|
+
}
|
|
470
|
+
if (!approved) {
|
|
471
|
+
throw new WorldModelStoreError('approval_reference_unresolved', `Approval ${adoption.approvalRef.id}@${adoption.approvalRef.revision} does not approve Model ${modelRef.id}@${modelRef.revision}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async mutateCatalog(mutator) {
|
|
475
|
+
await mutatePersonalOsWithSidecar(this.dataDir, WORLD_MODEL_EVIDENCE_SIDECAR, async (current, existingContent) => {
|
|
476
|
+
if (current.graph.version !== 2) {
|
|
477
|
+
throw new WorldModelStoreError('unsupported_graph', 'World-model records require canonical Graph v2');
|
|
478
|
+
}
|
|
479
|
+
// This callback already owns the canonical SSOT lock. Reading the
|
|
480
|
+
// existing evidence sidecar here avoids a lock re-entry and makes the
|
|
481
|
+
// read-modify-write sequence atomic with the Graph aggregate.
|
|
482
|
+
const catalog = parseWorldModelCatalog(existingContent);
|
|
483
|
+
const next = await mutator(catalog, current);
|
|
484
|
+
assertWorldModelCatalog(next);
|
|
485
|
+
return {
|
|
486
|
+
next: current,
|
|
487
|
+
sidecarContent: serializeWorldModelCatalog(next),
|
|
488
|
+
result: undefined
|
|
489
|
+
};
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function resolveFoundationAtAggregate(current, reference, expected, principal, action) {
|
|
494
|
+
if (current.graph.version !== 2) {
|
|
495
|
+
throw new WorldModelStoreError('unsupported_graph', 'World-model records require canonical Graph v2');
|
|
496
|
+
}
|
|
497
|
+
let actual;
|
|
498
|
+
let latest;
|
|
499
|
+
try {
|
|
500
|
+
const catalog = foundationCatalogOrEmpty(current.graph.foundation);
|
|
501
|
+
actual = findFoundationRecord(catalog, reference);
|
|
502
|
+
latest = findLatestFoundationRecord(catalog, reference.type, reference.id);
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
if (error instanceof WorldModelStoreError)
|
|
506
|
+
throw error;
|
|
507
|
+
throw new WorldModelStoreError('corrupt_record', formatError(error));
|
|
508
|
+
}
|
|
509
|
+
if (!actual) {
|
|
510
|
+
throw new WorldModelStoreError('not_found', `Foundation ${reference.type}/${reference.id}@${reference.revision} was not found in the commit aggregate`);
|
|
511
|
+
}
|
|
512
|
+
if (!latest) {
|
|
513
|
+
throw new WorldModelStoreError('corrupt_record', `Latest pointer is missing for ${reference.type}/${reference.id}`);
|
|
514
|
+
}
|
|
515
|
+
const readable = aclAllowsRead(latest.definition.acl, principal);
|
|
516
|
+
const authorized = action === 'read' ? readable : aclAllowsWrite(latest.definition.acl, principal);
|
|
517
|
+
if (!authorized) {
|
|
518
|
+
throw new WorldModelStoreError('authorization_denied', `Principal ${principal} cannot ${action} ${reference.type}/${reference.id}`);
|
|
519
|
+
}
|
|
520
|
+
if (actual.digest !== expected.digest) {
|
|
521
|
+
throw new WorldModelStoreError('revision_conflict', `Foundation ${reference.type}/${reference.id}@${reference.revision} changed before commit`);
|
|
522
|
+
}
|
|
523
|
+
return { definition: actual.definition, digest: actual.digest };
|
|
524
|
+
}
|
|
525
|
+
function assertFoundationForJudgment(definition, label) {
|
|
526
|
+
const result = validateFoundationDefinition(definition, { use: 'judgment' });
|
|
527
|
+
if (!result.valid) {
|
|
528
|
+
throw new WorldModelStoreError('invalid_input', `${label} is not valid for judgment: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join('; ')}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function normalizeStoreInput(input, label) {
|
|
532
|
+
try {
|
|
533
|
+
return normalizeWorldModelObservation(input);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
if (error instanceof WorldModelValidationError) {
|
|
537
|
+
throw new WorldModelStoreError('invalid_input', `${label} is invalid: ${error.message}`);
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function assertObservationCompatible(observation, variable) {
|
|
543
|
+
const result = validateWorldModelObservation(observation, variable);
|
|
544
|
+
if (!result.valid) {
|
|
545
|
+
throw new WorldModelStoreError('invalid_input', result.issues.map((issue) => `${issue.path}: ${issue.message}`).join('; '));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function assertStoreContext(context) {
|
|
549
|
+
if (!context || !isNonEmptyString(context.principal)) {
|
|
550
|
+
throw new WorldModelStoreError('authorization_denied', 'A trusted principal is required');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function assertModelReference(reference, label) {
|
|
554
|
+
if (!isVariableOrModelRevision(reference, 'model')) {
|
|
555
|
+
throw new WorldModelStoreError('invalid_input', `${label} must contain a model id and positive revision`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function isVariableOrModelRevision(value, type) {
|
|
559
|
+
return isRecord(value)
|
|
560
|
+
&& value.type === type
|
|
561
|
+
&& isNonEmptyString(value.id)
|
|
562
|
+
&& /^[1-9]\d*$/.test(String(value.revision));
|
|
563
|
+
}
|
|
564
|
+
function assertFoundationAclWrite(definition, principal) {
|
|
565
|
+
if (!aclAllowsWrite(definition.acl, principal)) {
|
|
566
|
+
throw new WorldModelStoreError('authorization_denied', `Principal ${principal} cannot write ${definition.type}/${definition.id}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function assertFoundationAclRead(definition, principal) {
|
|
570
|
+
if (!aclAllowsRead(definition.acl, principal)) {
|
|
571
|
+
throw new WorldModelStoreError('authorization_denied', `Principal ${principal} cannot read ${definition.type}/${definition.id}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function assertCandidateAclWrite(candidate, principal) {
|
|
575
|
+
if (!aclAllowsWrite(candidate.acl, principal)) {
|
|
576
|
+
throw new WorldModelStoreError('authorization_denied', `Principal ${principal} cannot write candidate ${candidate.candidateId}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function assertCandidateAclRead(candidate, principal) {
|
|
580
|
+
if (!aclAllowsRead(candidate.acl, principal)) {
|
|
581
|
+
throw new WorldModelStoreError('authorization_denied', `Principal ${principal} cannot read candidate ${candidate.candidateId}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function aclAllowsWrite(acl, principal) {
|
|
585
|
+
return acl.ownerId === principal || acl.writerIds.includes(principal);
|
|
586
|
+
}
|
|
587
|
+
function aclAllowsRead(acl, principal) {
|
|
588
|
+
return acl.visibility === 'public'
|
|
589
|
+
|| acl.ownerId === principal
|
|
590
|
+
|| acl.readerIds.includes(principal)
|
|
591
|
+
|| acl.writerIds.includes(principal);
|
|
592
|
+
}
|
|
593
|
+
function matchesObservationQuery(observation, query) {
|
|
594
|
+
if (!query)
|
|
595
|
+
return true;
|
|
596
|
+
if (query.subjectId !== undefined && observation.subjectId !== query.subjectId)
|
|
597
|
+
return false;
|
|
598
|
+
if (query.supersedes !== undefined && observation.supersedes !== query.supersedes)
|
|
599
|
+
return false;
|
|
600
|
+
if (query.variableRef !== undefined && (observation.variableRef.id !== query.variableRef.id
|
|
601
|
+
|| observation.variableRef.type !== query.variableRef.type
|
|
602
|
+
|| observation.variableRef.revision !== query.variableRef.revision))
|
|
603
|
+
return false;
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
async function readWorldModelCatalog(dataDir) {
|
|
607
|
+
try {
|
|
608
|
+
return parseWorldModelCatalog(await readPersonalOsSidecar(dataDir, WORLD_MODEL_EVIDENCE_SIDECAR));
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
if (error instanceof WorldModelStoreError)
|
|
612
|
+
throw error;
|
|
613
|
+
throw new WorldModelStoreError('corrupt_record', formatError(error));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function parseWorldModelCatalog(rawText) {
|
|
617
|
+
if (rawText === undefined)
|
|
618
|
+
return emptyWorldModelCatalog();
|
|
619
|
+
try {
|
|
620
|
+
const raw = JSON.parse(rawText);
|
|
621
|
+
assertWorldModelCatalog(raw);
|
|
622
|
+
return cloneWorldModelCatalog(raw);
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
if (error instanceof WorldModelStoreError)
|
|
626
|
+
throw error;
|
|
627
|
+
throw new WorldModelStoreError('corrupt_record', formatError(error));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function serializeWorldModelCatalog(catalog) {
|
|
631
|
+
return `${JSON.stringify(catalog, null, 2)}\n`;
|
|
632
|
+
}
|
|
633
|
+
function emptyWorldModelCatalog() {
|
|
634
|
+
return { version: WORLD_MODEL_RECORD_CATALOG_VERSION, observations: [], adoptions: [] };
|
|
635
|
+
}
|
|
636
|
+
function cloneWorldModelCatalog(catalog) {
|
|
637
|
+
return {
|
|
638
|
+
version: WORLD_MODEL_RECORD_CATALOG_VERSION,
|
|
639
|
+
observations: catalog.observations.map((observation) => freezeObservation(observation)),
|
|
640
|
+
adoptions: catalog.adoptions.map((adoption) => freezeAdoption(adoption))
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function assertWorldModelCatalog(value) {
|
|
644
|
+
if (!isRecord(value) || value.version !== WORLD_MODEL_RECORD_CATALOG_VERSION
|
|
645
|
+
|| !Array.isArray(value.observations) || !Array.isArray(value.adoptions)) {
|
|
646
|
+
throw new WorldModelStoreError('corrupt_record', 'world-model evidence must contain version 1 observations and adoptions arrays');
|
|
647
|
+
}
|
|
648
|
+
const observationIds = new Set();
|
|
649
|
+
for (const item of value.observations) {
|
|
650
|
+
const normalized = normalizeWorldModelObservation(item);
|
|
651
|
+
if (observationIds.has(normalized.id)) {
|
|
652
|
+
throw new WorldModelStoreError('corrupt_record', `Duplicate observation ${normalized.id}`);
|
|
653
|
+
}
|
|
654
|
+
observationIds.add(normalized.id);
|
|
655
|
+
}
|
|
656
|
+
const adoptionIds = new Set();
|
|
657
|
+
for (const item of value.adoptions) {
|
|
658
|
+
const normalized = normalizeStoredAdoption(item);
|
|
659
|
+
if (adoptionIds.has(normalized.adoptionId)) {
|
|
660
|
+
throw new WorldModelStoreError('corrupt_record', `Duplicate model adoption ${normalized.adoptionId}`);
|
|
661
|
+
}
|
|
662
|
+
adoptionIds.add(normalized.adoptionId);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function normalizeStoredAdoption(value) {
|
|
666
|
+
if (!isRecord(value)
|
|
667
|
+
|| !isNonEmptyString(value.adoptionId)
|
|
668
|
+
|| !isVariableOrModelRevision(value.modelRef, 'model')
|
|
669
|
+
|| !isDigest(value.modelDigest)
|
|
670
|
+
|| !isRecord(value.candidate)
|
|
671
|
+
|| !isNonEmptyString(value.candidate.candidateId)
|
|
672
|
+
|| !isNonEmptyString(value.candidate.hypothesis)
|
|
673
|
+
|| !Array.isArray(value.candidate.evidenceIds)
|
|
674
|
+
|| value.candidate.evidenceIds.length === 0
|
|
675
|
+
|| value.candidate.evidenceIds.some((item) => !isNonEmptyString(item))
|
|
676
|
+
|| !isCandidateAcl(value.candidate.acl)
|
|
677
|
+
|| !['unverified', 'hypothesis'].includes(String(value.candidate.epistemicState))
|
|
678
|
+
|| !['proposed', 'approved'].includes(String(value.adoptionState))
|
|
679
|
+
|| !isFoundationUse(String(value.authorizedUse))
|
|
680
|
+
|| !isIsoTimestamp(value.adoptedAt)
|
|
681
|
+
|| (value.adoptionState === 'approved' && !isDecisionRevision(value.approvalRef))
|
|
682
|
+
|| (value.approvalRef !== undefined && !isDecisionRevision(value.approvalRef))) {
|
|
683
|
+
throw new WorldModelStoreError('corrupt_record', 'world-model adoption record is malformed');
|
|
684
|
+
}
|
|
685
|
+
const candidate = {
|
|
686
|
+
candidateId: String(value.candidate.candidateId).trim(),
|
|
687
|
+
hypothesis: String(value.candidate.hypothesis),
|
|
688
|
+
evidenceIds: [...value.candidate.evidenceIds].map(String),
|
|
689
|
+
acl: freezeCloneAcl(value.candidate.acl),
|
|
690
|
+
epistemicState: value.candidate.epistemicState
|
|
691
|
+
};
|
|
692
|
+
return freezeAdoption({
|
|
693
|
+
adoptionId: String(value.adoptionId).trim(),
|
|
694
|
+
modelRef: freezeCloneRevision(value.modelRef),
|
|
695
|
+
candidate,
|
|
696
|
+
adoptionState: value.adoptionState,
|
|
697
|
+
authorizedUse: value.authorizedUse,
|
|
698
|
+
adoptedAt: String(value.adoptedAt),
|
|
699
|
+
modelDigest: String(value.modelDigest),
|
|
700
|
+
...(value.approvalRef === undefined ? {} : { approvalRef: freezeCloneDecisionRevision(value.approvalRef) })
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function isFoundationUse(value) {
|
|
704
|
+
return value === 'draft' || value === 'judgment' || value === 'evaluation' || value === 'execution';
|
|
705
|
+
}
|
|
706
|
+
function isDecisionRevision(value) {
|
|
707
|
+
return isRecord(value)
|
|
708
|
+
&& value.type === 'decision'
|
|
709
|
+
&& isNonEmptyString(value.id)
|
|
710
|
+
&& /^[1-9]\d*$/.test(String(value.revision));
|
|
711
|
+
}
|
|
712
|
+
function isDigest(value) {
|
|
713
|
+
return typeof value === 'string' && /^sha256:[0-9a-f]{64}$/.test(value);
|
|
714
|
+
}
|
|
715
|
+
function isCandidateAcl(value) {
|
|
716
|
+
return isRecord(value)
|
|
717
|
+
&& isNonEmptyString(value.ownerId)
|
|
718
|
+
&& ['private', 'project', 'organization', 'public'].includes(String(value.visibility))
|
|
719
|
+
&& Array.isArray(value.readerIds)
|
|
720
|
+
&& Array.isArray(value.writerIds)
|
|
721
|
+
&& value.readerIds.every((item) => isNonEmptyString(item))
|
|
722
|
+
&& value.writerIds.every((item) => isNonEmptyString(item));
|
|
723
|
+
}
|
|
724
|
+
function freezeObservation(observation) {
|
|
725
|
+
return deepFreeze({
|
|
726
|
+
id: observation.id,
|
|
727
|
+
variableRef: freezeCloneRevision(observation.variableRef),
|
|
728
|
+
subjectId: observation.subjectId,
|
|
729
|
+
value: cloneWorldModelValue(observation.value, 'value'),
|
|
730
|
+
occurredAt: observation.occurredAt,
|
|
731
|
+
period: freezeClonePeriod(observation.period),
|
|
732
|
+
recordedAt: observation.recordedAt,
|
|
733
|
+
sourceRef: freezeCloneProvenance(observation.sourceRef),
|
|
734
|
+
...(observation.supersedes === undefined ? {} : { supersedes: observation.supersedes })
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function freezeAdoption(adoption) {
|
|
738
|
+
return deepFreeze({
|
|
739
|
+
adoptionId: adoption.adoptionId,
|
|
740
|
+
modelRef: freezeCloneRevision(adoption.modelRef),
|
|
741
|
+
candidate: {
|
|
742
|
+
candidateId: adoption.candidate.candidateId,
|
|
743
|
+
hypothesis: adoption.candidate.hypothesis,
|
|
744
|
+
evidenceIds: [...adoption.candidate.evidenceIds],
|
|
745
|
+
acl: freezeCloneAcl(adoption.candidate.acl),
|
|
746
|
+
epistemicState: adoption.candidate.epistemicState
|
|
747
|
+
},
|
|
748
|
+
adoptionState: adoption.adoptionState,
|
|
749
|
+
authorizedUse: adoption.authorizedUse,
|
|
750
|
+
adoptedAt: adoption.adoptedAt,
|
|
751
|
+
modelDigest: adoption.modelDigest,
|
|
752
|
+
...(adoption.approvalRef === undefined ? {} : { approvalRef: freezeCloneDecisionRevision(adoption.approvalRef) })
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
function cloneModel(model) {
|
|
756
|
+
return JSON.parse(JSON.stringify(model));
|
|
757
|
+
}
|
|
758
|
+
function formatError(error) {
|
|
759
|
+
return error instanceof Error ? error.message : String(error);
|
|
760
|
+
}
|
|
761
|
+
function observationShapeIssues(input) {
|
|
762
|
+
const issues = [];
|
|
763
|
+
if (!isRecord(input))
|
|
764
|
+
return [invalid('observation', 'Observation must be a plain object.')];
|
|
765
|
+
if (!isNonEmptyString(input.id))
|
|
766
|
+
issues.push(missing('id'));
|
|
767
|
+
if (!isNonEmptyString(input.subjectId))
|
|
768
|
+
issues.push(missing('subjectId'));
|
|
769
|
+
if (!isVariableRevision(input.variableRef))
|
|
770
|
+
issues.push({
|
|
771
|
+
code: 'INVALID_VARIABLE_REFERENCE',
|
|
772
|
+
path: 'variableRef',
|
|
773
|
+
message: 'Observation variableRef requires a variable id and revision.'
|
|
774
|
+
});
|
|
775
|
+
if (input.value === null || input.value === undefined || !isWorldModelValue(input.value)) {
|
|
776
|
+
issues.push(invalid('value', 'Observation value must be a finite JSON scalar, array, or object; null is reserved for missing data.'));
|
|
777
|
+
}
|
|
778
|
+
if (!isIsoTimestamp(input.occurredAt))
|
|
779
|
+
issues.push(invalidTimestamp('occurredAt'));
|
|
780
|
+
if (!isValidPeriod(input.period))
|
|
781
|
+
issues.push({
|
|
782
|
+
code: 'INVALID_PERIOD',
|
|
783
|
+
path: 'period',
|
|
784
|
+
message: 'Observation period requires valid ISO from/until timestamps with until at or after from.'
|
|
785
|
+
});
|
|
786
|
+
if (!isIsoTimestamp(input.recordedAt))
|
|
787
|
+
issues.push(invalidTimestamp('recordedAt'));
|
|
788
|
+
if (!isProvenance(input.sourceRef))
|
|
789
|
+
issues.push({
|
|
790
|
+
code: 'MISSING_SOURCE',
|
|
791
|
+
path: 'sourceRef',
|
|
792
|
+
message: 'Observation requires a source id, source kind, and evidence reference list.'
|
|
793
|
+
});
|
|
794
|
+
if (isIsoTimestamp(input.occurredAt) && isIsoTimestamp(input.recordedAt)
|
|
795
|
+
&& Date.parse(input.recordedAt) < Date.parse(input.occurredAt)) {
|
|
796
|
+
issues.push({
|
|
797
|
+
code: 'RECORDED_BEFORE_OCCURRED',
|
|
798
|
+
path: 'recordedAt',
|
|
799
|
+
message: 'recordedAt cannot precede occurredAt for an observation.'
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
if (input.supersedes !== undefined
|
|
803
|
+
&& (!isNonEmptyString(input.supersedes) || input.supersedes.trim() === input.id?.trim())) {
|
|
804
|
+
issues.push({
|
|
805
|
+
code: 'INVALID_CORRECTION_REFERENCE',
|
|
806
|
+
path: 'supersedes',
|
|
807
|
+
message: 'A correction must reference a different non-empty observation id.'
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return issues;
|
|
811
|
+
}
|
|
812
|
+
function valueMatchesKind(value, kind) {
|
|
813
|
+
switch (kind) {
|
|
814
|
+
case 'number':
|
|
815
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
816
|
+
case 'boolean':
|
|
817
|
+
return typeof value === 'boolean';
|
|
818
|
+
case 'string':
|
|
819
|
+
case 'state':
|
|
820
|
+
return typeof value === 'string' && value.length > 0;
|
|
821
|
+
default:
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
function isWorldModelValue(value, seen = new Set()) {
|
|
826
|
+
if (typeof value === 'string' || typeof value === 'boolean')
|
|
827
|
+
return true;
|
|
828
|
+
if (typeof value === 'number')
|
|
829
|
+
return Number.isFinite(value);
|
|
830
|
+
if (Array.isArray(value)) {
|
|
831
|
+
if (seen.has(value))
|
|
832
|
+
return false;
|
|
833
|
+
seen.add(value);
|
|
834
|
+
const valid = value.every((item) => isWorldModelValue(item, seen));
|
|
835
|
+
seen.delete(value);
|
|
836
|
+
return valid;
|
|
837
|
+
}
|
|
838
|
+
if (!isRecord(value))
|
|
839
|
+
return false;
|
|
840
|
+
if (seen.has(value))
|
|
841
|
+
return false;
|
|
842
|
+
seen.add(value);
|
|
843
|
+
const valid = Object.values(value).every((item) => isWorldModelValue(item, seen));
|
|
844
|
+
seen.delete(value);
|
|
845
|
+
return valid;
|
|
846
|
+
}
|
|
847
|
+
function cloneWorldModelValue(value, path) {
|
|
848
|
+
if (typeof value === 'string' || typeof value === 'boolean')
|
|
849
|
+
return value;
|
|
850
|
+
if (typeof value === 'number') {
|
|
851
|
+
if (!Number.isFinite(value))
|
|
852
|
+
throw new WorldModelValidationError([invalid(path, 'Numbers must be finite.')]);
|
|
853
|
+
return value;
|
|
854
|
+
}
|
|
855
|
+
if (Array.isArray(value))
|
|
856
|
+
return value.map((item, index) => cloneWorldModelValue(item, `${path}[${index}]`));
|
|
857
|
+
const result = {};
|
|
858
|
+
for (const [key, item] of Object.entries(value))
|
|
859
|
+
result[key] = cloneWorldModelValue(item, `${path}.${key}`);
|
|
860
|
+
return result;
|
|
861
|
+
}
|
|
862
|
+
function isVariableRevision(value) {
|
|
863
|
+
return isRecord(value)
|
|
864
|
+
&& value.type === 'variable'
|
|
865
|
+
&& isNonEmptyString(value.id)
|
|
866
|
+
&& /^[1-9]\d*$/.test(String(value.revision));
|
|
867
|
+
}
|
|
868
|
+
function isProvenance(value) {
|
|
869
|
+
return isRecord(value)
|
|
870
|
+
&& isNonEmptyString(value.sourceId)
|
|
871
|
+
&& ['candidate', 'document', 'observation', 'decision', 'import'].includes(String(value.sourceKind))
|
|
872
|
+
&& Array.isArray(value.evidenceIds)
|
|
873
|
+
&& value.evidenceIds.length > 0
|
|
874
|
+
&& value.evidenceIds.every((item) => isNonEmptyString(item));
|
|
875
|
+
}
|
|
876
|
+
function isValidPeriod(value) {
|
|
877
|
+
return isRecord(value)
|
|
878
|
+
&& isIsoTimestamp(value.from)
|
|
879
|
+
&& isIsoTimestamp(value.until)
|
|
880
|
+
&& Date.parse(value.until) >= Date.parse(value.from);
|
|
881
|
+
}
|
|
882
|
+
function isIsoTimestamp(value) {
|
|
883
|
+
if (typeof value !== 'string')
|
|
884
|
+
return false;
|
|
885
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?(Z|[+-](\d{2}):(\d{2}))$/.exec(value);
|
|
886
|
+
if (!match)
|
|
887
|
+
return false;
|
|
888
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, , zone, offsetHourText, offsetMinuteText] = match;
|
|
889
|
+
const year = Number(yearText);
|
|
890
|
+
const month = Number(monthText);
|
|
891
|
+
const day = Number(dayText);
|
|
892
|
+
const hour = Number(hourText);
|
|
893
|
+
const minute = Number(minuteText);
|
|
894
|
+
const second = Number(secondText);
|
|
895
|
+
const offsetHour = zone === 'Z' ? 0 : Number(offsetHourText);
|
|
896
|
+
const offsetMinute = zone === 'Z' ? 0 : Number(offsetMinuteText);
|
|
897
|
+
const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
898
|
+
const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] ?? 0;
|
|
899
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth
|
|
900
|
+
&& hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 && second >= 0 && second <= 59
|
|
901
|
+
&& offsetHour >= 0 && offsetHour <= 23 && offsetMinute >= 0 && offsetMinute <= 59
|
|
902
|
+
&& Number.isFinite(Date.parse(value));
|
|
903
|
+
}
|
|
904
|
+
function parseIsoTimestamp(value) {
|
|
905
|
+
return isIsoTimestamp(value) ? Date.parse(value) : undefined;
|
|
906
|
+
}
|
|
907
|
+
function freezeCloneRevision(value) {
|
|
908
|
+
return deepFreeze({ id: value.id, type: value.type, revision: value.revision });
|
|
909
|
+
}
|
|
910
|
+
function freezeCloneDecisionRevision(value) {
|
|
911
|
+
return deepFreeze({ id: value.id, type: 'decision', revision: value.revision });
|
|
912
|
+
}
|
|
913
|
+
function freezeClonePeriod(value) {
|
|
914
|
+
return deepFreeze({ from: value.from, until: value.until });
|
|
915
|
+
}
|
|
916
|
+
function freezeCloneProvenance(value) {
|
|
917
|
+
return deepFreeze({ sourceId: value.sourceId, sourceKind: value.sourceKind, evidenceIds: [...value.evidenceIds] });
|
|
918
|
+
}
|
|
919
|
+
function freezeCloneAcl(value) {
|
|
920
|
+
return deepFreeze({
|
|
921
|
+
ownerId: value.ownerId,
|
|
922
|
+
visibility: value.visibility,
|
|
923
|
+
readerIds: [...value.readerIds],
|
|
924
|
+
writerIds: [...value.writerIds]
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function validationResult(issues) {
|
|
928
|
+
return { valid: issues.length === 0, status: issues.length === 0 ? 'valid' : 'invalid', issues: deepFreeze([...issues]) };
|
|
929
|
+
}
|
|
930
|
+
function missing(path) {
|
|
931
|
+
return { code: 'MISSING_FIELD', path, message: `${path} is required.` };
|
|
932
|
+
}
|
|
933
|
+
function invalid(path, message) {
|
|
934
|
+
return { code: 'INVALID_FIELD', path, message };
|
|
935
|
+
}
|
|
936
|
+
function invalidTimestamp(path) {
|
|
937
|
+
return { code: 'INVALID_TIMESTAMP', path, message: `${path} must be an ISO 8601 timestamp with timezone.` };
|
|
938
|
+
}
|
|
939
|
+
function isNonEmptyString(value) {
|
|
940
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
941
|
+
}
|
|
942
|
+
function isRecord(value) {
|
|
943
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
944
|
+
return false;
|
|
945
|
+
const prototype = Object.getPrototypeOf(value);
|
|
946
|
+
return prototype === Object.prototype || prototype === null;
|
|
947
|
+
}
|
|
948
|
+
function deepFreeze(value) {
|
|
949
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
950
|
+
Object.freeze(value);
|
|
951
|
+
for (const child of Object.values(value))
|
|
952
|
+
deepFreeze(child);
|
|
953
|
+
}
|
|
954
|
+
return value;
|
|
955
|
+
}
|