@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +142 -0
  4. package/docs/ADAPTERS.md +215 -0
  5. package/package.json +80 -0
  6. package/schemas/direction-delta.schema.json +55 -0
  7. package/schemas/exact-match.schema.json +88 -0
  8. package/schemas/outcomes.contract.json +4084 -0
  9. package/schemas/outcomes.schema.json +3936 -0
  10. package/src/adapters/direction-delta.d.ts +2 -0
  11. package/src/adapters/direction-delta.gen.d.ts +13 -0
  12. package/src/adapters/direction-delta.gen.js +3 -0
  13. package/src/adapters/direction-delta.js +29 -0
  14. package/src/adapters/exact-match.d.ts +2 -0
  15. package/src/adapters/exact-match.gen.d.ts +39 -0
  16. package/src/adapters/exact-match.gen.js +3 -0
  17. package/src/adapters/exact-match.js +40 -0
  18. package/src/adapters.d.ts +50 -0
  19. package/src/adapters.js +1 -0
  20. package/src/contract.d.ts +3285 -0
  21. package/src/contract.js +6 -0
  22. package/src/domain.d.ts +19 -0
  23. package/src/domain.js +45 -0
  24. package/src/errors.d.ts +23 -0
  25. package/src/errors.js +11 -0
  26. package/src/evaluation.d.ts +49 -0
  27. package/src/evaluation.js +174 -0
  28. package/src/handlers.d.ts +15 -0
  29. package/src/handlers.js +18 -0
  30. package/src/history.d.ts +4 -0
  31. package/src/history.js +56 -0
  32. package/src/identity.d.ts +9 -0
  33. package/src/identity.js +17 -0
  34. package/src/index.d.ts +15 -0
  35. package/src/index.js +9 -0
  36. package/src/operations.d.ts +16 -0
  37. package/src/operations.js +108 -0
  38. package/src/outcomes.contracts.gen.d.ts +1767 -0
  39. package/src/outcomes.contracts.gen.js +3 -0
  40. package/src/persistence.d.ts +10 -0
  41. package/src/persistence.js +88 -0
  42. package/src/projection.d.ts +13 -0
  43. package/src/projection.js +59 -0
  44. package/src/promotion.d.ts +17 -0
  45. package/src/promotion.js +96 -0
  46. package/src/proposal-operation.d.ts +17 -0
  47. package/src/proposal-operation.js +110 -0
  48. package/src/proposer.d.ts +107 -0
  49. package/src/proposer.js +89 -0
  50. package/src/refinement.d.ts +40 -0
  51. package/src/refinement.js +167 -0
  52. package/src/resolution.d.ts +12 -0
  53. package/src/resolution.js +62 -0
  54. package/src/schema.d.ts +8 -0
  55. package/src/schema.js +34 -0
  56. package/src/scoring.d.ts +15 -0
  57. package/src/scoring.js +37 -0
  58. package/src/service-context.d.ts +50 -0
  59. package/src/service-context.js +81 -0
  60. package/src/service.d.ts +22 -0
  61. package/src/service.js +159 -0
  62. package/src/store.d.ts +42 -0
  63. package/src/store.js +70 -0
  64. package/src/transitions.d.ts +7 -0
  65. package/src/transitions.js +56 -0
package/src/service.js ADDED
@@ -0,0 +1,159 @@
1
+ /** Explicit lifecycle commands. External host work runs outside transactions. */
2
+ import { historyPage, inspectRecord } from "./history.js";
3
+ import { equalsJson } from '@jarenjs/core/object';
4
+ import { checkShape, checkTime } from "./schema.js";
5
+ import { failure, reject } from "./errors.js";
6
+ import { keyId } from "./identity.js";
7
+ import { beginOperation, finishOperation } from "./operations.js";
8
+ import { readRecord, putRecord, unique, headFor } from "./persistence.js";
9
+ import { makeContext, asJson, recordOf, requireNew, seal } from "./service-context.js";
10
+ import { prepareResolution, commitResolution } from "./resolution.js";
11
+ import { prepareScore, commitScore } from "./scoring.js";
12
+ import { prepareProjection, commitProjection } from "./projection.js";
13
+ import { reflectionContext, commitReflection } from "./refinement.js";
14
+ import { prepareEvaluation, commitEvaluation } from "./evaluation.js";
15
+ import { commitApproval, commitActivation, checkedHead } from "./promotion.js";
16
+ import { modelProposal, prepareReconciliation, commitReconciliation } from "./proposal-operation.js";
17
+ const liveReflections = new WeakMap();
18
+ export async function createOutcomeService(options) {
19
+ const context = await makeContext(options);
20
+ function boundary(shape, value) {
21
+ const c = checkShape(shape, value);
22
+ if (c.scopeId !== context.scopeId)
23
+ reject('OUTC1003', 'Request scope differs from its host.');
24
+ context.atomic();
25
+ return c;
26
+ }
27
+ async function mutate(name, raw, prepare, commit) {
28
+ try {
29
+ const c = boundary(`${name}Command`, raw);
30
+ checkTime(c.at);
31
+ if ((name === 'approve' && !context.principal.approve) || (name === 'reconcile' && !context.principal.reconcile))
32
+ reject('OUTC1012', 'The host principal lacks this capability.');
33
+ const reservation = await beginOperation(context.store, name, c);
34
+ if (reservation.replay)
35
+ return reservation.replay;
36
+ let data;
37
+ try {
38
+ data = await prepare(c, reservation.operation);
39
+ }
40
+ catch (error) {
41
+ return await finishOperation(context.store, reservation.operation, c.at, async () => { throw error; });
42
+ }
43
+ return await finishOperation(context.store, reservation.operation, c.at, tx => commit(tx, c, data));
44
+ }
45
+ catch (error) {
46
+ return failure(error);
47
+ }
48
+ }
49
+ return Object.freeze({
50
+ scopeId: context.scopeId,
51
+ policyId: context.policyId,
52
+ gatePolicyId: context.gatePolicyId,
53
+ create: (raw) => mutate('create', raw, async (c) => {
54
+ const i = c.input;
55
+ checkTime(i.cutoffAt);
56
+ checkTime(i.decidedAt);
57
+ if (i.expectedResolutionAt !== null)
58
+ checkTime(i.expectedResolutionAt);
59
+ if (i.cutoffAt > i.decidedAt || c.at < i.decidedAt || (i.expectedResolutionAt !== null && i.expectedResolutionAt < i.decidedAt))
60
+ reject('OUTC1001', 'Decision chronology is invalid.');
61
+ const { domain } = context.adapter(i.adapter);
62
+ domain.input(i.input);
63
+ domain.output(i.output);
64
+ domain.artifact(i.staticPayload);
65
+ await context.configuration(i.configuration);
66
+ await context.authorization([...new Set(i.memoryIds)].sort());
67
+ }, async (tx, c) => {
68
+ const i = c.input, { adapter, domain } = context.adapter(i.adapter);
69
+ await requireNew(tx, c.scopeId, 'decision', i.decisionKey);
70
+ if (!equalsJson(domain.artifact(i.staticPayload), domain.artifact(adapter.staticPayload)))
71
+ reject('OUTC1008', 'Static baseline differs from the registered adapter.');
72
+ if (i.usedVersionId !== null) {
73
+ const checked = await checkedHead(tx, context, c.artifactKey);
74
+ const version = await recordOf(tx, i.usedVersionId, c.scopeId, c.artifactKey, 'artifactVersion');
75
+ const head = await headFor(tx, c.scopeId, c.artifactKey);
76
+ if (head.versionId !== version.id || !equalsJson(version.adapter, i.adapter))
77
+ reject('OUTC1013', 'Decision did not use the current checked artifact.');
78
+ if (!equalsJson(domain.output(adapter.interpret(i.input, checked.payload)), i.output))
79
+ reject('OUTC1008', 'Decision output does not reproduce with its checked artifact.');
80
+ }
81
+ const decision = await seal('decision', c.scopeId, c.artifactKey, c.at, { ...i, scope: context.scope });
82
+ await putRecord(tx, decision);
83
+ await unique(tx, c.scopeId, 'decision', i.decisionKey, decision.id);
84
+ return { decisionId: decision.id };
85
+ }),
86
+ resolve: (raw) => mutate('resolve', raw, c => prepareResolution(context, c), (tx, c, data) => commitResolution(tx, context, c, data)),
87
+ score: (raw) => mutate('score', raw, c => prepareScore(context, c), (tx, c, data) => commitScore(tx, context, c, data)),
88
+ project: (raw) => mutate('project', raw, c => prepareProjection(context, c), (tx, c, data) => commitProjection(tx, context, c, data)),
89
+ async reflect(raw) {
90
+ let activeId;
91
+ let active = liveReflections.get(context.store);
92
+ if (!active) {
93
+ active = new Set();
94
+ liveReflections.set(context.store, active);
95
+ }
96
+ try {
97
+ const c = boundary('reflectCommand', raw);
98
+ checkTime(c.at);
99
+ const requestId = await keyId(c.scopeId, 'request', c.requestKey);
100
+ if (active.has(requestId))
101
+ reject('OUTC1019', 'This proposal is already in progress in this host.');
102
+ activeId = requestId;
103
+ active.add(requestId);
104
+ const reservation = await beginOperation(context.store, 'reflect', c, context.policy);
105
+ if (reservation.replay)
106
+ return reservation.replay;
107
+ try {
108
+ const saved = reservation.operation.output === null ? null : checkShape('preparedModelOutput', reservation.operation.output);
109
+ const data = await reflectionContext(context, c, saved?.head);
110
+ const input = c.input.configuration.kind === 'scripted' ? c.input : await modelProposal(context, c, reservation.operation, data);
111
+ return await finishOperation(context.store, reservation.operation, c.at, tx => commitReflection(tx, context, c, reservation.operation, data, input));
112
+ }
113
+ catch (error) {
114
+ return await finishOperation(context.store, reservation.operation, c.at, async () => { throw error; });
115
+ }
116
+ }
117
+ catch (error) {
118
+ return failure(error);
119
+ }
120
+ finally {
121
+ if (activeId)
122
+ active.delete(activeId);
123
+ }
124
+ },
125
+ evaluate: (raw) => mutate('evaluate', raw, (c, op) => prepareEvaluation(context, c, op), (tx, c, data) => commitEvaluation(tx, context, c, data)),
126
+ approve: (raw) => mutate('approve', raw, async (_c, op) => op, (tx, c, op) => commitApproval(tx, context, c, op)),
127
+ promote: (raw) => mutate('promote', raw, async () => { }, (tx, c) => commitActivation(tx, context, c, 'promote')),
128
+ rollback: (raw) => mutate('rollback', raw, async () => { }, (tx, c) => commitActivation(tx, context, c, 'rollback')),
129
+ reconcile: (raw) => mutate('reconcile', raw, c => prepareReconciliation(context, c), (tx, c, data) => commitReconciliation(tx, context, c, data)),
130
+ async injectChecked(raw) {
131
+ try {
132
+ const c = boundary('injectCommand', raw);
133
+ return { ok: true, value: await context.atomic().transaction(tx => checkedHead(tx, context, c.artifactKey)), writes: 0, replayed: false };
134
+ }
135
+ catch (error) {
136
+ return failure(error);
137
+ }
138
+ },
139
+ async history(raw) {
140
+ try {
141
+ const c = boundary('historyCommand', raw);
142
+ return { ok: true, value: asJson(await historyPage(context, c)), writes: 0, replayed: false };
143
+ }
144
+ catch (error) {
145
+ return failure(error);
146
+ }
147
+ },
148
+ async inspect(raw) {
149
+ try {
150
+ const c = boundary('inspectCommand', raw);
151
+ const value = await inspectRecord(context, c);
152
+ return { ok: true, value: asJson(value), writes: 0, replayed: false };
153
+ }
154
+ catch (error) {
155
+ return failure(error);
156
+ }
157
+ },
158
+ });
159
+ }
package/src/store.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ import type { MemoryStore } from '@tangleai/memory/store';
2
+ import type { MemoryUnit } from '@tangleai/core/schemas/memory';
3
+ import type { StoredRecord, HeadRow, KeyRow, Operation } from './outcomes.contracts.gen.ts';
4
+ export interface Tables {
5
+ records: StoredRecord;
6
+ keys: KeyRow;
7
+ heads: HeadRow;
8
+ operations: Operation;
9
+ memories: MemoryUnit;
10
+ }
11
+ export interface Query {
12
+ scopeId?: string;
13
+ artifactKey?: string;
14
+ kind?: string;
15
+ after?: number;
16
+ upper?: number;
17
+ limit?: number;
18
+ reservedOnly?: boolean;
19
+ }
20
+ export interface OutcomeTransaction {
21
+ get<K extends keyof Tables>(table: K, id: string): Promise<Tables[K] | undefined>;
22
+ put<K extends keyof Tables>(table: K, value: Tables[K]): Promise<void>;
23
+ delete<K extends keyof Tables>(table: K, id: string): Promise<void>;
24
+ query<K extends keyof Tables>(table: K, query: Query): Promise<Tables[K][]>;
25
+ }
26
+ /** Trusted persistence extension; never a wire operation. All changes must roll back on throw. */
27
+ export interface OutcomePersistence {
28
+ transaction<T>(task: (view: OutcomeTransaction) => Promise<T>): Promise<T>;
29
+ }
30
+ export interface OutcomeStore {
31
+ readonly memories: MemoryStore;
32
+ readonly atomic: true;
33
+ }
34
+ export declare function checkedMemory(value: unknown): MemoryUnit;
35
+ /** Package-internal access; not exported as a public subpath. */
36
+ export declare function persistenceFor(store: OutcomeStore): OutcomePersistence;
37
+ export declare function createOutcomeStoreAdapter(persistence: OutcomePersistence): OutcomeStore;
38
+ export interface MemoryOutcomeStoreOptions {
39
+ memories?: readonly MemoryUnit[];
40
+ applyProbe?: (step: string) => void;
41
+ }
42
+ export declare function createMemoryOutcomeStore(options?: MemoryOutcomeStoreOptions): OutcomeStore;
package/src/store.js ADDED
@@ -0,0 +1,70 @@
1
+ /** Opaque atomic store handles. The trusted adapter is injected at construction. */
2
+ import { cloneJson } from '@jarenjs/core/object';
3
+ import { JarenValidator } from '@jarenjs/validate';
4
+ import { canonicalizeJson } from '@jarenjs/json/canonical';
5
+ import { MEMORY_UNIT_SCHEMA, MEMORY_RELATION_SCHEMA } from '@tangleai/core/schemas/memory';
6
+ import { reject } from "./errors.js";
7
+ const backends = new WeakMap();
8
+ const memoryValidator = new JarenValidator({ collectErrors: true, unknownFormats: 'ignore' });
9
+ memoryValidator.addSchema(MEMORY_RELATION_SCHEMA);
10
+ const validateMemory = memoryValidator.compile(MEMORY_UNIT_SCHEMA);
11
+ export function checkedMemory(value) {
12
+ try {
13
+ canonicalizeJson(value);
14
+ }
15
+ catch {
16
+ reject('OUTC1001', 'Memory must be finite JSON.');
17
+ }
18
+ if (!validateMemory(value).valid)
19
+ reject('OUTC1001', 'Memory schema rejected the value.');
20
+ return cloneJson(value);
21
+ }
22
+ /** Package-internal access; not exported as a public subpath. */
23
+ export function persistenceFor(store) {
24
+ const p = backends.get(store);
25
+ if (!p)
26
+ throw new TypeError('Use an atomic outcome store factory.');
27
+ return p;
28
+ }
29
+ export function createOutcomeStoreAdapter(persistence) {
30
+ if (typeof persistence?.transaction !== 'function')
31
+ throw new TypeError('An atomic transaction adapter is required.');
32
+ const store = Object.freeze({ atomic: true, memories: {
33
+ get: (id) => persistence.transaction(tx => tx.get('memories', id)),
34
+ put: async (unit) => { const valid = checkedMemory(unit); await persistence.transaction(tx => tx.put('memories', valid)); },
35
+ delete: (id) => persistence.transaction(tx => tx.delete('memories', id)),
36
+ list: () => persistence.transaction(tx => tx.query('memories', {})),
37
+ } });
38
+ backends.set(store, persistence);
39
+ return store;
40
+ }
41
+ export function createMemoryOutcomeStore(options = {}) {
42
+ let state = { records: new Map(), keys: new Map(), heads: new Map(), operations: new Map(), memories: new Map() };
43
+ for (const m of options.memories ?? [])
44
+ state.memories.set(m.id, checkedMemory(m));
45
+ let pending = Promise.resolve();
46
+ const persistence = { transaction(task) {
47
+ const result = pending.then(async () => {
48
+ const staged = Object.fromEntries(Object.entries(state).map(([k, v]) => [k, new Map([...v].map(([id, value]) => [id, cloneJson(value)]))]));
49
+ const view = {
50
+ async get(table, id) { return cloneJson(staged[table].get(id)); },
51
+ async put(table, value) { staged[table].set(value.id, cloneJson(value)); options.applyProbe?.(`put:${table}`); },
52
+ async delete(table, id) { staged[table].delete(id); options.applyProbe?.(`delete:${table}`); },
53
+ async query(table, q) {
54
+ let rows = [...staged[table].values()].filter(v => { const r = v; return (!q.reservedOnly || r.capacityReserved === true) && (q.scopeId === undefined || r.scopeId === q.scopeId) && (q.artifactKey === undefined || r.artifactKey === q.artifactKey) && (q.kind === undefined || r.kind === q.kind) && (q.after === undefined || Number(r.seq) > q.after) && (q.upper === undefined || Number(r.seq) <= q.upper); });
55
+ rows.sort((a, b) => { const x = a, y = b; return Number(x.seq ?? 0) - Number(y.seq ?? 0) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0); });
56
+ if (q.limit !== undefined)
57
+ rows = rows.slice(0, q.limit);
58
+ return cloneJson(rows);
59
+ },
60
+ };
61
+ const value = await task(view);
62
+ options.applyProbe?.('commit');
63
+ state = staged;
64
+ return value;
65
+ });
66
+ pending = result.then(() => undefined, () => undefined);
67
+ return result;
68
+ } };
69
+ return createOutcomeStoreAdapter(persistence);
70
+ }
@@ -0,0 +1,7 @@
1
+ import type { Head, ArtifactVersion, Evaluation, EvaluationRegistration, Approval, Issue } from './outcomes.contracts.gen.ts';
2
+ export declare const EMPTY_HEAD: Readonly<Head>;
3
+ export declare function assertHead(actual: Head, expected: Head): void;
4
+ export declare function planHeadTransition(actual: Head, expected: Head, target: string): Head;
5
+ export declare function assertCapacity(retained: number, reserved: number, maximum: number): void;
6
+ export declare function eligibilityIssues(e: Evaluation, r: EvaluationRegistration, v: ArtifactVersion): Issue[];
7
+ export declare function planPromotion(actual: Head, v: ArtifactVersion, e: Evaluation, r: EvaluationRegistration, a: Approval): Head;
@@ -0,0 +1,56 @@
1
+ /** Pure outcome transition guards; adapters do not define their own state machine. */
2
+ import { equalsJson } from '@jarenjs/core/object';
3
+ import { issue, reject, OutcomeRefusal } from "./errors.js";
4
+ import { scoreUtility } from "./domain.js";
5
+ export const EMPTY_HEAD = Object.freeze({ versionId: null, revision: 0 });
6
+ export function assertHead(actual, expected) {
7
+ if (!equalsJson(actual, expected))
8
+ reject('OUTC1013', 'The expected head version or revision is stale.', '/expectedHead');
9
+ }
10
+ export function planHeadTransition(actual, expected, target) { assertHead(actual, expected); return { versionId: target, revision: actual.revision + 1 }; }
11
+ export function assertCapacity(retained, reserved, maximum) {
12
+ if (retained + reserved >= maximum)
13
+ reject('OUTC1014', 'Artifact version capacity is exhausted.');
14
+ }
15
+ export function eligibilityIssues(e, r, v) {
16
+ const errors = [...e.issues];
17
+ const add = (detail) => errors.push(issue('OUTC1011', detail));
18
+ if (e.versionId !== v.id || r.versionId !== v.id || e.registrationId !== r.id || e.gatePolicyId !== r.gatePolicyId || e.evaluatorRevision !== r.evaluatorRevision || !equalsJson(e.expectedHead, r.expectedHead))
19
+ add('Evaluation identity bindings differ.');
20
+ if (v.issues.length)
21
+ errors.push(...v.issues);
22
+ if (!equalsJson(v.expectedHead, r.expectedHead) || v.parentVersionId !== r.expectedHead.versionId || !equalsJson(e.trainingScoreIds, r.trainingScoreIds))
23
+ add('Parent or training bindings differ.');
24
+ if (!r.cases.length || e.caseResults.length !== r.cases.length || new Set(e.caseResults.map(c => c.id)).size !== r.cases.length || !equalsJson(e.caseResults.map(c => c.id).sort(), r.cases.map(c => c.id).sort()))
25
+ add('Evaluation coverage is incomplete.');
26
+ const n = e.caseResults.length, delta = n ? e.caseResults.reduce((s, c) => s + c.utility - c.baselineUtility, 0) / n : null;
27
+ if (delta === null || delta <= 0 || e.meanDelta !== delta)
28
+ add('Paired held-out utility did not strictly improve.');
29
+ if (e.physicalRequests > r.maxPhysicalRequests)
30
+ add('The registered call bound was exceeded.');
31
+ if (r.maxCost !== null && (e.cost === null || e.cost > r.maxCost))
32
+ add('Known cost within the registered monetary bound is required.');
33
+ if (e.caseResults.some(c => c.utility !== scoreUtility(c.category) || c.baselineUtility !== scoreUtility(c.baselineCategory)))
34
+ add('Case utility differs from its scored category.');
35
+ for (const domain of new Set(r.cases.map(c => c.domain))) {
36
+ const ids = new Set(r.cases.filter(c => c.domain === domain).map(c => c.id));
37
+ if (e.caseResults.filter(c => ids.has(c.id)).reduce((s, c) => s + c.utility - c.baselineUtility, 0) < 0)
38
+ add('A registered domain regressed.');
39
+ }
40
+ return errors;
41
+ }
42
+ export function planPromotion(actual, v, e, r, a) {
43
+ if (new Set([v.scopeId, e.scopeId, r.scopeId, a.scopeId]).size !== 1 || new Set([v.artifactKey, e.artifactKey, r.artifactKey, a.artifactKey]).size !== 1)
44
+ reject('OUTC1003', 'Promotion records cross scope or lineage.');
45
+ assertHead(actual, a.expectedHead);
46
+ if (a.action !== 'promote' || a.versionId !== v.id || a.evaluationId !== e.id || a.gatePolicyId !== e.gatePolicyId || !equalsJson(a.expectedHead, e.expectedHead))
47
+ reject('OUTC1012', 'Approval does not bind this promotion.');
48
+ if (v.parentVersionId !== actual.versionId)
49
+ reject('OUTC1013', 'The candidate parent is stale.');
50
+ const issues = eligibilityIssues(e, r, v);
51
+ if (issues.length)
52
+ throw new OutcomeRefusal(issues);
53
+ if (!e.eligible)
54
+ reject('OUTC1011', 'The evaluation is ineligible.');
55
+ return planHeadTransition(actual, a.expectedHead, v.id);
56
+ }