@tangleai/outcomes 0.24.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/docs/ADAPTERS.md +215 -0
- package/package.json +80 -0
- package/schemas/direction-delta.schema.json +55 -0
- package/schemas/exact-match.schema.json +88 -0
- package/schemas/outcomes.contract.json +4084 -0
- package/schemas/outcomes.schema.json +3936 -0
- package/src/adapters/direction-delta.d.ts +2 -0
- package/src/adapters/direction-delta.gen.d.ts +13 -0
- package/src/adapters/direction-delta.gen.js +3 -0
- package/src/adapters/direction-delta.js +29 -0
- package/src/adapters/exact-match.d.ts +2 -0
- package/src/adapters/exact-match.gen.d.ts +39 -0
- package/src/adapters/exact-match.gen.js +3 -0
- package/src/adapters/exact-match.js +40 -0
- package/src/adapters.d.ts +50 -0
- package/src/adapters.js +1 -0
- package/src/contract.d.ts +3285 -0
- package/src/contract.js +6 -0
- package/src/domain.d.ts +19 -0
- package/src/domain.js +45 -0
- package/src/errors.d.ts +23 -0
- package/src/errors.js +11 -0
- package/src/evaluation.d.ts +49 -0
- package/src/evaluation.js +174 -0
- package/src/handlers.d.ts +15 -0
- package/src/handlers.js +18 -0
- package/src/history.d.ts +4 -0
- package/src/history.js +56 -0
- package/src/identity.d.ts +9 -0
- package/src/identity.js +17 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +9 -0
- package/src/operations.d.ts +16 -0
- package/src/operations.js +108 -0
- package/src/outcomes.contracts.gen.d.ts +1767 -0
- package/src/outcomes.contracts.gen.js +3 -0
- package/src/persistence.d.ts +10 -0
- package/src/persistence.js +88 -0
- package/src/projection.d.ts +13 -0
- package/src/projection.js +59 -0
- package/src/promotion.d.ts +17 -0
- package/src/promotion.js +96 -0
- package/src/proposal-operation.d.ts +17 -0
- package/src/proposal-operation.js +110 -0
- package/src/proposer.d.ts +107 -0
- package/src/proposer.js +89 -0
- package/src/refinement.d.ts +40 -0
- package/src/refinement.js +167 -0
- package/src/resolution.d.ts +12 -0
- package/src/resolution.js +62 -0
- package/src/schema.d.ts +8 -0
- package/src/schema.js +34 -0
- package/src/scoring.d.ts +15 -0
- package/src/scoring.js +37 -0
- package/src/service-context.d.ts +50 -0
- package/src/service-context.js +81 -0
- package/src/service.d.ts +22 -0
- package/src/service.js +159 -0
- package/src/store.d.ts +42 -0
- package/src/store.js +70 -0
- package/src/transitions.d.ts +7 -0
- package/src/transitions.js +56 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ServiceContext } from './service-context.ts';
|
|
2
|
+
import type { OutcomeTransaction } from './store.ts';
|
|
3
|
+
import type { OutcomeAdapter } from './adapters.ts';
|
|
4
|
+
import type { Json, Policy, ReflectInput, ReflectCommand, Issue, Operation, Head } from './outcomes.contracts.gen.ts';
|
|
5
|
+
/** A removed empty container and its new child leaves each count as changes. */
|
|
6
|
+
export declare function changedLeafPaths(before: Json, after: Json): string[];
|
|
7
|
+
export declare function preparePayload(adapter: OutcomeAdapter, policy: Policy, previous: Json, raw: ReflectInput): {
|
|
8
|
+
payload: Json;
|
|
9
|
+
issues: Issue[];
|
|
10
|
+
changed: string[];
|
|
11
|
+
noOp: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function trainingRecords(tx: OutcomeTransaction, context: ServiceContext, artifactKey: string, ids: string[], at: string): Promise<{
|
|
14
|
+
score: import("./outcomes.contracts.gen.ts").Score;
|
|
15
|
+
decision: import("./outcomes.contracts.gen.ts").Decision;
|
|
16
|
+
resolution: import("./outcomes.contracts.gen.ts").Resolution;
|
|
17
|
+
contentDigest: string;
|
|
18
|
+
}[]>;
|
|
19
|
+
export declare function reflectionContext(context: ServiceContext, c: ReflectCommand, capturedHead?: Head): Promise<{
|
|
20
|
+
training: {
|
|
21
|
+
score: import("./outcomes.contracts.gen.ts").Score;
|
|
22
|
+
decision: import("./outcomes.contracts.gen.ts").Decision;
|
|
23
|
+
resolution: import("./outcomes.contracts.gen.ts").Resolution;
|
|
24
|
+
contentDigest: string;
|
|
25
|
+
}[];
|
|
26
|
+
adapter: OutcomeAdapter;
|
|
27
|
+
head: Head;
|
|
28
|
+
previous: Json;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function commitReflection(tx: OutcomeTransaction, context: ServiceContext, c: ReflectCommand, op: Operation, data: Awaited<ReturnType<typeof reflectionContext>>, input: ReflectInput): Promise<{
|
|
31
|
+
reflectionId: string;
|
|
32
|
+
versionId: null;
|
|
33
|
+
noOp: boolean;
|
|
34
|
+
issues: Json;
|
|
35
|
+
} | {
|
|
36
|
+
reflectionId: string;
|
|
37
|
+
versionId: string;
|
|
38
|
+
noOp: boolean;
|
|
39
|
+
issues: Json;
|
|
40
|
+
}>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Bounded payload plans over Jaren's generic guarded engine and JSON Patch. */
|
|
2
|
+
import { checkedHead } from "./promotion.js";
|
|
3
|
+
import { createGuardedRefiner } from '@jarenjs/core/guarded';
|
|
4
|
+
import { cloneJson, equalsJson } from '@jarenjs/core/object';
|
|
5
|
+
import { applyJSONPatch } from '@jarenjs/json/patch';
|
|
6
|
+
import { parseJSONPointer, encodeJSONPointerSegment } from '@jarenjs/json/pointer';
|
|
7
|
+
import { domainValidator, scoreUtility } from "./domain.js";
|
|
8
|
+
import { checkShape, jsonBytes } from "./schema.js";
|
|
9
|
+
import { outcomeRevision } from "./identity.js";
|
|
10
|
+
import { issue, reject, OutcomeRefusal } from "./errors.js";
|
|
11
|
+
import { headFor, semantic, putRecord, unique } from "./persistence.js";
|
|
12
|
+
import { assertHead } from "./transitions.js";
|
|
13
|
+
import { recordOf, seal, asJson } from "./service-context.js";
|
|
14
|
+
const forbidden = new Set(['__proto__', 'prototype', 'constructor']);
|
|
15
|
+
const envelope = new Set(['scope', 'scopeId', 'artifactKey', 'adapter', 'payloadSchema', 'schemaVersion', 'policy', 'policyId', 'parentVersionId', 'approval', 'evaluation', 'evidence', 'id', 'recordedAt']);
|
|
16
|
+
function leaves(value, path = '', output = new Map()) {
|
|
17
|
+
if (value !== null && typeof value === 'object' && Object.keys(value).length) {
|
|
18
|
+
for (const [key, child] of Object.entries(value))
|
|
19
|
+
leaves(child, path + '/' + encodeJSONPointerSegment(key), output);
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
output.set(path, value);
|
|
23
|
+
return output;
|
|
24
|
+
}
|
|
25
|
+
/** A removed empty container and its new child leaves each count as changes. */
|
|
26
|
+
export function changedLeafPaths(before, after) {
|
|
27
|
+
const a = leaves(before), b = leaves(after);
|
|
28
|
+
return [...new Set([...a.keys(), ...b.keys()])].filter(path => !a.has(path) || !b.has(path) || !equalsJson(a.get(path), b.get(path))).sort();
|
|
29
|
+
}
|
|
30
|
+
function payloadPaths(value) {
|
|
31
|
+
if (value !== null && typeof value === 'object')
|
|
32
|
+
for (const [key, child] of Object.entries(value)) {
|
|
33
|
+
if (forbidden.has(key))
|
|
34
|
+
reject('OUTC1009', 'Prototype-related payload members are forbidden.');
|
|
35
|
+
payloadPaths(child);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function preparePayload(adapter, policy, previous, raw) {
|
|
39
|
+
const shape = domainValidator(adapter.schemas.artifact);
|
|
40
|
+
const guarded = createGuardedRefiner({
|
|
41
|
+
read: async () => previous,
|
|
42
|
+
validateProposal(value) {
|
|
43
|
+
const input = checkShape('reflectInput', value);
|
|
44
|
+
if (input.patch.length > policy.maxOperations)
|
|
45
|
+
reject('OUTC1009', 'Patch operation limit exceeded.');
|
|
46
|
+
if (new TextEncoder().encode(input.text).length > policy.maxReflectionBytes)
|
|
47
|
+
reject('OUTC1009', 'Reflection UTF-8 byte limit exceeded.');
|
|
48
|
+
if (input.mode === 'create' && input.patch.length)
|
|
49
|
+
reject('OUTC1009', 'Root creation requires a payload, without a patch.');
|
|
50
|
+
if (input.mode === 'evolve' && input.payload !== null)
|
|
51
|
+
reject('OUTC1009', 'Evolution requires a patch, with payload null.');
|
|
52
|
+
for (const patch of input.patch) {
|
|
53
|
+
let path;
|
|
54
|
+
try {
|
|
55
|
+
path = parseJSONPointer(patch.path);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
reject('OUTC1009', 'Invalid JSON Pointer.');
|
|
59
|
+
}
|
|
60
|
+
if (path.some(p => forbidden.has(p)) || envelope.has(path[0]))
|
|
61
|
+
reject('OUTC1009', 'Patch paths may address only the domain payload.');
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
},
|
|
65
|
+
apply(document, input) { return input.mode === 'create' ? input.payload : applyJSONPatch(document, input.patch); },
|
|
66
|
+
applyFailure: () => issue('OUTC1009', 'The patch does not apply to the captured payload.'),
|
|
67
|
+
validateCandidate(value) {
|
|
68
|
+
const payload = shape(value);
|
|
69
|
+
payloadPaths(payload);
|
|
70
|
+
if (jsonBytes(payload) > policy.maxPayloadBytes)
|
|
71
|
+
reject('OUTC1009', 'Canonical payload UTF-8 byte limit exceeded.');
|
|
72
|
+
return true;
|
|
73
|
+
},
|
|
74
|
+
planCommit(value, before) {
|
|
75
|
+
const payload = shape(adapter.normalizePayload ? adapter.normalizePayload(value) : value);
|
|
76
|
+
payloadPaths(payload);
|
|
77
|
+
if (jsonBytes(payload) > policy.maxPayloadBytes)
|
|
78
|
+
reject('OUTC1009', 'Normalized payload UTF-8 byte limit exceeded.');
|
|
79
|
+
const changed = changedLeafPaths(before, payload);
|
|
80
|
+
const issues = adapter.validatePayload(payload).map(i => checkShape('issue', i));
|
|
81
|
+
if (changed.length > policy.maxChangedLeaves)
|
|
82
|
+
issues.push(issue('OUTC1009', 'Changed leaf path limit exceeded.'));
|
|
83
|
+
return { payload, issues, changed, noOp: raw.mode === 'evolve' && equalsJson(before, payload) };
|
|
84
|
+
},
|
|
85
|
+
commit: async () => { throw new TypeError('Payload preparation never commits through the generic engine.'); },
|
|
86
|
+
});
|
|
87
|
+
const prepared = guarded.prepare(previous, raw);
|
|
88
|
+
if (!prepared.valid || !prepared.plan) {
|
|
89
|
+
const errors = prepared.errors;
|
|
90
|
+
const known = errors[0]?.code;
|
|
91
|
+
// Guarded catches private exceptions; preserve the outcome code without model text.
|
|
92
|
+
const code = known && /^OUTC10(0[1-9]|1[0-9])$/.test(known) ? known : 'OUTC1009';
|
|
93
|
+
throw new OutcomeRefusal([issue(code, 'Payload preparation refused the proposal.')]);
|
|
94
|
+
}
|
|
95
|
+
return prepared.plan;
|
|
96
|
+
}
|
|
97
|
+
export async function trainingRecords(tx, context, artifactKey, ids, at) {
|
|
98
|
+
if (!ids.length || new Set(ids).size !== ids.length)
|
|
99
|
+
reject('OUTC1001', 'Training score ids must be nonempty and unique.');
|
|
100
|
+
const training = [];
|
|
101
|
+
for (const id of [...ids].sort()) {
|
|
102
|
+
const score = await recordOf(tx, id, context.scopeId, artifactKey, 'score');
|
|
103
|
+
const decision = await recordOf(tx, score.decisionId, context.scopeId, artifactKey, 'decision');
|
|
104
|
+
const resolution = await recordOf(tx, score.resolutionId, context.scopeId, artifactKey, 'resolution');
|
|
105
|
+
const { adapter, domain } = context.adapter(decision.adapter);
|
|
106
|
+
const verdict = adapter.score(domain.output(decision.output), domain.resolution(resolution.payload));
|
|
107
|
+
if (resolution.decisionId !== decision.id || score.scorerRevision !== adapter.identity.scorerRevision || score.utility !== scoreUtility(verdict.outcome) || score.outcome !== verdict.outcome || score.recordedAt > at)
|
|
108
|
+
reject('OUTC1002', 'Training evidence or score binding differs.');
|
|
109
|
+
training.push({ score, decision, resolution, contentDigest: await outcomeRevision({ domain: decision.scope.domain, input: decision.input, outcome: resolution.payload }) });
|
|
110
|
+
}
|
|
111
|
+
if (new Set(training.map(t => t.decision.adapter.revision)).size !== 1)
|
|
112
|
+
reject('OUTC1008', 'One artifact lineage has one registered adapter revision.');
|
|
113
|
+
return training;
|
|
114
|
+
}
|
|
115
|
+
export async function reflectionContext(context, c, capturedHead) {
|
|
116
|
+
return context.atomic().transaction(async (tx) => {
|
|
117
|
+
const training = await trainingRecords(tx, context, c.artifactKey, c.input.scoreIds, c.at);
|
|
118
|
+
const adapter = context.adapter(training[0].decision.adapter).adapter;
|
|
119
|
+
const head = capturedHead ?? await headFor(tx, c.scopeId, c.artifactKey);
|
|
120
|
+
if (c.input.mode === 'create' && (head.versionId !== null || c.input.parentVersionId !== null))
|
|
121
|
+
reject('OUTC1013', 'A root can only be staged before a checked head exists.');
|
|
122
|
+
if (c.input.mode === 'evolve' && (head.versionId === null || head.versionId !== c.input.parentVersionId))
|
|
123
|
+
reject('OUTC1013', 'Evolution requires the current checked parent.');
|
|
124
|
+
if (head.versionId !== null && !capturedHead)
|
|
125
|
+
await checkedHead(tx, context, c.artifactKey);
|
|
126
|
+
let parent = null;
|
|
127
|
+
if (head.versionId !== null)
|
|
128
|
+
parent = await recordOf(tx, head.versionId, c.scopeId, c.artifactKey, 'artifactVersion');
|
|
129
|
+
const lineage = await semantic(tx, c.scopeId, 'lineage', c.artifactKey);
|
|
130
|
+
if (lineage) {
|
|
131
|
+
const root = await recordOf(tx, lineage, c.scopeId, c.artifactKey, 'artifactVersion');
|
|
132
|
+
if (root.policyId !== context.policyId || !equalsJson(root.adapter, adapter.identity))
|
|
133
|
+
reject('OUTC1008', 'A schema or policy change requires a new artifact key.');
|
|
134
|
+
}
|
|
135
|
+
if (parent && (parent.policyId !== context.policyId || !equalsJson(parent.adapter, adapter.identity)))
|
|
136
|
+
reject('OUTC1008', 'Parent policy or adapter differs.');
|
|
137
|
+
return { training, adapter, head, previous: parent?.payload ?? adapter.staticPayload };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export async function commitReflection(tx, context, c, op, data, input) {
|
|
141
|
+
const plan = preparePayload(data.adapter, context.policy, data.previous, input);
|
|
142
|
+
const scores = [...input.scoreIds].sort();
|
|
143
|
+
if (!equalsJson(scores, data.training.map(t => t.score.id)) || new Set(input.citations).size !== input.citations.length || input.citations.some(id => !scores.includes(id)))
|
|
144
|
+
reject('OUTC1006', 'Reflection citations must name the provided verified training scores.');
|
|
145
|
+
const current = await headFor(tx, c.scopeId, c.artifactKey);
|
|
146
|
+
try {
|
|
147
|
+
assertHead(current, data.head);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
plan.issues.push(issue('OUTC1013', 'The captured parent head changed during proposal preparation.'));
|
|
151
|
+
}
|
|
152
|
+
const lineage = await semantic(tx, c.scopeId, 'lineage', c.artifactKey);
|
|
153
|
+
if (lineage) {
|
|
154
|
+
const root = await recordOf(tx, lineage, c.scopeId, c.artifactKey, 'artifactVersion');
|
|
155
|
+
if (root.policyId !== context.policyId || !equalsJson(root.adapter, data.adapter.identity))
|
|
156
|
+
reject('OUTC1008', 'Lineage policy or adapter changed.');
|
|
157
|
+
}
|
|
158
|
+
const reflection = await seal('reflection', c.scopeId, c.artifactKey, c.at, { scoreIds: scores, parentVersionId: input.parentVersionId, configuration: input.configuration, text: input.text, citations: [...input.citations].sort(), attemptId: op.id });
|
|
159
|
+
await putRecord(tx, reflection);
|
|
160
|
+
if (plan.noOp)
|
|
161
|
+
return { reflectionId: reflection.id, versionId: null, noOp: true, issues: asJson(plan.issues) };
|
|
162
|
+
const version = await seal('artifactVersion', c.scopeId, c.artifactKey, c.at, { parentVersionId: input.parentVersionId, expectedHead: data.head, payload: plan.payload, payloadSchema: data.adapter.identity.artifactSchema, adapter: data.adapter.identity, reflectionId: reflection.id, mode: input.mode, policyId: context.policyId, policy: context.policy, issues: plan.issues });
|
|
163
|
+
await putRecord(tx, version);
|
|
164
|
+
if (!lineage)
|
|
165
|
+
await unique(tx, c.scopeId, 'lineage', c.artifactKey, version.id);
|
|
166
|
+
return { reflectionId: reflection.id, versionId: version.id, noOp: false, issues: asJson(plan.issues) };
|
|
167
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ServiceContext } from './service-context.ts';
|
|
2
|
+
import type { OutcomeTransaction } from './store.ts';
|
|
3
|
+
import type { Source, SourceRef, ResolveCommand } from './outcomes.contracts.gen.ts';
|
|
4
|
+
export declare function verifiedSource(context: ServiceContext, reference: SourceRef, earliest: string, receivedAt: string): Promise<Source>;
|
|
5
|
+
export declare function prepareResolution(context: ServiceContext, c: ResolveCommand): Promise<{
|
|
6
|
+
decision: import("./outcomes.contracts.gen.ts").Decision;
|
|
7
|
+
sources: Source[];
|
|
8
|
+
payload: import("./outcomes.contracts.gen.ts").Json;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function commitResolution(tx: OutcomeTransaction, context: ServiceContext, c: ResolveCommand, data: Awaited<ReturnType<typeof prepareResolution>>): Promise<{
|
|
11
|
+
resolutionId: string;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Accepted evidence is a verified host snapshot, independent of scoring. */
|
|
2
|
+
import { equalsJson } from '@jarenjs/core/object';
|
|
3
|
+
import { checkShape, checkTime, jsonBytes } from "./schema.js";
|
|
4
|
+
import { outcomeRevision } from "./identity.js";
|
|
5
|
+
import { reject } from "./errors.js";
|
|
6
|
+
import { putRecord, unique } from "./persistence.js";
|
|
7
|
+
import { recordOf, requireNew, seal } from "./service-context.js";
|
|
8
|
+
export async function verifiedSource(context, reference, earliest, receivedAt) {
|
|
9
|
+
const loaded = await context.resolver.resolve(reference, context.scope);
|
|
10
|
+
if (!loaded)
|
|
11
|
+
reject('OUTC1006', 'The trusted resolver cannot supply the pinned source.');
|
|
12
|
+
const source = checkShape('source', loaded);
|
|
13
|
+
if (source.scopeId !== context.scopeId || source.subject !== context.scope.subject)
|
|
14
|
+
reject('OUTC1003', 'Evidence belongs to another scope or subject.');
|
|
15
|
+
const { digest, ...bytes } = source;
|
|
16
|
+
if (source.sourceId !== reference.sourceId || digest !== reference.digest || await outcomeRevision(bytes) !== digest)
|
|
17
|
+
reject('OUTC1006', 'Evidence bytes differ from the pinned source.');
|
|
18
|
+
checkTime(source.observedAt);
|
|
19
|
+
checkTime(receivedAt);
|
|
20
|
+
if (source.observedAt < earliest || receivedAt < source.observedAt)
|
|
21
|
+
reject('OUTC1006', 'Evidence observation is outside the accepted time interval.');
|
|
22
|
+
if (jsonBytes(source) > 32768)
|
|
23
|
+
reject('OUTC1006', 'Evidence snapshot exceeds 32,768 canonical UTF-8 bytes.');
|
|
24
|
+
return source;
|
|
25
|
+
}
|
|
26
|
+
export async function prepareResolution(context, c) {
|
|
27
|
+
const decision = await context.atomic().transaction(async (tx) => {
|
|
28
|
+
await requireNew(tx, c.scopeId, 'resolution', c.input.decisionId);
|
|
29
|
+
return recordOf(tx, c.input.decisionId, c.scopeId, c.artifactKey, 'decision');
|
|
30
|
+
});
|
|
31
|
+
const { domain } = context.adapter(decision.adapter);
|
|
32
|
+
if (new Set(c.input.evidence.map(r => r.sourceId)).size !== c.input.evidence.length)
|
|
33
|
+
reject('OUTC1001', 'Evidence references must be unique.');
|
|
34
|
+
if (c.at < c.input.receivedAt)
|
|
35
|
+
reject('OUTC1006', 'Receipt time precedes evidence arrival.');
|
|
36
|
+
const sources = [];
|
|
37
|
+
for (const reference of c.input.evidence) {
|
|
38
|
+
const source = await verifiedSource(context, reference, decision.decidedAt, c.input.receivedAt);
|
|
39
|
+
if (source.decisionId !== decision.id)
|
|
40
|
+
reject('OUTC1003', 'Evidence was observed for another decision.');
|
|
41
|
+
sources.push(source);
|
|
42
|
+
}
|
|
43
|
+
const payload = domain.resolution(sources[0].payload);
|
|
44
|
+
if (jsonBytes(sources) > 32768)
|
|
45
|
+
reject('OUTC1006', 'Combined evidence snapshots exceed 32,768 canonical UTF-8 bytes.');
|
|
46
|
+
for (const source of sources)
|
|
47
|
+
if (!equalsJson(domain.resolution(source.payload), payload))
|
|
48
|
+
reject('OUTC1006', 'Sources disagree on the accepted outcome.');
|
|
49
|
+
return { decision, sources: sources.sort((a, b) => a.sourceId < b.sourceId ? -1 : 1), payload };
|
|
50
|
+
}
|
|
51
|
+
export async function commitResolution(tx, context, c, data) {
|
|
52
|
+
await requireNew(tx, c.scopeId, 'resolution', c.input.decisionId);
|
|
53
|
+
const resolution = await seal('resolution', c.scopeId, c.artifactKey, c.at, {
|
|
54
|
+
decisionId: data.decision.id, resolverRevision: context.resolverRevision, sources: data.sources,
|
|
55
|
+
payload: data.payload, receivedAt: c.input.receivedAt,
|
|
56
|
+
late: data.decision.expectedResolutionAt === null ? null : c.input.receivedAt > data.decision.expectedResolutionAt,
|
|
57
|
+
resolutionSchema: data.decision.adapter.resolutionSchema,
|
|
58
|
+
});
|
|
59
|
+
await putRecord(tx, resolution);
|
|
60
|
+
await unique(tx, c.scopeId, 'resolution', data.decision.id, resolution.id);
|
|
61
|
+
return { resolutionId: resolution.id };
|
|
62
|
+
}
|
package/src/schema.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import outcomesSchema from '../schemas/outcomes.schema.json' with { type: "json" };
|
|
2
|
+
import type { Json, OutcomeRecord, Policy } from './outcomes.contracts.gen.ts';
|
|
3
|
+
export { outcomesSchema };
|
|
4
|
+
export declare function checkShape<T>(name: string, value: unknown): T;
|
|
5
|
+
export declare function checkTime(value: string): string;
|
|
6
|
+
export declare const DEFAULT_OUTCOME_POLICY: Readonly<Policy>;
|
|
7
|
+
export declare function jsonBytes(value: Json): number;
|
|
8
|
+
export declare function checkRecordShape(value: unknown): OutcomeRecord;
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** One schema authority. Canonicalization rejects non-JSON input before cloning. */
|
|
2
|
+
import { JarenValidator } from '@jarenjs/validate';
|
|
3
|
+
import { canonicalizeJson } from '@jarenjs/json/canonical';
|
|
4
|
+
import { cloneJson, deepFreeze } from '@jarenjs/core/object';
|
|
5
|
+
import outcomesSchema from '../schemas/outcomes.schema.json' with { type: 'json' };
|
|
6
|
+
import { reject } from "./errors.js";
|
|
7
|
+
export { outcomesSchema };
|
|
8
|
+
const validators = new Map();
|
|
9
|
+
export function checkShape(name, value) {
|
|
10
|
+
try {
|
|
11
|
+
canonicalizeJson(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
reject('OUTC1001', 'Only finite JSON data is accepted.');
|
|
15
|
+
}
|
|
16
|
+
let validate = validators.get(name);
|
|
17
|
+
if (!validate) {
|
|
18
|
+
const v = new JarenValidator({ skipErrors: false, collectErrors: true, unknownFormats: 'ignore' });
|
|
19
|
+
validate = v.compile({ $defs: outcomesSchema.$defs, $ref: `#/$defs/${name}` });
|
|
20
|
+
validators.set(name, validate);
|
|
21
|
+
}
|
|
22
|
+
const result = validate(value);
|
|
23
|
+
if (!result.valid)
|
|
24
|
+
reject('OUTC1001', `Invalid ${name} data.`);
|
|
25
|
+
return deepFreeze(cloneJson(value));
|
|
26
|
+
}
|
|
27
|
+
export function checkTime(value) {
|
|
28
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value)
|
|
29
|
+
reject('OUTC1001', 'Expected a valid normalized UTC instant.');
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
export const DEFAULT_OUTCOME_POLICY = Object.freeze({ maxVersions: 10, maxPayloadBytes: 32768, maxOperations: 32, maxChangedLeaves: 32, maxReflectionBytes: 8192 });
|
|
33
|
+
export function jsonBytes(value) { return new TextEncoder().encode(canonicalizeJson(value)).length; }
|
|
34
|
+
export function checkRecordShape(value) { const r = checkShape('outcomeRecord', value); checkTime(r.recordedAt); return r; }
|
package/src/scoring.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ServiceContext } from './service-context.ts';
|
|
2
|
+
import type { OutcomeTransaction } from './store.ts';
|
|
3
|
+
import type { ScoreCommand } from './outcomes.contracts.gen.ts';
|
|
4
|
+
export declare function prepareScore(context: ServiceContext, c: ScoreCommand): Promise<{
|
|
5
|
+
resolution: import("./outcomes.contracts.gen.ts").Resolution;
|
|
6
|
+
decision: import("./outcomes.contracts.gen.ts").Decision;
|
|
7
|
+
outcome: "success" | "failure" | "partial";
|
|
8
|
+
diagnostics: import("./outcomes.contracts.gen.ts").Json;
|
|
9
|
+
memoryIds: string[];
|
|
10
|
+
authorizationId: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function commitScore(tx: OutcomeTransaction, context: ServiceContext, c: ScoreCommand, data: Awaited<ReturnType<typeof prepareScore>>): Promise<{
|
|
13
|
+
scoreId: string;
|
|
14
|
+
projectionIntentId: string;
|
|
15
|
+
}>;
|
package/src/scoring.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Scores commit with a projection intent; projection is independently retryable. */
|
|
2
|
+
import { checkShape } from "./schema.js";
|
|
3
|
+
import { outcomeRevision } from "./identity.js";
|
|
4
|
+
import { scoreUtility } from "./domain.js";
|
|
5
|
+
import { reject } from "./errors.js";
|
|
6
|
+
import { putRecord, unique } from "./persistence.js";
|
|
7
|
+
import { recordOf, requireNew, seal, asJson } from "./service-context.js";
|
|
8
|
+
export async function prepareScore(context, c) {
|
|
9
|
+
const { resolution, decision } = await context.atomic().transaction(async (tx) => {
|
|
10
|
+
const resolution = await recordOf(tx, c.input.resolutionId, c.scopeId, c.artifactKey, 'resolution');
|
|
11
|
+
await requireNew(tx, c.scopeId, 'score', resolution.decisionId);
|
|
12
|
+
const decision = await recordOf(tx, resolution.decisionId, c.scopeId, c.artifactKey, 'decision');
|
|
13
|
+
return { resolution, decision };
|
|
14
|
+
});
|
|
15
|
+
const { adapter, domain } = context.adapter(decision.adapter);
|
|
16
|
+
if (resolution.resolutionSchema !== adapter.identity.resolutionSchema || c.at < resolution.recordedAt)
|
|
17
|
+
reject('OUTC1008', 'Score schema or chronology differs from its resolution.');
|
|
18
|
+
const answer = adapter.score(domain.output(decision.output), domain.resolution(resolution.payload));
|
|
19
|
+
const outcome = checkShape('category', answer.outcome);
|
|
20
|
+
const diagnostics = asJson({ adapter: answer.diagnostics, inputDigest: await outcomeRevision(decision.input), outputDigest: await outcomeRevision(decision.output), resolutionDigest: await outcomeRevision(resolution.payload) });
|
|
21
|
+
const memoryIds = [...new Set(decision.memoryIds)].sort();
|
|
22
|
+
const authorizationId = await context.authorization(memoryIds);
|
|
23
|
+
return { resolution, decision, outcome, diagnostics, memoryIds, authorizationId };
|
|
24
|
+
}
|
|
25
|
+
export async function commitScore(tx, context, c, data) {
|
|
26
|
+
await requireNew(tx, c.scopeId, 'score', data.decision.id);
|
|
27
|
+
const score = await seal('score', c.scopeId, c.artifactKey, c.at, {
|
|
28
|
+
decisionId: data.decision.id, resolutionId: data.resolution.id, scorerRevision: data.decision.adapter.scorerRevision,
|
|
29
|
+
outcome: data.outcome, utility: scoreUtility(data.outcome), diagnostics: data.diagnostics, policyId: context.confidencePolicyId,
|
|
30
|
+
});
|
|
31
|
+
const intent = await seal('projectionIntent', c.scopeId, c.artifactKey, c.at, { scoreId: score.id, memoryIds: data.memoryIds, authorizationId: data.authorizationId, policyId: context.confidencePolicyId });
|
|
32
|
+
await putRecord(tx, score);
|
|
33
|
+
await putRecord(tx, intent);
|
|
34
|
+
await unique(tx, c.scopeId, 'score', data.decision.id, score.id);
|
|
35
|
+
await unique(tx, c.scopeId, 'projectionIntent', score.id, intent.id);
|
|
36
|
+
return { scoreId: score.id, projectionIntentId: intent.id };
|
|
37
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { OutcomeStore, OutcomeTransaction } from './store.ts';
|
|
2
|
+
import type { OutcomeAdapter, OutcomeHost, OutcomePrincipal } from './adapters.ts';
|
|
3
|
+
import type { AdapterIdentity, ConfidencePolicy, Json, OutcomeRecord, Policy, Configuration } from './outcomes.contracts.gen.ts';
|
|
4
|
+
export interface OutcomeServiceOptions extends OutcomeHost {
|
|
5
|
+
store: OutcomeStore;
|
|
6
|
+
policy?: Policy;
|
|
7
|
+
confidencePolicy?: ConfidencePolicy;
|
|
8
|
+
}
|
|
9
|
+
export declare const asJson: (value: unknown) => Json;
|
|
10
|
+
export declare function makeContext(options: OutcomeServiceOptions): Promise<{
|
|
11
|
+
principal: Readonly<OutcomePrincipal>;
|
|
12
|
+
scope: import("./outcomes.contracts.gen.ts").Scope;
|
|
13
|
+
scopeId: string;
|
|
14
|
+
policy: Policy;
|
|
15
|
+
policyId: string;
|
|
16
|
+
confidencePolicy: ConfidencePolicy;
|
|
17
|
+
confidencePolicyId: string;
|
|
18
|
+
resolverRevision: string;
|
|
19
|
+
gatePolicyId: string;
|
|
20
|
+
configuration(configuration: Configuration): Promise<import("@tangleai/config").RunIdentity | null>;
|
|
21
|
+
atomic(): import("./store.ts").OutcomePersistence;
|
|
22
|
+
adapter(identity: AdapterIdentity): {
|
|
23
|
+
adapter: OutcomeAdapter;
|
|
24
|
+
domain: {
|
|
25
|
+
input: (value: unknown) => Json;
|
|
26
|
+
output: (value: unknown) => Json;
|
|
27
|
+
resolution: (value: unknown) => Json;
|
|
28
|
+
artifact(value: unknown): Json;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
authorization(ids: readonly string[]): Promise<string>;
|
|
32
|
+
store: OutcomeStore;
|
|
33
|
+
adapters: readonly OutcomeAdapter[];
|
|
34
|
+
resolver: import("./adapters.ts").EvidenceResolver;
|
|
35
|
+
authorizeMemoryIds(ids: readonly string[], scope: import("./outcomes.contracts.gen.ts").Scope): Promise<{
|
|
36
|
+
allowed: boolean;
|
|
37
|
+
authorizationId: string;
|
|
38
|
+
}>;
|
|
39
|
+
evaluationSlot?(slotId: string, versionId: string, scope: import("./outcomes.contracts.gen.ts").Scope): Promise<import("./outcomes.contracts.gen.ts").EvaluationSlot | undefined>;
|
|
40
|
+
proposer?: import("./adapters.ts").OutcomeProposer;
|
|
41
|
+
resolveConfiguration?(identityId: string): Promise<import("@tangleai/config").RunIdentity | undefined>;
|
|
42
|
+
}>;
|
|
43
|
+
export type ServiceContext = Awaited<ReturnType<typeof makeContext>>;
|
|
44
|
+
export declare function recordOf<K extends OutcomeRecord['kind']>(tx: OutcomeTransaction, id: string, scopeId: string, artifactKey: string, kind: K): Promise<Extract<OutcomeRecord, {
|
|
45
|
+
kind: K;
|
|
46
|
+
}>>;
|
|
47
|
+
export declare const seal: <K extends OutcomeRecord["kind"]>(kind: K, scopeId: string, artifactKey: string, at: string, data: object) => Promise<Extract<OutcomeRecord, {
|
|
48
|
+
kind: K;
|
|
49
|
+
}>>;
|
|
50
|
+
export declare function requireNew(tx: OutcomeTransaction, scopeId: string, kind: string, key: Json): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** A service binds trusted host capabilities before accepting request JSON. */
|
|
2
|
+
import { equalsJson } from '@jarenjs/core/object';
|
|
3
|
+
import { DEFAULT_OUTCOME_OPTIONS } from '@tangleai/memory/outcome';
|
|
4
|
+
import { checkShape, DEFAULT_OUTCOME_POLICY } from "./schema.js";
|
|
5
|
+
import { outcomeRevision, scopeIdOf, sealRecord } from "./identity.js";
|
|
6
|
+
import { checkedAdapter } from "./domain.js";
|
|
7
|
+
import { reject } from "./errors.js";
|
|
8
|
+
import { readRecord, semantic } from "./persistence.js";
|
|
9
|
+
import { persistenceFor } from "./store.js";
|
|
10
|
+
import { outcomeGatePolicyId } from "./evaluation.js";
|
|
11
|
+
import { validateRunIdentity, identityIdOf } from '@tangleai/config';
|
|
12
|
+
export const asJson = (value) => checkShape('json', value);
|
|
13
|
+
export async function makeContext(options) {
|
|
14
|
+
const scope = checkShape('scope', options.scope), scopeId = await scopeIdOf(scope);
|
|
15
|
+
const policy = checkShape('policy', options.policy ?? DEFAULT_OUTCOME_POLICY);
|
|
16
|
+
const confidencePolicy = checkShape('confidencePolicy', options.confidencePolicy ?? DEFAULT_OUTCOME_OPTIONS);
|
|
17
|
+
if (confidencePolicy.minConfidence > confidencePolicy.maxConfidence)
|
|
18
|
+
throw new TypeError('Confidence minimum exceeds maximum.');
|
|
19
|
+
const confidencePolicyId = await outcomeRevision(confidencePolicy), policyId = await outcomeRevision(policy);
|
|
20
|
+
const principal = Object.freeze(checkShape('principal', options.principal ?? { id: 'unprivileged', authorityId: await outcomeRevision({ authority: 'none' }), approve: false, reconcile: false }));
|
|
21
|
+
const registry = new Map();
|
|
22
|
+
for (const adapter of options.adapters) {
|
|
23
|
+
const identity = checkShape('adapterIdentity', adapter.identity);
|
|
24
|
+
for (const key of ['input', 'output', 'resolution', 'artifact']) {
|
|
25
|
+
if (await outcomeRevision(adapter.schemas[key]) !== identity[`${key}Schema`])
|
|
26
|
+
throw new TypeError('Adapter schema identity differs.');
|
|
27
|
+
}
|
|
28
|
+
if (registry.has(identity.revision))
|
|
29
|
+
throw new TypeError('Duplicate adapter revision.');
|
|
30
|
+
registry.set(identity.revision, adapter);
|
|
31
|
+
}
|
|
32
|
+
const resolverRevision = checkShape('hash', options.resolver.revision);
|
|
33
|
+
return {
|
|
34
|
+
...options, principal, scope, scopeId, policy, policyId, confidencePolicy, confidencePolicyId, resolverRevision,
|
|
35
|
+
gatePolicyId: await outcomeGatePolicyId(asJson(policy)),
|
|
36
|
+
async configuration(configuration) {
|
|
37
|
+
if (configuration.kind === 'scripted')
|
|
38
|
+
return null;
|
|
39
|
+
const identity = options.proposer?.identity.identityId === configuration.identityId ? options.proposer.identity : await options.resolveConfiguration?.(configuration.identityId);
|
|
40
|
+
const checked = validateRunIdentity(identity);
|
|
41
|
+
if (!checked.ok)
|
|
42
|
+
reject('OUTC1008', 'The model configuration identity is not registered.');
|
|
43
|
+
const { identityId, ...payload } = checked.value;
|
|
44
|
+
if (identityId !== configuration.identityId || await identityIdOf(payload) !== identityId)
|
|
45
|
+
reject('OUTC1008', 'Model configuration bytes differ from their identity.');
|
|
46
|
+
return checked.value;
|
|
47
|
+
},
|
|
48
|
+
atomic() {
|
|
49
|
+
try {
|
|
50
|
+
return persistenceFor(options.store);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
reject('OUTC1018', 'An outcome store must own memory and receipts atomically.');
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
adapter(identity) {
|
|
57
|
+
const adapter = registry.get(identity.revision);
|
|
58
|
+
if (!adapter || !equalsJson(adapter.identity, identity))
|
|
59
|
+
reject('OUTC1008', 'The pinned adapter revision is not registered.');
|
|
60
|
+
return { adapter, domain: checkedAdapter(adapter) };
|
|
61
|
+
},
|
|
62
|
+
async authorization(ids) {
|
|
63
|
+
const answer = await options.authorizeMemoryIds(ids, scope);
|
|
64
|
+
if (!answer.allowed)
|
|
65
|
+
reject('OUTC1003', 'The host refused memory citations for this scope.');
|
|
66
|
+
return checkShape('hash', answer.authorizationId);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function recordOf(tx, id, scopeId, artifactKey, kind) {
|
|
71
|
+
const record = await readRecord(tx, id, scopeId, artifactKey);
|
|
72
|
+
if (record.kind !== kind)
|
|
73
|
+
reject('OUTC1005', `This transition requires a ${kind} record.`);
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
export const seal = async (kind, scopeId, artifactKey, at, data) => await sealRecord({ ...data, schemaVersion: 1, kind, scopeId, artifactKey, recordedAt: at });
|
|
77
|
+
export async function requireNew(tx, scopeId, kind, key) {
|
|
78
|
+
const id = await semantic(tx, scopeId, kind, key);
|
|
79
|
+
if (id)
|
|
80
|
+
reject('OUTC1007', `The ${kind} stage is already complete: ${id}.`);
|
|
81
|
+
}
|
package/src/service.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OutcomeServiceOptions } from './service-context.ts';
|
|
2
|
+
import type { Result } from './outcomes.contracts.gen.ts';
|
|
3
|
+
export type { OutcomeServiceOptions } from './service-context.ts';
|
|
4
|
+
export declare function createOutcomeService(options: OutcomeServiceOptions): Promise<Readonly<{
|
|
5
|
+
scopeId: string;
|
|
6
|
+
policyId: string;
|
|
7
|
+
gatePolicyId: string;
|
|
8
|
+
create: (raw: unknown) => Promise<Result>;
|
|
9
|
+
resolve: (raw: unknown) => Promise<Result>;
|
|
10
|
+
score: (raw: unknown) => Promise<Result>;
|
|
11
|
+
project: (raw: unknown) => Promise<Result>;
|
|
12
|
+
reflect(raw: unknown): Promise<Result>;
|
|
13
|
+
evaluate: (raw: unknown) => Promise<Result>;
|
|
14
|
+
approve: (raw: unknown) => Promise<Result>;
|
|
15
|
+
promote: (raw: unknown) => Promise<Result>;
|
|
16
|
+
rollback: (raw: unknown) => Promise<Result>;
|
|
17
|
+
reconcile: (raw: unknown) => Promise<Result>;
|
|
18
|
+
injectChecked(raw: unknown): Promise<Result>;
|
|
19
|
+
history(raw: unknown): Promise<Result>;
|
|
20
|
+
inspect(raw: unknown): Promise<Result>;
|
|
21
|
+
}>>;
|
|
22
|
+
export type OutcomeService = Awaited<ReturnType<typeof createOutcomeService>>;
|