@unson/brainbase-mcp 0.5.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/README.md +8 -0
- package/contracts/judgment-dag/digest.json +2 -2
- 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 +145 -3
- package/ui/icons/mana/LICENSE.tabler.txt +21 -0
- package/ui/icons/mana/affiliate.svg +20 -0
- package/ui/icons/mana/brand-github.svg +15 -0
- package/ui/icons/mana/brand-google-drive.svg +17 -0
- package/ui/icons/mana/brand-slack.svg +18 -0
- package/ui/icons/mana/cpu.svg +24 -0
- package/ui/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
- package/ui/outcome-knowledge.css +365 -0
- package/ui/outcome-knowledge.js +1570 -0
- package/ui/outcome-mana.css +255 -0
- package/ui/outcome-mana.js +2325 -0
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared semantic contract for the company judgment foundation.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally separate from the historical Graph entity
|
|
5
|
+
* kinds. Story 01 registers the definitions that a later canonical store can
|
|
6
|
+
* adopt; it does not reinterpret or mutate ontology releases 1.0.0/2.0.0.
|
|
7
|
+
*/
|
|
8
|
+
export const JUDGMENT_FOUNDATION_CONTRACT_VERSION = '0.1.0';
|
|
9
|
+
const typeContracts = {
|
|
10
|
+
objective: {
|
|
11
|
+
type: 'objective',
|
|
12
|
+
meaning: 'A desired state for a beneficiary, with explicit criteria and an evaluation period.',
|
|
13
|
+
requiredBase: ['id', 'type', 'revision', 'meaning', 'adoptionState', 'authorizedUses', 'acl', 'storage', 'provenance', 'scope'],
|
|
14
|
+
requiredForJudgment: ['beneficiaryIds', 'desiredState', 'criteria', 'evaluationPeriod'],
|
|
15
|
+
requiredForEvaluation: ['beneficiaryIds', 'desiredState', 'criteria', 'evaluationPeriod']
|
|
16
|
+
},
|
|
17
|
+
variable: {
|
|
18
|
+
type: 'variable',
|
|
19
|
+
meaning: 'A definition of a state or outcome value, including measurement and aggregation semantics.',
|
|
20
|
+
requiredBase: ['id', 'type', 'revision', 'meaning', 'adoptionState', 'authorizedUses', 'acl', 'storage', 'provenance', 'scope'],
|
|
21
|
+
requiredForJudgment: ['subject', 'valueKind', 'aggregation', 'granularity', 'measurementMethod'],
|
|
22
|
+
requiredForEvaluation: ['subject', 'valueKind', 'aggregation', 'granularity', 'measurementMethod'],
|
|
23
|
+
conditionalRequirements: ['unit when valueKind is number']
|
|
24
|
+
},
|
|
25
|
+
model: {
|
|
26
|
+
type: 'model',
|
|
27
|
+
meaning: 'A scoped relationship or calculation describing how variables may change; it is not an LLM model name.',
|
|
28
|
+
requiredBase: ['id', 'type', 'revision', 'meaning', 'adoptionState', 'authorizedUses', 'acl', 'storage', 'provenance', 'scope'],
|
|
29
|
+
requiredForJudgment: ['epistemicState', 'inputVariableRefs', 'outputVariableRefs', 'applicability', 'relationship', 'uncertainty', 'validationState'],
|
|
30
|
+
requiredForEvaluation: ['epistemicState', 'inputVariableRefs', 'outputVariableRefs', 'applicability', 'relationship', 'uncertainty', 'validationState']
|
|
31
|
+
},
|
|
32
|
+
constraint: {
|
|
33
|
+
type: 'constraint',
|
|
34
|
+
meaning: 'A condition that applies to a bounded target and validity period, with decision-backed exceptions.',
|
|
35
|
+
requiredBase: ['id', 'type', 'revision', 'meaning', 'adoptionState', 'authorizedUses', 'acl', 'storage', 'provenance', 'scope'],
|
|
36
|
+
requiredForJudgment: ['condition', 'appliesTo', 'exceptions', 'adoptionBasis'],
|
|
37
|
+
requiredForEvaluation: ['condition', 'appliesTo', 'exceptions', 'adoptionBasis']
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const judgmentFoundationRelations = deepFreeze({
|
|
41
|
+
contributes_to: {
|
|
42
|
+
id: 'contributes_to',
|
|
43
|
+
from: ['story', 'objective'],
|
|
44
|
+
to: ['objective'],
|
|
45
|
+
meaning: 'A Story or Objective aims to contribute to an Objective.',
|
|
46
|
+
permittedInferences: [],
|
|
47
|
+
prohibitedInferences: [
|
|
48
|
+
'story completion does not establish objective achievement',
|
|
49
|
+
'objective contribution does not establish objective achievement'
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
execution_depends_on: {
|
|
53
|
+
id: 'execution_depends_on',
|
|
54
|
+
from: ['story'],
|
|
55
|
+
to: ['story', 'objective'],
|
|
56
|
+
meaning: 'A Story judgment or execution requires a result or decision from the referenced Story or Objective.',
|
|
57
|
+
permittedInferences: [],
|
|
58
|
+
prohibitedInferences: [
|
|
59
|
+
'execution dependency does not establish contribution',
|
|
60
|
+
'execution dependency does not establish time ordering',
|
|
61
|
+
'execution dependency does not establish objective achievement'
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
time_condition: {
|
|
65
|
+
id: 'time_condition',
|
|
66
|
+
from: ['story'],
|
|
67
|
+
to: ['objective'],
|
|
68
|
+
meaning: 'A Story deadline or evaluation window is related to the referenced Objective period.',
|
|
69
|
+
permittedInferences: [],
|
|
70
|
+
prohibitedInferences: [
|
|
71
|
+
'time condition does not establish contribution',
|
|
72
|
+
'time condition does not establish execution dependency',
|
|
73
|
+
'story completion does not establish objective achievement'
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
evaluated_by: {
|
|
77
|
+
id: 'evaluated_by',
|
|
78
|
+
from: ['objective'],
|
|
79
|
+
to: ['variable'],
|
|
80
|
+
meaning: 'An Objective uses a Variable and its definition as an evaluation criterion.',
|
|
81
|
+
permittedInferences: [],
|
|
82
|
+
prohibitedInferences: ['one variable improvement does not establish objective achievement']
|
|
83
|
+
},
|
|
84
|
+
uses_as_input: {
|
|
85
|
+
id: 'uses_as_input',
|
|
86
|
+
from: ['model'],
|
|
87
|
+
to: ['variable'],
|
|
88
|
+
meaning: 'A Model declares a Variable as an input.',
|
|
89
|
+
permittedInferences: [],
|
|
90
|
+
prohibitedInferences: ['input declaration does not prove causality or model accuracy']
|
|
91
|
+
},
|
|
92
|
+
predicts: {
|
|
93
|
+
id: 'predicts',
|
|
94
|
+
from: ['model'],
|
|
95
|
+
to: ['variable'],
|
|
96
|
+
meaning: 'A Model declares a Variable as an output it predicts.',
|
|
97
|
+
permittedInferences: [],
|
|
98
|
+
prohibitedInferences: ['prediction declaration does not prove causality or accuracy']
|
|
99
|
+
},
|
|
100
|
+
applies_to: {
|
|
101
|
+
id: 'applies_to',
|
|
102
|
+
from: ['constraint'],
|
|
103
|
+
to: ['objective', 'variable', 'model', 'constraint', 'story', 'decision', 'project', 'judgment_problem', 'execution'],
|
|
104
|
+
meaning: 'A Constraint applies to the explicitly listed target and validity scope.',
|
|
105
|
+
permittedInferences: [],
|
|
106
|
+
prohibitedInferences: ['scope does not extend to unlisted targets or periods']
|
|
107
|
+
},
|
|
108
|
+
used_as_basis: {
|
|
109
|
+
id: 'used_as_basis',
|
|
110
|
+
from: ['decision', 'judgment_problem'],
|
|
111
|
+
to: ['objective', 'variable', 'model', 'constraint'],
|
|
112
|
+
meaning: 'A Decision or JudgmentProblem records that a revision was used as a historical basis.',
|
|
113
|
+
permittedInferences: [],
|
|
114
|
+
prohibitedInferences: ['historical use does not establish current validity or truth']
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
/**
|
|
118
|
+
* Extension manifest consumed by future canonical-store work. The historical
|
|
119
|
+
* portable ontology releases intentionally do not include these definitions.
|
|
120
|
+
*/
|
|
121
|
+
export const judgmentFoundationContract = deepFreeze({
|
|
122
|
+
version: JUDGMENT_FOUNDATION_CONTRACT_VERSION,
|
|
123
|
+
status: 'extension-manifest',
|
|
124
|
+
compatibility: 'read-compatible-write-gated',
|
|
125
|
+
historicalOntologyVersions: ['1.0.0', '2.0.0'],
|
|
126
|
+
canonicalStoreAdoption: 'future-story-02-plus',
|
|
127
|
+
graphActivation: 'deferred',
|
|
128
|
+
types: typeContracts,
|
|
129
|
+
relations: judgmentFoundationRelations,
|
|
130
|
+
inference: {
|
|
131
|
+
worldModelCycles: 'allowed',
|
|
132
|
+
executionDagCycles: 'rejected'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const FOUNDATION_TYPES = ['objective', 'variable', 'model', 'constraint'];
|
|
136
|
+
const FOUNDATION_USES = ['draft', 'judgment', 'evaluation', 'execution'];
|
|
137
|
+
const FOUNDATION_EPISTEMIC_STATES = ['unverified', 'hypothesis', 'supported', 'verified', 'refuted'];
|
|
138
|
+
const FOUNDATION_ADOPTION_STATES = ['draft', 'proposed', 'approved', 'retired'];
|
|
139
|
+
const FOUNDATION_STORAGE = ['candidate', 'ontology', 'evidence', 'execution'];
|
|
140
|
+
const FOUNDATION_VALUE_KINDS = ['number', 'boolean', 'string', 'state'];
|
|
141
|
+
const FOUNDATION_AGGREGATIONS = ['none', 'sum', 'average', 'count', 'min', 'max', 'last', 'custom'];
|
|
142
|
+
const FOUNDATION_OPERATORS = ['at_least', 'at_most', 'equals'];
|
|
143
|
+
const FOUNDATION_VALIDATION_STATES = ['unverified', 'in_progress', 'supported', 'verified', 'refuted'];
|
|
144
|
+
const FOUNDATION_TIME_CONDITION_KINDS = ['deadline', 'evaluation_window'];
|
|
145
|
+
const FOUNDATION_RELATION_IDS = [
|
|
146
|
+
'contributes_to',
|
|
147
|
+
'execution_depends_on',
|
|
148
|
+
'time_condition',
|
|
149
|
+
'evaluated_by',
|
|
150
|
+
'uses_as_input',
|
|
151
|
+
'predicts',
|
|
152
|
+
'applies_to',
|
|
153
|
+
'used_as_basis'
|
|
154
|
+
];
|
|
155
|
+
const FOUNDATION_RELATION_ENDPOINTS = [
|
|
156
|
+
...FOUNDATION_TYPES,
|
|
157
|
+
'story',
|
|
158
|
+
'decision',
|
|
159
|
+
'project',
|
|
160
|
+
'judgment_problem',
|
|
161
|
+
'execution',
|
|
162
|
+
'outcome',
|
|
163
|
+
'evaluation'
|
|
164
|
+
];
|
|
165
|
+
const FOUNDATION_VISIBILITIES = ['private', 'project', 'organization', 'public'];
|
|
166
|
+
const FOUNDATION_SOURCE_KINDS = ['candidate', 'document', 'observation', 'decision', 'import'];
|
|
167
|
+
function isRecord(value) {
|
|
168
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
169
|
+
}
|
|
170
|
+
function isOneOf(value, values) {
|
|
171
|
+
return typeof value === 'string' && values.includes(value);
|
|
172
|
+
}
|
|
173
|
+
function isFoundationType(value) {
|
|
174
|
+
return isOneOf(value, FOUNDATION_TYPES);
|
|
175
|
+
}
|
|
176
|
+
function isFoundationUse(value) {
|
|
177
|
+
return isOneOf(value, FOUNDATION_USES);
|
|
178
|
+
}
|
|
179
|
+
function isFoundationEpistemicState(value) {
|
|
180
|
+
return isOneOf(value, FOUNDATION_EPISTEMIC_STATES);
|
|
181
|
+
}
|
|
182
|
+
function isFoundationAdoptionState(value) {
|
|
183
|
+
return isOneOf(value, FOUNDATION_ADOPTION_STATES);
|
|
184
|
+
}
|
|
185
|
+
function isFoundationStorage(value) {
|
|
186
|
+
return isOneOf(value, FOUNDATION_STORAGE);
|
|
187
|
+
}
|
|
188
|
+
function isFoundationValueKind(value) {
|
|
189
|
+
return isOneOf(value, FOUNDATION_VALUE_KINDS);
|
|
190
|
+
}
|
|
191
|
+
function isFoundationAggregation(value) {
|
|
192
|
+
return isOneOf(value, FOUNDATION_AGGREGATIONS);
|
|
193
|
+
}
|
|
194
|
+
function isFoundationOperator(value) {
|
|
195
|
+
return isOneOf(value, FOUNDATION_OPERATORS);
|
|
196
|
+
}
|
|
197
|
+
function isFoundationValidationState(value) {
|
|
198
|
+
return isOneOf(value, FOUNDATION_VALIDATION_STATES);
|
|
199
|
+
}
|
|
200
|
+
function isFoundationTimeConditionKind(value) {
|
|
201
|
+
return isOneOf(value, FOUNDATION_TIME_CONDITION_KINDS);
|
|
202
|
+
}
|
|
203
|
+
function isFoundationRelationId(value) {
|
|
204
|
+
return isOneOf(value, FOUNDATION_RELATION_IDS);
|
|
205
|
+
}
|
|
206
|
+
function isFoundationRelationEndpoint(value) {
|
|
207
|
+
return isOneOf(value, FOUNDATION_RELATION_ENDPOINTS);
|
|
208
|
+
}
|
|
209
|
+
function isFoundationVisibility(value) {
|
|
210
|
+
return isOneOf(value, FOUNDATION_VISIBILITIES);
|
|
211
|
+
}
|
|
212
|
+
function isFoundationSourceKind(value) {
|
|
213
|
+
return isOneOf(value, FOUNDATION_SOURCE_KINDS);
|
|
214
|
+
}
|
|
215
|
+
function isNonEmptyString(value) {
|
|
216
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
217
|
+
}
|
|
218
|
+
function isScalar(value) {
|
|
219
|
+
return isNonEmptyString(value)
|
|
220
|
+
|| typeof value === 'boolean'
|
|
221
|
+
|| (typeof value === 'number' && Number.isFinite(value));
|
|
222
|
+
}
|
|
223
|
+
function isEmptyRequiredArray(value) {
|
|
224
|
+
return value === undefined || (Array.isArray(value) && value.length === 0);
|
|
225
|
+
}
|
|
226
|
+
function forEachArrayEntry(value, callback) {
|
|
227
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
228
|
+
callback(value[index], index);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function requiredString(record, key, issues) {
|
|
232
|
+
if (record[key] === undefined) {
|
|
233
|
+
issues.push({ code: 'MISSING_FIELD', path: key, message: `${key} is required.` });
|
|
234
|
+
}
|
|
235
|
+
else if (!isNonEmptyString(record[key])) {
|
|
236
|
+
issues.push({ code: 'INVALID_FIELD', path: key, message: `${key} must be a non-empty string.` });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function requiredEnum(record, key, guard, issues) {
|
|
240
|
+
if (record[key] === undefined) {
|
|
241
|
+
issues.push({ code: 'MISSING_FIELD', path: key, message: `${key} is required.` });
|
|
242
|
+
}
|
|
243
|
+
else if (!guard(record[key])) {
|
|
244
|
+
issues.push({ code: 'INVALID_FIELD', path: key, message: `${key} must be a registered value.` });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function optionalString(value, path, issues, allowEmpty = false) {
|
|
248
|
+
if (value !== undefined && !(allowEmpty && typeof value === 'string') && !isNonEmptyString(value)) {
|
|
249
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be a non-empty string when provided.` });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function optionalStringArray(value, path, issues) {
|
|
253
|
+
if (value === undefined)
|
|
254
|
+
return;
|
|
255
|
+
if (!Array.isArray(value)) {
|
|
256
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be an array of non-empty strings.` });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
forEachArrayEntry(value, (item, index) => {
|
|
260
|
+
if (!isNonEmptyString(item)) {
|
|
261
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}[${index}]`, message: `${path} entries must be non-empty strings.` });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function optionalArray(value, path, issues) {
|
|
266
|
+
if (value !== undefined && !Array.isArray(value)) {
|
|
267
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be an array.` });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function optionalEnum(value, path, guard, issues) {
|
|
271
|
+
if (value !== undefined && !guard(value)) {
|
|
272
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be a registered value.` });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function validateAcl(value, issues) {
|
|
276
|
+
if (!isRecord(value)) {
|
|
277
|
+
issues.push({ code: value === undefined ? 'MISSING_FIELD' : 'INVALID_FIELD', path: 'acl', message: 'ACL requires ownerId, visibility, readerIds, and writerIds.' });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
requiredString(value, 'ownerId', issues);
|
|
281
|
+
requiredEnum(value, 'visibility', isFoundationVisibility, issues);
|
|
282
|
+
validateStringArray(value.readerIds, 'acl.readerIds', issues);
|
|
283
|
+
validateStringArray(value.writerIds, 'acl.writerIds', issues);
|
|
284
|
+
}
|
|
285
|
+
function validateStringArray(value, path, issues) {
|
|
286
|
+
if (value === undefined) {
|
|
287
|
+
issues.push({ code: 'MISSING_FIELD', path, message: `${path} is required.` });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!Array.isArray(value)) {
|
|
291
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be an array of non-empty strings.` });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
forEachArrayEntry(value, (item, index) => {
|
|
295
|
+
if (!isNonEmptyString(item)) {
|
|
296
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}[${index}]`, message: `${path} entries must be non-empty strings.` });
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function validateProvenance(value, issues) {
|
|
301
|
+
if (value === undefined) {
|
|
302
|
+
issues.push({ code: 'MISSING_FIELD', path: 'provenance', message: 'provenance is required.' });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!Array.isArray(value)) {
|
|
306
|
+
issues.push({ code: 'INVALID_FIELD', path: 'provenance', message: 'provenance must be an array of evidence references.' });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
forEachArrayEntry(value, (item, index) => {
|
|
310
|
+
const path = `provenance[${index}]`;
|
|
311
|
+
if (!isRecord(item)) {
|
|
312
|
+
issues.push({ code: 'INVALID_FIELD', path, message: 'Provenance entries must be objects.' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
requiredStringAtPath(item, 'sourceId', `${path}.sourceId`, issues);
|
|
316
|
+
requiredEnumAtPath(item, 'sourceKind', isFoundationSourceKind, `${path}.sourceKind`, issues);
|
|
317
|
+
validateStringArrayAtPath(item.evidenceIds, `${path}.evidenceIds`, issues);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function requiredStringAtPath(record, key, path, issues) {
|
|
321
|
+
if (record[key] === undefined) {
|
|
322
|
+
issues.push({ code: 'MISSING_FIELD', path, message: `${path} is required.` });
|
|
323
|
+
}
|
|
324
|
+
else if (!isNonEmptyString(record[key])) {
|
|
325
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be a non-empty string.` });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function requiredEnumAtPath(record, key, guard, path, issues) {
|
|
329
|
+
if (record[key] === undefined) {
|
|
330
|
+
issues.push({ code: 'MISSING_FIELD', path, message: `${path} is required.` });
|
|
331
|
+
}
|
|
332
|
+
else if (!guard(record[key])) {
|
|
333
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be a registered value.` });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function validateStringArrayAtPath(value, path, issues) {
|
|
337
|
+
if (value === undefined) {
|
|
338
|
+
issues.push({ code: 'MISSING_FIELD', path, message: `${path} is required.` });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!Array.isArray(value)) {
|
|
342
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be an array of non-empty strings.` });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
forEachArrayEntry(value, (item, index) => {
|
|
346
|
+
if (!isNonEmptyString(item)) {
|
|
347
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}[${index}]`, message: `${path} entries must be non-empty strings.` });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
function isPeriodValue(value) {
|
|
352
|
+
return isRecord(value)
|
|
353
|
+
&& isNonEmptyString(value.from)
|
|
354
|
+
&& isNonEmptyString(value.until)
|
|
355
|
+
&& isValidPeriod({ from: value.from, until: value.until });
|
|
356
|
+
}
|
|
357
|
+
function isScopeValue(value) {
|
|
358
|
+
if (!isRecord(value) || !isNonEmptyStringArray(value.subjectIds)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (!isNonEmptyString(value.validFrom))
|
|
362
|
+
return false;
|
|
363
|
+
if (value.validUntil !== undefined && !isNonEmptyString(value.validUntil))
|
|
364
|
+
return false;
|
|
365
|
+
if (parseStrictRfc3339(value.validFrom) === undefined)
|
|
366
|
+
return false;
|
|
367
|
+
if (value.validUntil === undefined)
|
|
368
|
+
return true;
|
|
369
|
+
return isValidPeriod({ from: value.validFrom, until: value.validUntil });
|
|
370
|
+
}
|
|
371
|
+
function isNonEmptyStringArray(value) {
|
|
372
|
+
if (!Array.isArray(value))
|
|
373
|
+
return false;
|
|
374
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
375
|
+
if (!isNonEmptyString(value[index]))
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
function validateRevisionValue(value, expectedType, path, issues) {
|
|
381
|
+
if (!isRecord(value)
|
|
382
|
+
|| !isNonEmptyString(value.id)
|
|
383
|
+
|| value.type !== expectedType
|
|
384
|
+
|| !isNonEmptyString(value.revision)) {
|
|
385
|
+
issues.push({
|
|
386
|
+
code: 'INVALID_REVISION_REFERENCE',
|
|
387
|
+
path,
|
|
388
|
+
message: `${path} must reference ${expectedType} by id, type, and revision.`
|
|
389
|
+
});
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
if (expectedType === 'decision') {
|
|
393
|
+
return { id: value.id, type: 'decision', revision: value.revision };
|
|
394
|
+
}
|
|
395
|
+
return { id: value.id, type: expectedType, revision: value.revision };
|
|
396
|
+
}
|
|
397
|
+
function validateDecisionValue(value, path, issues) {
|
|
398
|
+
validateRevisionValue(value, 'decision', path, issues);
|
|
399
|
+
}
|
|
400
|
+
function optionalRevisionArray(value, path, expectedType, issues) {
|
|
401
|
+
if (value === undefined)
|
|
402
|
+
return;
|
|
403
|
+
if (!Array.isArray(value)) {
|
|
404
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be an array of versioned references.` });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
forEachArrayEntry(value, (item, index) => validateRevisionValue(item, expectedType, `${path}[${index}]`, issues));
|
|
408
|
+
}
|
|
409
|
+
function validateRelationEndpoint(value, path, issues) {
|
|
410
|
+
if (!isRecord(value)) {
|
|
411
|
+
issues.push({ code: 'INVALID_FIELD', path, message: `${path} must be an endpoint object.` });
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
if (!isNonEmptyString(value.id)) {
|
|
415
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}.id`, message: `${path}.id must be a non-empty string.` });
|
|
416
|
+
}
|
|
417
|
+
if (!isFoundationRelationEndpoint(value.type)) {
|
|
418
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}.type`, message: `${path}.type is not a registered relation endpoint.` });
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
if (value.revision !== undefined && !isNonEmptyString(value.revision)) {
|
|
422
|
+
issues.push({ code: 'INVALID_REVISION_REFERENCE', path: `${path}.revision`, message: `${path}.revision must be a non-empty revision.` });
|
|
423
|
+
}
|
|
424
|
+
if (isRevisionEndpoint(value.type) && !isNonEmptyString(value.revision)) {
|
|
425
|
+
issues.push({ code: 'INVALID_REVISION_REFERENCE', path: `${path}.revision`, message: `Versioned ${value.type} relation endpoints require a revision.` });
|
|
426
|
+
}
|
|
427
|
+
return value.type;
|
|
428
|
+
}
|
|
429
|
+
export function validateFoundationDefinition(definition, options) {
|
|
430
|
+
const issues = [];
|
|
431
|
+
const requestedUse = isRecord(options) ? options.use : undefined;
|
|
432
|
+
const use = isFoundationUse(requestedUse) ? requestedUse : undefined;
|
|
433
|
+
if (!use) {
|
|
434
|
+
issues.push({ code: 'INVALID_FIELD', path: 'use', message: `Unknown foundation use ${String(requestedUse)}.` });
|
|
435
|
+
}
|
|
436
|
+
if (!isRecord(definition)) {
|
|
437
|
+
issues.push({ code: 'INVALID_FIELD', path: 'definition', message: 'A foundation definition must be an object.' });
|
|
438
|
+
return { valid: false, status: 'invalid', issues };
|
|
439
|
+
}
|
|
440
|
+
const type = isFoundationType(definition.type) ? definition.type : undefined;
|
|
441
|
+
if (definition.type === undefined) {
|
|
442
|
+
issues.push({ code: 'MISSING_FIELD', path: 'type', message: 'A foundation definition requires a registered type.' });
|
|
443
|
+
}
|
|
444
|
+
else if (!type) {
|
|
445
|
+
issues.push({ code: 'INVALID_FIELD', path: 'type', message: `Unknown foundation type ${String(definition.type)}.` });
|
|
446
|
+
}
|
|
447
|
+
validateCommonDefinition(definition, use, issues);
|
|
448
|
+
if (type) {
|
|
449
|
+
validateDefinitionShape(definition, type, issues, use !== 'draft');
|
|
450
|
+
if (use && use !== 'draft') {
|
|
451
|
+
validateTypeSpecificDefinition(definition, type, use, issues);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const hasError = issues.length > 0;
|
|
455
|
+
const unverifiedModel = type === 'model' && definition.epistemicState !== 'verified';
|
|
456
|
+
return {
|
|
457
|
+
valid: !hasError,
|
|
458
|
+
status: hasError ? 'invalid' : unverifiedModel ? 'unverified' : 'valid',
|
|
459
|
+
issues
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
export function validateFoundationRelation(reference) {
|
|
463
|
+
const issues = [];
|
|
464
|
+
if (!isRecord(reference)) {
|
|
465
|
+
return {
|
|
466
|
+
valid: false,
|
|
467
|
+
status: 'invalid',
|
|
468
|
+
issues: [{ code: 'INVALID_FIELD', path: 'relation', message: 'A foundation relation must be an object.' }]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const relation = isFoundationRelationId(reference.relation) ? reference.relation : undefined;
|
|
472
|
+
if (!relation) {
|
|
473
|
+
issues.push({ code: 'INVALID_FIELD', path: 'relation', message: `Unknown foundation relation ${String(reference.relation)}.` });
|
|
474
|
+
return { valid: false, status: 'invalid', issues };
|
|
475
|
+
}
|
|
476
|
+
const definition = judgmentFoundationRelations[relation];
|
|
477
|
+
const sourceType = validateRelationEndpoint(reference.source, 'source', issues);
|
|
478
|
+
const targetType = validateRelationEndpoint(reference.target, 'target', issues);
|
|
479
|
+
if (sourceType && !definition.from.includes(sourceType)) {
|
|
480
|
+
issues.push({
|
|
481
|
+
code: 'RELATION_SOURCE_TYPE_MISMATCH',
|
|
482
|
+
path: 'source.type',
|
|
483
|
+
message: `${relation} does not accept source type ${sourceType}.`
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (targetType && !definition.to.includes(targetType)) {
|
|
487
|
+
issues.push({
|
|
488
|
+
code: 'RELATION_TARGET_TYPE_MISMATCH',
|
|
489
|
+
path: 'target.type',
|
|
490
|
+
message: `${relation} does not accept target type ${targetType}.`
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (relation === 'time_condition') {
|
|
494
|
+
validateTimeCondition(reference.timeCondition, issues);
|
|
495
|
+
}
|
|
496
|
+
else if (reference.timeCondition !== undefined) {
|
|
497
|
+
issues.push({
|
|
498
|
+
code: 'INVALID_FIELD',
|
|
499
|
+
path: 'timeCondition',
|
|
500
|
+
message: `timeCondition is only valid for the time_condition relation.`
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
valid: issues.length === 0,
|
|
505
|
+
status: issues.length === 0 ? 'valid' : 'invalid',
|
|
506
|
+
issues
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function validateTimeCondition(value, issues) {
|
|
510
|
+
if (!isRecord(value)) {
|
|
511
|
+
issues.push({
|
|
512
|
+
code: 'MISSING_FIELD',
|
|
513
|
+
path: 'timeCondition',
|
|
514
|
+
message: 'time_condition relations require a timeCondition object.'
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (!isFoundationTimeConditionKind(value.kind)) {
|
|
519
|
+
issues.push({
|
|
520
|
+
code: 'INVALID_FIELD',
|
|
521
|
+
path: 'timeCondition.kind',
|
|
522
|
+
message: `Unknown time condition kind ${String(value.kind)}.`
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
if (!isRecord(value.period)) {
|
|
526
|
+
issues.push({
|
|
527
|
+
code: 'MISSING_FIELD',
|
|
528
|
+
path: 'timeCondition.period',
|
|
529
|
+
message: 'timeCondition.period must contain from and until.'
|
|
530
|
+
});
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!isNonEmptyString(value.period.from) || !isNonEmptyString(value.period.until)) {
|
|
534
|
+
issues.push({
|
|
535
|
+
code: 'INVALID_FIELD',
|
|
536
|
+
path: 'timeCondition.period',
|
|
537
|
+
message: 'timeCondition.period.from and until must be non-empty ISO date strings.'
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (!isValidPeriod({ from: value.period.from, until: value.period.until })) {
|
|
542
|
+
issues.push({
|
|
543
|
+
code: 'INVALID_FIELD',
|
|
544
|
+
path: 'timeCondition.period',
|
|
545
|
+
message: 'timeCondition.period must contain valid dates with until at or after from.'
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
export function validateEvaluationCompatibility(variable, actual, expected = {}) {
|
|
550
|
+
const issues = [];
|
|
551
|
+
if (isRecord(variable) && variable.type === 'variable') {
|
|
552
|
+
const variableResult = validateFoundationDefinition(variable, { use: 'evaluation' });
|
|
553
|
+
issues.push(...variableResult.issues);
|
|
554
|
+
}
|
|
555
|
+
if (!isRecord(variable) || variable.type !== 'variable') {
|
|
556
|
+
issues.push({ code: 'INVALID_FIELD', path: 'variable.type', message: 'Evaluation compatibility requires a Variable definition.' });
|
|
557
|
+
}
|
|
558
|
+
const variableId = isRecord(variable) && typeof variable.id === 'string' ? variable.id : undefined;
|
|
559
|
+
const variableRevision = isRecord(variable) && typeof variable.revision === 'string' ? variable.revision : undefined;
|
|
560
|
+
const variableUnit = isRecord(variable) && (variable.unit === undefined || typeof variable.unit === 'string')
|
|
561
|
+
? variable.unit
|
|
562
|
+
: undefined;
|
|
563
|
+
const variableAggregation = isRecord(variable) && isFoundationAggregation(variable.aggregation)
|
|
564
|
+
? variable.aggregation
|
|
565
|
+
: undefined;
|
|
566
|
+
const variableGranularity = isRecord(variable) && typeof variable.granularity === 'string'
|
|
567
|
+
? variable.granularity
|
|
568
|
+
: undefined;
|
|
569
|
+
if (!variableId || !variableRevision) {
|
|
570
|
+
issues.push({ code: 'INVALID_FIELD', path: 'variable.id/revision', message: 'Variable compatibility requires a versioned Variable definition.' });
|
|
571
|
+
}
|
|
572
|
+
if (!variableAggregation) {
|
|
573
|
+
issues.push({ code: 'INVALID_FIELD', path: 'variable.aggregation', message: 'Variable compatibility requires a registered aggregation.' });
|
|
574
|
+
}
|
|
575
|
+
if (!variableGranularity) {
|
|
576
|
+
issues.push({ code: 'INVALID_FIELD', path: 'variable.granularity', message: 'Variable compatibility requires a granularity.' });
|
|
577
|
+
}
|
|
578
|
+
const actualRecord = isRecord(actual) ? actual : undefined;
|
|
579
|
+
if (!actualRecord) {
|
|
580
|
+
issues.push({ code: 'INVALID_FIELD', path: 'actual', message: 'Evaluation descriptor must be an object.' });
|
|
581
|
+
}
|
|
582
|
+
const actualReference = actualRecord ? actualRecord.variableRef : undefined;
|
|
583
|
+
const actualRevision = validateRevisionValue(actualReference, 'variable', 'variableRef', issues);
|
|
584
|
+
const actualUnit = actualRecord && (actualRecord.unit === undefined || typeof actualRecord.unit === 'string')
|
|
585
|
+
? actualRecord.unit
|
|
586
|
+
: undefined;
|
|
587
|
+
if (actualRecord && actualRecord.unit !== undefined && typeof actualRecord.unit !== 'string') {
|
|
588
|
+
issues.push({ code: 'INVALID_FIELD', path: 'unit', message: 'Evaluation unit must be a string when provided.' });
|
|
589
|
+
}
|
|
590
|
+
const actualAggregation = actualRecord && isFoundationAggregation(actualRecord.aggregation)
|
|
591
|
+
? actualRecord.aggregation
|
|
592
|
+
: undefined;
|
|
593
|
+
if (!actualAggregation) {
|
|
594
|
+
issues.push({ code: 'INVALID_FIELD', path: 'aggregation', message: 'Evaluation requires a registered aggregation.' });
|
|
595
|
+
}
|
|
596
|
+
const actualGranularity = actualRecord && typeof actualRecord.granularity === 'string'
|
|
597
|
+
? actualRecord.granularity
|
|
598
|
+
: undefined;
|
|
599
|
+
if (!actualGranularity) {
|
|
600
|
+
issues.push({ code: 'INVALID_FIELD', path: 'granularity', message: 'Evaluation requires a granularity.' });
|
|
601
|
+
}
|
|
602
|
+
const actualScope = actualRecord ? actualRecord.scope : undefined;
|
|
603
|
+
if (!isScopeValue(actualScope)) {
|
|
604
|
+
issues.push({ code: 'INVALID_FIELD', path: 'scope', message: 'Evaluation requires a valid scope.' });
|
|
605
|
+
}
|
|
606
|
+
const actualPeriod = actualRecord ? actualRecord.period : undefined;
|
|
607
|
+
if (!isPeriodValue(actualPeriod)) {
|
|
608
|
+
issues.push({ code: 'INVALID_FIELD', path: 'period', message: 'Evaluation requires a valid period.' });
|
|
609
|
+
}
|
|
610
|
+
const definitionScope = isRecord(variable) && isScopeValue(variable.scope)
|
|
611
|
+
? variable.scope
|
|
612
|
+
: undefined;
|
|
613
|
+
if (definitionScope && isScopeValue(actualScope) && !scopeWithin(actualScope, definitionScope)) {
|
|
614
|
+
issues.push({
|
|
615
|
+
code: 'EVALUATION_SCOPE_MISMATCH',
|
|
616
|
+
path: 'scope',
|
|
617
|
+
message: 'Evaluation scope must stay within the Variable subject and validity scope.'
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (definitionScope && isPeriodValue(actualPeriod) && !periodWithinScope(actualPeriod, definitionScope)) {
|
|
621
|
+
issues.push({
|
|
622
|
+
code: 'EVALUATION_PERIOD_MISMATCH',
|
|
623
|
+
path: 'period',
|
|
624
|
+
message: 'Evaluation period must stay within the Variable validity scope.'
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (actualRevision && variableId && variableRevision &&
|
|
628
|
+
(actualRevision.id !== variableId || actualRevision.revision !== variableRevision)) {
|
|
629
|
+
issues.push({
|
|
630
|
+
code: 'EVALUATION_DEFINITION_VERSION_MISMATCH',
|
|
631
|
+
path: 'variableRef',
|
|
632
|
+
message: `Evaluation uses ${actualRevision.id}@${actualRevision.revision}, expected ${variableId}@${variableRevision}.`
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (variableId && variableRevision && actualRevision && variableUnit !== actualUnit) {
|
|
636
|
+
issues.push({
|
|
637
|
+
code: 'EVALUATION_UNIT_MISMATCH',
|
|
638
|
+
path: 'unit',
|
|
639
|
+
message: `Evaluation unit ${JSON.stringify(actualUnit)} does not match variable unit ${JSON.stringify(variableUnit)}.`
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (variableAggregation && actualAggregation && variableAggregation !== actualAggregation) {
|
|
643
|
+
issues.push({
|
|
644
|
+
code: 'EVALUATION_AGGREGATION_MISMATCH',
|
|
645
|
+
path: 'aggregation',
|
|
646
|
+
message: `Evaluation aggregation ${actualAggregation} does not match variable aggregation ${variableAggregation}.`
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (variableGranularity && actualGranularity && variableGranularity !== actualGranularity) {
|
|
650
|
+
issues.push({
|
|
651
|
+
code: 'EVALUATION_GRANULARITY_MISMATCH',
|
|
652
|
+
path: 'granularity',
|
|
653
|
+
message: `Evaluation granularity ${actualGranularity} does not match variable granularity ${variableGranularity}.`
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const expectedRecord = isRecord(expected) ? expected : undefined;
|
|
657
|
+
if (!expectedRecord) {
|
|
658
|
+
issues.push({ code: 'INVALID_FIELD', path: 'expected', message: 'Evaluation expectations must be an object.' });
|
|
659
|
+
}
|
|
660
|
+
const expectedScope = expectedRecord?.scope;
|
|
661
|
+
if (expectedScope !== undefined && !isScopeValue(expectedScope)) {
|
|
662
|
+
issues.push({ code: 'INVALID_FIELD', path: 'expected.scope', message: 'Expected evaluation scope is invalid.' });
|
|
663
|
+
}
|
|
664
|
+
const expectedPeriod = expectedRecord?.period;
|
|
665
|
+
if (expectedPeriod !== undefined && !isPeriodValue(expectedPeriod)) {
|
|
666
|
+
issues.push({ code: 'INVALID_FIELD', path: 'expected.period', message: 'Expected evaluation period is invalid.' });
|
|
667
|
+
}
|
|
668
|
+
if (isScopeValue(expectedScope) && isScopeValue(actualScope) && !sameScope(expectedScope, actualScope)) {
|
|
669
|
+
issues.push({
|
|
670
|
+
code: 'EVALUATION_SCOPE_MISMATCH',
|
|
671
|
+
path: 'scope',
|
|
672
|
+
message: 'Evaluation scope does not match the expected subject or validity scope.'
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (isPeriodValue(expectedPeriod) && isPeriodValue(actualPeriod) && !samePeriod(expectedPeriod, actualPeriod)) {
|
|
676
|
+
issues.push({
|
|
677
|
+
code: 'EVALUATION_PERIOD_MISMATCH',
|
|
678
|
+
path: 'period',
|
|
679
|
+
message: 'Evaluation period does not match the expected evaluation period.'
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
valid: issues.length === 0,
|
|
684
|
+
status: issues.length === 0 ? 'valid' : 'invalid',
|
|
685
|
+
issues
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Relation declarations carry no achievement, causality, accuracy, or truth
|
|
690
|
+
* inference. This intentionally returns no derived conclusions; callers must
|
|
691
|
+
* use explicit observations, evaluations, or decisions for those claims.
|
|
692
|
+
*/
|
|
693
|
+
export function inferFoundationConclusions(reference) {
|
|
694
|
+
validateFoundationRelation(reference);
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
function validateCommonDefinition(definition, use, issues) {
|
|
698
|
+
requiredString(definition, 'id', issues);
|
|
699
|
+
requiredString(definition, 'revision', issues);
|
|
700
|
+
requiredString(definition, 'meaning', issues);
|
|
701
|
+
requiredEnum(definition, 'adoptionState', isFoundationAdoptionState, issues);
|
|
702
|
+
requiredEnum(definition, 'storage', isFoundationStorage, issues);
|
|
703
|
+
if (definition.epistemicState !== undefined && !isFoundationEpistemicState(definition.epistemicState)) {
|
|
704
|
+
issues.push({ code: 'INVALID_FIELD', path: 'epistemicState', message: `Unknown epistemic state ${String(definition.epistemicState)}.` });
|
|
705
|
+
}
|
|
706
|
+
if (!Array.isArray(definition.authorizedUses)) {
|
|
707
|
+
issues.push({ code: 'INVALID_FIELD', path: 'authorizedUses', message: 'authorizedUses must be an array of registered uses.' });
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
forEachArrayEntry(definition.authorizedUses, (value, index) => {
|
|
711
|
+
if (!isFoundationUse(value)) {
|
|
712
|
+
issues.push({ code: 'INVALID_FIELD', path: `authorizedUses[${index}]`, message: `Unknown authorized use ${String(value)}.` });
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
if (use && (!Array.isArray(definition.authorizedUses) || !definition.authorizedUses.includes(use))) {
|
|
717
|
+
issues.push({
|
|
718
|
+
code: 'UNAUTHORIZED_USE',
|
|
719
|
+
path: 'authorizedUses',
|
|
720
|
+
message: `Definition ${String(definition.id)} is not authorized for ${use} use.`
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
validateAcl(definition.acl, issues);
|
|
724
|
+
validateScope(definition.scope, 'scope', issues, use !== 'draft');
|
|
725
|
+
validateProvenance(definition.provenance, issues);
|
|
726
|
+
if (definition.adoptionState === 'retired' && use && use !== 'draft') {
|
|
727
|
+
issues.push({ code: 'UNAUTHORIZED_USE', path: 'adoptionState', message: 'A retired definition cannot be used for judgment, evaluation, or execution.' });
|
|
728
|
+
}
|
|
729
|
+
if (definition.epistemicState === 'refuted' && use && use !== 'draft') {
|
|
730
|
+
issues.push({ code: 'UNAUTHORIZED_USE', path: 'epistemicState', message: 'A refuted definition cannot be used for judgment, evaluation, or execution.' });
|
|
731
|
+
}
|
|
732
|
+
if (use === 'execution' && definition.adoptionState !== 'approved') {
|
|
733
|
+
issues.push({ code: 'UNAUTHORIZED_USE', path: 'adoptionState', message: 'Execution requires an approved definition.' });
|
|
734
|
+
}
|
|
735
|
+
if (definition.storage === 'ontology' && definition.adoptionState === 'draft' && use === 'execution') {
|
|
736
|
+
issues.push({ code: 'UNAUTHORIZED_USE', path: 'adoptionState', message: 'A draft ontology definition cannot be used for execution.' });
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function validateTypeSpecificDefinition(definition, type, use, issues) {
|
|
740
|
+
switch (type) {
|
|
741
|
+
case 'objective':
|
|
742
|
+
if (isEmptyRequiredArray(definition.beneficiaryIds))
|
|
743
|
+
issues.push(missing('beneficiaryIds'));
|
|
744
|
+
if (!isNonEmptyString(definition.desiredState))
|
|
745
|
+
issues.push(missing('desiredState'));
|
|
746
|
+
if (isEmptyRequiredArray(definition.criteria))
|
|
747
|
+
issues.push(missing('criteria'));
|
|
748
|
+
validatePeriod(definition.evaluationPeriod, 'evaluationPeriod', issues);
|
|
749
|
+
break;
|
|
750
|
+
case 'variable':
|
|
751
|
+
if (!isNonEmptyString(definition.subject))
|
|
752
|
+
issues.push(missing('subject'));
|
|
753
|
+
if (!isFoundationValueKind(definition.valueKind))
|
|
754
|
+
issues.push(missing('valueKind'));
|
|
755
|
+
if (definition.valueKind === 'number' && !isNonEmptyString(definition.unit))
|
|
756
|
+
issues.push(missing('unit'));
|
|
757
|
+
if (!isFoundationAggregation(definition.aggregation))
|
|
758
|
+
issues.push(missing('aggregation'));
|
|
759
|
+
if (!isNonEmptyString(definition.granularity))
|
|
760
|
+
issues.push(missing('granularity'));
|
|
761
|
+
if (!isNonEmptyString(definition.measurementMethod))
|
|
762
|
+
issues.push(missing('measurementMethod'));
|
|
763
|
+
break;
|
|
764
|
+
case 'model':
|
|
765
|
+
if (!isFoundationEpistemicState(definition.epistemicState))
|
|
766
|
+
issues.push(missing('epistemicState'));
|
|
767
|
+
if (isEmptyRequiredArray(definition.inputVariableRefs))
|
|
768
|
+
issues.push(missing('inputVariableRefs'));
|
|
769
|
+
if (isEmptyRequiredArray(definition.outputVariableRefs))
|
|
770
|
+
issues.push(missing('outputVariableRefs'));
|
|
771
|
+
if (!isScopeValue(definition.applicability))
|
|
772
|
+
issues.push(missing('applicability'));
|
|
773
|
+
if (!isNonEmptyString(definition.relationship))
|
|
774
|
+
issues.push(missing('relationship'));
|
|
775
|
+
if (!isNonEmptyString(definition.uncertainty))
|
|
776
|
+
issues.push(missing('uncertainty'));
|
|
777
|
+
if (!isFoundationValidationState(definition.validationState))
|
|
778
|
+
issues.push(missing('validationState'));
|
|
779
|
+
if (definition.epistemicState === 'refuted' || definition.validationState === 'refuted') {
|
|
780
|
+
issues.push({ code: 'UNVERIFIED_MODEL', path: 'validationState', message: 'A refuted Model cannot be used for judgment, evaluation, or execution.' });
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case 'constraint':
|
|
784
|
+
if (!isNonEmptyString(definition.condition))
|
|
785
|
+
issues.push(missing('condition'));
|
|
786
|
+
if (isEmptyRequiredArray(definition.appliesTo))
|
|
787
|
+
issues.push(missing('appliesTo'));
|
|
788
|
+
if (definition.exceptions === undefined)
|
|
789
|
+
issues.push(missing('exceptions'));
|
|
790
|
+
if (isEmptyRequiredArray(definition.adoptionBasis))
|
|
791
|
+
issues.push(missing('adoptionBasis'));
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
function validateDefinitionShape(definition, type, issues, requireEvaluationCriteria) {
|
|
796
|
+
switch (type) {
|
|
797
|
+
case 'objective':
|
|
798
|
+
optionalStringArray(definition.beneficiaryIds, 'beneficiaryIds', issues);
|
|
799
|
+
optionalString(definition.desiredState, 'desiredState', issues, !requireEvaluationCriteria);
|
|
800
|
+
optionalArray(definition.criteria, 'criteria', issues);
|
|
801
|
+
if (Array.isArray(definition.criteria)) {
|
|
802
|
+
forEachArrayEntry(definition.criteria, (criterion, index) => validateCriterion(criterion, `criteria[${index}]`, issues, requireEvaluationCriteria));
|
|
803
|
+
}
|
|
804
|
+
if (definition.evaluationPeriod !== undefined)
|
|
805
|
+
validatePeriod(definition.evaluationPeriod, 'evaluationPeriod', issues);
|
|
806
|
+
optionalString(definition.accountableId, 'accountableId', issues, !requireEvaluationCriteria);
|
|
807
|
+
break;
|
|
808
|
+
case 'variable':
|
|
809
|
+
optionalString(definition.subject, 'subject', issues, !requireEvaluationCriteria);
|
|
810
|
+
optionalEnum(definition.valueKind, 'valueKind', isFoundationValueKind, issues);
|
|
811
|
+
optionalString(definition.unit, 'unit', issues, !requireEvaluationCriteria);
|
|
812
|
+
optionalEnum(definition.aggregation, 'aggregation', isFoundationAggregation, issues);
|
|
813
|
+
optionalString(definition.granularity, 'granularity', issues, !requireEvaluationCriteria);
|
|
814
|
+
optionalString(definition.measurementMethod, 'measurementMethod', issues, !requireEvaluationCriteria);
|
|
815
|
+
break;
|
|
816
|
+
case 'model':
|
|
817
|
+
if (definition.epistemicState === undefined)
|
|
818
|
+
issues.push(missing('epistemicState'));
|
|
819
|
+
optionalRevisionArray(definition.inputVariableRefs, 'inputVariableRefs', 'variable', issues);
|
|
820
|
+
optionalRevisionArray(definition.outputVariableRefs, 'outputVariableRefs', 'variable', issues);
|
|
821
|
+
if (definition.applicability !== undefined)
|
|
822
|
+
validateScope(definition.applicability, 'applicability', issues, false);
|
|
823
|
+
optionalString(definition.relationship, 'relationship', issues, !requireEvaluationCriteria);
|
|
824
|
+
optionalString(definition.uncertainty, 'uncertainty', issues, !requireEvaluationCriteria);
|
|
825
|
+
optionalEnum(definition.validationState, 'validationState', isFoundationValidationState, issues);
|
|
826
|
+
break;
|
|
827
|
+
case 'constraint':
|
|
828
|
+
optionalString(definition.condition, 'condition', issues, !requireEvaluationCriteria);
|
|
829
|
+
optionalStringArray(definition.appliesTo, 'appliesTo', issues);
|
|
830
|
+
optionalArray(definition.exceptions, 'exceptions', issues);
|
|
831
|
+
if (Array.isArray(definition.exceptions)) {
|
|
832
|
+
forEachArrayEntry(definition.exceptions, (exception, index) => {
|
|
833
|
+
if (!isRecord(exception)) {
|
|
834
|
+
issues.push({ code: 'INVALID_FIELD', path: `exceptions[${index}]`, message: 'Constraint exceptions must be objects.' });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
validateDecisionValue(exception.decisionRef, `exceptions[${index}].decisionRef`, issues);
|
|
838
|
+
validateScope(exception.scope, `exceptions[${index}].scope`, issues, true);
|
|
839
|
+
if (isScopeValue(exception.scope) && isScopeValue(definition.scope)
|
|
840
|
+
&& !scopeWithin(exception.scope, definition.scope)) {
|
|
841
|
+
issues.push({
|
|
842
|
+
code: 'INVALID_FIELD',
|
|
843
|
+
path: `exceptions[${index}].scope`,
|
|
844
|
+
message: 'Constraint exception scope must be non-empty and remain within the parent Constraint scope.'
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
optionalArray(definition.adoptionBasis, 'adoptionBasis', issues);
|
|
850
|
+
if (Array.isArray(definition.adoptionBasis)) {
|
|
851
|
+
forEachArrayEntry(definition.adoptionBasis, (decisionRef, index) => validateDecisionValue(decisionRef, `adoptionBasis[${index}]`, issues));
|
|
852
|
+
}
|
|
853
|
+
break;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function validateCriterion(criterion, path, issues, requireTarget) {
|
|
857
|
+
if (!isRecord(criterion)) {
|
|
858
|
+
issues.push({ code: 'INVALID_FIELD', path, message: 'Objective criteria must be objects.' });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
if (criterion.variableRef === undefined) {
|
|
862
|
+
if (requireTarget) {
|
|
863
|
+
issues.push({ code: 'MISSING_FIELD', path: `${path}.variableRef`, message: 'Objective criteria require a versioned Variable reference.' });
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
else {
|
|
867
|
+
validateRevisionValue(criterion.variableRef, 'variable', `${path}.variableRef`, issues);
|
|
868
|
+
}
|
|
869
|
+
if (criterion.operator === undefined) {
|
|
870
|
+
if (requireTarget) {
|
|
871
|
+
issues.push({ code: 'MISSING_FIELD', path: `${path}.operator`, message: 'Objective criteria require an operator.' });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else if (!isFoundationOperator(criterion.operator)) {
|
|
875
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}.operator`, message: `Unsupported Objective criterion operator ${String(criterion.operator)}.` });
|
|
876
|
+
}
|
|
877
|
+
if (criterion.target === undefined && requireTarget) {
|
|
878
|
+
issues.push({ code: 'MISSING_FIELD', path: `${path}.target`, message: 'Objective criterion target is required for threshold evaluation.' });
|
|
879
|
+
}
|
|
880
|
+
else if (criterion.target !== undefined && !isScalar(criterion.target)) {
|
|
881
|
+
issues.push({ code: 'INVALID_FIELD', path: `${path}.target`, message: 'Objective criterion targets must be scalar values.' });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function validatePeriod(period, path, issues) {
|
|
885
|
+
if (!isPeriodValue(period)) {
|
|
886
|
+
issues.push({ code: 'INVALID_FIELD', path, message: 'Period requires ISO from/until values with until after from.' });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function validateScope(scope, path, issues, requireSubject) {
|
|
890
|
+
if (!isScopeValue(scope)) {
|
|
891
|
+
issues.push({ code: 'INVALID_FIELD', path, message: 'Scope requires subjectIds and validFrom.' });
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (requireSubject && scope.subjectIds.length === 0) {
|
|
895
|
+
issues.push({ code: 'MISSING_FIELD', path: `${path}.subjectIds`, message: 'Scope requires at least one subject.' });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function isValidPeriod(period) {
|
|
899
|
+
const from = parseStrictRfc3339(period.from);
|
|
900
|
+
const until = parseStrictRfc3339(period.until);
|
|
901
|
+
return from !== undefined && until !== undefined && until > from;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Parse only RFC3339 date-time values and validate calendar components before
|
|
905
|
+
* converting them. Date.parse accepts values such as 2026-02-30 by silently
|
|
906
|
+
* normalizing them into March, which would make a scope or evaluation period
|
|
907
|
+
* mean something other than what its author wrote.
|
|
908
|
+
*/
|
|
909
|
+
function parseStrictRfc3339(value) {
|
|
910
|
+
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);
|
|
911
|
+
if (!match)
|
|
912
|
+
return undefined;
|
|
913
|
+
const year = Number(match[1]);
|
|
914
|
+
const month = Number(match[2]);
|
|
915
|
+
const day = Number(match[3]);
|
|
916
|
+
const hour = Number(match[4]);
|
|
917
|
+
const minute = Number(match[5]);
|
|
918
|
+
const second = Number(match[6]);
|
|
919
|
+
const fraction = match[7] ?? '';
|
|
920
|
+
const zone = match[8];
|
|
921
|
+
if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
|
|
922
|
+
return undefined;
|
|
923
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
924
|
+
return undefined;
|
|
925
|
+
let offsetMinutes = 0;
|
|
926
|
+
if (zone !== 'Z') {
|
|
927
|
+
const sign = zone[0] === '-' ? -1 : 1;
|
|
928
|
+
const offsetHour = Number(zone.slice(1, 3));
|
|
929
|
+
const offsetMinute = Number(zone.slice(4, 6));
|
|
930
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
931
|
+
return undefined;
|
|
932
|
+
offsetMinutes = sign * (offsetHour * 60 + offsetMinute);
|
|
933
|
+
}
|
|
934
|
+
// Date.UTC treats years 0..99 as 1900..1999; the contract's four-digit
|
|
935
|
+
// year range is handled explicitly so the conversion remains unambiguous.
|
|
936
|
+
const milliseconds = Number((fraction + '000').slice(0, 3));
|
|
937
|
+
const timestamp = Date.UTC(year, month - 1, day, hour, minute, second, milliseconds)
|
|
938
|
+
- offsetMinutes * 60_000;
|
|
939
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
940
|
+
}
|
|
941
|
+
function daysInMonth(year, month) {
|
|
942
|
+
if (month === 2) {
|
|
943
|
+
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
|
|
944
|
+
}
|
|
945
|
+
return [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
946
|
+
}
|
|
947
|
+
function sameScope(left, right) {
|
|
948
|
+
return [...left.subjectIds].sort().join('\u0000') === [...right.subjectIds].sort().join('\u0000')
|
|
949
|
+
&& left.validFrom === right.validFrom
|
|
950
|
+
&& left.validUntil === right.validUntil;
|
|
951
|
+
}
|
|
952
|
+
function samePeriod(left, right) {
|
|
953
|
+
return left.from === right.from && left.until === right.until;
|
|
954
|
+
}
|
|
955
|
+
function scopeWithin(inner, outer) {
|
|
956
|
+
const outerSubjects = new Set(outer.subjectIds);
|
|
957
|
+
const subjectsContained = inner.subjectIds.every((subjectId) => outerSubjects.has(subjectId));
|
|
958
|
+
if (!subjectsContained)
|
|
959
|
+
return false;
|
|
960
|
+
const innerFrom = parseStrictRfc3339(inner.validFrom);
|
|
961
|
+
const outerFrom = parseStrictRfc3339(outer.validFrom);
|
|
962
|
+
if (innerFrom === undefined || outerFrom === undefined || innerFrom < outerFrom)
|
|
963
|
+
return false;
|
|
964
|
+
if (inner.validUntil === undefined)
|
|
965
|
+
return outer.validUntil === undefined;
|
|
966
|
+
const innerUntil = parseStrictRfc3339(inner.validUntil);
|
|
967
|
+
if (innerUntil === undefined)
|
|
968
|
+
return false;
|
|
969
|
+
if (outer.validUntil === undefined)
|
|
970
|
+
return true;
|
|
971
|
+
const outerUntil = parseStrictRfc3339(outer.validUntil);
|
|
972
|
+
return outerUntil !== undefined && innerUntil <= outerUntil;
|
|
973
|
+
}
|
|
974
|
+
function periodWithinScope(period, scope) {
|
|
975
|
+
const periodFrom = parseStrictRfc3339(period.from);
|
|
976
|
+
const periodUntil = parseStrictRfc3339(period.until);
|
|
977
|
+
const scopeFrom = parseStrictRfc3339(scope.validFrom);
|
|
978
|
+
if (periodFrom === undefined || periodUntil === undefined || scopeFrom === undefined || periodFrom < scopeFrom) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
if (scope.validUntil === undefined)
|
|
982
|
+
return true;
|
|
983
|
+
const scopeUntil = parseStrictRfc3339(scope.validUntil);
|
|
984
|
+
return scopeUntil !== undefined && periodUntil <= scopeUntil;
|
|
985
|
+
}
|
|
986
|
+
function isRevisionEndpoint(type) {
|
|
987
|
+
return type === 'objective' || type === 'variable' || type === 'model' || type === 'constraint' || type === 'decision';
|
|
988
|
+
}
|
|
989
|
+
function missing(path) {
|
|
990
|
+
return { code: 'MISSING_FIELD', path, message: `${path} is required for this use.` };
|
|
991
|
+
}
|
|
992
|
+
function deepFreeze(value) {
|
|
993
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
994
|
+
Object.freeze(value);
|
|
995
|
+
for (const child of Object.values(value)) {
|
|
996
|
+
deepFreeze(child);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return value;
|
|
1000
|
+
}
|