@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
@@ -0,0 +1,6 @@
1
+ /** The portable operation contract; host authentication is supplied by handlers. */
2
+ import { compileContract } from '@jarenjs/contract';
3
+ import document from '../schemas/outcomes.contract.json' with { type: 'json' };
4
+ export const outcomeContractDocument = document;
5
+ export function createOutcomeContract() { return compileContract(document); }
6
+ export { createOutcomeHandlers, OUTCOME_MODEL_OPERATIONS } from "./handlers.js";
@@ -0,0 +1,19 @@
1
+ import type { OutcomeAdapter } from './adapters.ts';
2
+ import type { Json } from './outcomes.contracts.gen.ts';
3
+ export declare function domainValidator(schema: object): (value: unknown) => Json;
4
+ export declare function adapterIdentity(id: string, schemas: OutcomeAdapter['schemas'], rules: Json): Promise<Readonly<{
5
+ revision: string;
6
+ id: string;
7
+ inputSchema: string;
8
+ outputSchema: string;
9
+ resolutionSchema: string;
10
+ artifactSchema: string;
11
+ scorerRevision: string;
12
+ }>>;
13
+ export declare function checkedAdapter(adapter: OutcomeAdapter): {
14
+ input: (value: unknown) => Json;
15
+ output: (value: unknown) => Json;
16
+ resolution: (value: unknown) => Json;
17
+ artifact(value: unknown): Json;
18
+ };
19
+ export declare const scoreUtility: (category: "success" | "partial" | "failure") => number;
package/src/domain.js ADDED
@@ -0,0 +1,45 @@
1
+ /** Trusted adapter registration and schema validation share one boundary. */
2
+ import { JarenValidator } from '@jarenjs/validate';
3
+ import { canonicalizeJson } from '@jarenjs/json/canonical';
4
+ import { cloneJson, deepFreeze } from '@jarenjs/core/object';
5
+ import { outcomeRevision } from "./identity.js";
6
+ import { reject } from "./errors.js";
7
+ export function domainValidator(schema) {
8
+ const validate = new JarenValidator({ collectErrors: true, unknownFormats: 'ignore' }).compile(schema);
9
+ return value => {
10
+ try {
11
+ canonicalizeJson(value);
12
+ }
13
+ catch {
14
+ reject('OUTC1001', 'Expected finite JSON domain data.');
15
+ }
16
+ if (!validate(value).valid)
17
+ reject('OUTC1001', 'Domain schema rejected the value.');
18
+ return deepFreeze(cloneJson(value));
19
+ };
20
+ }
21
+ export async function adapterIdentity(id, schemas, rules) {
22
+ const [inputSchema, outputSchema, resolutionSchema, artifactSchema, scorerRevision] = await Promise.all([
23
+ outcomeRevision(schemas.input), outcomeRevision(schemas.output), outcomeRevision(schemas.resolution),
24
+ outcomeRevision(schemas.artifact), outcomeRevision({ id, rules }),
25
+ ]);
26
+ const identity = { id, inputSchema, outputSchema, resolutionSchema, artifactSchema, scorerRevision };
27
+ return Object.freeze({ ...identity, revision: await outcomeRevision(identity) });
28
+ }
29
+ export function checkedAdapter(adapter) {
30
+ const input = domainValidator(adapter.schemas.input), output = domainValidator(adapter.schemas.output);
31
+ const resolution = domainValidator(adapter.schemas.resolution), artifact = domainValidator(adapter.schemas.artifact);
32
+ return {
33
+ input, output, resolution,
34
+ artifact(value) {
35
+ let payload = artifact(value);
36
+ if (adapter.normalizePayload)
37
+ payload = artifact(adapter.normalizePayload(payload));
38
+ const issues = adapter.validatePayload(payload);
39
+ if (issues.length)
40
+ reject('OUTC1010', issues.map(i => i.detail).join(' '));
41
+ return payload;
42
+ },
43
+ };
44
+ }
45
+ export const scoreUtility = (category) => category === 'success' ? 1 : category === 'partial' ? 0.5 : 0;
@@ -0,0 +1,23 @@
1
+ /** Stable content failures shared by stores, services and wire handlers. */
2
+ import type { Issue, Result, Json } from './outcomes.contracts.gen.ts';
3
+ export type OutcomeIssue = Issue;
4
+ export type OutcomeResult<T extends Json = Json> = {
5
+ ok: true;
6
+ value: T;
7
+ replayed: boolean;
8
+ writes: number;
9
+ } | {
10
+ ok: false;
11
+ issues: Issue[];
12
+ };
13
+ export declare function issue(code: Issue['code'], detail: string, path?: string, retryable?: boolean): Issue;
14
+ export declare const refuse: (code: Issue["code"], detail: string, path?: string, retryable?: boolean) => OutcomeResult;
15
+ /** Internal transaction abort; caught at the supported content boundary. */
16
+ export declare class OutcomeRefusal extends Error {
17
+ readonly issues: Issue[];
18
+ get code(): Issue['code'];
19
+ get docPath(): string;
20
+ constructor(issues: Issue[]);
21
+ }
22
+ export declare function reject(code: Issue['code'], detail: string, path?: string, retryable?: boolean): never;
23
+ export declare function failure(error: unknown): Result;
package/src/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ export function issue(code, detail, path = '', retryable = false) { return { code, path, detail, retryable }; }
2
+ export const refuse = (code, detail, path = '', retryable = false) => ({ ok: false, issues: [issue(code, detail, path, retryable)] });
3
+ /** Internal transaction abort; caught at the supported content boundary. */
4
+ export class OutcomeRefusal extends Error {
5
+ issues;
6
+ get code() { return this.issues[0].code; }
7
+ get docPath() { return this.issues[0].path; }
8
+ constructor(issues) { super(issues[0]?.detail ?? 'outcome refused'); this.issues = issues; }
9
+ }
10
+ export function reject(code, detail, path = '', retryable = false) { throw new OutcomeRefusal([issue(code, detail, path, retryable)]); }
11
+ export function failure(error) { return error instanceof OutcomeRefusal ? { ok: false, issues: error.issues } : refuse('OUTC1015', 'The storage operation failed before publication.', '', true); }
@@ -0,0 +1,49 @@
1
+ import type { ServiceContext } from './service-context.ts';
2
+ import type { OutcomeTransaction } from './store.ts';
3
+ import type { ArtifactVersion, EvaluateCommand, EvaluationRegistration, Evaluation, CaseResult, Issue, Operation, Json } from './outcomes.contracts.gen.ts';
4
+ export declare const RETROSPECTIVE_RULES: Readonly<{
5
+ revision: "outcome-retrospective/v1";
6
+ utility: {
7
+ success: number;
8
+ partial: number;
9
+ failure: number;
10
+ };
11
+ strictPairedImprovement: true;
12
+ noDomainRegression: true;
13
+ completeCoverage: true;
14
+ }>;
15
+ export declare const outcomeGatePolicyId: (policy: Json) => Promise<string>;
16
+ export declare function reserveEvaluation(context: ServiceContext, c: EvaluateCommand, op: Operation): Promise<{
17
+ version: ArtifactVersion;
18
+ reflection: import("./outcomes.contracts.gen.ts").Reflection;
19
+ training: {
20
+ score: import("./outcomes.contracts.gen.ts").Score;
21
+ decision: import("./outcomes.contracts.gen.ts").Decision;
22
+ resolution: import("./outcomes.contracts.gen.ts").Resolution;
23
+ contentDigest: string;
24
+ }[];
25
+ }>;
26
+ export declare function prepareEvaluation(context: ServiceContext, c: EvaluateCommand, op: Operation): Promise<{
27
+ registration: EvaluationRegistration;
28
+ version: ArtifactVersion;
29
+ reflection: import("./outcomes.contracts.gen.ts").Reflection;
30
+ training: {
31
+ score: import("./outcomes.contracts.gen.ts").Score;
32
+ decision: import("./outcomes.contracts.gen.ts").Decision;
33
+ resolution: import("./outcomes.contracts.gen.ts").Resolution;
34
+ contentDigest: string;
35
+ }[];
36
+ }>;
37
+ export declare function pairedResults(context: ServiceContext, version: ArtifactVersion, registration: EvaluationRegistration, baseline: Json): Promise<{
38
+ rows: CaseResult[];
39
+ issues: Issue[];
40
+ }>;
41
+ export declare function baselinePayload(tx: OutcomeTransaction, context: ServiceContext, version: ArtifactVersion): Promise<Json>;
42
+ export declare function commitEvaluation(tx: OutcomeTransaction, context: ServiceContext, c: EvaluateCommand, data: Awaited<ReturnType<typeof prepareEvaluation>>): Promise<{
43
+ evaluationId: string;
44
+ eligible: boolean;
45
+ issues: Json;
46
+ caseReportId: string;
47
+ }>;
48
+ /** Recompute from retained independent bytes at the authority boundary. */
49
+ export declare function checkedEvaluation(tx: OutcomeTransaction, context: ServiceContext, version: ArtifactVersion, evaluation: Evaluation): Promise<EvaluationRegistration>;
@@ -0,0 +1,174 @@
1
+ /** One-use held-out registrations and independently recomputed paired gates. */
2
+ import { equalsJson } from '@jarenjs/core/object';
3
+ import { checkShape, checkTime, jsonBytes } from "./schema.js";
4
+ import { outcomeRevision } from "./identity.js";
5
+ import { issue, reject, OutcomeRefusal } from "./errors.js";
6
+ import { scoreUtility } from "./domain.js";
7
+ import { headFor, semantic, unique, putRecord } from "./persistence.js";
8
+ import { recordOf, seal, asJson } from "./service-context.js";
9
+ import { trainingRecords } from "./refinement.js";
10
+ import { verifiedSource } from "./resolution.js";
11
+ import { assertHead, eligibilityIssues } from "./transitions.js";
12
+ export const RETROSPECTIVE_RULES = Object.freeze({ revision: 'outcome-retrospective/v1', utility: { success: 1, partial: 0.5, failure: 0 }, strictPairedImprovement: true, noDomainRegression: true, completeCoverage: true });
13
+ export const outcomeGatePolicyId = (policy) => outcomeRevision({ rules: RETROSPECTIVE_RULES, policy });
14
+ export async function reserveEvaluation(context, c, op) {
15
+ return context.atomic().transaction(async (tx) => {
16
+ const previous = await semantic(tx, c.scopeId, 'evaluation', c.input.versionId);
17
+ if (previous)
18
+ reject('OUTC1007', `The evaluation stage is already complete: ${previous}.`);
19
+ const version = await recordOf(tx, c.input.versionId, c.scopeId, c.artifactKey, 'artifactVersion');
20
+ if (version.policyId !== context.policyId)
21
+ reject('OUTC1008', 'Candidate policy differs from its host.');
22
+ const reflection = await recordOf(tx, version.reflectionId, c.scopeId, c.artifactKey, 'reflection');
23
+ const training = await trainingRecords(tx, context, c.artifactKey, reflection.scoreIds, version.recordedAt);
24
+ if (!equalsJson(version.adapter, training[0].decision.adapter))
25
+ reject('OUTC1008', 'Candidate adapter differs from its training.');
26
+ assertHead(await headFor(tx, c.scopeId, c.artifactKey), version.expectedHead);
27
+ const slotKey = [c.artifactKey, c.input.slotId];
28
+ const owner = await semantic(tx, c.scopeId, 'evaluationSlotOwner', slotKey);
29
+ if (owner && owner !== op.id)
30
+ reject('OUTC1011', 'The held-out slot has already been reserved.');
31
+ const candidateOwner = await semantic(tx, c.scopeId, 'evaluationCandidateOwner', version.id);
32
+ if (candidateOwner && candidateOwner !== op.id)
33
+ reject('OUTC1011', 'The candidate already has an evaluation reservation.');
34
+ let changes = 0;
35
+ if (!owner) {
36
+ await unique(tx, c.scopeId, 'evaluationSlotOwner', slotKey, op.id);
37
+ changes++;
38
+ }
39
+ if (!candidateOwner) {
40
+ await unique(tx, c.scopeId, 'evaluationCandidateOwner', version.id, op.id);
41
+ changes++;
42
+ }
43
+ if (changes) {
44
+ const current = await tx.get('operations', op.id);
45
+ if (!current || current.attempt !== op.attempt)
46
+ reject('OUTC1019', 'Evaluation reservation changed.');
47
+ await tx.put('operations', { ...current, preparationWrites: current.preparationWrites + changes + 1 });
48
+ }
49
+ return { version, reflection, training };
50
+ });
51
+ }
52
+ export async function prepareEvaluation(context, c, op) {
53
+ const data = await reserveEvaluation(context, c, op);
54
+ // An existing registration is the retained exact corpus after a retry.
55
+ const retained = await context.atomic().transaction(async (tx) => {
56
+ const id = await semantic(tx, c.scopeId, 'evaluationRegistration', data.version.id);
57
+ return id ? recordOf(tx, id, c.scopeId, c.artifactKey, 'evaluationRegistration') : null;
58
+ });
59
+ if (retained)
60
+ return { ...data, registration: retained };
61
+ if (!context.evaluationSlot)
62
+ reject('OUTC1011', 'A trusted held-out registration provider is required.');
63
+ const raw = await context.evaluationSlot(c.input.slotId, data.version.id, context.scope);
64
+ let slot;
65
+ try {
66
+ slot = checkShape('evaluationSlot', raw);
67
+ }
68
+ catch {
69
+ reject('OUTC1011', 'The host supplied an invalid or missing held-out registration.');
70
+ }
71
+ if (slot.slotId !== c.input.slotId || slot.versionId !== data.version.id || !equalsJson(slot.expectedHead, data.version.expectedHead) || !equalsJson([...slot.trainingScoreIds].sort(), data.reflection.scoreIds) || slot.gatePolicyId !== context.gatePolicyId)
72
+ reject('OUTC1011', 'Held-out registration bindings differ.');
73
+ if (jsonBytes(asJson(slot)) > 262144)
74
+ reject('OUTC1011', 'Held-out registration exceeds 262,144 canonical UTF-8 bytes.');
75
+ const trainingIds = new Set(data.training.map(t => t.decision.decisionKey)), trainingDigests = new Set(data.training.map(t => t.contentDigest));
76
+ const content = new Set(), names = new Set();
77
+ for (const item of slot.cases) {
78
+ if (names.has(item.id) || trainingIds.has(item.id))
79
+ reject('OUTC1011', 'Training and held-out case ids must be disjoint and unique.');
80
+ names.add(item.id);
81
+ const source = await verifiedSource(context, { sourceId: item.source.sourceId, digest: item.source.digest }, '0000-01-01T00:00:00.000Z', c.at);
82
+ if (source.decisionId !== null || !equalsJson(source, item.source))
83
+ reject('OUTC1006', 'Held-out evidence snapshot differs from its independent source.');
84
+ const digest = await outcomeRevision({ domain: item.domain, input: item.input, outcome: source.payload });
85
+ if (content.has(digest) || trainingDigests.has(digest))
86
+ reject('OUTC1011', 'Training and held-out content must be disjoint and unique.');
87
+ content.add(digest);
88
+ }
89
+ const registration = await context.atomic().transaction(async (tx) => {
90
+ const current = await tx.get('operations', op.id);
91
+ if (!current || current.attempt !== op.attempt)
92
+ reject('OUTC1019', 'Evaluation attempt changed.');
93
+ for (const digest of content) {
94
+ const used = await semantic(tx, c.scopeId, 'heldOutContent', digest);
95
+ if (used && used !== data.version.id)
96
+ reject('OUTC1011', 'Previously consumed held-out content cannot be renamed or reused.');
97
+ }
98
+ const registration = await seal('evaluationRegistration', c.scopeId, c.artifactKey, c.at, slot);
99
+ await putRecord(tx, registration);
100
+ await unique(tx, c.scopeId, 'evaluationRegistration', data.version.id, registration.id);
101
+ for (const digest of content)
102
+ await unique(tx, c.scopeId, 'heldOutContent', digest, data.version.id);
103
+ await tx.put('operations', { ...current, preparationWrites: current.preparationWrites + 4 + content.size });
104
+ return registration;
105
+ });
106
+ return { ...data, registration };
107
+ }
108
+ export async function pairedResults(context, version, registration, baseline) {
109
+ const { adapter, domain } = context.adapter(version.adapter), rows = [], issues = [];
110
+ for (const c of registration.cases) {
111
+ try {
112
+ const input = domain.input(c.input), actual = domain.resolution(c.source.payload);
113
+ const output = domain.output(adapter.interpret(input, version.payload)), baselineOutput = domain.output(adapter.interpret(input, baseline));
114
+ const category = checkShape('category', adapter.score(output, actual).outcome), baselineCategory = checkShape('category', adapter.score(baselineOutput, actual).outcome);
115
+ rows.push({ id: c.id, contentDigest: await outcomeRevision({ domain: c.domain, input: c.input, outcome: c.source.payload }), output, baselineOutput, category, baselineCategory, utility: scoreUtility(category), baselineUtility: scoreUtility(baselineCategory) });
116
+ }
117
+ catch {
118
+ issues.push(issue('OUTC1011', `Held-out case could not be scored: ${c.id}.`));
119
+ }
120
+ }
121
+ return { rows, issues };
122
+ }
123
+ export async function baselinePayload(tx, context, version) {
124
+ if (version.parentVersionId === null)
125
+ return context.adapter(version.adapter).adapter.staticPayload;
126
+ const parent = await recordOf(tx, version.parentVersionId, version.scopeId, version.artifactKey, 'artifactVersion');
127
+ if (!equalsJson(parent.adapter, version.adapter) || parent.policyId !== version.policyId)
128
+ reject('OUTC1008', 'Evaluation parent adapter or policy differs.');
129
+ return parent.payload;
130
+ }
131
+ export async function commitEvaluation(tx, context, c, data) {
132
+ const exists = await semantic(tx, c.scopeId, 'evaluation', data.version.id);
133
+ if (exists)
134
+ reject('OUTC1007', `The evaluation stage is already complete: ${exists}.`);
135
+ const { rows, issues } = await pairedResults(context, data.version, data.registration, await baselinePayload(tx, context, data.version));
136
+ const base = { versionId: data.version.id, registrationId: data.registration.id, expectedHead: data.version.expectedHead, trainingScoreIds: data.reflection.scoreIds, caseResults: rows,
137
+ caseReportId: await outcomeRevision(rows), evaluatorRevision: data.registration.evaluatorRevision, gatePolicyId: data.registration.gatePolicyId,
138
+ eligible: false, issues, meanDelta: rows.length === data.registration.cases.length ? rows.reduce((n, c) => n + c.utility - c.baselineUtility, 0) / rows.length : null,
139
+ physicalRequests: 0, cost: 0,
140
+ };
141
+ const provisional = await seal('evaluation', c.scopeId, c.artifactKey, c.at, base);
142
+ const gates = eligibilityIssues(provisional, data.registration, data.version);
143
+ const evaluation = await seal('evaluation', c.scopeId, c.artifactKey, c.at, { ...base, issues: gates, eligible: gates.length === 0 });
144
+ await putRecord(tx, evaluation);
145
+ await unique(tx, c.scopeId, 'evaluation', data.version.id, evaluation.id);
146
+ return { evaluationId: evaluation.id, eligible: evaluation.eligible, issues: asJson(evaluation.issues), caseReportId: evaluation.caseReportId };
147
+ }
148
+ /** Recompute from retained independent bytes at the authority boundary. */
149
+ export async function checkedEvaluation(tx, context, version, evaluation) {
150
+ const registration = await recordOf(tx, evaluation.registrationId, version.scopeId, version.artifactKey, 'evaluationRegistration');
151
+ const reflection = await recordOf(tx, version.reflectionId, version.scopeId, version.artifactKey, 'reflection');
152
+ const training = await trainingRecords(tx, context, version.artifactKey, reflection.scoreIds, version.recordedAt);
153
+ if (version.policyId !== context.policyId || await outcomeRevision(version.policy) !== version.policyId || version.payloadSchema !== version.adapter.artifactSchema || !equalsJson(version.adapter, training[0].decision.adapter) || registration.gatePolicyId !== context.gatePolicyId || !equalsJson(evaluation.trainingScoreIds, reflection.scoreIds) || !equalsJson(registration.trainingScoreIds, reflection.scoreIds) || !equalsJson(version.expectedHead, registration.expectedHead))
154
+ reject('OUTC1008', 'Checked artifact identity bindings differ.');
155
+ const baseline = await baselinePayload(tx, context, version);
156
+ context.adapter(version.adapter).domain.artifact(version.payload);
157
+ const actual = await pairedResults(context, version, registration, baseline);
158
+ if (actual.issues.length || !equalsJson(actual.rows, evaluation.caseResults) || await outcomeRevision(actual.rows) !== evaluation.caseReportId)
159
+ reject('OUTC1011', 'Retrospective result bytes do not reproduce.');
160
+ const trainingContent = new Set(training.map(t => t.contentDigest)), trainingIds = new Set(training.map(t => t.decision.decisionKey));
161
+ const content = new Set();
162
+ for (const c of registration.cases) {
163
+ const { digest, ...bytes } = c.source;
164
+ checkTime(c.source.observedAt);
165
+ const hash = await outcomeRevision({ domain: c.domain, input: c.input, outcome: c.source.payload });
166
+ if (await outcomeRevision(bytes) !== digest || c.source.scopeId !== context.scopeId || c.source.subject !== context.scope.subject || c.source.decisionId !== null || content.has(hash) || trainingContent.has(hash) || trainingIds.has(c.id))
167
+ reject('OUTC1011', 'Retained held-out evidence is not disjoint or valid.');
168
+ content.add(hash);
169
+ }
170
+ const issues = eligibilityIssues(evaluation, registration, version);
171
+ if (!evaluation.eligible || issues.length)
172
+ throw new OutcomeRefusal(issues.length ? issues : [issue('OUTC1011', 'The evaluation is ineligible.')]);
173
+ return registration;
174
+ }
@@ -0,0 +1,15 @@
1
+ /** One dispatch table over the direct service; no transport-owned business ledger. */
2
+ import type { Handler, RequestContext } from '@jarenjs/contract/http';
3
+ import type { OutcomeService } from './service.ts';
4
+ export declare const OUTCOME_MODEL_OPERATIONS: readonly string[];
5
+ export interface OutcomeHandlerBinding {
6
+ /** Constructed with this authenticated host's principal and registry. */
7
+ service: OutcomeService;
8
+ /** Host scope access policy, evaluated before every read, write and replay. */
9
+ allowScope(scopeId: string, context: RequestContext): boolean | Promise<boolean>;
10
+ }
11
+ export interface OutcomeHandlerOptions {
12
+ /** A closure for local calls, or a resolver of already authenticated HTTP ctx.host. */
13
+ resolveHost(context: RequestContext): OutcomeHandlerBinding | undefined | Promise<OutcomeHandlerBinding | undefined>;
14
+ }
15
+ export declare function createOutcomeHandlers(options: OutcomeHandlerOptions): Record<string, Handler>;
@@ -0,0 +1,18 @@
1
+ import { refuse } from "./errors.js";
2
+ const methods = {
3
+ 'outcomes.create': 'create', 'outcomes.resolve': 'resolve', 'outcomes.score': 'score',
4
+ 'outcomes.project': 'project', 'outcomes.reflect': 'reflect', 'outcomes.evaluate': 'evaluate',
5
+ 'outcomes.approve': 'approve', 'outcomes.promote': 'promote', 'outcomes.rollback': 'rollback',
6
+ 'outcomes.inject': 'injectChecked', 'outcomes.inspect': 'inspect', 'outcomes.history': 'history',
7
+ 'outcomes.reconcile': 'reconcile',
8
+ };
9
+ export const OUTCOME_MODEL_OPERATIONS = Object.freeze(Object.keys(methods).filter(id => id !== 'outcomes.approve' && id !== 'outcomes.reconcile'));
10
+ export function createOutcomeHandlers(options) {
11
+ return Object.fromEntries(Object.entries(methods).map(([id, method]) => [id, async (input, context) => {
12
+ const binding = await options.resolveHost(context);
13
+ const scopeId = input?.scopeId;
14
+ if (!binding || typeof scopeId !== 'string' || !await binding.allowScope(scopeId, context))
15
+ return refuse('OUTC1003', 'The authenticated host refused scope access.');
16
+ return await binding.service[method](input);
17
+ }]));
18
+ }
@@ -0,0 +1,4 @@
1
+ import type { ServiceContext } from './service-context.ts';
2
+ import type { HistoryCommand, HistoryPage, InspectCommand, Inspection, OutcomeRecord } from './outcomes.contracts.gen.ts';
3
+ export declare function historyPage(context: ServiceContext, command: HistoryCommand): Promise<HistoryPage>;
4
+ export declare function inspectRecord(context: ServiceContext, command: InspectCommand): Promise<OutcomeRecord | Inspection>;
package/src/history.js ADDED
@@ -0,0 +1,56 @@
1
+ /** Snapshot-bounded audit paging and finite immutable ancestry inspection. */
2
+ import { checkShape } from "./schema.js";
3
+ import { keyId, outcomeRevision } from "./identity.js";
4
+ import { readRecord } from "./persistence.js";
5
+ import { reject } from "./errors.js";
6
+ export async function historyPage(context, command) {
7
+ return context.atomic().transaction(async (tx) => {
8
+ const { scopeId, artifactKey, input } = command;
9
+ const sequenceId = await keyId(scopeId, 'sequence', null);
10
+ const sequence = await tx.get('keys', sequenceId);
11
+ const current = sequence?.value ?? 0;
12
+ if (!Number.isSafeInteger(current) || Number(current) < 0 || (sequence && (sequence.id !== sequenceId || sequence.scopeId !== scopeId)))
13
+ reject('OUTC1002', 'Audit sequence is corrupted.');
14
+ const filterId = await outcomeRevision({ scopeId, artifactKey, revision: 'outcome-history/v1', kind: 'all' });
15
+ const cursor = input.cursor;
16
+ if (cursor && (cursor.scopeId !== scopeId || cursor.artifactKey !== artifactKey || cursor.filterId !== filterId))
17
+ reject('OUTC1003', 'Cursor scope or filter differs.');
18
+ const upper = cursor?.upper ?? Number(current), after = cursor?.after ?? 0;
19
+ if (after > upper || upper > Number(current))
20
+ reject('OUTC1001', 'Cursor sequence is invalid.');
21
+ const pageSize = input.pageSize ?? 50;
22
+ const rows = await tx.query('records', { scopeId, artifactKey, after, upper, limit: pageSize + 1 });
23
+ const entries = [];
24
+ let previous = after;
25
+ for (const row of rows.slice(0, pageSize)) {
26
+ checkShape('storedRecord', row);
27
+ if (row.seq <= previous || row.seq > upper)
28
+ reject('OUTC1002', 'Audit ordering is corrupted.');
29
+ entries.push({ sequence: row.seq, record: await readRecord(tx, row.id, scopeId, artifactKey) });
30
+ previous = row.seq;
31
+ }
32
+ return { entries, upper, cursor: rows.length > pageSize ? { scopeId, artifactKey, filterId, after: previous, upper } : null };
33
+ });
34
+ }
35
+ export async function inspectRecord(context, command) {
36
+ return context.atomic().transaction(async (tx) => {
37
+ const record = await readRecord(tx, command.input.id, command.scopeId, command.artifactKey);
38
+ if (!command.input.includeLineage)
39
+ return record;
40
+ const lineage = [], visited = new Set();
41
+ let next = record.kind === 'artifactVersion' ? record.id : null;
42
+ while (next !== null) {
43
+ if (visited.has(next) || lineage.length >= context.policy.maxVersions)
44
+ reject('OUTC1002', 'Artifact ancestry cycles or exceeds its retained bound.');
45
+ visited.add(next);
46
+ const version = await readRecord(tx, next, command.scopeId, command.artifactKey);
47
+ if (version.kind !== 'artifactVersion')
48
+ reject('OUTC1002', 'Artifact ancestry points to another record kind.');
49
+ if (record.kind === 'artifactVersion' && (version.policyId !== record.policyId || version.adapter.revision !== record.adapter.revision || version.payloadSchema !== record.payloadSchema))
50
+ reject('OUTC1002', 'Artifact ancestry revisions differ.');
51
+ lineage.push(version);
52
+ next = version.parentVersionId;
53
+ }
54
+ return { record, lineage };
55
+ });
56
+ }
@@ -0,0 +1,9 @@
1
+ /** Immutable addresses include every semantic field, except the address itself. */
2
+ import { canonicalSha256 } from '@jarenjs/json/canonical';
3
+ import type { OutcomeRecord, Json } from './outcomes.contracts.gen.ts';
4
+ export declare const outcomeRevision: typeof canonicalSha256;
5
+ export declare function scopeIdOf(value: unknown): Promise<string>;
6
+ export declare function recordIdOf(value: Omit<OutcomeRecord, 'id'> | OutcomeRecord): Promise<string>;
7
+ export declare function validateRecord(value: unknown): Promise<OutcomeRecord>;
8
+ export declare function sealRecord(value: unknown): Promise<OutcomeRecord>;
9
+ export declare const keyId: (scopeId: string, kind: string, value: Json) => Promise<string>;
@@ -0,0 +1,17 @@
1
+ /** Immutable addresses include every semantic field, except the address itself. */
2
+ import { canonicalSha256 } from '@jarenjs/json/canonical';
3
+ import { checkRecordShape, checkShape } from "./schema.js";
4
+ import { reject } from "./errors.js";
5
+ export const outcomeRevision = canonicalSha256;
6
+ export async function scopeIdOf(value) { return canonicalSha256(checkShape('scope', value)); }
7
+ export async function recordIdOf(value) { const { id: _, ...data } = value; return canonicalSha256(data); }
8
+ export async function validateRecord(value) {
9
+ const r = checkRecordShape(value);
10
+ if (await recordIdOf(r) !== r.id)
11
+ reject('OUTC1002', 'Immutable record bytes do not match their id.');
12
+ if (r.kind === 'decision' && await scopeIdOf(r.scope) !== r.scopeId)
13
+ reject('OUTC1003', 'Decision scope does not match its id.');
14
+ return r;
15
+ }
16
+ export async function sealRecord(value) { const data = value; const id = await canonicalSha256(data); return validateRecord({ ...data, id }); }
17
+ export const keyId = (scopeId, kind, value) => canonicalSha256({ scopeId, kind, value });
package/src/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** Evidenced decision records and atomic, host-injected persistence. */
2
+ export { outcomesSchema, DEFAULT_OUTCOME_POLICY } from './schema.ts';
3
+ export { scopeIdOf, recordIdOf, validateRecord, outcomeRevision } from './identity.ts';
4
+ export { createMemoryOutcomeStore, createOutcomeStoreAdapter } from './store.ts';
5
+ export type { OutcomeStore, OutcomePersistence, OutcomeTransaction, Tables, Query, MemoryOutcomeStoreOptions } from './store.ts';
6
+ export type { OutcomeAdapter, EvidenceResolver, EvaluationSlot, OutcomeHost, OutcomePrincipal, OutcomeProposer } from './adapters.ts';
7
+ export type { OutcomeResult, OutcomeIssue } from './errors.ts';
8
+ export { EMPTY_HEAD, planHeadTransition, assertCapacity, planPromotion, eligibilityIssues } from './transitions.ts';
9
+ export type * from './outcomes.contracts.gen.ts';
10
+ export { createOutcomeService } from './service.ts';
11
+ export type { OutcomeService, OutcomeServiceOptions } from './service.ts';
12
+ export { changedLeafPaths, preparePayload } from './refinement.ts';
13
+ export { outcomeGatePolicyId, RETROSPECTIVE_RULES } from './evaluation.ts';
14
+ export { createOutcomeContract, outcomeContractDocument, createOutcomeHandlers, OUTCOME_MODEL_OPERATIONS } from './contract.ts';
15
+ export type { OutcomeHandlerOptions, OutcomeHandlerBinding } from './handlers.ts';
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Evidenced decision records and atomic, host-injected persistence. */
2
+ export { outcomesSchema, DEFAULT_OUTCOME_POLICY } from "./schema.js";
3
+ export { scopeIdOf, recordIdOf, validateRecord, outcomeRevision } from "./identity.js";
4
+ export { createMemoryOutcomeStore, createOutcomeStoreAdapter } from "./store.js";
5
+ export { EMPTY_HEAD, planHeadTransition, assertCapacity, planPromotion, eligibilityIssues } from "./transitions.js";
6
+ export { createOutcomeService } from "./service.js";
7
+ export { changedLeafPaths, preparePayload } from "./refinement.js";
8
+ export { outcomeGatePolicyId, RETROSPECTIVE_RULES } from "./evaluation.js";
9
+ export { createOutcomeContract, outcomeContractDocument, createOutcomeHandlers, OUTCOME_MODEL_OPERATIONS } from "./contract.js";
@@ -0,0 +1,16 @@
1
+ /** Request identity, capacity reservations and terminal replay belong to one owner. */
2
+ import type { OutcomeStore, OutcomeTransaction } from './store.ts';
3
+ import type { Json, Operation, Result, Policy } from './outcomes.contracts.gen.ts';
4
+ export interface OperationCommand {
5
+ scopeId: string;
6
+ artifactKey: string;
7
+ requestKey: string;
8
+ at: string;
9
+ input: unknown;
10
+ }
11
+ export declare function beginOperation(store: OutcomeStore, name: string, command: OperationCommand, policy?: Policy): Promise<{
12
+ operation: Operation;
13
+ replay?: Result;
14
+ }>;
15
+ export declare function recordAttempt(tx: OutcomeTransaction, op: Operation, stage: 'reserved' | 'dispatched' | 'ready' | 'retryable' | 'uncertain' | 'refused' | 'completed' | 'reconciled', at: string, details: Json): Promise<string>;
16
+ export declare function finishOperation(store: OutcomeStore, op: Operation, at: string, apply: (tx: OutcomeTransaction) => Promise<Json>): Promise<Result>;
@@ -0,0 +1,108 @@
1
+ import { persistenceFor } from "./store.js";
2
+ import { keyId, outcomeRevision, sealRecord } from "./identity.js";
3
+ import { replay, complete, putRecord, unique } from "./persistence.js";
4
+ import { assertCapacity } from "./transitions.js";
5
+ import { checkTime } from "./schema.js";
6
+ import { reject, OutcomeRefusal, refuse } from "./errors.js";
7
+ export async function beginOperation(store, name, command, policy) {
8
+ checkTime(command.at);
9
+ const id = await keyId(command.scopeId, 'request', command.requestKey), inputDigest = await outcomeRevision({ name, command });
10
+ let created;
11
+ try {
12
+ return await persistenceFor(store).transaction(async (tx) => {
13
+ const old = await tx.get('operations', id);
14
+ if (old) {
15
+ const result = await replay(tx, old, inputDigest);
16
+ if (result)
17
+ return { operation: old, replay: result };
18
+ if (old.state === 'reserved')
19
+ reject('OUTC1019', 'The operation is already in progress.');
20
+ if (old.state === 'ready')
21
+ return { operation: old };
22
+ }
23
+ if (policy && !(old?.capacityReserved)) {
24
+ const versions = await tx.query('records', { scopeId: command.scopeId, artifactKey: command.artifactKey, kind: 'artifactVersion', limit: policy.maxVersions + 1 });
25
+ const reservations = await tx.query('operations', { scopeId: command.scopeId, artifactKey: command.artifactKey, reservedOnly: true, limit: policy.maxVersions + 1 });
26
+ assertCapacity(versions.length, reservations.length, policy.maxVersions);
27
+ }
28
+ const operation = { id, scopeId: command.scopeId, artifactKey: command.artifactKey, requestKey: command.requestKey, operation: name, inputDigest, state: old?.output !== null && old?.output !== undefined ? 'ready' : 'reserved', receiptId: null, output: old?.output ?? null, capacityReserved: policy !== undefined, attempt: (old?.attempt ?? 0) + 1, preparationWrites: (old?.preparationWrites ?? 0) + 4 };
29
+ created = operation;
30
+ const reservationId = await recordAttempt(tx, operation, 'reserved', command.at, { operation: name, requestKey: command.requestKey });
31
+ await unique(tx, command.scopeId, 'reservationReceipt', [operation.id, operation.attempt], reservationId);
32
+ await tx.put('operations', operation);
33
+ return { operation };
34
+ });
35
+ }
36
+ catch (error) {
37
+ if (error instanceof OutcomeRefusal)
38
+ throw error;
39
+ if (created)
40
+ return { operation: created, replay: await recoverOperation(store, created, command.at, true) };
41
+ throw error;
42
+ }
43
+ }
44
+ export async function recordAttempt(tx, op, stage, at, details) {
45
+ const record = await sealRecord({ schemaVersion: 1, kind: 'attemptEvent', scopeId: op.scopeId, artifactKey: op.artifactKey, recordedAt: at, requestId: op.id, stage, inputDigest: op.inputDigest, details: { attempt: op.attempt, value: details } });
46
+ await putRecord(tx, record);
47
+ return record.id;
48
+ }
49
+ async function recoverOperation(store, op, at, allowAbsent = false) {
50
+ try {
51
+ return await persistenceFor(store).transaction(async (tx) => {
52
+ const current = await tx.get('operations', op.id);
53
+ if (!current)
54
+ return allowAbsent ? refuse('OUTC1015', 'The reservation rolled back; retry the same request.', '', true) : refuse('OUTC1002', 'Operation reservation disappeared.');
55
+ if (current.state === 'completed')
56
+ return (await replay(tx, current, op.inputDigest));
57
+ if (current.attempt !== op.attempt)
58
+ return refuse('OUTC1019', 'A different attempt owns this operation.');
59
+ const state = current.state === 'dispatched' || current.state === 'uncertain' ? 'uncertain' : 'retryable';
60
+ await recordAttempt(tx, current, state, at, { code: state === 'uncertain' ? 'OUTC1017' : 'OUTC1015' });
61
+ await tx.put('operations', { ...current, state, preparationWrites: current.preparationWrites + 3 });
62
+ return state === 'uncertain' ? refuse('OUTC1017', 'External completion is uncertain; reconcile before retry.') : refuse('OUTC1015', 'The transaction rolled back; retry the same request.', '', true);
63
+ });
64
+ }
65
+ catch {
66
+ return refuse('OUTC1017', 'Commit acknowledgement and recovery are unavailable.');
67
+ }
68
+ }
69
+ export async function finishOperation(store, op, at, apply) {
70
+ try {
71
+ return await persistenceFor(store).transaction(async (raw) => {
72
+ const current = await raw.get('operations', op.id);
73
+ if (!current)
74
+ reject('OUTC1002', 'Operation reservation disappeared.');
75
+ const old = await replay(raw, current, op.inputDigest);
76
+ if (old)
77
+ return old;
78
+ if (current.attempt !== op.attempt)
79
+ reject('OUTC1019', 'A different attempt owns this operation.');
80
+ let writes = 0;
81
+ const tx = { ...raw, async put(table, value) { await raw.put(table, value); writes++; }, async delete(table, id) { await raw.delete(table, id); writes++; } };
82
+ const value = await apply(tx);
83
+ const result = { ok: true, value, replayed: false, writes: writes + current.preparationWrites + 3 };
84
+ await complete(tx, current, result, at);
85
+ return result;
86
+ });
87
+ }
88
+ catch (error) {
89
+ if (error instanceof OutcomeRefusal && !error.issues.some(i => i.retryable || i.code === 'OUTC1017')) {
90
+ const result = { ok: false, issues: error.issues };
91
+ try {
92
+ return await persistenceFor(store).transaction(async (tx) => {
93
+ const current = await tx.get('operations', op.id);
94
+ if (!current || current.attempt !== op.attempt)
95
+ return refuse('OUTC1019', 'A different attempt owns this operation.');
96
+ if (current.state === 'completed')
97
+ return (await replay(tx, current, op.inputDigest));
98
+ await complete(tx, current, result, at);
99
+ return result;
100
+ });
101
+ }
102
+ catch {
103
+ return recoverOperation(store, op, at);
104
+ }
105
+ }
106
+ return recoverOperation(store, op, at);
107
+ }
108
+ }