@voce-engine/core 0.1.0-rc.1
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/LICENSE +1 -0
- package/README.md +9 -0
- package/dist/canonical.d.ts +4 -0
- package/dist/canonical.js +30 -0
- package/dist/evidence.d.ts +31 -0
- package/dist/evidence.js +1053 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +608 -0
- package/dist/m4.d.ts +157 -0
- package/dist/m4.js +2250 -0
- package/dist/m5.d.ts +183 -0
- package/dist/m5.js +2433 -0
- package/dist/m6.d.ts +182 -0
- package/dist/m6.js +1183 -0
- package/package.json +50 -0
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import { BINDING_DECISION_SCHEMA_VERSION, CONFLICT_SCHEMA_VERSION, DECISION_TRACE_SCHEMA_VERSION, OBSERVATION_DECISION_SCHEMA_VERSION, OBSERVATION_SCHEMA_VERSION, ONTOLOGY_FACT_SCHEMA_VERSION, ONTOLOGY_INSTANCE_SCHEMA_VERSION, QUESTION_SCHEMA_VERSION, REFERENCE_INTERPRETER_RESULT_SCHEMA_VERSION, RESOLVER_RESULT_SCHEMA_VERSION, SOURCE_BINDING_SCHEMA_VERSION, UNRESOLVED_ITEM_SCHEMA_VERSION, } from '@voce-engine/contracts';
|
|
2
|
+
import { canonicalize, sha256 } from './canonical.js';
|
|
3
|
+
export const EVIDENCE_RESOLVER_VERSION = 'voce.evidence-source-resolver/v1alpha1';
|
|
4
|
+
export const MANUAL_REFERENCE_INTERPRETER_VERSION = 'voce.manual-reference-interpreter/v1alpha1';
|
|
5
|
+
export const FIXTURE_REFERENCE_INTERPRETER_VERSION = 'voce.fixture-reference-interpreter/v1alpha1';
|
|
6
|
+
export const FIXED_DECISION_TIME = '1970-01-01T00:00:00.000Z';
|
|
7
|
+
function jsonReady(value) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
11
|
+
return value;
|
|
12
|
+
if (typeof value === 'number') {
|
|
13
|
+
if (!Number.isFinite(value))
|
|
14
|
+
throw new Error('JSON_VALUE_INVALID');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return value.map((item) => jsonReady(item) ?? null);
|
|
19
|
+
if (typeof value === 'object') {
|
|
20
|
+
const object = {};
|
|
21
|
+
for (const [key, item] of Object.entries(value)) {
|
|
22
|
+
const ready = jsonReady(item);
|
|
23
|
+
if (ready !== undefined)
|
|
24
|
+
object[key] = ready;
|
|
25
|
+
}
|
|
26
|
+
return object;
|
|
27
|
+
}
|
|
28
|
+
throw new Error('JSON_VALUE_INVALID');
|
|
29
|
+
}
|
|
30
|
+
function clone(value) {
|
|
31
|
+
const ready = jsonReady(value);
|
|
32
|
+
return (ready === undefined ? undefined : ready);
|
|
33
|
+
}
|
|
34
|
+
function compareCodeUnits(left, right) {
|
|
35
|
+
const length = Math.min(left.length, right.length);
|
|
36
|
+
for (let index = 0; index < length; index += 1) {
|
|
37
|
+
const difference = left.charCodeAt(index) - right.charCodeAt(index);
|
|
38
|
+
if (difference !== 0)
|
|
39
|
+
return difference;
|
|
40
|
+
}
|
|
41
|
+
return left.length - right.length;
|
|
42
|
+
}
|
|
43
|
+
function canonicalValue(value) {
|
|
44
|
+
return canonicalize(jsonReady(value) ?? null);
|
|
45
|
+
}
|
|
46
|
+
function stableUnique(values) {
|
|
47
|
+
return [...new Set(values)].sort(compareCodeUnits);
|
|
48
|
+
}
|
|
49
|
+
function sortedBy(values, key) {
|
|
50
|
+
return values.map((value) => clone(value)).sort((left, right) => compareCodeUnits(key(left), key(right)) || compareCodeUnits(canonicalValue(left), canonicalValue(right)));
|
|
51
|
+
}
|
|
52
|
+
function sortedStrings(values) {
|
|
53
|
+
return [...(values ?? [])].sort(compareCodeUnits);
|
|
54
|
+
}
|
|
55
|
+
function provenanceProjection(provenance) {
|
|
56
|
+
return {
|
|
57
|
+
source: provenance.source,
|
|
58
|
+
sourceIds: sortedStrings(provenance.sourceIds),
|
|
59
|
+
createdBy: provenance.createdBy,
|
|
60
|
+
createdAt: provenance.createdAt,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function regionProjection(region) {
|
|
64
|
+
if (region.kind === 'rectangle')
|
|
65
|
+
return { kind: region.kind, x: region.x, y: region.y, width: region.width, height: region.height };
|
|
66
|
+
if (region.kind === 'polygon')
|
|
67
|
+
return { kind: region.kind, points: region.points.map((point) => ({ x: point.x, y: point.y })) };
|
|
68
|
+
return { kind: region.kind, maskArtifactId: region.maskArtifactId };
|
|
69
|
+
}
|
|
70
|
+
function analyzerProjection(analyzer) {
|
|
71
|
+
const result = { schemaVersion: analyzer.schemaVersion, adapterId: analyzer.adapterId, model: analyzer.model, promptVersion: analyzer.promptVersion };
|
|
72
|
+
if (analyzer.fixtureId !== undefined)
|
|
73
|
+
result.fixtureId = analyzer.fixtureId;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function observationProjection(observation) {
|
|
77
|
+
const result = {
|
|
78
|
+
schemaVersion: observation.schemaVersion,
|
|
79
|
+
id: observation.id,
|
|
80
|
+
assetId: observation.assetId,
|
|
81
|
+
ontologyPath: observation.ontologyPath,
|
|
82
|
+
value: clone(observation.value),
|
|
83
|
+
provenance: provenanceProjection(observation.provenance),
|
|
84
|
+
warnings: sortedStrings(observation.warnings),
|
|
85
|
+
};
|
|
86
|
+
if (observation.confidence !== undefined)
|
|
87
|
+
result.confidence = observation.confidence;
|
|
88
|
+
if (observation.evidenceRegion !== undefined)
|
|
89
|
+
result.evidenceRegion = regionProjection(observation.evidenceRegion);
|
|
90
|
+
if (observation.analyzer !== undefined)
|
|
91
|
+
result.analyzer = analyzerProjection(observation.analyzer);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
export function computeObservationContentHash(observation) {
|
|
95
|
+
return sha256(observationProjection(observation));
|
|
96
|
+
}
|
|
97
|
+
export function createObservation(input) {
|
|
98
|
+
const observation = clone({ ...input, contentHash: '' });
|
|
99
|
+
observation.contentHash = computeObservationContentHash(observation);
|
|
100
|
+
return clone(observation);
|
|
101
|
+
}
|
|
102
|
+
function bindingProjection(binding) {
|
|
103
|
+
return {
|
|
104
|
+
schemaVersion: binding.schemaVersion,
|
|
105
|
+
id: binding.id,
|
|
106
|
+
targetPath: binding.targetPath,
|
|
107
|
+
observationIds: sortedStrings(binding.observationIds),
|
|
108
|
+
relation: binding.relation,
|
|
109
|
+
priority: binding.priority,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function computeSourceBindingContentHash(binding) {
|
|
113
|
+
return sha256(bindingProjection(binding));
|
|
114
|
+
}
|
|
115
|
+
export const computeBindingHash = computeSourceBindingContentHash;
|
|
116
|
+
export const computeSourceBindingHash = computeSourceBindingContentHash;
|
|
117
|
+
export const computeObservationHash = computeObservationContentHash;
|
|
118
|
+
export function createSourceBinding(input) {
|
|
119
|
+
const binding = clone({ ...input, contentHash: '' });
|
|
120
|
+
binding.observationIds = sortedStrings(binding.observationIds);
|
|
121
|
+
binding.contentHash = computeSourceBindingContentHash(binding);
|
|
122
|
+
return clone(binding);
|
|
123
|
+
}
|
|
124
|
+
function observationDecisionProjection(decision) {
|
|
125
|
+
const result = {
|
|
126
|
+
schemaVersion: decision.schemaVersion,
|
|
127
|
+
decisionId: decision.decisionId,
|
|
128
|
+
observationId: decision.observationId,
|
|
129
|
+
observationHash: decision.observationHash,
|
|
130
|
+
contextHash: decision.contextHash,
|
|
131
|
+
status: decision.status,
|
|
132
|
+
authority: decision.authority,
|
|
133
|
+
decidedBy: decision.decidedBy,
|
|
134
|
+
decidedAt: decision.decidedAt,
|
|
135
|
+
reasonCode: decision.reasonCode,
|
|
136
|
+
};
|
|
137
|
+
if (decision.policyVersion !== undefined)
|
|
138
|
+
result.policyVersion = decision.policyVersion;
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
export function computeObservationDecisionHash(decision) {
|
|
142
|
+
return sha256(observationDecisionProjection(decision));
|
|
143
|
+
}
|
|
144
|
+
export function createObservationDecision(input) {
|
|
145
|
+
const decision = clone({ ...input, decisionHash: '' });
|
|
146
|
+
decision.decisionHash = computeObservationDecisionHash(decision);
|
|
147
|
+
return clone(decision);
|
|
148
|
+
}
|
|
149
|
+
function bindingDecisionProjection(decision) {
|
|
150
|
+
const result = {
|
|
151
|
+
schemaVersion: decision.schemaVersion,
|
|
152
|
+
decisionId: decision.decisionId,
|
|
153
|
+
bindingId: decision.bindingId,
|
|
154
|
+
bindingHash: decision.bindingHash,
|
|
155
|
+
contextHash: decision.contextHash,
|
|
156
|
+
status: decision.status,
|
|
157
|
+
authority: decision.authority,
|
|
158
|
+
decidedBy: decision.decidedBy,
|
|
159
|
+
reasonCode: decision.reasonCode,
|
|
160
|
+
};
|
|
161
|
+
if (decision.policyVersion !== undefined)
|
|
162
|
+
result.policyVersion = decision.policyVersion;
|
|
163
|
+
if (decision.decidedAt !== undefined)
|
|
164
|
+
result.decidedAt = decision.decidedAt;
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
export function computeBindingDecisionHash(decision) {
|
|
168
|
+
return sha256(bindingDecisionProjection(decision));
|
|
169
|
+
}
|
|
170
|
+
export function createBindingDecision(input) {
|
|
171
|
+
const decision = clone({ ...input, decisionHash: '' });
|
|
172
|
+
if (decision.decidedAt === undefined)
|
|
173
|
+
decision.decidedAt = FIXED_DECISION_TIME;
|
|
174
|
+
decision.decisionHash = computeBindingDecisionHash(decision);
|
|
175
|
+
return clone(decision);
|
|
176
|
+
}
|
|
177
|
+
function hashId(prefix, value) {
|
|
178
|
+
return `${prefix}-${sha256(jsonReady(value) ?? null).slice('sha256:'.length, 'sha256:'.length + 24)}`;
|
|
179
|
+
}
|
|
180
|
+
function question(value) {
|
|
181
|
+
const base = { schemaVersion: QUESTION_SCHEMA_VERSION, ...value, assetIds: sortedStrings(value.assetIds), relatedIds: sortedStrings(value.relatedIds) };
|
|
182
|
+
return { ...clone(base), id: hashId('question', base) };
|
|
183
|
+
}
|
|
184
|
+
function conflict(value) {
|
|
185
|
+
const base = { schemaVersion: CONFLICT_SCHEMA_VERSION, ...value, candidateIds: sortedStrings(value.candidateIds), relatedIds: sortedStrings(value.relatedIds) };
|
|
186
|
+
return { ...clone(base), id: hashId('conflict', base) };
|
|
187
|
+
}
|
|
188
|
+
function unresolved(value) {
|
|
189
|
+
const base = { schemaVersion: UNRESOLVED_ITEM_SCHEMA_VERSION, ...value, relatedIds: sortedStrings(value.relatedIds) };
|
|
190
|
+
return { ...clone(base), id: hashId('unresolved', base) };
|
|
191
|
+
}
|
|
192
|
+
function trace(value) {
|
|
193
|
+
const base = { schemaVersion: DECISION_TRACE_SCHEMA_VERSION, ...value, subjectIds: sortedStrings(value.subjectIds) };
|
|
194
|
+
return { ...clone(base), id: hashId('trace', base) };
|
|
195
|
+
}
|
|
196
|
+
/** A declared scope authorizes itself and descendants, never its ancestors. */
|
|
197
|
+
function scopeContainsPath(authorizedScopePath, candidatePath) {
|
|
198
|
+
return authorizedScopePath === candidatePath || candidatePath.startsWith(`${authorizedScopePath}.`);
|
|
199
|
+
}
|
|
200
|
+
/** Path comparison for explicit M3 relationships; no implicit field projection. */
|
|
201
|
+
function exactPathMatch(leftPath, rightPath) {
|
|
202
|
+
return leftPath === rightPath;
|
|
203
|
+
}
|
|
204
|
+
function excludedBy(plan, path) {
|
|
205
|
+
return plan.excludedScopes.some((excluded) => scopeContainsPath(excluded, path));
|
|
206
|
+
}
|
|
207
|
+
function scenarioPathAllowed(scenario, path) {
|
|
208
|
+
if (!scenario || scenario.interpretationScopes.length === 0)
|
|
209
|
+
return true;
|
|
210
|
+
return scenario.interpretationScopes.some((raw) => {
|
|
211
|
+
const value = raw;
|
|
212
|
+
const candidate = typeof value.ontologyPath === 'string' ? value.ontologyPath : typeof value.scopePath === 'string' ? value.scopePath : undefined;
|
|
213
|
+
return candidate ? scopeContainsPath(candidate, path) : false;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function planPathAllowed(plan, path, assetId, scenario) {
|
|
217
|
+
if (excludedBy(plan, path) || !scenarioPathAllowed(scenario, path))
|
|
218
|
+
return false;
|
|
219
|
+
return plan.scopes.some((scope) => {
|
|
220
|
+
if (!scopeContainsPath(scope.ontologyPath, path))
|
|
221
|
+
return false;
|
|
222
|
+
if (assetId === undefined)
|
|
223
|
+
return true;
|
|
224
|
+
return scope.assetIds.includes('*') || scope.assetIds.includes(assetId);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function normalizeScope(scope) {
|
|
228
|
+
return clone({ ...scope, assetIds: sortedStrings(scope.assetIds) });
|
|
229
|
+
}
|
|
230
|
+
function requestedScopePlanProjection(plan) {
|
|
231
|
+
return {
|
|
232
|
+
schemaVersion: plan.schemaVersion,
|
|
233
|
+
id: plan.id,
|
|
234
|
+
caseId: plan.caseId,
|
|
235
|
+
caseRevision: plan.caseRevision,
|
|
236
|
+
scopes: sortedBy(plan.scopes.map(normalizeScope), (item) => `${item.id}|${item.ontologyPath}`),
|
|
237
|
+
excludedScopes: sortedStrings(plan.excludedScopes),
|
|
238
|
+
questions: sortedBy(plan.questions, (item) => item.id),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export function computeRequestedScopePlanHash(plan) {
|
|
242
|
+
return sha256(requestedScopePlanProjection(plan));
|
|
243
|
+
}
|
|
244
|
+
function validateRequestedScopePlan(plan, caseId, caseRevision) {
|
|
245
|
+
const candidate = plan;
|
|
246
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate) || candidate.schemaVersion !== 'voce.requested-scope-plan/v1alpha1') {
|
|
247
|
+
return { code: 'REQUESTED_SCOPE_PLAN_SCHEMA_INVALID', message: 'RequestedScopePlan schemaVersion or object shape is not supported.' };
|
|
248
|
+
}
|
|
249
|
+
if (candidate.caseId !== caseId)
|
|
250
|
+
return { code: 'REQUESTED_SCOPE_PLAN_CASE_MISMATCH', message: 'RequestedScopePlan caseId does not match the current case input.' };
|
|
251
|
+
if (candidate.caseRevision !== caseRevision)
|
|
252
|
+
return { code: 'REQUESTED_SCOPE_PLAN_REVISION_MISMATCH', message: 'RequestedScopePlan caseRevision does not match the current case input.' };
|
|
253
|
+
if (!Array.isArray(candidate.scopes) || !Array.isArray(candidate.excludedScopes) || !Array.isArray(candidate.questions) || typeof candidate.planHash !== 'string') {
|
|
254
|
+
return { code: 'REQUESTED_SCOPE_PLAN_SCHEMA_INVALID', message: 'RequestedScopePlan required collections or planHash are missing or malformed.' };
|
|
255
|
+
}
|
|
256
|
+
if (candidate.scopes.some((scope) => !scope || typeof scope !== 'object' || Array.isArray(scope) || scope.schemaVersion !== 'voce.requested-scope/v1alpha1')) {
|
|
257
|
+
return { code: 'REQUESTED_SCOPE_PLAN_SCHEMA_INVALID', message: 'RequestedScopePlan contains a scope with an unsupported schemaVersion or shape.' };
|
|
258
|
+
}
|
|
259
|
+
if (candidate.questions.some((item) => !item || typeof item !== 'object' || Array.isArray(item) || item.schemaVersion !== QUESTION_SCHEMA_VERSION)) {
|
|
260
|
+
return { code: 'REQUESTED_SCOPE_PLAN_SCHEMA_INVALID', message: 'RequestedScopePlan contains a question with an unsupported schemaVersion or shape.' };
|
|
261
|
+
}
|
|
262
|
+
if (candidate.planHash !== computeRequestedScopePlanHash(plan)) {
|
|
263
|
+
return { code: 'REQUESTED_SCOPE_PLAN_HASH_MISMATCH', message: 'RequestedScopePlan planHash does not match its canonical semantic projection.' };
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
function orderedPlanView(plan) {
|
|
268
|
+
const raw = plan && typeof plan === 'object' ? plan : {};
|
|
269
|
+
return clone({
|
|
270
|
+
...raw,
|
|
271
|
+
scopes: sortedBy(Array.isArray(raw.scopes) ? raw.scopes.map(normalizeScope) : [], (item) => `${item.id}|${item.ontologyPath}`),
|
|
272
|
+
excludedScopes: sortedStrings(Array.isArray(raw.excludedScopes) ? raw.excludedScopes : []),
|
|
273
|
+
questions: sortedBy(Array.isArray(raw.questions) ? raw.questions : [], (item) => item.id),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function normalizePlan(plan) {
|
|
277
|
+
return orderedPlanView(plan);
|
|
278
|
+
}
|
|
279
|
+
function normalizeObservation(observation) {
|
|
280
|
+
const result = clone(observation);
|
|
281
|
+
result.warnings = sortedStrings(result.warnings);
|
|
282
|
+
result.provenance.sourceIds = sortedStrings(result.provenance.sourceIds);
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
function normalizeBinding(binding) {
|
|
286
|
+
const result = clone(binding);
|
|
287
|
+
result.observationIds = sortedStrings(result.observationIds);
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
function referenceInputProjection(input) {
|
|
291
|
+
const result = {
|
|
292
|
+
schemaVersion: input.schemaVersion,
|
|
293
|
+
caseId: input.caseId,
|
|
294
|
+
caseRevision: input.caseRevision,
|
|
295
|
+
contextHash: input.contextHash,
|
|
296
|
+
assets: sortedBy(input.assets, (item) => item.id).map((item) => clone(item)),
|
|
297
|
+
requestedScopePlan: orderedPlanView(input.requestedScopePlan),
|
|
298
|
+
};
|
|
299
|
+
if (input.effectiveScenario !== undefined)
|
|
300
|
+
result.effectiveScenario = clone(input.effectiveScenario);
|
|
301
|
+
if (input.manualDeclarations !== undefined)
|
|
302
|
+
result.manualDeclarations = sortedBy(input.manualDeclarations, (item) => item.id).map((item) => clone(item));
|
|
303
|
+
if (input.fixtureId !== undefined)
|
|
304
|
+
result.fixtureId = input.fixtureId;
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
function resolverInputProjection(input) {
|
|
308
|
+
const result = {
|
|
309
|
+
schemaVersion: input.schemaVersion,
|
|
310
|
+
caseId: input.caseId,
|
|
311
|
+
caseRevision: input.caseRevision,
|
|
312
|
+
contextHash: input.contextHash,
|
|
313
|
+
requestedScopePlan: orderedPlanView(input.requestedScopePlan),
|
|
314
|
+
changeIntents: sortedBy(input.changeIntents ?? [], (item) => item.id).map((item) => clone({ ...item, sourceHintIds: sortedStrings(item.sourceHintIds), provenance: provenanceProjection(item.provenance) })),
|
|
315
|
+
observations: sortedBy((input.observations ?? []).map(normalizeObservation), (item) => item.id).map((item) => clone(item)),
|
|
316
|
+
observationDecisions: sortedBy(input.observationDecisions ?? [], (item) => item.decisionId).map((item) => clone(item)),
|
|
317
|
+
sourceBindings: sortedBy((input.sourceBindings ?? []).map(normalizeBinding), (item) => item.id).map((item) => clone(item)),
|
|
318
|
+
bindingDecisions: sortedBy(input.bindingDecisions ?? [], (item) => item.decisionId).map((item) => clone(item)),
|
|
319
|
+
trustedMetadata: sortedBy(input.trustedMetadata ?? [], (item) => item.id).map((item) => clone({ ...item, provenance: provenanceProjection(item.provenance) })),
|
|
320
|
+
};
|
|
321
|
+
if (input.effectiveScenario !== undefined)
|
|
322
|
+
result.effectiveScenario = clone(input.effectiveScenario);
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
function inputHash(input) {
|
|
326
|
+
return sha256(input);
|
|
327
|
+
}
|
|
328
|
+
function finalReferenceResult(value) {
|
|
329
|
+
const withoutHash = clone({
|
|
330
|
+
...value,
|
|
331
|
+
observations: sortedBy(value.observations, (item) => item.id),
|
|
332
|
+
unresolvedItems: sortedBy(value.unresolvedItems, (item) => item.id),
|
|
333
|
+
warnings: sortedStrings(value.warnings),
|
|
334
|
+
});
|
|
335
|
+
delete withoutHash.resultHash;
|
|
336
|
+
return clone({ ...withoutHash, resultHash: sha256(jsonReady(withoutHash) ?? null) });
|
|
337
|
+
}
|
|
338
|
+
function makeManualObservation(declaration) {
|
|
339
|
+
const analyzer = { schemaVersion: 'voce.analyzer-metadata/v1alpha1', adapterId: MANUAL_REFERENCE_INTERPRETER_VERSION, model: 'manual', promptVersion: 'v1' };
|
|
340
|
+
return createObservation({
|
|
341
|
+
schemaVersion: OBSERVATION_SCHEMA_VERSION,
|
|
342
|
+
id: declaration.id,
|
|
343
|
+
assetId: declaration.assetId,
|
|
344
|
+
ontologyPath: declaration.ontologyPath,
|
|
345
|
+
value: clone(declaration.value),
|
|
346
|
+
...(declaration.confidence === undefined ? {} : { confidence: declaration.confidence }),
|
|
347
|
+
...(declaration.evidenceRegion === undefined ? {} : { evidenceRegion: clone(declaration.evidenceRegion) }),
|
|
348
|
+
provenance: clone(declaration.provenance),
|
|
349
|
+
analyzer,
|
|
350
|
+
warnings: sortedStrings(declaration.warnings),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function interpreterUnresolved(code, message, path, assetId, relatedIds = []) {
|
|
354
|
+
return unresolved({ code, message, status: 'unresolved', ...(path ? { targetPath: path } : {}), ...(assetId ? { assetId } : {}), relatedIds });
|
|
355
|
+
}
|
|
356
|
+
function blockedReferenceResult(input, interpreterId, analyzer, error) {
|
|
357
|
+
const plan = input.requestedScopePlan;
|
|
358
|
+
const planId = typeof plan?.id === 'string' ? plan.id : 'requested-scope-plan';
|
|
359
|
+
const item = interpreterUnresolved(error.code, error.message, undefined, undefined, [planId]);
|
|
360
|
+
return finalReferenceResult({
|
|
361
|
+
schemaVersion: REFERENCE_INTERPRETER_RESULT_SCHEMA_VERSION,
|
|
362
|
+
status: 'blocked',
|
|
363
|
+
interpreterId,
|
|
364
|
+
inputHash: inputHash(referenceInputProjection(input)),
|
|
365
|
+
observations: [],
|
|
366
|
+
unresolvedItems: [item],
|
|
367
|
+
warnings: [error.code],
|
|
368
|
+
analyzer,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
export class ManualReferenceInterpreter {
|
|
372
|
+
interpret(input) {
|
|
373
|
+
const safeInput = clone(input);
|
|
374
|
+
const analyzer = { schemaVersion: 'voce.analyzer-metadata/v1alpha1', adapterId: MANUAL_REFERENCE_INTERPRETER_VERSION, model: 'manual', promptVersion: 'v1' };
|
|
375
|
+
const planError = validateRequestedScopePlan(safeInput.requestedScopePlan, safeInput.caseId, safeInput.caseRevision);
|
|
376
|
+
if (planError)
|
|
377
|
+
return blockedReferenceResult(safeInput, MANUAL_REFERENCE_INTERPRETER_VERSION, analyzer, planError);
|
|
378
|
+
safeInput.requestedScopePlan = normalizePlan(safeInput.requestedScopePlan);
|
|
379
|
+
const resultUnresolved = [];
|
|
380
|
+
const resultWarnings = [];
|
|
381
|
+
const observations = [];
|
|
382
|
+
const declarations = [...(safeInput.manualDeclarations ?? [])].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
383
|
+
const assets = new Map(safeInput.assets.map((asset) => [asset.id, asset]));
|
|
384
|
+
const seenIds = new Set();
|
|
385
|
+
for (const declaration of declarations) {
|
|
386
|
+
const asset = assets.get(declaration.assetId);
|
|
387
|
+
if (!asset) {
|
|
388
|
+
resultUnresolved.push(interpreterUnresolved('ASSET_NOT_DECLARED', 'Manual observation refers to an asset outside the interpreter input.', declaration.ontologyPath, declaration.assetId, [declaration.id]));
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, declaration.ontologyPath, declaration.assetId, safeInput.effectiveScenario)) {
|
|
392
|
+
resultUnresolved.push(interpreterUnresolved('SCOPE_NOT_PERMITTED', 'Manual observation is outside the current RequestedScopePlan.', declaration.ontologyPath, declaration.assetId, [declaration.id]));
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (seenIds.has(declaration.id)) {
|
|
396
|
+
resultUnresolved.push(interpreterUnresolved('OBSERVATION_ID_DUPLICATE', 'Manual observations must use stable unique IDs.', declaration.ontologyPath, declaration.assetId, [declaration.id]));
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
seenIds.add(declaration.id);
|
|
400
|
+
observations.push(makeManualObservation(declaration));
|
|
401
|
+
}
|
|
402
|
+
const projection = referenceInputProjection(safeInput);
|
|
403
|
+
return finalReferenceResult({
|
|
404
|
+
schemaVersion: REFERENCE_INTERPRETER_RESULT_SCHEMA_VERSION,
|
|
405
|
+
status: 'ok',
|
|
406
|
+
interpreterId: MANUAL_REFERENCE_INTERPRETER_VERSION,
|
|
407
|
+
inputHash: inputHash(projection),
|
|
408
|
+
observations,
|
|
409
|
+
unresolvedItems: resultUnresolved,
|
|
410
|
+
warnings: resultWarnings,
|
|
411
|
+
analyzer,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const FIXTURE_SPECS = [
|
|
416
|
+
{ ontologyPath: 'person.identity', value: { kind: 'subject', label: 'fixture-subject' } },
|
|
417
|
+
{ ontologyPath: 'person.hair', value: { style: 'shoulder-length', color: 'dark-brown' } },
|
|
418
|
+
{ ontologyPath: 'expression', value: { emotion: 'neutral', intensity: 0.25 } },
|
|
419
|
+
{ ontologyPath: 'pose', value: { orientation: 'frontal', stance: 'relaxed' } },
|
|
420
|
+
{ ontologyPath: 'wardrobe.top', value: { category: 'jacket', color: 'black' } },
|
|
421
|
+
{ ontologyPath: 'environment.background', value: { kind: 'city-street', depth: 'midground' } },
|
|
422
|
+
{ ontologyPath: 'camera.framing', value: { framing: 'portrait', angle: 'eye-level' } },
|
|
423
|
+
];
|
|
424
|
+
function fixtureValue(spec, assetContentHash) {
|
|
425
|
+
if (spec.ontologyPath !== 'person.identity')
|
|
426
|
+
return clone(spec.value);
|
|
427
|
+
return { ...spec.value, assetContentHash };
|
|
428
|
+
}
|
|
429
|
+
export class FixtureReferenceInterpreter {
|
|
430
|
+
interpret(input) {
|
|
431
|
+
const safeInput = clone(input);
|
|
432
|
+
const fixtureId = safeInput.fixtureId ?? (safeInput.assets.length === 1 && safeInput.assets[0].id === 'ref-01' ? 'ref-01' : undefined);
|
|
433
|
+
const analyzer = { schemaVersion: 'voce.analyzer-metadata/v1alpha1', adapterId: FIXTURE_REFERENCE_INTERPRETER_VERSION, model: 'fixture', promptVersion: 'v1', ...(fixtureId ? { fixtureId } : {}) };
|
|
434
|
+
const planError = validateRequestedScopePlan(safeInput.requestedScopePlan, safeInput.caseId, safeInput.caseRevision);
|
|
435
|
+
if (planError)
|
|
436
|
+
return blockedReferenceResult(safeInput, FIXTURE_REFERENCE_INTERPRETER_VERSION, analyzer, planError);
|
|
437
|
+
safeInput.requestedScopePlan = normalizePlan(safeInput.requestedScopePlan);
|
|
438
|
+
const resultUnresolved = [];
|
|
439
|
+
const observations = [];
|
|
440
|
+
if (!fixtureId) {
|
|
441
|
+
for (const asset of safeInput.assets)
|
|
442
|
+
resultUnresolved.push(interpreterUnresolved('FIXTURE_ID_REQUIRED', 'FixtureReferenceInterpreter requires a fixtureId or the explicit ref-01 fixture asset.', undefined, asset.id));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const specs = [...FIXTURE_SPECS].sort((left, right) => compareCodeUnits(left.ontologyPath, right.ontologyPath));
|
|
446
|
+
for (const asset of [...safeInput.assets].sort((left, right) => compareCodeUnits(left.id, right.id))) {
|
|
447
|
+
if (asset.availability !== 'available') {
|
|
448
|
+
resultUnresolved.push(interpreterUnresolved('ASSET_UNAVAILABLE', 'Fixture asset is not available for offline interpretation.', undefined, asset.id));
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
let emitted = 0;
|
|
452
|
+
for (const spec of specs) {
|
|
453
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, spec.ontologyPath, asset.id, safeInput.effectiveScenario))
|
|
454
|
+
continue;
|
|
455
|
+
const observation = createObservation({
|
|
456
|
+
schemaVersion: OBSERVATION_SCHEMA_VERSION,
|
|
457
|
+
id: `observation-${asset.id}-${spec.ontologyPath.replaceAll('.', '-')}`,
|
|
458
|
+
assetId: asset.id,
|
|
459
|
+
ontologyPath: spec.ontologyPath,
|
|
460
|
+
value: fixtureValue(spec, asset.contentHash),
|
|
461
|
+
confidence: 0.75,
|
|
462
|
+
provenance: { source: 'reference_observed', sourceIds: [asset.id, asset.contentHash].sort(compareCodeUnits), createdBy: FIXTURE_REFERENCE_INTERPRETER_VERSION, createdAt: FIXED_DECISION_TIME },
|
|
463
|
+
analyzer,
|
|
464
|
+
warnings: [],
|
|
465
|
+
});
|
|
466
|
+
observations.push(observation);
|
|
467
|
+
emitted += 1;
|
|
468
|
+
}
|
|
469
|
+
if (emitted === 0)
|
|
470
|
+
resultUnresolved.push(interpreterUnresolved('SCOPE_NOT_PERMITTED', 'No fixture observation scope is permitted for this asset.', undefined, asset.id));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const projection = referenceInputProjection(safeInput);
|
|
474
|
+
return finalReferenceResult({
|
|
475
|
+
schemaVersion: REFERENCE_INTERPRETER_RESULT_SCHEMA_VERSION,
|
|
476
|
+
status: 'ok',
|
|
477
|
+
interpreterId: FIXTURE_REFERENCE_INTERPRETER_VERSION,
|
|
478
|
+
inputHash: inputHash(projection),
|
|
479
|
+
observations,
|
|
480
|
+
unresolvedItems: resultUnresolved,
|
|
481
|
+
warnings: fixtureId ? [] : ['FIXTURE_SCOPE_OUTPUT_EMPTY'],
|
|
482
|
+
analyzer,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function candidateFromObservation(observation, binding, decisionIds) {
|
|
487
|
+
const acceptedByDecisionIds = stableUnique(decisionIds);
|
|
488
|
+
return { id: binding.id, path: binding.targetPath, value: clone(observation.value), provenance: clone(observation.provenance), acceptedByIds: acceptedByDecisionIds, acceptedByDecisionIds, sourceBindingIds: [binding.id] };
|
|
489
|
+
}
|
|
490
|
+
function candidateFromIntent(intent) {
|
|
491
|
+
if (intent.requestedValue === undefined || intent.operation === 'remove')
|
|
492
|
+
return undefined;
|
|
493
|
+
return { id: intent.id, path: intent.targetPath, value: clone(intent.requestedValue), provenance: clone(intent.provenance), acceptedByIds: [intent.id], acceptedByDecisionIds: [], sourceBindingIds: [] };
|
|
494
|
+
}
|
|
495
|
+
function candidateFromMetadata(metadata) {
|
|
496
|
+
return { id: metadata.id, path: metadata.targetPath, value: clone(metadata.value), provenance: clone(metadata.provenance), acceptedByIds: [metadata.id], acceptedByDecisionIds: [], sourceBindingIds: [] };
|
|
497
|
+
}
|
|
498
|
+
function validDecisionAuthority(value) {
|
|
499
|
+
return value === 'user' || value === 'host_policy' || value === 'trusted_metadata' || value === 'auto_policy';
|
|
500
|
+
}
|
|
501
|
+
function normalizedDecisionHash(decision) {
|
|
502
|
+
return decision instanceof Object && 'observationId' in decision
|
|
503
|
+
? sha256(observationDecisionProjection(decision))
|
|
504
|
+
: sha256(bindingDecisionProjection(decision));
|
|
505
|
+
}
|
|
506
|
+
function makeBindingFor(intent, observation) {
|
|
507
|
+
const relation = intent.operation === 'preserve' ? 'preserve' : intent.operation === 'replace' ? 'reproduce' : 'inspire';
|
|
508
|
+
const body = {
|
|
509
|
+
schemaVersion: SOURCE_BINDING_SCHEMA_VERSION,
|
|
510
|
+
id: hashId('binding', { targetPath: intent.targetPath, observationId: observation.id, relation, priority: intent.importance }),
|
|
511
|
+
targetPath: intent.targetPath,
|
|
512
|
+
observationIds: [observation.id],
|
|
513
|
+
relation,
|
|
514
|
+
priority: intent.importance,
|
|
515
|
+
};
|
|
516
|
+
return createSourceBinding(body);
|
|
517
|
+
}
|
|
518
|
+
function candidateKey(binding) {
|
|
519
|
+
return `${binding.targetPath}|${binding.relation}|${binding.priority}|${sortedStrings(binding.observationIds).join(',')}`;
|
|
520
|
+
}
|
|
521
|
+
function decisionKey(value) {
|
|
522
|
+
return `${value.bindingId}|${value.bindingHash}|${value.contextHash}`;
|
|
523
|
+
}
|
|
524
|
+
function factForCandidates(path, candidates) {
|
|
525
|
+
const byValue = new Map();
|
|
526
|
+
for (const candidate of candidates) {
|
|
527
|
+
const key = canonicalValue(candidate.value);
|
|
528
|
+
const existing = byValue.get(key) ?? [];
|
|
529
|
+
existing.push(candidate);
|
|
530
|
+
byValue.set(key, existing);
|
|
531
|
+
}
|
|
532
|
+
if (byValue.size > 1) {
|
|
533
|
+
const allCandidates = candidates.flatMap((candidate) => [candidate.id]).sort(compareCodeUnits);
|
|
534
|
+
return { conflict: conflict({ code: 'SOURCE_CONFLICT_UNRESOLVED', message: 'Confirmed sources disagree for one target path and no explicit adjudication selected a winner.', targetPath: path, candidateIds: allCandidates, relatedIds: allCandidates, blocking: true }) };
|
|
535
|
+
}
|
|
536
|
+
const sameValue = [...byValue.values()][0];
|
|
537
|
+
if (!sameValue || sameValue.length === 0)
|
|
538
|
+
return {};
|
|
539
|
+
const sourceBindingIds = stableUnique(sameValue.flatMap((candidate) => candidate.sourceBindingIds));
|
|
540
|
+
const sourceIds = stableUnique(sameValue.flatMap((candidate) => candidate.provenance.sourceIds));
|
|
541
|
+
const acceptedByIds = stableUnique(sameValue.flatMap((candidate) => candidate.acceptedByIds));
|
|
542
|
+
const acceptedByDecisionIds = stableUnique(sameValue.flatMap((candidate) => candidate.acceptedByDecisionIds));
|
|
543
|
+
const first = sameValue[0];
|
|
544
|
+
const factBase = {
|
|
545
|
+
schemaVersion: ONTOLOGY_FACT_SCHEMA_VERSION,
|
|
546
|
+
path,
|
|
547
|
+
value: clone(first.value),
|
|
548
|
+
provenance: { ...clone(first.provenance), sourceIds },
|
|
549
|
+
acceptedByIds,
|
|
550
|
+
acceptedByDecisionIds,
|
|
551
|
+
sourceBindingIds,
|
|
552
|
+
};
|
|
553
|
+
return { fact: { ...clone(factBase), id: hashId('fact', factBase) } };
|
|
554
|
+
}
|
|
555
|
+
function finalOntology(value) {
|
|
556
|
+
const base = clone({
|
|
557
|
+
...value,
|
|
558
|
+
facts: sortedBy(value.facts, (item) => item.path),
|
|
559
|
+
unknownPaths: stableUnique(value.unknownPaths),
|
|
560
|
+
unspecifiedPaths: stableUnique(value.unspecifiedPaths),
|
|
561
|
+
unresolvedItems: sortedBy(value.unresolvedItems, (item) => item.id),
|
|
562
|
+
conflicts: sortedBy(value.conflicts, (item) => item.id),
|
|
563
|
+
decisionTrace: sortedBy(value.decisionTrace, (item) => item.id),
|
|
564
|
+
});
|
|
565
|
+
const withoutHash = clone({ ...base, instanceHash: undefined });
|
|
566
|
+
delete withoutHash.instanceHash;
|
|
567
|
+
return clone({ ...withoutHash, instanceHash: sha256(jsonReady(withoutHash) ?? null) });
|
|
568
|
+
}
|
|
569
|
+
function finalResolverResult(value) {
|
|
570
|
+
const withoutHash = clone({
|
|
571
|
+
...value,
|
|
572
|
+
proposedBindings: sortedBy(value.proposedBindings, (item) => item.id),
|
|
573
|
+
proposedBindingDecisions: sortedBy(value.proposedBindingDecisions, (item) => item.decisionId),
|
|
574
|
+
confirmedBindings: sortedBy(value.confirmedBindings, (item) => item.id),
|
|
575
|
+
confirmedBindingDecisions: sortedBy(value.confirmedBindingDecisions, (item) => item.decisionId),
|
|
576
|
+
questions: sortedBy(value.questions, (item) => item.id),
|
|
577
|
+
conflicts: sortedBy(value.conflicts, (item) => item.id),
|
|
578
|
+
unresolvedItems: sortedBy(value.unresolvedItems, (item) => item.id),
|
|
579
|
+
decisionTrace: sortedBy(value.decisionTrace, (item) => item.id),
|
|
580
|
+
warnings: sortedStrings(value.warnings),
|
|
581
|
+
});
|
|
582
|
+
delete withoutHash.resultHash;
|
|
583
|
+
return clone({ ...withoutHash, resultHash: sha256(jsonReady(withoutHash) ?? null) });
|
|
584
|
+
}
|
|
585
|
+
function blockedPlanHash(input) {
|
|
586
|
+
const candidate = input.requestedScopePlan;
|
|
587
|
+
return typeof candidate?.planHash === 'string' && /^sha256:[0-9a-f]{64}$/.test(candidate.planHash)
|
|
588
|
+
? candidate.planHash
|
|
589
|
+
: sha256({ invalidRequestedScopePlan: jsonReady(input.requestedScopePlan) ?? null });
|
|
590
|
+
}
|
|
591
|
+
function blockedResolverResult(input, resolverInputHash, error) {
|
|
592
|
+
const plan = input.requestedScopePlan;
|
|
593
|
+
const planId = typeof plan?.id === 'string' ? plan.id : 'requested-scope-plan';
|
|
594
|
+
const relatedIds = [planId];
|
|
595
|
+
const unresolvedItem = unresolved({ code: error.code, message: error.message, status: 'unresolved', relatedIds });
|
|
596
|
+
const conflictItem = conflict({ code: error.code, message: error.message, candidateIds: relatedIds, relatedIds, blocking: true });
|
|
597
|
+
const traceItem = trace({ kind: 'scope', outcome: 'conflict', code: error.code, message: error.message, subjectIds: relatedIds });
|
|
598
|
+
const ontology = finalOntology({
|
|
599
|
+
schemaVersion: ONTOLOGY_INSTANCE_SCHEMA_VERSION,
|
|
600
|
+
id: hashId('ontology', { caseId: input.caseId, caseRevision: input.caseRevision, contextHash: input.contextHash, requestedScopePlanHash: blockedPlanHash(input) }),
|
|
601
|
+
caseId: input.caseId,
|
|
602
|
+
caseRevision: input.caseRevision,
|
|
603
|
+
contextHash: input.contextHash,
|
|
604
|
+
requestedScopePlanHash: blockedPlanHash(input),
|
|
605
|
+
facts: [],
|
|
606
|
+
unknownPaths: [],
|
|
607
|
+
unspecifiedPaths: [],
|
|
608
|
+
unresolvedItems: [unresolvedItem],
|
|
609
|
+
conflicts: [conflictItem],
|
|
610
|
+
decisionTrace: [traceItem],
|
|
611
|
+
});
|
|
612
|
+
return finalResolverResult({
|
|
613
|
+
schemaVersion: RESOLVER_RESULT_SCHEMA_VERSION,
|
|
614
|
+
status: 'blocked',
|
|
615
|
+
resolverId: EVIDENCE_RESOLVER_VERSION,
|
|
616
|
+
inputHash: resolverInputHash,
|
|
617
|
+
proposedBindings: [],
|
|
618
|
+
proposedBindingDecisions: [],
|
|
619
|
+
confirmedBindings: [],
|
|
620
|
+
confirmedBindingDecisions: [],
|
|
621
|
+
ontologyInstance: ontology,
|
|
622
|
+
questions: [],
|
|
623
|
+
conflicts: [conflictItem],
|
|
624
|
+
unresolvedItems: [unresolvedItem],
|
|
625
|
+
decisionTrace: [traceItem],
|
|
626
|
+
warnings: [error.code],
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
export class EvidenceAndSourceResolver {
|
|
630
|
+
resolve(input) {
|
|
631
|
+
const safeInput = clone(input);
|
|
632
|
+
const resolverProjection = resolverInputProjection(safeInput);
|
|
633
|
+
const resolverInputHash = inputHash(resolverProjection);
|
|
634
|
+
const planError = validateRequestedScopePlan(safeInput.requestedScopePlan, safeInput.caseId, safeInput.caseRevision);
|
|
635
|
+
if (planError)
|
|
636
|
+
return blockedResolverResult(safeInput, resolverInputHash, planError);
|
|
637
|
+
safeInput.requestedScopePlan = normalizePlan(safeInput.requestedScopePlan);
|
|
638
|
+
const unresolvedItems = [];
|
|
639
|
+
const conflicts = [];
|
|
640
|
+
const questions = [];
|
|
641
|
+
const decisionTrace = [];
|
|
642
|
+
const warnings = [];
|
|
643
|
+
const addUnresolved = (item) => { if (!unresolvedItems.some((existing) => existing.id === item.id))
|
|
644
|
+
unresolvedItems.push(item); };
|
|
645
|
+
const addConflict = (item) => { if (!conflicts.some((existing) => existing.id === item.id))
|
|
646
|
+
conflicts.push(item); };
|
|
647
|
+
const addTrace = (item) => { if (!decisionTrace.some((existing) => existing.id === item.id))
|
|
648
|
+
decisionTrace.push(item); };
|
|
649
|
+
const observations = sortedBy((safeInput.observations ?? []).map(normalizeObservation), (item) => item.id);
|
|
650
|
+
const validObservations = new Map();
|
|
651
|
+
const observationIdCollisions = new Set();
|
|
652
|
+
const observationsById = new Map();
|
|
653
|
+
for (const observation of observations)
|
|
654
|
+
observationsById.set(observation.id, [...(observationsById.get(observation.id) ?? []), observation]);
|
|
655
|
+
for (const [observationId, group] of [...observationsById.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]))) {
|
|
656
|
+
const semanticVariants = stableUnique(group.map((item) => canonicalValue(observationProjection(item))));
|
|
657
|
+
if (semanticVariants.length > 1) {
|
|
658
|
+
observationIdCollisions.add(observationId);
|
|
659
|
+
const targetPath = group[0].ontologyPath;
|
|
660
|
+
const message = 'The same Observation ID was supplied with different semantic content; no variant is selected by insertion order.';
|
|
661
|
+
const relatedIds = stableUnique(group.map((item) => item.id));
|
|
662
|
+
addConflict(conflict({ code: 'OBSERVATION_ID_COLLISION', message, targetPath, candidateIds: relatedIds, relatedIds, blocking: true }));
|
|
663
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_ID_COLLISION', message, status: 'unresolved', targetPath, assetId: group[0].assetId, relatedIds }));
|
|
664
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'OBSERVATION_ID_COLLISION', message, targetPath, subjectIds: relatedIds }));
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const valid = group.find((item) => item.schemaVersion === OBSERVATION_SCHEMA_VERSION && computeObservationContentHash(item) === item.contentHash);
|
|
668
|
+
const selected = valid ?? group[0];
|
|
669
|
+
if (!valid) {
|
|
670
|
+
const item = unresolved({ code: 'OBSERVATION_HASH_MISMATCH', message: 'Observation contentHash does not match its canonical semantic projection.', status: 'unresolved', targetPath: selected.ontologyPath, assetId: selected.assetId, relatedIds: [selected.id] });
|
|
671
|
+
addUnresolved(item);
|
|
672
|
+
addTrace(trace({ kind: 'observation', outcome: 'rejected', code: 'OBSERVATION_HASH_MISMATCH', message: 'Candidate observation was rejected because its content hash is stale or malformed.', targetPath: selected.ontologyPath, subjectIds: [selected.id] }));
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
validObservations.set(observationId, selected);
|
|
676
|
+
}
|
|
677
|
+
const confirmedObservationIds = new Set();
|
|
678
|
+
const conflictedObservationIds = new Set(observationIdCollisions);
|
|
679
|
+
const observationDecisionsById = new Map();
|
|
680
|
+
for (const decision of sortedBy(safeInput.observationDecisions ?? [], (item) => item.decisionId))
|
|
681
|
+
observationDecisionsById.set(decision.decisionId, [...(observationDecisionsById.get(decision.decisionId) ?? []), decision]);
|
|
682
|
+
const eligibleObservationDecisions = new Map();
|
|
683
|
+
for (const [decisionId, group] of [...observationDecisionsById.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]))) {
|
|
684
|
+
const semanticVariants = stableUnique(group.map((item) => canonicalValue(observationDecisionProjection(item))));
|
|
685
|
+
if (semanticVariants.length > 1) {
|
|
686
|
+
const relatedIds = stableUnique(group.flatMap((item) => [item.decisionId, item.observationId]));
|
|
687
|
+
const message = 'The same ObservationDecision decisionId was supplied with conflicting semantic content; no variant is selected by insertion order.';
|
|
688
|
+
addConflict(conflict({ code: 'OBSERVATION_DECISION_ID_COLLISION', message, candidateIds: [decisionId], relatedIds, blocking: true }));
|
|
689
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_DECISION_ID_COLLISION', message, status: 'unresolved', relatedIds }));
|
|
690
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'OBSERVATION_DECISION_ID_COLLISION', message, subjectIds: relatedIds }));
|
|
691
|
+
for (const item of group) {
|
|
692
|
+
conflictedObservationIds.add(item.observationId);
|
|
693
|
+
const observation = validObservations.get(item.observationId);
|
|
694
|
+
if (observation && item.observationHash !== observation.contentHash) {
|
|
695
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_HASH_MISMATCH', message: 'ObservationDecision is bound to a different observation content hash.', status: 'unresolved', targetPath: observation.ontologyPath, assetId: observation.assetId, relatedIds: [item.decisionId, observation.id] }));
|
|
696
|
+
}
|
|
697
|
+
if (item.contextHash !== safeInput.contextHash) {
|
|
698
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_CONTEXT_MISMATCH', message: 'ObservationDecision belongs to a different compilation context.', status: 'unresolved', targetPath: observation?.ontologyPath, assetId: observation?.assetId, relatedIds: [item.decisionId, item.observationId] }));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const selected = group.find((item) => item.schemaVersion === OBSERVATION_DECISION_SCHEMA_VERSION && normalizedDecisionHash(item) === item.decisionHash) ?? group[0];
|
|
704
|
+
const observation = validObservations.get(selected.observationId);
|
|
705
|
+
const validHash = selected.schemaVersion === OBSERVATION_DECISION_SCHEMA_VERSION && normalizedDecisionHash(selected) === selected.decisionHash;
|
|
706
|
+
if (!validHash) {
|
|
707
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_DECISION_HASH_MISMATCH', message: 'ObservationDecision decisionHash is stale or malformed.', status: 'unresolved', relatedIds: [selected.decisionId, selected.observationId] }));
|
|
708
|
+
addTrace(trace({ kind: 'observation', outcome: 'rejected', code: 'OBSERVATION_DECISION_HASH_MISMATCH', message: 'Observation decision was not accepted because its decision hash is invalid.', subjectIds: [selected.decisionId, selected.observationId] }));
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (!observation || !validObservations.has(observation.id)) {
|
|
712
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_NOT_FOUND', message: 'ObservationDecision does not refer to a valid current Observation.', status: 'unresolved', relatedIds: [selected.decisionId, selected.observationId] }));
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (selected.observationHash !== observation.contentHash) {
|
|
716
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_HASH_MISMATCH', message: 'ObservationDecision is bound to a different observation content hash.', status: 'unresolved', targetPath: observation.ontologyPath, assetId: observation.assetId, relatedIds: [selected.decisionId, observation.id] }));
|
|
717
|
+
addTrace(trace({ kind: 'observation', outcome: 'rejected', code: 'OBSERVATION_HASH_MISMATCH', message: 'Observation decision is stale because the candidate observation changed.', targetPath: observation.ontologyPath, subjectIds: [selected.decisionId, observation.id] }));
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (selected.contextHash !== safeInput.contextHash) {
|
|
721
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_CONTEXT_MISMATCH', message: 'ObservationDecision belongs to a different compilation context.', status: 'unresolved', targetPath: observation.ontologyPath, assetId: observation.assetId, relatedIds: [selected.decisionId, observation.id] }));
|
|
722
|
+
addTrace(trace({ kind: 'observation', outcome: 'rejected', code: 'OBSERVATION_CONTEXT_MISMATCH', message: 'Observation decision is stale because its contextHash differs from the current context.', targetPath: observation.ontologyPath, subjectIds: [selected.decisionId, observation.id] }));
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (!validDecisionAuthority(selected.authority)) {
|
|
726
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_AUTHORITY_INVALID', message: 'ObservationDecision authority is not permitted by the public contract.', status: 'unresolved', relatedIds: [selected.decisionId] }));
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const subjectKey = canonicalValue({ observationId: selected.observationId, observationHash: selected.observationHash, contextHash: selected.contextHash });
|
|
730
|
+
eligibleObservationDecisions.set(subjectKey, [...(eligibleObservationDecisions.get(subjectKey) ?? []), selected]);
|
|
731
|
+
}
|
|
732
|
+
for (const decisions of [...eligibleObservationDecisions.values()].sort((left, right) => compareCodeUnits(left[0].observationId, right[0].observationId) || compareCodeUnits(left[0].decisionId, right[0].decisionId))) {
|
|
733
|
+
const observation = validObservations.get(decisions[0].observationId);
|
|
734
|
+
if (!observation)
|
|
735
|
+
continue;
|
|
736
|
+
const statuses = new Set(decisions.map((item) => item.status));
|
|
737
|
+
const decisionIds = stableUnique(decisions.map((item) => item.decisionId));
|
|
738
|
+
if (statuses.has('confirmed') && statuses.has('rejected')) {
|
|
739
|
+
const message = 'Confirmed and rejected authoritative ObservationDecisions disagree for the same observation, content, and context.';
|
|
740
|
+
conflictedObservationIds.add(observation.id);
|
|
741
|
+
addConflict(conflict({ code: 'OBSERVATION_DECISION_CONFLICT', message, targetPath: observation.ontologyPath, candidateIds: decisionIds, relatedIds: [observation.id, ...decisionIds], blocking: true }));
|
|
742
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_DECISION_CONFLICT', message, status: 'unresolved', targetPath: observation.ontologyPath, assetId: observation.assetId, relatedIds: [observation.id, ...decisionIds] }));
|
|
743
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'OBSERVATION_DECISION_CONFLICT', message, targetPath: observation.ontologyPath, subjectIds: [observation.id, ...decisionIds] }));
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
for (const decision of decisions) {
|
|
747
|
+
if (decision.status === 'confirmed') {
|
|
748
|
+
confirmedObservationIds.add(observation.id);
|
|
749
|
+
addTrace(trace({ kind: 'observation', outcome: 'accepted', code: 'OBSERVATION_CONFIRMED', message: 'Candidate observation is eligible for source resolution under the current context.', targetPath: observation.ontologyPath, subjectIds: [decision.decisionId, observation.id] }));
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
addTrace(trace({ kind: 'observation', outcome: decision.status === 'proposed' ? 'proposed' : 'rejected', code: `OBSERVATION_${decision.status.toUpperCase()}`, message: `Observation decision status is ${decision.status}; it cannot admit evidence into the ontology.`, targetPath: observation.ontologyPath, subjectIds: [decision.decisionId, observation.id] }));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const observation of validObservations.values()) {
|
|
757
|
+
if (!confirmedObservationIds.has(observation.id) || conflictedObservationIds.has(observation.id))
|
|
758
|
+
addUnresolved(unresolved({ code: 'OBSERVATION_NOT_CONFIRMED', message: 'Candidate observation has no unconflicted current confirmed ObservationDecision.', status: 'unknown', targetPath: observation.ontologyPath, assetId: observation.assetId, relatedIds: [observation.id] }));
|
|
759
|
+
}
|
|
760
|
+
const sourceBindings = sortedBy((safeInput.sourceBindings ?? []).map(normalizeBinding), (item) => item.id);
|
|
761
|
+
const bindingIdCollisions = new Set();
|
|
762
|
+
const sourceBindingsById = new Map();
|
|
763
|
+
for (const binding of sourceBindings)
|
|
764
|
+
sourceBindingsById.set(binding.id, [...(sourceBindingsById.get(binding.id) ?? []), binding]);
|
|
765
|
+
const validSourceBindings = new Map();
|
|
766
|
+
for (const [bindingId, group] of [...sourceBindingsById.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]))) {
|
|
767
|
+
const semanticVariants = stableUnique(group.map((item) => canonicalValue(bindingProjection(item))));
|
|
768
|
+
if (semanticVariants.length > 1) {
|
|
769
|
+
bindingIdCollisions.add(bindingId);
|
|
770
|
+
const targetPath = group[0].targetPath;
|
|
771
|
+
const message = 'The same SourceBinding ID was supplied with different semantic content; no variant is selected by insertion order.';
|
|
772
|
+
const relatedIds = stableUnique(group.map((item) => item.id));
|
|
773
|
+
addConflict(conflict({ code: 'SOURCE_BINDING_ID_COLLISION', message, targetPath, candidateIds: relatedIds, relatedIds, blocking: true }));
|
|
774
|
+
addUnresolved(unresolved({ code: 'SOURCE_BINDING_ID_COLLISION', message, status: 'unresolved', targetPath, relatedIds }));
|
|
775
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'SOURCE_BINDING_ID_COLLISION', message, targetPath, subjectIds: relatedIds }));
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const valid = group.find((item) => item.schemaVersion === SOURCE_BINDING_SCHEMA_VERSION && computeSourceBindingContentHash(item) === item.contentHash);
|
|
779
|
+
if (valid)
|
|
780
|
+
validSourceBindings.set(bindingId, valid);
|
|
781
|
+
else {
|
|
782
|
+
const selected = group[0];
|
|
783
|
+
addUnresolved(unresolved({ code: 'BINDING_HASH_MISMATCH', message: 'SourceBinding contentHash does not match its canonical semantic projection.', status: 'unresolved', targetPath: selected.targetPath, relatedIds: [selected.id] }));
|
|
784
|
+
addTrace(trace({ kind: 'binding', outcome: 'rejected', code: 'BINDING_HASH_MISMATCH', message: 'Source binding was rejected because its content hash is stale or malformed.', targetPath: selected.targetPath, subjectIds: [selected.id] }));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const confirmedBindings = [];
|
|
788
|
+
const confirmedBindingDecisions = [];
|
|
789
|
+
const confirmedBindingIds = new Set();
|
|
790
|
+
const confirmedDecisionIdsByBinding = new Map();
|
|
791
|
+
const bindingDecisionsById = new Map();
|
|
792
|
+
for (const decision of sortedBy(safeInput.bindingDecisions ?? [], (item) => item.decisionId))
|
|
793
|
+
bindingDecisionsById.set(decision.decisionId, [...(bindingDecisionsById.get(decision.decisionId) ?? []), decision]);
|
|
794
|
+
const eligibleBindingDecisions = new Map();
|
|
795
|
+
const conflictedBindingIds = new Set(bindingIdCollisions);
|
|
796
|
+
for (const [decisionId, group] of [...bindingDecisionsById.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]))) {
|
|
797
|
+
const semanticVariants = stableUnique(group.map((item) => canonicalValue(bindingDecisionProjection(item))));
|
|
798
|
+
if (semanticVariants.length > 1) {
|
|
799
|
+
const relatedIds = stableUnique(group.flatMap((item) => [item.decisionId, item.bindingId]));
|
|
800
|
+
const message = 'The same BindingDecision decisionId was supplied with conflicting semantic content; no variant is selected by insertion order.';
|
|
801
|
+
addConflict(conflict({ code: 'BINDING_DECISION_ID_COLLISION', message, candidateIds: [decisionId], relatedIds, blocking: true }));
|
|
802
|
+
addUnresolved(unresolved({ code: 'BINDING_DECISION_ID_COLLISION', message, status: 'unresolved', relatedIds }));
|
|
803
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'BINDING_DECISION_ID_COLLISION', message, subjectIds: relatedIds }));
|
|
804
|
+
for (const item of group)
|
|
805
|
+
conflictedBindingIds.add(item.bindingId);
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const selected = group.find((item) => item.schemaVersion === BINDING_DECISION_SCHEMA_VERSION && normalizedDecisionHash(item) === item.decisionHash) ?? group[0];
|
|
809
|
+
const binding = validSourceBindings.get(selected.bindingId);
|
|
810
|
+
const validHash = selected.schemaVersion === BINDING_DECISION_SCHEMA_VERSION && normalizedDecisionHash(selected) === selected.decisionHash;
|
|
811
|
+
if (!validHash) {
|
|
812
|
+
addUnresolved(unresolved({ code: 'BINDING_DECISION_HASH_MISMATCH', message: 'BindingDecision decisionHash is stale or malformed.', status: 'unresolved', relatedIds: [selected.decisionId, selected.bindingId] }));
|
|
813
|
+
addTrace(trace({ kind: 'binding', outcome: 'rejected', code: 'BINDING_DECISION_HASH_MISMATCH', message: 'Binding decision was not accepted because its decision hash is invalid.', subjectIds: [selected.decisionId, selected.bindingId] }));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (!binding) {
|
|
817
|
+
addUnresolved(unresolved({ code: 'BINDING_NOT_FOUND', message: 'BindingDecision does not refer to a current SourceBinding.', status: 'unresolved', relatedIds: [selected.decisionId, selected.bindingId] }));
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (binding.schemaVersion !== SOURCE_BINDING_SCHEMA_VERSION || computeSourceBindingContentHash(binding) !== binding.contentHash || selected.bindingHash !== binding.contentHash) {
|
|
821
|
+
addUnresolved(unresolved({ code: 'BINDING_HASH_MISMATCH', message: 'BindingDecision is bound to a different SourceBinding content hash.', status: 'unresolved', targetPath: binding.targetPath, relatedIds: [selected.decisionId, binding.id] }));
|
|
822
|
+
addTrace(trace({ kind: 'binding', outcome: 'rejected', code: 'BINDING_HASH_MISMATCH', message: 'Binding decision is stale because the source binding changed.', targetPath: binding.targetPath, subjectIds: [selected.decisionId, binding.id] }));
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (selected.contextHash !== safeInput.contextHash) {
|
|
826
|
+
addUnresolved(unresolved({ code: 'BINDING_CONTEXT_MISMATCH', message: 'BindingDecision belongs to a different compilation context.', status: 'unresolved', targetPath: binding.targetPath, relatedIds: [selected.decisionId, binding.id] }));
|
|
827
|
+
addTrace(trace({ kind: 'binding', outcome: 'rejected', code: 'BINDING_CONTEXT_MISMATCH', message: 'Binding decision is stale because its contextHash differs from the current context.', targetPath: binding.targetPath, subjectIds: [selected.decisionId, binding.id] }));
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (!validDecisionAuthority(selected.authority)) {
|
|
831
|
+
addUnresolved(unresolved({ code: 'BINDING_AUTHORITY_INVALID', message: 'BindingDecision authority is not permitted by the public contract.', status: 'unresolved', relatedIds: [selected.decisionId] }));
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const subjectKey = canonicalValue({ bindingId: selected.bindingId, bindingHash: selected.bindingHash, contextHash: selected.contextHash });
|
|
835
|
+
eligibleBindingDecisions.set(subjectKey, [...(eligibleBindingDecisions.get(subjectKey) ?? []), selected]);
|
|
836
|
+
}
|
|
837
|
+
for (const decisions of [...eligibleBindingDecisions.values()].sort((left, right) => compareCodeUnits(left[0].bindingId, right[0].bindingId) || compareCodeUnits(left[0].decisionId, right[0].decisionId))) {
|
|
838
|
+
const binding = validSourceBindings.get(decisions[0].bindingId);
|
|
839
|
+
if (!binding)
|
|
840
|
+
continue;
|
|
841
|
+
const statuses = new Set(decisions.map((item) => item.status));
|
|
842
|
+
const decisionIds = stableUnique(decisions.map((item) => item.decisionId));
|
|
843
|
+
if (statuses.has('confirmed') && statuses.has('rejected')) {
|
|
844
|
+
const message = 'Confirmed and rejected authoritative BindingDecisions disagree for the same binding, content, and context.';
|
|
845
|
+
conflictedBindingIds.add(binding.id);
|
|
846
|
+
addConflict(conflict({ code: 'BINDING_DECISION_CONFLICT', message, targetPath: binding.targetPath, candidateIds: decisionIds, relatedIds: [binding.id, ...decisionIds], blocking: true }));
|
|
847
|
+
addUnresolved(unresolved({ code: 'BINDING_DECISION_CONFLICT', message, status: 'unresolved', targetPath: binding.targetPath, relatedIds: [binding.id, ...decisionIds] }));
|
|
848
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'BINDING_DECISION_CONFLICT', message, targetPath: binding.targetPath, subjectIds: [binding.id, ...decisionIds] }));
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
for (const decision of decisions) {
|
|
852
|
+
if (decision.status === 'confirmed') {
|
|
853
|
+
confirmedBindingIds.add(binding.id);
|
|
854
|
+
if (!confirmedBindings.some((item) => item.id === binding.id))
|
|
855
|
+
confirmedBindings.push(binding);
|
|
856
|
+
confirmedBindingDecisions.push(decision);
|
|
857
|
+
confirmedDecisionIdsByBinding.set(binding.id, stableUnique([...(confirmedDecisionIdsByBinding.get(binding.id) ?? []), decision.decisionId]));
|
|
858
|
+
addTrace(trace({ kind: 'binding', outcome: 'accepted', code: 'BINDING_CONFIRMED', message: 'Source binding is eligible for fact construction under the current context.', targetPath: binding.targetPath, subjectIds: [decision.decisionId, binding.id] }));
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
addTrace(trace({ kind: 'binding', outcome: decision.status === 'proposed' ? 'proposed' : 'rejected', code: `BINDING_${decision.status.toUpperCase()}`, message: `Binding decision status is ${decision.status}; it cannot contribute an OntologyFact.`, targetPath: binding.targetPath, subjectIds: [decision.decisionId, binding.id] }));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const changeIntents = sortedBy(safeInput.changeIntents ?? [], (item) => item.id);
|
|
866
|
+
const removePaths = changeIntents.filter((intent) => intent.operation === 'remove').map((intent) => intent.targetPath);
|
|
867
|
+
const candidatesByPath = new Map();
|
|
868
|
+
const addCandidate = (candidate) => {
|
|
869
|
+
const existing = candidatesByPath.get(candidate.path) ?? [];
|
|
870
|
+
existing.push(candidate);
|
|
871
|
+
candidatesByPath.set(candidate.path, existing);
|
|
872
|
+
};
|
|
873
|
+
for (const intent of changeIntents) {
|
|
874
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, intent.targetPath, undefined, safeInput.effectiveScenario)) {
|
|
875
|
+
addUnresolved(unresolved({ code: 'SCOPE_NOT_PERMITTED', message: 'ChangeIntent target is outside the current RequestedScopePlan.', status: 'unresolved', targetPath: intent.targetPath, relatedIds: [intent.id] }));
|
|
876
|
+
addTrace(trace({ kind: 'scope', outcome: 'unresolved', code: 'SCOPE_NOT_PERMITTED', message: 'Target directive cannot be resolved because its ontology path is not allowed for this case.', targetPath: intent.targetPath, subjectIds: [intent.id] }));
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (intent.operation === 'remove') {
|
|
880
|
+
addTrace(trace({ kind: 'intent', outcome: 'accepted', code: 'TARGET_REMOVE', message: 'ChangeIntent remove excludes the target from the result; it does not exclude a source observation.', targetPath: intent.targetPath, subjectIds: [intent.id] }));
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const explicitCandidate = candidateFromIntent(intent);
|
|
884
|
+
if (explicitCandidate)
|
|
885
|
+
addCandidate(explicitCandidate);
|
|
886
|
+
else if (intent.operation === 'create') {
|
|
887
|
+
addUnresolved(unresolved({ code: 'TARGET_VALUE_UNSPECIFIED', message: 'Create intent has no requested value; the path remains unspecified rather than being filled by a default.', status: 'unknown', targetPath: intent.targetPath, relatedIds: [intent.id] }));
|
|
888
|
+
addTrace(trace({ kind: 'intent', outcome: 'unresolved', code: 'TARGET_VALUE_UNSPECIFIED', message: 'Create intent did not specify a value and no default was inferred.', targetPath: intent.targetPath, subjectIds: [intent.id] }));
|
|
889
|
+
}
|
|
890
|
+
addTrace(trace({ kind: 'intent', outcome: 'accepted', code: 'TARGET_DIRECTIVE_RECEIVED', message: 'Target directive was considered without changing its separate source-binding semantics.', targetPath: intent.targetPath, subjectIds: [intent.id] }));
|
|
891
|
+
}
|
|
892
|
+
for (const metadata of sortedBy(safeInput.trustedMetadata ?? [], (item) => item.id)) {
|
|
893
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, metadata.targetPath, metadata.assetId, safeInput.effectiveScenario)) {
|
|
894
|
+
addUnresolved(unresolved({ code: 'SCOPE_NOT_PERMITTED', message: 'Trusted metadata target is outside the current RequestedScopePlan.', status: 'unresolved', targetPath: metadata.targetPath, assetId: metadata.assetId, relatedIds: [metadata.id] }));
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
addCandidate(candidateFromMetadata(metadata));
|
|
898
|
+
addTrace(trace({ kind: 'fact', outcome: 'accepted', code: 'TRUSTED_METADATA_ACCEPTED', message: 'Trusted metadata may support a sparse fact under the current scope plan.', targetPath: metadata.targetPath, subjectIds: [metadata.id] }));
|
|
899
|
+
}
|
|
900
|
+
const proposedBindings = new Map();
|
|
901
|
+
const proposedBindingDecisions = new Map();
|
|
902
|
+
const existingByKey = new Map([...validSourceBindings.values()].map((binding) => [candidateKey(binding), binding]));
|
|
903
|
+
for (const intent of changeIntents) {
|
|
904
|
+
if (intent.operation === 'create' || intent.operation === 'remove')
|
|
905
|
+
continue;
|
|
906
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, intent.targetPath, undefined, safeInput.effectiveScenario))
|
|
907
|
+
continue;
|
|
908
|
+
const candidates = [...validObservations.values()].filter((observation) => {
|
|
909
|
+
if (!confirmedObservationIds.has(observation.id))
|
|
910
|
+
return false;
|
|
911
|
+
if (!exactPathMatch(intent.targetPath, observation.ontologyPath))
|
|
912
|
+
return false;
|
|
913
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, observation.ontologyPath, observation.assetId, safeInput.effectiveScenario))
|
|
914
|
+
return false;
|
|
915
|
+
if (!intent.sourceHintIds || intent.sourceHintIds.length === 0)
|
|
916
|
+
return true;
|
|
917
|
+
return intent.sourceHintIds.includes(observation.id) || intent.sourceHintIds.includes(observation.assetId);
|
|
918
|
+
}).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
919
|
+
if (candidates.length === 0) {
|
|
920
|
+
const rawCandidates = [...validObservations.values()].filter((observation) => exactPathMatch(intent.targetPath, observation.ontologyPath) && (!intent.sourceHintIds || intent.sourceHintIds.length === 0 || intent.sourceHintIds.includes(observation.id) || intent.sourceHintIds.includes(observation.assetId)));
|
|
921
|
+
if (rawCandidates.length > 0)
|
|
922
|
+
addUnresolved(unresolved({ code: 'SOURCE_CANDIDATE_NOT_CONFIRMED', message: 'Matching candidate observations exist, but none has a current confirmed ObservationDecision.', status: 'unknown', targetPath: intent.targetPath, relatedIds: rawCandidates.map((item) => item.id) }));
|
|
923
|
+
else
|
|
924
|
+
addUnresolved(unresolved({ code: 'SOURCE_CANDIDATE_NOT_FOUND', message: 'No confirmed observation matches the target path and source hints.', status: 'unknown', targetPath: intent.targetPath, relatedIds: [intent.id] }));
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
for (const observation of candidates) {
|
|
928
|
+
const generated = makeBindingFor(intent, observation);
|
|
929
|
+
const binding = existingByKey.get(candidateKey(generated)) ?? generated;
|
|
930
|
+
if (confirmedBindingIds.has(binding.id))
|
|
931
|
+
continue;
|
|
932
|
+
proposedBindings.set(binding.id, binding);
|
|
933
|
+
const proposedDecision = createBindingDecision({ schemaVersion: BINDING_DECISION_SCHEMA_VERSION, decisionId: hashId('binding-decision', { bindingId: binding.id, bindingHash: binding.contentHash, contextHash: safeInput.contextHash }), bindingId: binding.id, bindingHash: binding.contentHash, contextHash: safeInput.contextHash, status: 'proposed', authority: 'auto_policy', decidedBy: EVIDENCE_RESOLVER_VERSION, decidedAt: FIXED_DECISION_TIME, reasonCode: 'BINDING_PROPOSED' });
|
|
934
|
+
proposedBindingDecisions.set(decisionKey(proposedDecision), proposedDecision);
|
|
935
|
+
addTrace(trace({ kind: 'binding', outcome: 'proposed', code: 'BINDING_PROPOSED', message: 'Resolver proposed a source relationship; proposal remains unusable until a separate confirmed BindingDecision matches it exactly.', targetPath: binding.targetPath, subjectIds: [binding.id, observation.id] }));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const binding of confirmedBindings) {
|
|
939
|
+
if (!planPathAllowed(safeInput.requestedScopePlan, binding.targetPath, undefined, safeInput.effectiveScenario)) {
|
|
940
|
+
addUnresolved(unresolved({ code: 'SCOPE_NOT_PERMITTED', message: 'Confirmed SourceBinding target is outside the current RequestedScopePlan.', status: 'unresolved', targetPath: binding.targetPath, relatedIds: [binding.id] }));
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const observationsForBinding = binding.observationIds.map((id) => validObservations.get(id)).filter((item) => item !== undefined);
|
|
944
|
+
if (observationsForBinding.length !== binding.observationIds.length || observationsForBinding.some((observation) => !confirmedObservationIds.has(observation.id))) {
|
|
945
|
+
addUnresolved(unresolved({ code: 'BINDING_OBSERVATION_NOT_CONFIRMED', message: 'Confirmed SourceBinding cannot contribute because every referenced Observation is not currently confirmed.', status: 'unknown', targetPath: binding.targetPath, relatedIds: [binding.id, ...binding.observationIds] }));
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (observationsForBinding.some((observation) => !exactPathMatch(binding.targetPath, observation.ontologyPath))) {
|
|
949
|
+
addUnresolved(unresolved({ code: 'BINDING_PATH_MISMATCH', message: 'SourceBinding targetPath must exactly match every referenced Observation ontologyPath until an explicit field projection contract exists.', status: 'unresolved', targetPath: binding.targetPath, relatedIds: [binding.id, ...binding.observationIds] }));
|
|
950
|
+
addTrace(trace({ kind: 'binding', outcome: 'rejected', code: 'BINDING_PATH_MISMATCH', message: 'Source binding was rejected because M3 does not infer parent/child ontology field projections.', targetPath: binding.targetPath, subjectIds: [binding.id, ...binding.observationIds] }));
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (binding.relation === 'exclude') {
|
|
954
|
+
addTrace(trace({ kind: 'binding', outcome: 'excluded', code: 'SOURCE_EXCLUDED', message: 'SourceBinding exclude prevents this evidence from supplying a fact; it does not remove a target property.', targetPath: binding.targetPath, subjectIds: [binding.id, ...binding.observationIds] }));
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
if (conflictedBindingIds.has(binding.id))
|
|
958
|
+
continue;
|
|
959
|
+
if (removePaths.some((path) => scopeContainsPath(path, binding.targetPath))) {
|
|
960
|
+
const item = conflict({ code: 'REMOVE_SOURCE_CONFLICT', message: 'A target remove intent conflicts with a confirmed source binding for the same ontology path.', targetPath: binding.targetPath, candidateIds: [binding.id], relatedIds: [binding.id], blocking: true });
|
|
961
|
+
addConflict(item);
|
|
962
|
+
addUnresolved(unresolved({ code: 'REMOVE_SOURCE_CONFLICT', message: 'The target remove intent prevents this source binding from contributing a fact.', status: 'unresolved', targetPath: binding.targetPath, relatedIds: [binding.id] }));
|
|
963
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: 'REMOVE_SOURCE_CONFLICT', message: 'Target remove and source inheritance are separate decisions and cannot both supply this path.', targetPath: binding.targetPath, subjectIds: [binding.id] }));
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const bindingDecisionIds = confirmedDecisionIdsByBinding.get(binding.id) ?? [];
|
|
967
|
+
if (observationsForBinding.length === 1)
|
|
968
|
+
addCandidate(candidateFromObservation(observationsForBinding[0], binding, bindingDecisionIds));
|
|
969
|
+
else {
|
|
970
|
+
const values = observationsForBinding.map((observation) => canonicalValue(observation.value));
|
|
971
|
+
if (new Set(values).size !== 1) {
|
|
972
|
+
addConflict(conflict({ code: 'BINDING_INTERNAL_CONFLICT', message: 'One confirmed source binding references incompatible observations for the same target path.', targetPath: binding.targetPath, candidateIds: binding.observationIds, relatedIds: [binding.id, ...binding.observationIds], blocking: true }));
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
addCandidate({ ...candidateFromObservation(observationsForBinding[0], binding, bindingDecisionIds), id: binding.id, sourceBindingIds: [binding.id] });
|
|
976
|
+
}
|
|
977
|
+
addTrace(trace({ kind: 'fact', outcome: 'accepted', code: 'SOURCE_BINDING_ELIGIBLE', message: 'Confirmed observation and confirmed source binding jointly support a candidate ontology fact.', targetPath: binding.targetPath, subjectIds: [binding.id, ...binding.observationIds] }));
|
|
978
|
+
}
|
|
979
|
+
const facts = [];
|
|
980
|
+
for (const [path, candidates] of [...candidatesByPath.entries()].sort((left, right) => compareCodeUnits(left[0], right[0]))) {
|
|
981
|
+
if (removePaths.some((removePath) => scopeContainsPath(removePath, path)))
|
|
982
|
+
continue;
|
|
983
|
+
const resolved = factForCandidates(path, candidates);
|
|
984
|
+
if (resolved.conflict) {
|
|
985
|
+
addConflict(resolved.conflict);
|
|
986
|
+
addUnresolved(unresolved({ code: resolved.conflict.code, message: resolved.conflict.message, status: 'unresolved', targetPath: path, relatedIds: resolved.conflict.candidateIds }));
|
|
987
|
+
addTrace(trace({ kind: 'conflict', outcome: 'conflict', code: resolved.conflict.code, message: resolved.conflict.message, targetPath: path, subjectIds: resolved.conflict.candidateIds }));
|
|
988
|
+
questions.push(question({ code: 'SOURCE_CONFLICT_REQUIRES_ADJUDICATION', prompt: 'Which confirmed source should supply this target path?', targetPath: path, assetIds: [], relatedIds: resolved.conflict.candidateIds, blocking: true, status: 'open' }));
|
|
989
|
+
}
|
|
990
|
+
else if (resolved.fact) {
|
|
991
|
+
facts.push(resolved.fact);
|
|
992
|
+
addTrace(trace({ kind: 'fact', outcome: 'accepted', code: 'ONTOLOGY_FACT_ACCEPTED', message: 'Sparse fact admitted from explicit intent, trusted metadata, or jointly confirmed evidence and binding decisions.', targetPath: path, subjectIds: [resolved.fact.id, ...resolved.fact.sourceBindingIds, ...resolved.fact.acceptedByDecisionIds] }));
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const factPaths = new Set(facts.map((fact) => fact.path));
|
|
996
|
+
const unknownPaths = [];
|
|
997
|
+
const unspecifiedPaths = [];
|
|
998
|
+
for (const excluded of safeInput.requestedScopePlan.excludedScopes)
|
|
999
|
+
unspecifiedPaths.push(excluded);
|
|
1000
|
+
for (const intent of changeIntents)
|
|
1001
|
+
if (intent.operation === 'create' && intent.requestedValue === undefined)
|
|
1002
|
+
unspecifiedPaths.push(intent.targetPath);
|
|
1003
|
+
for (const scope of safeInput.requestedScopePlan.scopes) {
|
|
1004
|
+
if (excludedBy(safeInput.requestedScopePlan, scope.ontologyPath)) {
|
|
1005
|
+
unspecifiedPaths.push(scope.ontologyPath);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (!factPaths.has(scope.ontologyPath) && !facts.some((fact) => scopeContainsPath(scope.ontologyPath, fact.path)) && !unresolvedItems.some((item) => item.targetPath && scopeContainsPath(scope.ontologyPath, item.targetPath)))
|
|
1009
|
+
unknownPaths.push(scope.ontologyPath);
|
|
1010
|
+
}
|
|
1011
|
+
if (proposedBindings.size > 0)
|
|
1012
|
+
warnings.push('PROPOSALS_REQUIRE_BINDING_CONFIRMATION');
|
|
1013
|
+
if (conflicts.length > 0)
|
|
1014
|
+
warnings.push('CONFLICTS_REQUIRE_EXPLICIT_ADJUDICATION');
|
|
1015
|
+
const ontology = finalOntology({
|
|
1016
|
+
schemaVersion: ONTOLOGY_INSTANCE_SCHEMA_VERSION,
|
|
1017
|
+
id: hashId('ontology', { caseId: safeInput.caseId, caseRevision: safeInput.caseRevision, contextHash: safeInput.contextHash, requestedScopePlanHash: safeInput.requestedScopePlan.planHash }),
|
|
1018
|
+
caseId: safeInput.caseId,
|
|
1019
|
+
caseRevision: safeInput.caseRevision,
|
|
1020
|
+
contextHash: safeInput.contextHash,
|
|
1021
|
+
requestedScopePlanHash: safeInput.requestedScopePlan.planHash,
|
|
1022
|
+
facts,
|
|
1023
|
+
unknownPaths,
|
|
1024
|
+
unspecifiedPaths,
|
|
1025
|
+
unresolvedItems,
|
|
1026
|
+
conflicts,
|
|
1027
|
+
decisionTrace,
|
|
1028
|
+
});
|
|
1029
|
+
return finalResolverResult({
|
|
1030
|
+
schemaVersion: RESOLVER_RESULT_SCHEMA_VERSION,
|
|
1031
|
+
status: 'ok',
|
|
1032
|
+
resolverId: EVIDENCE_RESOLVER_VERSION,
|
|
1033
|
+
inputHash: resolverInputHash,
|
|
1034
|
+
proposedBindings: [...proposedBindings.values()],
|
|
1035
|
+
proposedBindingDecisions: [...proposedBindingDecisions.values()],
|
|
1036
|
+
confirmedBindings,
|
|
1037
|
+
confirmedBindingDecisions,
|
|
1038
|
+
ontologyInstance: ontology,
|
|
1039
|
+
questions,
|
|
1040
|
+
conflicts,
|
|
1041
|
+
unresolvedItems,
|
|
1042
|
+
decisionTrace,
|
|
1043
|
+
warnings,
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
export const DeterministicEvidenceAndSourceResolver = EvidenceAndSourceResolver;
|
|
1048
|
+
export function resolveEvidenceAndSource(input) {
|
|
1049
|
+
return new EvidenceAndSourceResolver().resolve(input);
|
|
1050
|
+
}
|
|
1051
|
+
export function createEvidenceAndSourceResolver() {
|
|
1052
|
+
return new EvidenceAndSourceResolver();
|
|
1053
|
+
}
|