@unson/brainbase-mcp 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/contracts/foundation/graph-history.sql +634 -0
  2. package/contracts/judgment-value-proof/schema.json +43 -1
  3. package/dist/canonical-task-contract.d.ts +9 -0
  4. package/dist/canonical-task-contract.js +39 -0
  5. package/dist/canonical-task-principal.d.ts +16 -0
  6. package/dist/canonical-task-principal.js +36 -0
  7. package/dist/canonical-task-service.d.ts +251 -0
  8. package/dist/canonical-task-service.js +820 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +107 -0
  11. package/dist/company-os-evaluation.d.ts +193 -0
  12. package/dist/company-os-evaluation.js +1150 -0
  13. package/dist/company-os-impact-review.d.ts +248 -0
  14. package/dist/company-os-impact-review.js +663 -0
  15. package/dist/company-os-learning-adoption.d.ts +288 -0
  16. package/dist/company-os-learning-adoption.js +944 -0
  17. package/dist/company-os-objectives.d.ts +63 -0
  18. package/dist/company-os-objectives.js +234 -0
  19. package/dist/company-os-receipt-adapter.d.ts +109 -0
  20. package/dist/company-os-receipt-adapter.js +636 -0
  21. package/dist/constraint-exception-store.d.ts +23 -0
  22. package/dist/constraint-exception-store.js +108 -0
  23. package/dist/constraint-resolution.d.ts +175 -0
  24. package/dist/constraint-resolution.js +922 -0
  25. package/dist/decision-adapter.d.ts +145 -0
  26. package/dist/decision-adapter.js +586 -0
  27. package/dist/durable-wait-http.d.ts +51 -0
  28. package/dist/durable-wait-http.js +497 -0
  29. package/dist/durable-waits.d.ts +234 -0
  30. package/dist/durable-waits.js +944 -0
  31. package/dist/execution-authority-http.d.ts +28 -0
  32. package/dist/execution-authority-http.js +359 -0
  33. package/dist/execution-authority.d.ts +201 -0
  34. package/dist/execution-authority.js +634 -0
  35. package/dist/foundation-authenticated-tools.d.ts +56 -0
  36. package/dist/foundation-authenticated-tools.js +347 -0
  37. package/dist/foundation-catalog.d.ts +26 -0
  38. package/dist/foundation-catalog.js +206 -0
  39. package/dist/foundation-constraint-store.d.ts +50 -0
  40. package/dist/foundation-constraint-store.js +215 -0
  41. package/dist/foundation-graph-http.d.ts +43 -0
  42. package/dist/foundation-graph-http.js +243 -0
  43. package/dist/foundation-graph-write.d.ts +90 -0
  44. package/dist/foundation-graph-write.js +447 -0
  45. package/dist/foundation-http.d.ts +90 -0
  46. package/dist/foundation-http.js +439 -0
  47. package/dist/foundation-public-provider.d.ts +140 -0
  48. package/dist/foundation-public-provider.js +192 -0
  49. package/dist/foundation-store.d.ts +132 -0
  50. package/dist/foundation-store.js +654 -0
  51. package/dist/graph-corrections.d.ts +157 -0
  52. package/dist/graph-corrections.js +620 -0
  53. package/dist/graph-foundation-reader.d.ts +60 -0
  54. package/dist/graph-foundation-reader.js +494 -0
  55. package/dist/graph-retrieval.d.ts +6 -1
  56. package/dist/graph-revision-reader.d.ts +80 -0
  57. package/dist/graph-revision-reader.js +246 -0
  58. package/dist/graph-web-http.d.ts +26 -0
  59. package/dist/graph-web-http.js +215 -0
  60. package/dist/graph-web.d.ts +364 -0
  61. package/dist/graph-web.js +639 -0
  62. package/dist/judgment-dag-composition.d.ts +224 -0
  63. package/dist/judgment-dag-composition.js +731 -0
  64. package/dist/judgment-dag.d.ts +2 -0
  65. package/dist/judgment-dag.js +1 -0
  66. package/dist/judgment-problem-snapshot.d.ts +213 -0
  67. package/dist/judgment-problem-snapshot.js +1073 -0
  68. package/dist/judgment-value-proof-review.d.ts +166 -0
  69. package/dist/judgment-value-proof-review.js +437 -0
  70. package/dist/judgment-value-proof.d.ts +28 -1
  71. package/dist/judgment-value-proof.js +53 -8
  72. package/dist/judgment-view-http.d.ts +22 -0
  73. package/dist/judgment-view-http.js +128 -0
  74. package/dist/judgment-view.d.ts +222 -0
  75. package/dist/judgment-view.js +627 -0
  76. package/dist/knowledge-adapter.d.ts +112 -0
  77. package/dist/knowledge-adapter.js +691 -0
  78. package/dist/knowledge-continuation.d.ts +127 -0
  79. package/dist/knowledge-continuation.js +446 -0
  80. package/dist/knowledge-lookup.d.ts +178 -0
  81. package/dist/knowledge-lookup.js +758 -0
  82. package/dist/local-web-fetch-bridge.d.ts +5 -0
  83. package/dist/local-web-fetch-bridge.js +37 -0
  84. package/dist/local-web-host.d.ts +109 -0
  85. package/dist/local-web-host.js +688 -0
  86. package/dist/local-web-security.d.ts +25 -0
  87. package/dist/local-web-security.js +95 -0
  88. package/dist/ontology-foundation.d.ts +209 -0
  89. package/dist/ontology-foundation.js +1000 -0
  90. package/dist/ontology.d.ts +13 -0
  91. package/dist/ontology.js +14 -0
  92. package/dist/organization-connection.d.ts +164 -0
  93. package/dist/organization-connection.js +428 -0
  94. package/dist/organization-graph.d.ts +15 -0
  95. package/dist/organization-graph.js +24 -4
  96. package/dist/personal-knowledge.d.ts +132 -0
  97. package/dist/personal-knowledge.js +779 -0
  98. package/dist/personal-memory-handover.d.ts +108 -0
  99. package/dist/personal-memory-handover.js +369 -0
  100. package/dist/philosophy-revision-reader.d.ts +85 -0
  101. package/dist/philosophy-revision-reader.js +241 -0
  102. package/dist/portable-graph.d.ts +24 -0
  103. package/dist/portable-graph.js +77 -0
  104. package/dist/problem-candidates.d.ts +149 -0
  105. package/dist/problem-candidates.js +898 -0
  106. package/dist/problem-selection.d.ts +238 -0
  107. package/dist/problem-selection.js +995 -0
  108. package/dist/resource-reservation-http.d.ts +31 -0
  109. package/dist/resource-reservation-http.js +371 -0
  110. package/dist/resource-reservations.d.ts +221 -0
  111. package/dist/resource-reservations.js +577 -0
  112. package/dist/server.d.ts +104 -2
  113. package/dist/server.js +186 -4
  114. package/dist/ssot.d.ts +6 -1
  115. package/dist/ssot.js +63 -5
  116. package/dist/streamable-http.d.ts +42 -0
  117. package/dist/streamable-http.js +288 -0
  118. package/dist/types.d.ts +20 -0
  119. package/dist/value-proof-review-http.d.ts +25 -0
  120. package/dist/value-proof-review-http.js +185 -0
  121. package/dist/world-model.d.ts +199 -0
  122. package/dist/world-model.js +955 -0
  123. package/package.json +230 -3
  124. package/ui/brainbase-tokens.css +56 -0
  125. package/ui/graph-own-share.js +367 -0
  126. package/ui/graph-projects-view.css +23 -0
  127. package/ui/graph-projects-view.js +730 -0
  128. package/ui/graph-registry-view.css +101 -0
  129. package/ui/graph-registry-view.js +804 -0
  130. package/ui/graph-view-shared.css +522 -0
  131. package/ui/graph-view-shared.js +1227 -0
  132. package/ui/judgment-view.css +312 -0
  133. package/ui/judgment-view.js +508 -0
  134. package/ui/local-web-shell.css +396 -0
  135. package/ui/local-web-shell.js +378 -0
  136. package/ui/objective-editor-http-port.js +154 -0
  137. package/ui/objective-editor.css +286 -0
  138. package/ui/objective-editor.js +1586 -0
  139. package/ui/outcome-knowledge.css +50 -14
  140. package/ui/outcome-knowledge.js +15 -1
  141. package/ui/outcome-mana.css +91 -14
  142. package/ui/outcome-mana.js +958 -61
  143. package/ui/value-proof-review.css +504 -0
  144. package/ui/value-proof-review.js +1104 -0
  145. package/ui/workspace-kit.css +485 -0
  146. package/ui/workspace-kit.js +229 -0
  147. package/ui/world-model-view.css +188 -0
  148. package/ui/world-model-view.js +486 -0
@@ -0,0 +1,995 @@
1
+ /**
2
+ * Problem selection is the comparison boundary between evidence-sidecar
3
+ * candidates and a future JudgmentProblem.
4
+ *
5
+ * The module deliberately does not rank candidates, reserve resources, grant
6
+ * authority, or mutate an Objective. A caller supplies a versioned DAG
7
+ * composition evaluator. This module freezes the selection inputs, checks
8
+ * current candidate access and exact digests, validates the evaluator result,
9
+ * and stores an immutable SelectionRecord.
10
+ */
11
+ import { createHash, randomUUID } from 'node:crypto';
12
+ import { link, lstat, mkdir, open, readFile, unlink } from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ import { createJudgmentDAGCompositionDefinition, executeJudgmentDAGComposition } from './judgment-dag-composition.js';
15
+ export const PROBLEM_SELECTION_CONTRACT_VERSION = 'problem-selection.v1';
16
+ export const PROBLEM_SELECTION_RECORD_VERSION = 'problem-selection-record.v1';
17
+ export const PROBLEM_SELECTION_DIRECTORY = 'problem-selections';
18
+ export class ProblemSelectionError extends Error {
19
+ code;
20
+ constructor(code, message) {
21
+ super(message);
22
+ this.name = 'ProblemSelectionError';
23
+ this.code = code;
24
+ }
25
+ }
26
+ const RECORD_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
27
+ /** All existing Brainbase content digests are algorithm-qualified. */
28
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
29
+ const SNAPSHOT_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
30
+ function isRecord(value) {
31
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
32
+ }
33
+ function isPlainRecord(value) {
34
+ return isRecord(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
35
+ }
36
+ function compareCodePoints(left, right) {
37
+ const a = Array.from(left, (character) => character.codePointAt(0));
38
+ const b = Array.from(right, (character) => character.codePointAt(0));
39
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
40
+ if (a[index] !== b[index])
41
+ return a[index] - b[index];
42
+ }
43
+ return a.length - b.length;
44
+ }
45
+ function error(code, message) {
46
+ return new ProblemSelectionError(code, message);
47
+ }
48
+ function requireString(value, field) {
49
+ if (typeof value !== 'string' || value.trim().length === 0 || /[\u0000-\u001F\u007F]/u.test(value)) {
50
+ throw error('invalid_request', `${field} must be a non-empty string without control characters`);
51
+ }
52
+ return value;
53
+ }
54
+ function requireIsoTimestamp(value, field) {
55
+ const timestamp = requireString(value, field);
56
+ if (!Number.isFinite(Date.parse(timestamp)))
57
+ throw error('invalid_request', `${field} must be an ISO timestamp`);
58
+ return timestamp;
59
+ }
60
+ function requireFiniteNumber(value, field) {
61
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
62
+ throw error('invalid_request', `${field} must be a finite non-negative number`);
63
+ }
64
+ return Object.is(value, -0) ? 0 : value;
65
+ }
66
+ function exactKeys(value, allowed, field) {
67
+ for (const key of Object.keys(value)) {
68
+ if (!allowed.includes(key))
69
+ throw error('invalid_request', `${field}.${key} is not allowed`);
70
+ }
71
+ }
72
+ function cloneJson(value, field, active = new Set()) {
73
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
74
+ return value;
75
+ if (typeof value === 'number') {
76
+ if (!Number.isFinite(value))
77
+ throw error('invalid_request', `${field} must contain finite numbers`);
78
+ return Object.is(value, -0) ? 0 : value;
79
+ }
80
+ if (typeof value !== 'object')
81
+ throw error('invalid_request', `${field} must be JSON-compatible`);
82
+ if (active.has(value))
83
+ throw error('invalid_request', `${field} must not contain cycles`);
84
+ active.add(value);
85
+ try {
86
+ if (Array.isArray(value))
87
+ return value.map((entry, index) => cloneJson(entry, `${field}[${index}]`, active));
88
+ if (!isPlainRecord(value))
89
+ throw error('invalid_request', `${field} must contain plain objects`);
90
+ const result = Object.create(null);
91
+ for (const key of Object.keys(value).sort(compareCodePoints))
92
+ result[key] = cloneJson(value[key], `${field}.${key}`, active);
93
+ return result;
94
+ }
95
+ finally {
96
+ active.delete(value);
97
+ }
98
+ }
99
+ function canonicalJson(value) {
100
+ return JSON.stringify(cloneJson(value, 'value'));
101
+ }
102
+ function deepFreeze(value) {
103
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
104
+ Object.freeze(value);
105
+ for (const child of Object.values(value))
106
+ deepFreeze(child);
107
+ }
108
+ return value;
109
+ }
110
+ function normalizeScope(value, field) {
111
+ if (!isPlainRecord(value))
112
+ throw error('invalid_request', `${field} must be an object`);
113
+ exactKeys(value, ['subjectIds', 'validFrom', 'validUntil'], field);
114
+ if (!Array.isArray(value.subjectIds) || value.subjectIds.length === 0)
115
+ throw error('invalid_request', `${field}.subjectIds must be non-empty`);
116
+ const subjectIds = value.subjectIds.map((entry, index) => requireString(entry, `${field}.subjectIds[${index}]`));
117
+ if (new Set(subjectIds).size !== subjectIds.length)
118
+ throw error('invalid_request', `${field}.subjectIds must be unique`);
119
+ const scope = {
120
+ subjectIds: [...subjectIds].sort(compareCodePoints),
121
+ validFrom: requireIsoTimestamp(value.validFrom, `${field}.validFrom`),
122
+ ...(value.validUntil === undefined ? {} : { validUntil: requireIsoTimestamp(value.validUntil, `${field}.validUntil`) })
123
+ };
124
+ if (scope.validUntil !== undefined && Date.parse(scope.validUntil) < Date.parse(scope.validFrom)) {
125
+ throw error('invalid_request', `${field}.validUntil cannot precede validFrom`);
126
+ }
127
+ return scope;
128
+ }
129
+ function normalizeAcl(value, field) {
130
+ if (!isPlainRecord(value))
131
+ throw error('invalid_request', `${field} must be an object`);
132
+ exactKeys(value, ['ownerId', 'visibility', 'readerIds', 'writerIds'], field);
133
+ const ownerId = requireString(value.ownerId, `${field}.ownerId`);
134
+ if (!['private', 'project', 'organization', 'public'].includes(value.visibility))
135
+ throw error('invalid_request', `${field}.visibility is invalid`);
136
+ const readIds = (entry, child) => {
137
+ if (!Array.isArray(entry))
138
+ throw error('invalid_request', `${child} must be an array`);
139
+ const ids = entry.map((id, index) => requireString(id, `${child}[${index}]`));
140
+ if (new Set(ids).size !== ids.length)
141
+ throw error('invalid_request', `${child} must be unique`);
142
+ return [...ids].sort(compareCodePoints);
143
+ };
144
+ return {
145
+ ownerId,
146
+ visibility: value.visibility,
147
+ readerIds: readIds(value.readerIds, `${field}.readerIds`),
148
+ writerIds: readIds(value.writerIds, `${field}.writerIds`)
149
+ };
150
+ }
151
+ function equalScope(left, right) {
152
+ return canonicalJson(left) === canonicalJson(right);
153
+ }
154
+ function scopeContains(container, target) {
155
+ const allowedSubjects = new Set(container.subjectIds);
156
+ if (target.subjectIds.length === 0 || target.subjectIds.some((subjectId) => !allowedSubjects.has(subjectId)))
157
+ return false;
158
+ if (Date.parse(target.validFrom) < Date.parse(container.validFrom))
159
+ return false;
160
+ if (container.validUntil !== undefined) {
161
+ if (target.validUntil === undefined || Date.parse(target.validUntil) > Date.parse(container.validUntil))
162
+ return false;
163
+ }
164
+ return true;
165
+ }
166
+ function candidateScopeViolation(selectionScope, candidateRefs) {
167
+ const outside = candidateRefs.find((reference) => !scopeContains(selectionScope, reference.ownerScope));
168
+ return outside === undefined
169
+ ? undefined
170
+ : `candidate ${outside.candidateId} owner scope is outside selection owner scope`;
171
+ }
172
+ function normalizeDigest(value, field) {
173
+ const digest = requireString(value, field);
174
+ if (!DIGEST_PATTERN.test(digest))
175
+ throw error('invalid_request', `${field} must be a sha256:<64 lowercase hex> digest`);
176
+ return digest;
177
+ }
178
+ function normalizeSnapshotReference(value) {
179
+ if (!isPlainRecord(value))
180
+ throw error('invalid_request', 'selectionProblem must be an object');
181
+ exactKeys(value, ['snapshot_id', 'problem_id', 'revision'], 'selectionProblem');
182
+ const snapshotId = requireString(value.snapshot_id, 'selectionProblem.snapshot_id');
183
+ if (!SNAPSHOT_ID_PATTERN.test(snapshotId))
184
+ throw error('invalid_request', 'selectionProblem.snapshot_id is invalid');
185
+ const revision = requireString(value.revision, 'selectionProblem.revision');
186
+ if (!/^[1-9]\d*$/u.test(revision))
187
+ throw error('invalid_request', 'selectionProblem.revision is invalid');
188
+ return { snapshot_id: snapshotId, problem_id: requireString(value.problem_id, 'selectionProblem.problem_id'), revision };
189
+ }
190
+ function normalizeFoundationReference(value, field) {
191
+ if (!isPlainRecord(value))
192
+ throw error('invalid_request', `${field} must be an object`);
193
+ exactKeys(value, ['id', 'type', 'revision', 'digest'], field);
194
+ const type = requireString(value.type, `${field}.type`);
195
+ if (!['objective', 'variable', 'model', 'constraint'].includes(type))
196
+ throw error('invalid_request', `${field}.type is invalid`);
197
+ const revision = requireString(value.revision, `${field}.revision`);
198
+ if (!/^[1-9]\d*$/u.test(revision))
199
+ throw error('invalid_request', `${field}.revision is invalid`);
200
+ return { id: requireString(value.id, `${field}.id`), type: type, revision, digest: normalizeDigest(value.digest, `${field}.digest`) };
201
+ }
202
+ function normalizeKnownOrUnknown(value, field, parse) {
203
+ if (!isPlainRecord(value))
204
+ throw error('invalid_request', `${field} must be an object`);
205
+ exactKeys(value, ['status', 'value', 'reason'], field);
206
+ if (value.status === 'unknown')
207
+ return { status: 'unknown', reason: requireString(value.reason, `${field}.reason`) };
208
+ if (value.status !== 'known')
209
+ throw error('invalid_request', `${field}.status is invalid`);
210
+ return { status: 'known', value: parse(value.value, `${field}.value`) };
211
+ }
212
+ function parseNumber(value, field) { return requireFiniteNumber(value, field); }
213
+ function normalizeCosts(value, field) {
214
+ if (!isPlainRecord(value))
215
+ throw error('invalid_request', `${field} must be an object`);
216
+ exactKeys(value, ['switching', 'opportunity', 'exploration'], field);
217
+ return {
218
+ switching: normalizeKnownOrUnknown(value.switching, `${field}.switching`, parseNumber),
219
+ opportunity: normalizeKnownOrUnknown(value.opportunity, `${field}.opportunity`, parseNumber),
220
+ exploration: normalizeKnownOrUnknown(value.exploration, `${field}.exploration`, parseNumber)
221
+ };
222
+ }
223
+ function normalizeConditions(value) {
224
+ if (!isPlainRecord(value))
225
+ throw error('invalid_request', 'fixedConditions must be an object');
226
+ exactKeys(value, ['objectiveRefs', 'constraintRefs', 'conditionalPreferences', 'delegatedJudgments', 'costs', 'explorationLimit', 'evaluatedAt'], 'fixedConditions');
227
+ if (!Array.isArray(value.objectiveRefs) || !Array.isArray(value.constraintRefs))
228
+ throw error('invalid_request', 'fixedConditions references must be arrays');
229
+ const objectiveRefs = value.objectiveRefs.map((entry, index) => normalizeFoundationReference(entry, `fixedConditions.objectiveRefs[${index}]`));
230
+ const constraintRefs = value.constraintRefs.map((entry, index) => normalizeFoundationReference(entry, `fixedConditions.constraintRefs[${index}]`));
231
+ if (objectiveRefs.some((entry) => entry.type !== 'objective'))
232
+ throw error('invalid_request', 'objectiveRefs must reference Objectives');
233
+ if (constraintRefs.some((entry) => entry.type !== 'constraint'))
234
+ throw error('invalid_request', 'constraintRefs must reference Constraints');
235
+ const parseList = (entry, field) => {
236
+ if (!Array.isArray(entry))
237
+ throw error('invalid_request', `${field} must be an array`);
238
+ return entry.map((item, index) => {
239
+ if (!isPlainRecord(item))
240
+ throw error('invalid_request', `${field}[${index}] must be an object`);
241
+ return item;
242
+ });
243
+ };
244
+ const conditionalPreferences = parseList(value.conditionalPreferences, 'fixedConditions.conditionalPreferences').map((item, index) => {
245
+ exactKeys(item, ['id', 'condition', 'preference'], `fixedConditions.conditionalPreferences[${index}]`);
246
+ return { id: requireString(item.id, `fixedConditions.conditionalPreferences[${index}].id`), condition: requireString(item.condition, `fixedConditions.conditionalPreferences[${index}].condition`), preference: requireString(item.preference, `fixedConditions.conditionalPreferences[${index}].preference`) };
247
+ });
248
+ const delegatedJudgments = parseList(value.delegatedJudgments, 'fixedConditions.delegatedJudgments').map((item, index) => {
249
+ exactKeys(item, ['id', 'question', 'ownerId'], `fixedConditions.delegatedJudgments[${index}]`);
250
+ return { id: requireString(item.id, `fixedConditions.delegatedJudgments[${index}].id`), question: requireString(item.question, `fixedConditions.delegatedJudgments[${index}].question`), ownerId: requireString(item.ownerId, `fixedConditions.delegatedJudgments[${index}].ownerId`) };
251
+ });
252
+ if (!isPlainRecord(value.explorationLimit))
253
+ throw error('invalid_request', 'fixedConditions.explorationLimit must be an object');
254
+ exactKeys(value.explorationLimit, ['maxCandidates', 'maxExplorationCost'], 'fixedConditions.explorationLimit');
255
+ const maxCandidates = requireFiniteNumber(value.explorationLimit.maxCandidates, 'fixedConditions.explorationLimit.maxCandidates');
256
+ if (!Number.isInteger(maxCandidates) || maxCandidates < 1)
257
+ throw error('invalid_request', 'maxCandidates must be a positive integer');
258
+ return deepFreeze({
259
+ objectiveRefs,
260
+ constraintRefs,
261
+ conditionalPreferences,
262
+ delegatedJudgments,
263
+ costs: normalizeCosts(value.costs, 'fixedConditions.costs'),
264
+ explorationLimit: { maxCandidates, maxExplorationCost: normalizeKnownOrUnknown(value.explorationLimit.maxExplorationCost, 'fixedConditions.explorationLimit.maxExplorationCost', parseNumber) },
265
+ evaluatedAt: requireIsoTimestamp(value.evaluatedAt, 'fixedConditions.evaluatedAt')
266
+ });
267
+ }
268
+ function normalizeCandidateReference(value, field) {
269
+ if (!isPlainRecord(value))
270
+ throw error('invalid_request', `${field} must be an object`);
271
+ exactKeys(value, ['candidateId', 'payloadDigest', 'ownerScope'], field);
272
+ return deepFreeze({
273
+ candidateId: requireString(value.candidateId, `${field}.candidateId`),
274
+ payloadDigest: normalizeDigest(value.payloadDigest, `${field}.payloadDigest`),
275
+ ownerScope: normalizeScope(value.ownerScope, `${field}.ownerScope`)
276
+ });
277
+ }
278
+ function normalizeUnknowns(value, field) {
279
+ if (!Array.isArray(value))
280
+ throw error('invalid_record', `${field} must be an array`);
281
+ return value.map((entry, index) => {
282
+ if (!isPlainRecord(entry))
283
+ throw error('invalid_record', `${field}[${index}] must be an object`);
284
+ exactKeys(entry, ['status', 'reason'], `${field}[${index}]`);
285
+ if (entry.status !== 'unknown')
286
+ throw error('invalid_record', `${field}[${index}].status must be unknown`);
287
+ return { status: 'unknown', reason: requireString(entry.reason, `${field}[${index}].reason`) };
288
+ });
289
+ }
290
+ function normalizeObjectiveConflicts(value, field) {
291
+ if (!Array.isArray(value))
292
+ throw error('invalid_record', `${field} must be an array`);
293
+ return value.map((entry, index) => {
294
+ if (!isPlainRecord(entry))
295
+ throw error('invalid_record', `${field}[${index}] must be an object`);
296
+ exactKeys(entry, ['objectiveIds', 'reason'], `${field}[${index}]`);
297
+ if (!Array.isArray(entry.objectiveIds))
298
+ throw error('invalid_record', `${field}[${index}].objectiveIds must be an array`);
299
+ return {
300
+ objectiveIds: entry.objectiveIds.map((id, idIndex) => requireString(id, `${field}[${index}].objectiveIds[${idIndex}]`)),
301
+ reason: requireString(entry.reason, `${field}[${index}].reason`)
302
+ };
303
+ });
304
+ }
305
+ function normalizeRecordCandidateAssessment(value, field) {
306
+ if (!isPlainRecord(value))
307
+ throw error('invalid_request', `${field} must be an object`);
308
+ exactKeys(value, ['reference', 'status', 'action', 'rationale', 'ownerId', 'conditions', 'reviewAt', 'costs', 'unknowns', 'objectiveConflicts'], field);
309
+ const status = value.status;
310
+ if (!['eligible', 'incomparable', 'unavailable', 'unknown'].includes(status))
311
+ throw error('invalid_request', `${field}.status is invalid`);
312
+ const conditions = value.conditions === undefined ? [] : value.conditions;
313
+ if (!Array.isArray(conditions))
314
+ throw error('invalid_request', `${field}.conditions must be an array`);
315
+ const unknowns = value.unknowns === undefined ? [] : value.unknowns;
316
+ if (!Array.isArray(unknowns))
317
+ throw error('invalid_request', `${field}.unknowns must be an array`);
318
+ const parsedUnknowns = unknowns.map((entry, index) => {
319
+ if (!isPlainRecord(entry))
320
+ throw error('invalid_request', `${field}.unknowns[${index}] must be an object`);
321
+ exactKeys(entry, ['status', 'reason'], `${field}.unknowns[${index}]`);
322
+ if (entry.status !== 'unknown')
323
+ throw error('invalid_request', `${field}.unknowns[${index}].status must be unknown`);
324
+ return { status: 'unknown', reason: requireString(entry.reason, `${field}.unknowns[${index}].reason`) };
325
+ });
326
+ const conflicts = value.objectiveConflicts === undefined ? [] : value.objectiveConflicts;
327
+ if (!Array.isArray(conflicts))
328
+ throw error('invalid_request', `${field}.objectiveConflicts must be an array`);
329
+ const parsedConflicts = conflicts.map((entry, index) => {
330
+ if (!isPlainRecord(entry))
331
+ throw error('invalid_request', `${field}.objectiveConflicts[${index}] must be an object`);
332
+ exactKeys(entry, ['objectiveIds', 'reason'], `${field}.objectiveConflicts[${index}]`);
333
+ if (!Array.isArray(entry.objectiveIds))
334
+ throw error('invalid_request', `${field}.objectiveConflicts[${index}].objectiveIds must be an array`);
335
+ return { objectiveIds: entry.objectiveIds.map((id, idIndex) => requireString(id, `${field}.objectiveConflicts[${index}].objectiveIds[${idIndex}]`)), reason: requireString(entry.reason, `${field}.objectiveConflicts[${index}].reason`) };
336
+ });
337
+ return deepFreeze({
338
+ reference: normalizeCandidateReference(value.reference, `${field}.reference`),
339
+ status,
340
+ ...(value.action === undefined ? {} : { action: requireAction(value.action, `${field}.action`) }),
341
+ rationale: requireString(value.rationale, `${field}.rationale`),
342
+ ...(value.ownerId === undefined ? {} : { ownerId: requireString(value.ownerId, `${field}.ownerId`) }),
343
+ conditions: conditions.map((entry, index) => requireString(entry, `${field}.conditions[${index}]`)),
344
+ ...(value.reviewAt === undefined ? {} : { reviewAt: requireIsoTimestamp(value.reviewAt, `${field}.reviewAt`) }),
345
+ costs: normalizeCosts(value.costs, `${field}.costs`),
346
+ unknowns: parsedUnknowns,
347
+ objectiveConflicts: parsedConflicts
348
+ });
349
+ }
350
+ function requireAction(value, field) {
351
+ if (!['start', 'continue', 'observe', 'hold', 'stop'].includes(value))
352
+ throw error('invalid_request', `${field} is invalid`);
353
+ return value;
354
+ }
355
+ function assertCandidateReferencesUnique(refs) {
356
+ const ids = refs.map((ref) => ref.candidateId);
357
+ if (new Set(ids).size !== ids.length)
358
+ throw error('invalid_request', 'candidateRefs must not contain duplicates');
359
+ }
360
+ function assessableCandidate(reference, costs) {
361
+ return { reference, status: 'eligible', rationale: 'candidate resolved and its exact digest is current', conditions: [], costs, unknowns: [], objectiveConflicts: [] };
362
+ }
363
+ function reviewAssessment(reference, reason, costs) {
364
+ return { reference, status: 'unavailable', rationale: reason, conditions: [], costs, unknowns: [{ status: 'unknown', reason }], objectiveConflicts: [] };
365
+ }
366
+ function assertReviewAt(decision) {
367
+ if (decision.action === 'hold' && decision.reviewAt === undefined)
368
+ throw error('invalid_request', 'hold decisions require reviewAt');
369
+ }
370
+ function normalizeEvaluationResult(raw, candidateRefs, fixedConditions) {
371
+ if (!isPlainRecord(raw))
372
+ throw error('comparison_unavailable', 'selection evaluator returned a non-object result');
373
+ exactKeys(raw, ['status', 'selectedCandidateId', 'action', 'reason', 'ownerId', 'conditions', 'reviewAt', 'assessments', 'unknowns', 'objectiveConflicts'], 'evaluation result');
374
+ const status = raw.status;
375
+ if (status !== 'selected' && status !== 'human_review_required')
376
+ throw error('comparison_unavailable', 'selection evaluator returned an invalid status');
377
+ const reason = requireString(raw.reason, 'evaluation result.reason');
378
+ const selectedCandidateId = raw.selectedCandidateId === undefined ? undefined : requireString(raw.selectedCandidateId, 'evaluation result.selectedCandidateId');
379
+ if (selectedCandidateId !== undefined && !candidateRefs.some((ref) => ref.candidateId === selectedCandidateId))
380
+ throw error('comparison_unavailable', 'evaluator selected a candidate outside the fixed candidate set');
381
+ const action = raw.action === undefined ? undefined : requireAction(raw.action, 'evaluation result.action');
382
+ if (status === 'selected' && (selectedCandidateId === undefined || action === undefined)) {
383
+ throw error('comparison_unavailable', 'selected evaluation must include selectedCandidateId and action');
384
+ }
385
+ const conditions = raw.conditions === undefined ? [] : raw.conditions;
386
+ if (!Array.isArray(conditions))
387
+ throw error('comparison_unavailable', 'evaluation result.conditions must be an array');
388
+ const unknowns = raw.unknowns === undefined ? [] : raw.unknowns;
389
+ if (!Array.isArray(unknowns))
390
+ throw error('comparison_unavailable', 'evaluation result.unknowns must be an array');
391
+ const parsedUnknowns = unknowns.map((entry, index) => {
392
+ if (!isPlainRecord(entry) || entry.status !== 'unknown')
393
+ throw error('comparison_unavailable', `evaluation result.unknowns[${index}] is invalid`);
394
+ return { status: 'unknown', reason: requireString(entry.reason, `evaluation result.unknowns[${index}].reason`) };
395
+ });
396
+ const conflicts = raw.objectiveConflicts === undefined ? [] : raw.objectiveConflicts;
397
+ if (!Array.isArray(conflicts))
398
+ throw error('comparison_unavailable', 'evaluation result.objectiveConflicts must be an array');
399
+ const parsedConflicts = conflicts.map((entry, index) => {
400
+ if (!isPlainRecord(entry) || !Array.isArray(entry.objectiveIds))
401
+ throw error('comparison_unavailable', `evaluation result.objectiveConflicts[${index}] is invalid`);
402
+ return { objectiveIds: entry.objectiveIds.map((id, idIndex) => requireString(id, `evaluation result.objectiveConflicts[${index}].objectiveIds[${idIndex}]`)), reason: requireString(entry.reason, `evaluation result.objectiveConflicts[${index}].reason`) };
403
+ });
404
+ const assessments = raw.assessments === undefined
405
+ ? candidateRefs.map((ref) => assessableCandidate(ref, fixedConditions.costs))
406
+ : raw.assessments.map((entry, index) => normalizeRecordCandidateAssessment(entry, `evaluation result.assessments[${index}]`));
407
+ if (assessments.length !== candidateRefs.length)
408
+ throw error('comparison_unavailable', 'evaluator must return one assessment per candidate');
409
+ for (const ref of candidateRefs) {
410
+ const assessment = assessments.find((item) => item.reference.candidateId === ref.candidateId);
411
+ if (assessment === undefined)
412
+ throw error('comparison_unavailable', `evaluator omitted assessment for ${ref.candidateId}`);
413
+ if (canonicalJson(assessment.reference) !== canonicalJson(ref)) {
414
+ throw error('comparison_unavailable', `evaluator changed the fixed reference for ${ref.candidateId}`);
415
+ }
416
+ }
417
+ return deepFreeze({
418
+ status,
419
+ ...(selectedCandidateId === undefined ? {} : { selectedCandidateId }),
420
+ ...(action === undefined ? {} : { action }),
421
+ reason,
422
+ ...(raw.ownerId === undefined ? {} : { ownerId: requireString(raw.ownerId, 'evaluation result.ownerId') }),
423
+ conditions: conditions.map((entry, index) => requireString(entry, `evaluation result.conditions[${index}]`)),
424
+ ...(raw.reviewAt === undefined ? {} : { reviewAt: requireIsoTimestamp(raw.reviewAt, 'evaluation result.reviewAt') }),
425
+ assessments,
426
+ unknowns: parsedUnknowns,
427
+ objectiveConflicts: parsedConflicts
428
+ });
429
+ }
430
+ function canonicalCandidateDigest(record) {
431
+ return record.payloadDigest;
432
+ }
433
+ function decisionFromEvaluation(result, candidateRefs, assessments) {
434
+ const candidate = result.selectedCandidateId === undefined ? undefined : candidateRefs.find((ref) => ref.candidateId === result.selectedCandidateId);
435
+ const assessment = candidate === undefined ? undefined : assessments.find((item) => item.reference.candidateId === candidate.candidateId);
436
+ const status = result.status;
437
+ const decision = {
438
+ status,
439
+ ...(candidate === undefined ? {} : { candidate }),
440
+ ...(result.action === undefined ? {} : { action: result.action }),
441
+ reason: result.reason,
442
+ ...(result.ownerId === undefined ? {} : { ownerId: result.ownerId }),
443
+ conditions: result.conditions ?? [],
444
+ ...(result.reviewAt === undefined ? {} : { reviewAt: result.reviewAt })
445
+ };
446
+ if (assessment?.status === 'incomparable' || assessment?.status === 'unavailable') {
447
+ return { ...decision, status: 'human_review_required', reason: `${result.reason}; selected candidate is not comparable or available` };
448
+ }
449
+ assertReviewAt(decision);
450
+ return decision;
451
+ }
452
+ function fixedUnknowns(fixed) {
453
+ const unknowns = [];
454
+ for (const [key, value] of Object.entries(fixed.costs))
455
+ if (value.status === 'unknown')
456
+ unknowns.push(value);
457
+ if (fixed.explorationLimit.maxExplorationCost.status === 'unknown')
458
+ unknowns.push(fixed.explorationLimit.maxExplorationCost);
459
+ return unknowns;
460
+ }
461
+ function assessmentUnknowns(assessment) {
462
+ const unknowns = [...assessment.unknowns];
463
+ if (assessment.status === 'unknown')
464
+ unknowns.push({ status: 'unknown', reason: assessment.rationale });
465
+ for (const value of Object.values(assessment.costs)) {
466
+ if (value.status === 'unknown')
467
+ unknowns.push(value);
468
+ }
469
+ return unknowns;
470
+ }
471
+ function mergeUnknowns(...groups) {
472
+ const seen = new Set();
473
+ const merged = [];
474
+ for (const group of groups) {
475
+ for (const unknown of group) {
476
+ const key = `${unknown.status}\u0000${unknown.reason}`;
477
+ if (seen.has(key))
478
+ continue;
479
+ seen.add(key);
480
+ merged.push(unknown);
481
+ }
482
+ }
483
+ return merged;
484
+ }
485
+ function candidateCountViolation(fixedConditions, candidateRefs) {
486
+ if (candidateRefs.length <= fixedConditions.explorationLimit.maxCandidates)
487
+ return undefined;
488
+ return `candidate count ${candidateRefs.length} exceeds the fixed exploration limit ${fixedConditions.explorationLimit.maxCandidates}`;
489
+ }
490
+ function explorationLimitViolation(fixedConditions, selectedCandidateId, assessments) {
491
+ if (selectedCandidateId === undefined)
492
+ return undefined;
493
+ const limit = fixedConditions.explorationLimit.maxExplorationCost;
494
+ if (limit.status !== 'known')
495
+ return undefined;
496
+ const assessment = assessments.find((item) => item.reference.candidateId === selectedCandidateId);
497
+ const exploration = assessment?.costs.exploration;
498
+ if (exploration === undefined || exploration.status !== 'known')
499
+ return undefined;
500
+ if (exploration.value <= limit.value)
501
+ return undefined;
502
+ return `selected candidate exploration cost ${exploration.value} exceeds the fixed exploration limit ${limit.value}`;
503
+ }
504
+ /**
505
+ * Conditions that make a selected record unsafe to persist. This is shared
506
+ * with generation so the record boundary cannot accept a state that the
507
+ * selection run would have routed to human review.
508
+ */
509
+ function selectedRecordPolicyViolation(ownerScope, fixedConditions, candidateRefs, assessments, objectiveConflicts, selectedCandidateId, action, unknowns) {
510
+ const candidateViolation = candidateCountViolation(fixedConditions, candidateRefs);
511
+ if (candidateViolation !== undefined)
512
+ return candidateViolation;
513
+ const scopeViolation = candidateScopeViolation(ownerScope, candidateRefs);
514
+ if (scopeViolation !== undefined)
515
+ return scopeViolation;
516
+ if (objectiveConflicts.length > 0 || assessments.some((assessment) => assessment.objectiveConflicts.length > 0)) {
517
+ return 'objective criteria are incomparable';
518
+ }
519
+ if (assessments.some((assessment) => assessment.status === 'unavailable' || assessment.status === 'incomparable')) {
520
+ return 'comparison is not fully available';
521
+ }
522
+ const limitViolation = explorationLimitViolation(fixedConditions, selectedCandidateId, assessments);
523
+ if (limitViolation !== undefined)
524
+ return limitViolation;
525
+ const unresolved = mergeUnknowns(unknowns, fixedUnknowns(fixedConditions), assessments.flatMap(assessmentUnknowns));
526
+ if (unresolved.length > 0 && action !== 'observe')
527
+ return 'unresolved unknowns remain';
528
+ return undefined;
529
+ }
530
+ /**
531
+ * Resolve candidates and create one immutable selection record. Candidate
532
+ * bodies are used only inside the evaluator call and are never copied into
533
+ * the record; the record keeps exact candidate references and digests.
534
+ */
535
+ export async function createProblemSelection(request) {
536
+ if (!isPlainRecord(request))
537
+ throw error('invalid_request', 'selection request must be an object');
538
+ const selectionId = requireString(request.selectionId, 'selectionId');
539
+ const selectionProblem = normalizeSnapshotReference(request.selectionProblem);
540
+ const composition = createJudgmentDAGCompositionDefinition(request.selectionComposition);
541
+ const ownerScope = normalizeScope(request.ownerScope, 'ownerScope');
542
+ const readPolicy = normalizeAcl(request.readPolicy, 'readPolicy');
543
+ const fixedConditions = normalizeConditions(request.fixedConditions);
544
+ if (!Array.isArray(request.candidateRefs))
545
+ throw error('invalid_request', 'candidateRefs must be an array');
546
+ const candidateRefs = request.candidateRefs.map((entry, index) => normalizeCandidateReference(entry, `candidateRefs[${index}]`));
547
+ assertCandidateReferencesUnique(candidateRefs);
548
+ if (candidateRefs.length === 0)
549
+ throw error('invalid_request', 'candidateRefs must contain at least one candidate');
550
+ const candidateViolation = candidateCountViolation(fixedConditions, candidateRefs);
551
+ if (candidateViolation !== undefined) {
552
+ const reason = candidateViolation;
553
+ return deepFreeze({
554
+ recordVersion: PROBLEM_SELECTION_RECORD_VERSION,
555
+ contractVersion: PROBLEM_SELECTION_CONTRACT_VERSION,
556
+ selectionId,
557
+ selectionProblem,
558
+ selectionComposition: composition,
559
+ ownerScope,
560
+ readPolicy,
561
+ fixedConditions,
562
+ candidateRefs,
563
+ assessments: candidateRefs.map((ref) => reviewAssessment(ref, reason, fixedConditions.costs)),
564
+ objectiveConflicts: [],
565
+ unknowns: [],
566
+ status: 'human_review_required',
567
+ decision: { status: 'human_review_required', reason, conditions: [] },
568
+ executionPermission: 'none',
569
+ resourceAuthority: 'none',
570
+ objectiveChange: 'none',
571
+ createdAt: request.createdAt === undefined ? new Date().toISOString() : requireIsoTimestamp(request.createdAt, 'createdAt')
572
+ });
573
+ }
574
+ if (!request.candidateStore || typeof request.candidateStore.readCandidate !== 'function')
575
+ throw error('invalid_request', 'candidateStore.readCandidate is required');
576
+ const resolved = [];
577
+ const assessments = [];
578
+ let resolutionFailure = false;
579
+ for (const reference of candidateRefs) {
580
+ if (!scopeContains(ownerScope, reference.ownerScope)) {
581
+ resolutionFailure = true;
582
+ assessments.push(reviewAssessment(reference, 'candidate owner scope is outside selection owner scope', fixedConditions.costs));
583
+ continue;
584
+ }
585
+ try {
586
+ const record = await request.candidateStore.readCandidate(reference.candidateId, request.candidateContext);
587
+ if (record === null) {
588
+ resolutionFailure = true;
589
+ assessments.push(reviewAssessment(reference, 'candidate is unavailable under the current access policy', fixedConditions.costs));
590
+ continue;
591
+ }
592
+ if (record.status !== 'candidate') {
593
+ resolutionFailure = true;
594
+ assessments.push(reviewAssessment(reference, `candidate status ${record.status} is not selectable`, fixedConditions.costs));
595
+ continue;
596
+ }
597
+ if (canonicalCandidateDigest(record) !== reference.payloadDigest) {
598
+ resolutionFailure = true;
599
+ assessments.push(reviewAssessment(reference, 'candidate payload digest no longer matches the fixed selection input', fixedConditions.costs));
600
+ continue;
601
+ }
602
+ if (!scopeContains(ownerScope, record.ownerScope)) {
603
+ resolutionFailure = true;
604
+ assessments.push(reviewAssessment(reference, 'candidate owner scope is outside selection owner scope', fixedConditions.costs));
605
+ continue;
606
+ }
607
+ if (!equalScope(record.ownerScope, reference.ownerScope)) {
608
+ resolutionFailure = true;
609
+ assessments.push(reviewAssessment(reference, 'candidate owner scope no longer matches the fixed selection input', fixedConditions.costs));
610
+ continue;
611
+ }
612
+ resolved.push({ reference, record });
613
+ assessments.push(assessableCandidate(reference, fixedConditions.costs));
614
+ }
615
+ catch (cause) {
616
+ resolutionFailure = true;
617
+ const message = cause instanceof Error ? cause.message : 'candidate access failed';
618
+ assessments.push(reviewAssessment(reference, `candidate access could not be verified: ${message}`, fixedConditions.costs));
619
+ }
620
+ }
621
+ if (resolutionFailure) {
622
+ const reason = 'one or more candidates could not be resolved with their fixed digest and current access';
623
+ return deepFreeze({
624
+ recordVersion: PROBLEM_SELECTION_RECORD_VERSION,
625
+ contractVersion: PROBLEM_SELECTION_CONTRACT_VERSION,
626
+ selectionId,
627
+ selectionProblem,
628
+ selectionComposition: composition,
629
+ ownerScope,
630
+ readPolicy,
631
+ fixedConditions,
632
+ candidateRefs,
633
+ assessments,
634
+ objectiveConflicts: [],
635
+ unknowns: [],
636
+ status: 'human_review_required',
637
+ decision: { status: 'human_review_required', reason, conditions: [] },
638
+ executionPermission: 'none',
639
+ resourceAuthority: 'none',
640
+ objectiveChange: 'none',
641
+ createdAt: request.createdAt === undefined ? new Date().toISOString() : requireIsoTimestamp(request.createdAt, 'createdAt')
642
+ });
643
+ }
644
+ let evaluation;
645
+ try {
646
+ if (!request.evaluator || typeof request.evaluator.evaluate !== 'function')
647
+ throw new Error('evaluator.evaluate is required');
648
+ evaluation = normalizeEvaluationResult(await request.evaluator.evaluate({ selectionId, selectionProblem, composition, candidates: resolved, fixedConditions }), candidateRefs, fixedConditions);
649
+ }
650
+ catch (cause) {
651
+ const message = cause instanceof Error ? cause.message : 'comparison evaluator failed';
652
+ evaluation = {
653
+ status: 'human_review_required',
654
+ reason: `comparison could not be completed: ${message}`,
655
+ assessments,
656
+ unknowns: [{ status: 'unknown', reason: message }],
657
+ objectiveConflicts: []
658
+ };
659
+ }
660
+ const normalizedAssessments = evaluation.assessments ?? assessments;
661
+ const unknowns = mergeUnknowns(evaluation.unknowns ?? [], fixedUnknowns(fixedConditions), normalizedAssessments.flatMap(assessmentUnknowns));
662
+ const objectiveConflicts = [...(evaluation.objectiveConflicts ?? []), ...normalizedAssessments.flatMap((item) => item.objectiveConflicts)];
663
+ const policyViolation = selectedRecordPolicyViolation(ownerScope, fixedConditions, candidateRefs, normalizedAssessments, objectiveConflicts, evaluation.selectedCandidateId, evaluation.action, unknowns);
664
+ const shouldRequireReview = evaluation.status === 'human_review_required' || policyViolation !== undefined;
665
+ const effectiveEvaluation = shouldRequireReview && evaluation.status !== 'human_review_required'
666
+ ? { ...evaluation, status: 'human_review_required', reason: policyViolation !== undefined ? `${evaluation.reason}; ${policyViolation}` : `${evaluation.reason}; comparison is not fully available` }
667
+ : evaluation;
668
+ const decision = decisionFromEvaluation(effectiveEvaluation, candidateRefs, normalizedAssessments);
669
+ const problemCreationRequest = decision.status === 'selected' && (decision.action === 'start' || decision.action === 'continue') && decision.candidate !== undefined
670
+ ? { status: 'required', candidate: decision.candidate, action: decision.action, requiredConditions: decision.conditions, completeSnapshotRequired: true }
671
+ : undefined;
672
+ return deepFreeze({
673
+ recordVersion: PROBLEM_SELECTION_RECORD_VERSION,
674
+ contractVersion: PROBLEM_SELECTION_CONTRACT_VERSION,
675
+ selectionId,
676
+ selectionProblem,
677
+ selectionComposition: composition,
678
+ ownerScope,
679
+ readPolicy,
680
+ fixedConditions,
681
+ candidateRefs,
682
+ assessments: normalizedAssessments,
683
+ objectiveConflicts,
684
+ unknowns,
685
+ status: decision.status,
686
+ decision,
687
+ ...(problemCreationRequest === undefined ? {} : { problemCreationRequest }),
688
+ executionPermission: 'none',
689
+ resourceAuthority: 'none',
690
+ objectiveChange: 'none',
691
+ createdAt: request.createdAt === undefined ? new Date().toISOString() : requireIsoTimestamp(request.createdAt, 'createdAt')
692
+ });
693
+ }
694
+ /** Alias for callers that name the operation as a run. */
695
+ export const runProblemSelection = createProblemSelection;
696
+ /** Alias for callers that name the operation as a selection. */
697
+ export const selectProblem = createProblemSelection;
698
+ /**
699
+ * Adapter helper for a composition whose designated child returns the
700
+ * selection contract in its conclusion. The meta Problem remains the exact
701
+ * snapshot supplied to the composition request; a selected candidate still
702
+ * requires a later Problem creation request.
703
+ */
704
+ export async function evaluateProblemSelectionWithComposition(input) {
705
+ let run;
706
+ try {
707
+ run = await executeJudgmentDAGComposition(input.compositionRequest);
708
+ }
709
+ catch (cause) {
710
+ const reason = cause instanceof Error ? cause.message : 'DAG composition failed';
711
+ return { status: 'human_review_required', reason: `selection composition unavailable: ${reason}`, unknowns: [{ status: 'unknown', reason }], objectiveConflicts: [] };
712
+ }
713
+ if (run.status !== 'completed') {
714
+ const reason = `selection composition status is ${run.status}`;
715
+ return { status: 'human_review_required', reason, unknowns: [{ status: 'unknown', reason }], objectiveConflicts: [] };
716
+ }
717
+ const child = input.resultInvocationId === undefined
718
+ ? run.children[run.children.length - 1]
719
+ : run.children.find((entry) => entry.invocation_id === input.resultInvocationId);
720
+ if (child === undefined)
721
+ return { status: 'human_review_required', reason: 'selection composition returned no designated result child', unknowns: [{ status: 'unknown', reason: 'missing selection result child' }], objectiveConflicts: [] };
722
+ if (child.result.status !== 'completed' || !isPlainRecord(child.result.conclusion)) {
723
+ const reason = child.result.reason ?? `selection child status is ${child.result.status}`;
724
+ return { status: 'human_review_required', reason, unknowns: [{ status: 'unknown', reason }], objectiveConflicts: [] };
725
+ }
726
+ return child.result.conclusion;
727
+ }
728
+ function selectionRecordPayload(record) {
729
+ return record;
730
+ }
731
+ function recordIdForPayload(record) {
732
+ const digest = createHash('sha256').update(canonicalJson(selectionRecordPayload(record))).digest('hex');
733
+ return `sha256:${digest}`;
734
+ }
735
+ export function computeProblemSelectionRecordId(record) {
736
+ validateProblemSelectionRecord(record);
737
+ return recordIdForPayload(record);
738
+ }
739
+ function defaultAuthorize(operation, context, record) {
740
+ const acl = record.readPolicy;
741
+ if (operation === 'save')
742
+ return context.principal === acl.ownerId || acl.writerIds.includes(context.principal);
743
+ if (acl.visibility === 'public')
744
+ return true;
745
+ if (context.principal === acl.ownerId || acl.readerIds.includes(context.principal) || acl.writerIds.includes(context.principal))
746
+ return true;
747
+ return acl.visibility === 'organization' && context.scope !== undefined && equalScope(context.scope, record.ownerScope);
748
+ }
749
+ async function authorize(provider, operation, context, record) {
750
+ const allowed = provider === undefined
751
+ ? defaultAuthorize(operation, context, record)
752
+ : await provider.authorize({ operation, context, record });
753
+ if (allowed !== undefined && !allowed)
754
+ throw error('access_denied', `problem selection ${operation} is not authorized`);
755
+ }
756
+ function assertRoot(root) {
757
+ if (typeof root !== 'string' || root.trim().length === 0)
758
+ throw error('invalid_request', 'root is required');
759
+ return path.resolve(root);
760
+ }
761
+ function directory(root) { return path.join(root, PROBLEM_SELECTION_DIRECTORY); }
762
+ function locator(root, recordId) {
763
+ return path.join(directory(root), `${recordId.slice('sha256:'.length)}.json`);
764
+ }
765
+ async function regularFile(file) {
766
+ try {
767
+ const stats = await lstat(file);
768
+ if (!stats.isFile())
769
+ throw error('integrity_mismatch', 'selection record path is not a regular file');
770
+ return await readFile(file, 'utf8');
771
+ }
772
+ catch (cause) {
773
+ if (cause instanceof ProblemSelectionError)
774
+ throw cause;
775
+ if (cause.code === 'ENOENT')
776
+ throw error('not_found', 'selection record was not found');
777
+ throw error('storage_io_error', 'selection record could not be read');
778
+ }
779
+ }
780
+ async function ensureDirectory(root) {
781
+ const target = directory(root);
782
+ try {
783
+ await mkdir(target, { recursive: true, mode: 0o700 });
784
+ const stats = await lstat(target);
785
+ if (!stats.isDirectory())
786
+ throw error('storage_io_error', 'selection record directory is not a directory');
787
+ return target;
788
+ }
789
+ catch (cause) {
790
+ if (cause instanceof ProblemSelectionError)
791
+ throw cause;
792
+ throw error('storage_io_error', 'selection record directory could not be prepared');
793
+ }
794
+ }
795
+ function validateProblemSelectionRecord(value) {
796
+ if (!isPlainRecord(value))
797
+ throw error('invalid_record', 'selection record must be a plain object');
798
+ exactKeys(value, ['recordVersion', 'contractVersion', 'selectionId', 'selectionProblem', 'selectionComposition', 'ownerScope', 'readPolicy', 'fixedConditions', 'candidateRefs', 'assessments', 'objectiveConflicts', 'unknowns', 'status', 'decision', 'problemCreationRequest', 'executionPermission', 'resourceAuthority', 'objectiveChange', 'createdAt'], 'record');
799
+ if (value.recordVersion !== PROBLEM_SELECTION_RECORD_VERSION || value.contractVersion !== PROBLEM_SELECTION_CONTRACT_VERSION)
800
+ throw error('invalid_record', 'selection record version is unsupported');
801
+ const selectionId = requireString(value.selectionId, 'record.selectionId');
802
+ const selectionProblem = normalizeSnapshotReference(value.selectionProblem);
803
+ const selectionComposition = createJudgmentDAGCompositionDefinition(value.selectionComposition);
804
+ const ownerScope = normalizeScope(value.ownerScope, 'record.ownerScope');
805
+ const readPolicy = normalizeAcl(value.readPolicy, 'record.readPolicy');
806
+ const fixedConditions = normalizeConditions(value.fixedConditions);
807
+ if (!Array.isArray(value.candidateRefs) || !Array.isArray(value.assessments))
808
+ throw error('invalid_record', 'record candidate arrays are invalid');
809
+ const candidateRefs = value.candidateRefs.map((entry, index) => normalizeCandidateReference(entry, `record.candidateRefs[${index}]`));
810
+ assertCandidateReferencesUnique(candidateRefs);
811
+ const assessments = value.assessments.map((entry, index) => normalizeRecordCandidateAssessment(entry, `record.assessments[${index}]`));
812
+ if (assessments.length !== candidateRefs.length)
813
+ throw error('invalid_record', 'record assessments do not match candidates');
814
+ for (const ref of candidateRefs) {
815
+ const assessment = assessments.find((item) => item.reference.candidateId === ref.candidateId);
816
+ if (assessment === undefined || canonicalJson(assessment.reference) !== canonicalJson(ref)) {
817
+ throw error('invalid_record', `record assessment reference does not match ${ref.candidateId}`);
818
+ }
819
+ }
820
+ const objectiveConflicts = normalizeObjectiveConflicts(value.objectiveConflicts, 'record.objectiveConflicts');
821
+ const unknowns = normalizeUnknowns(value.unknowns, 'record.unknowns');
822
+ if (value.status !== 'selected' && value.status !== 'human_review_required')
823
+ throw error('invalid_record', 'record.status is invalid');
824
+ if (value.executionPermission !== 'none' || value.resourceAuthority !== 'none' || value.objectiveChange !== 'none')
825
+ throw error('invalid_record', 'selection record cannot carry authority');
826
+ if (!isPlainRecord(value.decision))
827
+ throw error('invalid_record', 'record.decision is invalid');
828
+ exactKeys(value.decision, ['status', 'candidate', 'action', 'reason', 'ownerId', 'conditions', 'reviewAt'], 'record.decision');
829
+ const decisionStatus = value.decision.status;
830
+ if (decisionStatus !== value.status)
831
+ throw error('invalid_record', 'record decision status must match record status');
832
+ const decision = {
833
+ status: decisionStatus,
834
+ ...(value.decision.candidate === undefined ? {} : { candidate: normalizeCandidateReference(value.decision.candidate, 'record.decision.candidate') }),
835
+ ...(value.decision.action === undefined ? {} : { action: requireAction(value.decision.action, 'record.decision.action') }),
836
+ reason: requireString(value.decision.reason, 'record.decision.reason'),
837
+ ...(value.decision.ownerId === undefined ? {} : { ownerId: requireString(value.decision.ownerId, 'record.decision.ownerId') }),
838
+ conditions: Array.isArray(value.decision.conditions) ? value.decision.conditions.map((entry, index) => requireString(entry, `record.decision.conditions[${index}]`)) : (() => { throw error('invalid_record', 'record.decision.conditions is invalid'); })(),
839
+ ...(value.decision.reviewAt === undefined ? {} : { reviewAt: requireIsoTimestamp(value.decision.reviewAt, 'record.decision.reviewAt') })
840
+ };
841
+ assertReviewAt(decision);
842
+ if (value.status === 'selected' && (decision.candidate === undefined || decision.action === undefined)) {
843
+ throw error('invalid_record', 'selected record decisions must include candidate and action');
844
+ }
845
+ if (decision.candidate !== undefined
846
+ && !candidateRefs.some((ref) => canonicalJson(ref) === canonicalJson(decision.candidate))) {
847
+ throw error('invalid_record', 'record.decision.candidate is outside the fixed candidate set');
848
+ }
849
+ let problemCreationRequest;
850
+ if (value.problemCreationRequest !== undefined) {
851
+ if (!isPlainRecord(value.problemCreationRequest))
852
+ throw error('invalid_record', 'record.problemCreationRequest is invalid');
853
+ exactKeys(value.problemCreationRequest, ['status', 'candidate', 'action', 'requiredConditions', 'completeSnapshotRequired'], 'record.problemCreationRequest');
854
+ if (value.problemCreationRequest.status !== 'required' || value.problemCreationRequest.completeSnapshotRequired !== true)
855
+ throw error('invalid_record', 'problemCreationRequest flags are invalid');
856
+ const action = requireAction(value.problemCreationRequest.action, 'record.problemCreationRequest.action');
857
+ if (action !== 'start' && action !== 'continue')
858
+ throw error('invalid_record', 'problemCreationRequest action is invalid');
859
+ if (!Array.isArray(value.problemCreationRequest.requiredConditions))
860
+ throw error('invalid_record', 'problemCreationRequest.requiredConditions is invalid');
861
+ problemCreationRequest = { status: 'required', candidate: normalizeCandidateReference(value.problemCreationRequest.candidate, 'record.problemCreationRequest.candidate'), action, requiredConditions: value.problemCreationRequest.requiredConditions.map((entry, index) => requireString(entry, `record.problemCreationRequest.requiredConditions[${index}]`)), completeSnapshotRequired: true };
862
+ if (value.status !== 'selected' || decision.candidate === undefined
863
+ || canonicalJson(problemCreationRequest.candidate) !== canonicalJson(decision.candidate)
864
+ || decision.action !== problemCreationRequest.action) {
865
+ throw error('invalid_record', 'problemCreationRequest must match a selected decision');
866
+ }
867
+ }
868
+ if (value.status === 'selected' && (decision.action === 'start' || decision.action === 'continue') && problemCreationRequest === undefined) {
869
+ throw error('invalid_record', 'start and continue decisions require problemCreationRequest');
870
+ }
871
+ if (value.status === 'selected') {
872
+ const policyViolation = selectedRecordPolicyViolation(ownerScope, fixedConditions, candidateRefs, assessments, objectiveConflicts, decision.candidate?.candidateId, decision.action, unknowns);
873
+ if (policyViolation !== undefined)
874
+ throw error('invalid_record', `selected record violates selection policy: ${policyViolation}`);
875
+ }
876
+ return deepFreeze({
877
+ recordVersion: PROBLEM_SELECTION_RECORD_VERSION,
878
+ contractVersion: PROBLEM_SELECTION_CONTRACT_VERSION,
879
+ selectionId,
880
+ selectionProblem,
881
+ selectionComposition,
882
+ ownerScope,
883
+ readPolicy,
884
+ fixedConditions,
885
+ candidateRefs,
886
+ assessments,
887
+ objectiveConflicts,
888
+ unknowns,
889
+ status: value.status,
890
+ decision,
891
+ ...(problemCreationRequest === undefined ? {} : { problemCreationRequest }),
892
+ executionPermission: 'none',
893
+ resourceAuthority: 'none',
894
+ objectiveChange: 'none',
895
+ createdAt: requireIsoTimestamp(value.createdAt, 'record.createdAt')
896
+ });
897
+ }
898
+ function envelope(recordId, record) {
899
+ return { recordId, recordVersion: PROBLEM_SELECTION_RECORD_VERSION, record };
900
+ }
901
+ export async function saveProblemSelectionRecord(request) {
902
+ if (!isPlainRecord(request))
903
+ throw error('invalid_request', 'save request must be an object');
904
+ const root = assertRoot(request.root);
905
+ const record = validateProblemSelectionRecord(request.record);
906
+ const access = request.access ?? { principal: record.readPolicy.ownerId };
907
+ await authorize(request.accessProvider, 'save', access, record);
908
+ const recordId = recordIdForPayload(record);
909
+ const targetDirectory = await ensureDirectory(root);
910
+ const file = locator(root, recordId);
911
+ const bytes = `${canonicalJson(envelope(recordId, record))}\n`;
912
+ try {
913
+ const existing = await regularFile(file);
914
+ if (existing !== bytes)
915
+ throw error('integrity_mismatch', 'existing selection record bytes do not match its content address');
916
+ return { recordId, recordVersion: PROBLEM_SELECTION_RECORD_VERSION, selectionId: record.selectionId, status: 'existing' };
917
+ }
918
+ catch (cause) {
919
+ if (!(cause instanceof ProblemSelectionError) || cause.code !== 'not_found')
920
+ throw cause;
921
+ }
922
+ const temporary = path.join(targetDirectory, `.tmp-${process.pid}-${randomUUID()}`);
923
+ let handle;
924
+ try {
925
+ handle = await open(temporary, 'wx', 0o600);
926
+ await handle.writeFile(bytes, 'utf8');
927
+ await handle.sync();
928
+ await handle.close();
929
+ handle = undefined;
930
+ try {
931
+ await link(temporary, file);
932
+ }
933
+ catch (cause) {
934
+ if (cause.code !== 'EEXIST')
935
+ throw cause;
936
+ const existing = await regularFile(file);
937
+ if (existing !== bytes)
938
+ throw error('integrity_mismatch', 'existing selection record bytes do not match its content address');
939
+ return { recordId, recordVersion: PROBLEM_SELECTION_RECORD_VERSION, selectionId: record.selectionId, status: 'existing' };
940
+ }
941
+ return { recordId, recordVersion: PROBLEM_SELECTION_RECORD_VERSION, selectionId: record.selectionId, status: 'created' };
942
+ }
943
+ catch (cause) {
944
+ if (cause instanceof ProblemSelectionError)
945
+ throw cause;
946
+ throw error('storage_io_error', 'selection record could not be saved');
947
+ }
948
+ finally {
949
+ if (handle !== undefined)
950
+ await handle.close().catch(() => undefined);
951
+ await unlink(temporary).catch(() => undefined);
952
+ }
953
+ }
954
+ export async function loadProblemSelectionRecord(request) {
955
+ if (!isPlainRecord(request))
956
+ throw error('invalid_request', 'load request must be an object');
957
+ const root = assertRoot(request.root);
958
+ const recordId = requireString(request.recordId, 'recordId');
959
+ if (!RECORD_ID_PATTERN.test(recordId))
960
+ throw error('invalid_request', 'recordId must be sha256:<64 lowercase hex>');
961
+ const bytes = await regularFile(locator(root, recordId));
962
+ if (!bytes.endsWith('\n'))
963
+ throw error('integrity_mismatch', 'selection record bytes are not canonical');
964
+ let parsed;
965
+ try {
966
+ parsed = JSON.parse(bytes.slice(0, -1));
967
+ }
968
+ catch {
969
+ throw error('integrity_mismatch', 'selection record bytes are invalid JSON');
970
+ }
971
+ if (!isPlainRecord(parsed) || Object.keys(parsed).sort().join(',') !== ['record', 'recordId', 'recordVersion'].sort().join(',') || parsed.recordVersion !== PROBLEM_SELECTION_RECORD_VERSION || parsed.recordId !== recordId)
972
+ throw error('invalid_record', 'selection record envelope is invalid');
973
+ const record = validateProblemSelectionRecord(parsed.record);
974
+ const computed = recordIdForPayload(record);
975
+ if (computed !== recordId)
976
+ throw error('integrity_mismatch', 'selection record content address does not match its content');
977
+ const canonical = `${canonicalJson(envelope(computed, record))}\n`;
978
+ if (bytes !== canonical)
979
+ throw error('integrity_mismatch', 'selection record bytes are not canonical');
980
+ await authorize(request.accessProvider, 'read', request.access, record);
981
+ return record;
982
+ }
983
+ export function createProblemSelectionRecordStore(options) {
984
+ if (!isPlainRecord(options))
985
+ throw error('invalid_request', 'store options must be an object');
986
+ const root = assertRoot(options.root);
987
+ return {
988
+ save: (record, access) => saveProblemSelectionRecord({ root, record, access, accessProvider: options.accessProvider }),
989
+ read: (recordId, access) => loadProblemSelectionRecord({ root, recordId, access, accessProvider: options.accessProvider })
990
+ };
991
+ }
992
+ /** Compatibility aliases for consumers that use SelectionRecord terminology. */
993
+ export const saveSelectionRecord = saveProblemSelectionRecord;
994
+ export const loadSelectionRecord = loadProblemSelectionRecord;
995
+ export const computeSelectionRecordId = computeProblemSelectionRecordId;