@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,586 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { initializePersonalOs, loadPersonalOs, mutatePersonalOsWithSidecar, readPersonalOsSidecar } from './ssot.js';
3
+ /** Public version of the adapter contract. It is independent from the old organization route. */
4
+ export const DECISION_ADAPTER_VERSION = 'decision-adapter.v1';
5
+ /** The sidecar contains references and compatibility metadata, never canonical definitions. */
6
+ export const DECISION_ADAPTER_SIDECAR = 'evidence/decision-adapter.json';
7
+ export class DecisionAdapterError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.name = 'DecisionAdapterError';
12
+ this.code = code;
13
+ }
14
+ }
15
+ /**
16
+ * Personal canonical implementation. It never creates a second Decision
17
+ * store: `decisions.jsonl` and the Graph entity remain the canonical records.
18
+ */
19
+ export class GraphDecisionAdapterStore {
20
+ dataDir;
21
+ sidecarPath;
22
+ conditionValidator;
23
+ authorization;
24
+ constructor(options) {
25
+ if (!options.dataDir)
26
+ throw new DecisionAdapterError('validation_error', 'dataDir is required');
27
+ this.dataDir = options.dataDir;
28
+ this.sidecarPath = options.sidecarPath ?? DECISION_ADAPTER_SIDECAR;
29
+ assertSidecarPath(this.sidecarPath);
30
+ this.conditionValidator = options.conditionValidator;
31
+ this.authorization = options.authorization;
32
+ }
33
+ async createDecision(input, context) {
34
+ const request = normalizeDecisionRequest(input);
35
+ assertContext(context);
36
+ await initializePersonalOs(this.dataDir);
37
+ const decisionId = request.decision_id ?? `dec-${randomUUID()}`;
38
+ const eventId = `evt-${randomUUID()}`;
39
+ const now = new Date().toISOString();
40
+ const decision = omitUndefined({
41
+ id: decisionId,
42
+ title: request.title,
43
+ decision: request.decision,
44
+ topic: request.topic,
45
+ rationale: request.rationale,
46
+ effectiveAt: request.effectiveAt,
47
+ tags: request.tags === undefined ? undefined : [...request.tags],
48
+ updatedAt: now
49
+ });
50
+ const entity = omitUndefined({
51
+ id: decisionId,
52
+ type: 'decision',
53
+ name: request.title,
54
+ summary: request.decision,
55
+ tags: request.tags === undefined ? undefined : [...request.tags]
56
+ });
57
+ const snapshot = await this.loadSnapshot();
58
+ const sidecar = parseSidecar(snapshot.sidecarContent);
59
+ assertCreateDecisionState(snapshot.current, sidecar, decisionId);
60
+ // Trusted providers may read the SSOT. Run them before acquiring the
61
+ // mutation lock; the commit callback below only performs pure checks and
62
+ // the optimistic canonical/sidecar compare-and-swap.
63
+ await this.validateConditions(request.conditions, context);
64
+ await this.authorize({
65
+ action: 'create',
66
+ context,
67
+ current: snapshot.current,
68
+ decision,
69
+ graph_entity: entity
70
+ });
71
+ const result = await this.mutateSidecar((current, sidecarContent) => {
72
+ assertDecisionSnapshotUnchanged(snapshot, current, sidecarContent);
73
+ const currentSidecar = parseSidecar(sidecarContent);
74
+ assertCreateDecisionState(current, currentSidecar, decisionId);
75
+ if (current.graph.version !== 2) {
76
+ throw new DecisionAdapterError('graph_unsupported', 'New Decision writes require canonical Graph v2');
77
+ }
78
+ const stored = {
79
+ decision_id: decisionId,
80
+ conditions: clone(request.conditions),
81
+ created_at: now,
82
+ ai_logs: []
83
+ };
84
+ const nextSidecar = {
85
+ ...currentSidecar,
86
+ decisions: { ...currentSidecar.decisions, [decisionId]: stored }
87
+ };
88
+ const nextGraph = {
89
+ ...current.graph,
90
+ entities: [...current.graph.entities, entity]
91
+ };
92
+ const next = {
93
+ ...current,
94
+ decisions: [...current.decisions, decision],
95
+ graph: nextGraph
96
+ };
97
+ assertStagedDecision(next, serializeSidecar(nextSidecar), decisionId, request.conditions);
98
+ return {
99
+ next,
100
+ sidecarContent: serializeSidecar(nextSidecar),
101
+ result: { decision_id: decisionId, event_id: eventId }
102
+ };
103
+ });
104
+ return result;
105
+ }
106
+ async createAiDecisionLog(input, context) {
107
+ const request = normalizeAiDecisionLogRequest(input);
108
+ assertContext(context);
109
+ await initializePersonalOs(this.dataDir);
110
+ const aiDecisionId = `aid-${randomUUID()}`;
111
+ const eventId = `evt-${randomUUID()}`;
112
+ const now = new Date().toISOString();
113
+ const aiLog = omitUndefined({
114
+ ai_decision_id: aiDecisionId,
115
+ event_id: eventId,
116
+ summary: request.summary,
117
+ decision_type: request.decision_type,
118
+ rationale: request.rationale,
119
+ confidence: request.confidence,
120
+ related_entity_id: request.related_entity_id,
121
+ references: request.references === undefined ? undefined : [...request.references],
122
+ conditions: clone(request.conditions),
123
+ created_at: now
124
+ });
125
+ const snapshot = await this.loadSnapshot();
126
+ const snapshotDecision = snapshot.current.decisions.find((candidate) => candidate.id === request.decision_id);
127
+ if (!snapshotDecision) {
128
+ throw new DecisionAdapterError('decision_not_found', `Decision ${request.decision_id} was not found`);
129
+ }
130
+ if (snapshot.current.graph.version !== 2) {
131
+ throw new DecisionAdapterError('graph_unsupported', 'AI decision-log writes require canonical Graph v2');
132
+ }
133
+ const snapshotGraphEntity = snapshot.current.graph.entities.find((candidate) => candidate.id === request.decision_id);
134
+ if (!snapshotGraphEntity || snapshotGraphEntity.type !== 'decision') {
135
+ throw new DecisionAdapterError('readback_mismatch', `Canonical Graph entity ${request.decision_id} is missing`);
136
+ }
137
+ const snapshotSidecar = parseSidecar(snapshot.sidecarContent);
138
+ const snapshotExisting = snapshotSidecar.decisions[request.decision_id];
139
+ if (snapshotExisting && !sameConditions(snapshotExisting.conditions, request.conditions)) {
140
+ throw new DecisionAdapterError('condition_unavailable', `Decision ${request.decision_id} has different recorded conditions`);
141
+ }
142
+ // Trusted providers may read the SSOT. They must complete before the
143
+ // mutation lock is acquired; the callback below is intentionally pure.
144
+ await this.validateConditions(request.conditions, context);
145
+ await this.authorize({
146
+ action: 'create',
147
+ context,
148
+ current: snapshot.current,
149
+ decision: snapshotDecision,
150
+ graph_entity: snapshotGraphEntity
151
+ });
152
+ const result = await this.mutateSidecar((current, sidecarContent) => {
153
+ assertDecisionSnapshotUnchanged(snapshot, current, sidecarContent);
154
+ const decision = current.decisions.find((candidate) => candidate.id === request.decision_id);
155
+ if (!decision) {
156
+ throw new DecisionAdapterError('decision_not_found', `Decision ${request.decision_id} was not found`);
157
+ }
158
+ if (current.graph.version !== 2) {
159
+ throw new DecisionAdapterError('graph_unsupported', 'AI decision-log writes require canonical Graph v2');
160
+ }
161
+ const graphEntity = current.graph.entities.find((candidate) => candidate.id === request.decision_id);
162
+ if (!graphEntity || graphEntity.type !== 'decision') {
163
+ throw new DecisionAdapterError('readback_mismatch', `Canonical Graph entity ${request.decision_id} is missing`);
164
+ }
165
+ const sidecar = parseSidecar(sidecarContent);
166
+ const existing = sidecar.decisions[request.decision_id];
167
+ if (existing && !sameConditions(existing.conditions, request.conditions)) {
168
+ throw new DecisionAdapterError('condition_unavailable', `Decision ${request.decision_id} has different recorded conditions`);
169
+ }
170
+ const stored = existing ?? {
171
+ decision_id: request.decision_id,
172
+ conditions: clone(request.conditions),
173
+ created_at: now,
174
+ ai_logs: []
175
+ };
176
+ const nextStored = {
177
+ ...stored,
178
+ ai_logs: [...stored.ai_logs, aiLog]
179
+ };
180
+ const nextSidecar = {
181
+ ...sidecar,
182
+ decisions: { ...sidecar.decisions, [request.decision_id]: nextStored }
183
+ };
184
+ assertStagedAiDecisionLog(current, serializeSidecar(nextSidecar), request.decision_id, aiDecisionId, request.conditions);
185
+ return {
186
+ next: current,
187
+ sidecarContent: serializeSidecar(nextSidecar),
188
+ result: { ai_decision_id: aiDecisionId, event_id: eventId }
189
+ };
190
+ });
191
+ return result;
192
+ }
193
+ async readDecision(input, context) {
194
+ const decisionId = normalizeIdentifier(input?.decision_id, 'decision_id');
195
+ assertContext(context);
196
+ await initializePersonalOs(this.dataDir);
197
+ const current = await this.loadCurrent();
198
+ const decision = current.decisions.find((candidate) => candidate.id === decisionId);
199
+ if (!decision)
200
+ throw new DecisionAdapterError('decision_not_found', `Decision ${decisionId} was not found`);
201
+ const graphEntity = current.graph.version === 2
202
+ ? current.graph.entities.find((candidate) => candidate.id === decisionId)
203
+ : undefined;
204
+ if (graphEntity && graphEntity.type !== 'decision') {
205
+ throw new DecisionAdapterError('store_corrupt', `Canonical Graph ID ${decisionId} is not a Decision`);
206
+ }
207
+ await this.authorize({ action: 'read', context, current, decision, graph_entity: graphEntity });
208
+ const sidecar = parseSidecar(await readPersonalOsSidecar(this.dataDir, this.sidecarPath));
209
+ const stored = sidecar.decisions[decisionId];
210
+ if (!stored) {
211
+ return {
212
+ decision: clone(decision),
213
+ ...(graphEntity ? { graph_entity: clone(graphEntity) } : {}),
214
+ condition_status: 'unrecorded',
215
+ authority_status: 'unrecorded',
216
+ ai_logs: []
217
+ };
218
+ }
219
+ validateStoredDecision(stored, decisionId);
220
+ await this.validateConditions(stored.conditions, context);
221
+ return {
222
+ decision: clone(decision),
223
+ ...(graphEntity ? { graph_entity: clone(graphEntity) } : {}),
224
+ condition_status: 'recorded',
225
+ conditions: clone(stored.conditions),
226
+ authority_status: 'unrecorded',
227
+ ai_logs: stored.ai_logs.map(clone)
228
+ };
229
+ }
230
+ async mutateSidecar(mutator) {
231
+ try {
232
+ return await mutatePersonalOsWithSidecar(this.dataDir, this.sidecarPath, mutator);
233
+ }
234
+ catch (error) {
235
+ if (error instanceof DecisionAdapterError)
236
+ throw error;
237
+ throw new DecisionAdapterError('store_corrupt', formatError(error));
238
+ }
239
+ }
240
+ async loadCurrent() {
241
+ try {
242
+ return await loadPersonalOs(this.dataDir);
243
+ }
244
+ catch (error) {
245
+ throw new DecisionAdapterError('store_corrupt', formatError(error));
246
+ }
247
+ }
248
+ async loadSnapshot() {
249
+ try {
250
+ return {
251
+ current: await loadPersonalOs(this.dataDir),
252
+ sidecarContent: await readPersonalOsSidecar(this.dataDir, this.sidecarPath)
253
+ };
254
+ }
255
+ catch (error) {
256
+ throw new DecisionAdapterError('store_corrupt', formatError(error));
257
+ }
258
+ }
259
+ async validateConditions(conditions, context) {
260
+ validateDecisionAdapterConditions(conditions);
261
+ if (!this.conditionValidator)
262
+ return;
263
+ try {
264
+ const allowed = await this.conditionValidator.validate({ conditions: clone(conditions), context });
265
+ if (allowed === false) {
266
+ throw new DecisionAdapterError('condition_unavailable', 'A trusted condition provider denied the references');
267
+ }
268
+ }
269
+ catch (error) {
270
+ if (error instanceof DecisionAdapterError)
271
+ throw error;
272
+ throw new DecisionAdapterError('condition_unavailable', formatError(error));
273
+ }
274
+ }
275
+ async authorize(request) {
276
+ assertContext(request.context);
277
+ if (this.authorization) {
278
+ try {
279
+ const allowed = await this.authorization.authorize({
280
+ ...request,
281
+ current: clone(request.current),
282
+ ...(request.decision ? { decision: clone(request.decision) } : {}),
283
+ ...(request.graph_entity ? { graph_entity: clone(request.graph_entity) } : {})
284
+ });
285
+ if (allowed === false) {
286
+ throw new DecisionAdapterError('authorization_denied', 'Decision adapter policy denied the operation');
287
+ }
288
+ }
289
+ catch (error) {
290
+ if (error instanceof DecisionAdapterError)
291
+ throw error;
292
+ throw new DecisionAdapterError('authorization_denied', formatError(error));
293
+ }
294
+ return;
295
+ }
296
+ const ownerId = request.current.graph.version === 2 ? request.current.graph.owner?.id : undefined;
297
+ if (ownerId && ownerId !== request.context.principal) {
298
+ throw new DecisionAdapterError('authorization_denied', 'The personal canonical Graph owner denied the operation');
299
+ }
300
+ }
301
+ }
302
+ export function createDecisionAdapter(options) {
303
+ return new GraphDecisionAdapterStore(options);
304
+ }
305
+ export const createGraphDecisionAdapter = createDecisionAdapter;
306
+ export function validateDecisionAdapterConditions(value) {
307
+ if (!value || typeof value !== 'object') {
308
+ throw new DecisionAdapterError('validation_error', 'conditions are required');
309
+ }
310
+ const conditions = value;
311
+ const snapshot = conditions.problem_snapshot;
312
+ if (!snapshot || typeof snapshot !== 'object') {
313
+ throw new DecisionAdapterError('validation_error', 'conditions.problem_snapshot is required');
314
+ }
315
+ const snapshotRecord = snapshot;
316
+ const snapshotId = normalizeIdentifier(snapshotRecord.snapshot_id, 'conditions.problem_snapshot.snapshot_id');
317
+ if (!/^sha256:[0-9a-f]{64}$/u.test(snapshotId)) {
318
+ throw new DecisionAdapterError('validation_error', 'conditions.problem_snapshot.snapshot_id must be sha256:<64 lowercase hex>');
319
+ }
320
+ normalizeIdentifier(snapshotRecord.problem_id, 'conditions.problem_snapshot.problem_id');
321
+ assertRevision(snapshotRecord.revision, 'conditions.problem_snapshot.revision');
322
+ const method = conditions.method;
323
+ if (!method || typeof method !== 'object') {
324
+ throw new DecisionAdapterError('validation_error', 'conditions.method is required');
325
+ }
326
+ const methodRecord = method;
327
+ normalizeIdentifier(methodRecord.id, 'conditions.method.id');
328
+ normalizeIdentifier(methodRecord.version, 'conditions.method.version');
329
+ if (!Array.isArray(conditions.objective_refs) || conditions.objective_refs.length === 0) {
330
+ throw new DecisionAdapterError('validation_error', 'conditions.objective_refs must contain at least one Objective revision');
331
+ }
332
+ const seen = new Set();
333
+ for (const [index, ref] of conditions.objective_refs.entries()) {
334
+ if (!ref || typeof ref !== 'object') {
335
+ throw new DecisionAdapterError('validation_error', `conditions.objective_refs[${index}] must be an object`);
336
+ }
337
+ const record = ref;
338
+ const id = normalizeIdentifier(record.id, `conditions.objective_refs[${index}].id`);
339
+ if (record.type !== 'objective') {
340
+ throw new DecisionAdapterError('validation_error', `conditions.objective_refs[${index}].type must be objective`);
341
+ }
342
+ const revision = assertRevision(record.revision, `conditions.objective_refs[${index}].revision`);
343
+ const key = `${id}@${revision}`;
344
+ if (seen.has(key))
345
+ throw new DecisionAdapterError('validation_error', `Duplicate Objective reference ${key}`);
346
+ seen.add(key);
347
+ }
348
+ }
349
+ function normalizeDecisionRequest(input) {
350
+ if (!input || typeof input !== 'object')
351
+ throw new DecisionAdapterError('validation_error', 'Decision request is required');
352
+ const request = input;
353
+ return {
354
+ ...(request.decision_id === undefined ? {} : { decision_id: normalizeIdentifier(request.decision_id, 'decision_id') }),
355
+ title: normalizeText(request.title, 'title'),
356
+ decision: normalizeText(request.decision, 'decision'),
357
+ ...(request.topic === undefined ? {} : { topic: normalizeText(request.topic, 'topic') }),
358
+ ...(request.rationale === undefined ? {} : { rationale: normalizeText(request.rationale, 'rationale', 8_192) }),
359
+ ...(request.effectiveAt === undefined ? {} : { effectiveAt: normalizeTimestamp(request.effectiveAt, 'effectiveAt') }),
360
+ ...(request.tags === undefined ? {} : { tags: normalizeStringArray(request.tags, 'tags') }),
361
+ conditions: normalizeConditions(request.conditions)
362
+ };
363
+ }
364
+ function normalizeAiDecisionLogRequest(input) {
365
+ if (!input || typeof input !== 'object')
366
+ throw new DecisionAdapterError('validation_error', 'AI decision-log request is required');
367
+ const request = input;
368
+ const decisionId = request.decision_id === undefined
369
+ ? request.related_decision_id
370
+ : request.related_decision_id === undefined
371
+ ? request.decision_id
372
+ : normalizeIdentifier(request.decision_id, 'decision_id') === normalizeIdentifier(request.related_decision_id, 'related_decision_id')
373
+ ? request.decision_id
374
+ : undefined;
375
+ if (decisionId === undefined)
376
+ throw new DecisionAdapterError('validation_error', 'decision_id or related_decision_id is required and must match');
377
+ return {
378
+ decision_id: normalizeIdentifier(decisionId, 'decision_id'),
379
+ summary: normalizeText(request.summary, 'summary'),
380
+ ...(request.decision_type === undefined ? {} : { decision_type: normalizeText(request.decision_type, 'decision_type') }),
381
+ ...(request.rationale === undefined ? {} : { rationale: normalizeText(request.rationale, 'rationale', 8_192) }),
382
+ ...(request.confidence === undefined ? {} : { confidence: normalizeConfidence(request.confidence) }),
383
+ ...(request.related_entity_id === undefined ? {} : { related_entity_id: normalizeIdentifier(request.related_entity_id, 'related_entity_id') }),
384
+ ...(request.references === undefined ? {} : { references: normalizeStringArray(request.references, 'references') }),
385
+ conditions: normalizeConditions(request.conditions)
386
+ };
387
+ }
388
+ function normalizeConditions(value) {
389
+ if (!value || typeof value !== 'object')
390
+ throw new DecisionAdapterError('validation_error', 'conditions are required');
391
+ const input = value;
392
+ const snapshot = input.problem_snapshot;
393
+ const method = input.method;
394
+ const objectives = input.objective_refs;
395
+ const normalized = {
396
+ problem_snapshot: {
397
+ snapshot_id: String(snapshot?.snapshot_id ?? ''),
398
+ problem_id: String(snapshot?.problem_id ?? ''),
399
+ revision: String(snapshot?.revision ?? '')
400
+ },
401
+ method: {
402
+ id: String(method?.id ?? ''),
403
+ version: String(method?.version ?? '')
404
+ },
405
+ objective_refs: Array.isArray(objectives)
406
+ ? objectives.map((ref) => {
407
+ const record = ref;
408
+ return {
409
+ id: String(record?.id ?? ''),
410
+ type: 'objective',
411
+ revision: String(record?.revision ?? '')
412
+ };
413
+ })
414
+ : []
415
+ };
416
+ validateDecisionAdapterConditions(normalized);
417
+ return normalized;
418
+ }
419
+ function normalizeText(value, field, max = 2_048) {
420
+ if (typeof value !== 'string')
421
+ throw new DecisionAdapterError('validation_error', `${field} is required`);
422
+ const normalized = value.normalize('NFKC').trim();
423
+ if (!normalized || normalized.length > max || /[\u0000-\u001f\u007f-\u009f]/u.test(normalized)) {
424
+ throw new DecisionAdapterError('validation_error', `${field} must be bounded text`);
425
+ }
426
+ return normalized;
427
+ }
428
+ function normalizeIdentifier(value, field) {
429
+ return normalizeText(value, field, 512);
430
+ }
431
+ function assertRevision(value, field) {
432
+ const revision = normalizeIdentifier(value, field);
433
+ if (!/^[1-9][0-9]*$/u.test(revision)) {
434
+ throw new DecisionAdapterError('validation_error', `${field} must be a positive decimal revision`);
435
+ }
436
+ return revision;
437
+ }
438
+ function normalizeTimestamp(value, field) {
439
+ const timestamp = normalizeText(value, field, 64);
440
+ if (!Number.isFinite(Date.parse(timestamp)))
441
+ throw new DecisionAdapterError('validation_error', `${field} must be an ISO timestamp`);
442
+ return timestamp;
443
+ }
444
+ function normalizeStringArray(value, field) {
445
+ if (!Array.isArray(value))
446
+ throw new DecisionAdapterError('validation_error', `${field} must be an array`);
447
+ return value.map((entry, index) => normalizeText(entry, `${field}[${index}]`, 512));
448
+ }
449
+ function normalizeConfidence(value) {
450
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1) {
451
+ throw new DecisionAdapterError('validation_error', 'confidence must be a number between 0 and 1');
452
+ }
453
+ return value;
454
+ }
455
+ function assertContext(context) {
456
+ normalizeIdentifier(context?.principal, 'context.principal');
457
+ }
458
+ function assertSidecarPath(path) {
459
+ if (!path || path.startsWith('/') || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..')) {
460
+ throw new DecisionAdapterError('validation_error', 'sidecarPath must remain relative to the SSOT root');
461
+ }
462
+ }
463
+ function parseSidecar(content) {
464
+ if (content === undefined || content.trim() === '')
465
+ return { version: DECISION_ADAPTER_VERSION, decisions: {} };
466
+ let parsed;
467
+ try {
468
+ parsed = JSON.parse(content);
469
+ }
470
+ catch (error) {
471
+ throw new DecisionAdapterError('store_corrupt', `Decision adapter sidecar is not valid JSON: ${formatError(error)}`);
472
+ }
473
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
474
+ throw new DecisionAdapterError('store_corrupt', 'Decision adapter sidecar must be an object');
475
+ }
476
+ const value = parsed;
477
+ if (value.version !== DECISION_ADAPTER_VERSION || !value.decisions || typeof value.decisions !== 'object' || Array.isArray(value.decisions)) {
478
+ throw new DecisionAdapterError('store_corrupt', 'Decision adapter sidecar has an unsupported schema');
479
+ }
480
+ const decisions = {};
481
+ for (const [key, candidate] of Object.entries(value.decisions)) {
482
+ validateStoredDecision(candidate, key);
483
+ decisions[key] = clone(candidate);
484
+ }
485
+ return { version: DECISION_ADAPTER_VERSION, decisions };
486
+ }
487
+ function validateStoredDecision(value, key) {
488
+ if (!value || typeof value !== 'object' || value.decision_id !== key) {
489
+ throw new DecisionAdapterError('store_corrupt', `Decision adapter sidecar entry ${key} has an invalid ID`);
490
+ }
491
+ validateDecisionAdapterConditions(value.conditions);
492
+ normalizeTimestamp(value.created_at, `decisions.${key}.created_at`);
493
+ if (!Array.isArray(value.ai_logs))
494
+ throw new DecisionAdapterError('store_corrupt', `Decision adapter sidecar entry ${key} has invalid ai_logs`);
495
+ const ids = new Set();
496
+ for (const log of value.ai_logs) {
497
+ if (!log || typeof log !== 'object')
498
+ throw new DecisionAdapterError('store_corrupt', `Decision adapter sidecar entry ${key} has an invalid AI log`);
499
+ normalizeIdentifier(log.ai_decision_id, 'ai_decision_id');
500
+ normalizeIdentifier(log.event_id, 'event_id');
501
+ normalizeText(log.summary, 'summary');
502
+ if (log.decision_type !== undefined)
503
+ normalizeText(log.decision_type, 'decision_type');
504
+ if (log.rationale !== undefined)
505
+ normalizeText(log.rationale, 'rationale', 8_192);
506
+ if (log.confidence !== undefined)
507
+ normalizeConfidence(log.confidence);
508
+ if (log.related_entity_id !== undefined)
509
+ normalizeIdentifier(log.related_entity_id, 'related_entity_id');
510
+ if (log.references !== undefined)
511
+ normalizeStringArray(log.references, 'references');
512
+ validateDecisionAdapterConditions(log.conditions);
513
+ if (!sameConditions(log.conditions, value.conditions)) {
514
+ throw new DecisionAdapterError('store_corrupt', `AI log ${log.ai_decision_id} has conditions different from ${key}`);
515
+ }
516
+ normalizeTimestamp(log.created_at, 'created_at');
517
+ if (ids.has(log.ai_decision_id))
518
+ throw new DecisionAdapterError('store_corrupt', `Duplicate AI log ${log.ai_decision_id}`);
519
+ ids.add(log.ai_decision_id);
520
+ }
521
+ }
522
+ function assertCreateDecisionState(current, sidecar, decisionId) {
523
+ if (current.graph.version !== 2) {
524
+ throw new DecisionAdapterError('graph_unsupported', 'New Decision writes require canonical Graph v2');
525
+ }
526
+ if (current.decisions.some((candidate) => candidate.id === decisionId)
527
+ || current.graph.entities.some((candidate) => candidate.id === decisionId)) {
528
+ throw new DecisionAdapterError('decision_conflict', `Decision ${decisionId} already exists`);
529
+ }
530
+ if (Object.prototype.hasOwnProperty.call(sidecar.decisions, decisionId)) {
531
+ throw new DecisionAdapterError('decision_conflict', `Decision sidecar entry ${decisionId} already exists`);
532
+ }
533
+ }
534
+ function assertDecisionSnapshotUnchanged(snapshot, current, sidecarContent) {
535
+ if (!sameCanonicalAggregate(snapshot.current, current) || snapshot.sidecarContent !== sidecarContent) {
536
+ throw new DecisionAdapterError('decision_conflict', 'Canonical Decision aggregate or sidecar changed while trusted references were being validated');
537
+ }
538
+ }
539
+ function sameCanonicalAggregate(left, right) {
540
+ return JSON.stringify(left) === JSON.stringify(right);
541
+ }
542
+ function assertStagedDecision(next, sidecarContent, decisionId, expectedConditions) {
543
+ assertStagedCanonicalDecision(next, decisionId);
544
+ const sidecar = parseSidecar(sidecarContent);
545
+ const stored = sidecar.decisions[decisionId];
546
+ if (!stored || !sameConditions(stored.conditions, expectedConditions)) {
547
+ throw new DecisionAdapterError('readback_mismatch', `Staged Decision ${decisionId} does not contain its canonical references`);
548
+ }
549
+ }
550
+ function assertStagedAiDecisionLog(next, sidecarContent, decisionId, aiDecisionId, expectedConditions) {
551
+ assertStagedCanonicalDecision(next, decisionId);
552
+ const sidecar = parseSidecar(sidecarContent);
553
+ const stored = sidecar.decisions[decisionId];
554
+ const log = stored?.ai_logs.find((candidate) => candidate.ai_decision_id === aiDecisionId);
555
+ if (!stored || !log || !sameConditions(stored.conditions, expectedConditions) || !sameConditions(log.conditions, expectedConditions)) {
556
+ throw new DecisionAdapterError('readback_mismatch', `Staged AI decision-log ${aiDecisionId} is not attached to ${decisionId}`);
557
+ }
558
+ }
559
+ function assertStagedCanonicalDecision(next, decisionId) {
560
+ if (next.graph.version !== 2) {
561
+ throw new DecisionAdapterError('readback_mismatch', `Staged canonical Graph ${decisionId} is not Graph v2`);
562
+ }
563
+ const decision = next.decisions.find((candidate) => candidate.id === decisionId);
564
+ const graphEntity = next.graph.entities.find((candidate) => candidate.id === decisionId);
565
+ if (!decision || !graphEntity || graphEntity.type !== 'decision') {
566
+ throw new DecisionAdapterError('readback_mismatch', `Staged canonical Decision ${decisionId} is incomplete`);
567
+ }
568
+ }
569
+ function serializeSidecar(sidecar) {
570
+ return `${JSON.stringify({
571
+ version: DECISION_ADAPTER_VERSION,
572
+ decisions: Object.fromEntries(Object.entries(sidecar.decisions).sort(([left], [right]) => left.localeCompare(right, 'en')))
573
+ }, null, 2)}\n`;
574
+ }
575
+ function sameConditions(left, right) {
576
+ return JSON.stringify(left) === JSON.stringify(right);
577
+ }
578
+ function omitUndefined(value) {
579
+ return Object.fromEntries(Object.entries(value).filter(([, candidate]) => candidate !== undefined));
580
+ }
581
+ function clone(value) {
582
+ return structuredClone(value);
583
+ }
584
+ function formatError(error) {
585
+ return error instanceof Error ? error.message : String(error);
586
+ }
@@ -0,0 +1,51 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
2
+ import { type DurableWaitStore } from './durable-waits.js';
3
+ /** Version of the HTTP boundary. The store version is returned separately. */
4
+ export declare const DURABLE_WAIT_HTTP_VERSION: "durable-wait-http.v1";
5
+ type MaybePromise<T> = T | Promise<T>;
6
+ /** Context resolved by the host after authentication and tenant selection. */
7
+ export interface TrustedDurableWaitRequestContext {
8
+ readonly tenantId: string;
9
+ readonly principal: string;
10
+ readonly scopeId: string;
11
+ /** A non-empty value means that the host already verified the mutation origin. */
12
+ readonly verifiedMutationOrigin?: string;
13
+ }
14
+ /** A host creates the store with current ACL and Problem snapshot providers. */
15
+ export type DurableWaitStoreFactory = (context: TrustedDurableWaitRequestContext) => MaybePromise<DurableWaitStore>;
16
+ export interface DurableWaitMutationVerificationInput {
17
+ readonly request: IncomingMessage;
18
+ readonly context: TrustedDurableWaitRequestContext;
19
+ }
20
+ export interface DurableWaitHttpOptions {
21
+ readonly storeFactory: DurableWaitStoreFactory;
22
+ readonly basePath?: string;
23
+ readonly bodyLimitBytes?: number;
24
+ /** Required for hosts that do not attach verifiedMutationOrigin to context. */
25
+ readonly verifyMutationRequest?: (input: DurableWaitMutationVerificationInput) => MaybePromise<boolean>;
26
+ }
27
+ /**
28
+ * The smallest canonical Node host composition for this transport.
29
+ *
30
+ * Authentication and tenant/scope resolution remain host-owned. The resolver
31
+ * must return a context only after those checks have completed; a missing or
32
+ * failed resolution is deliberately passed to the handler as an unauthenticated
33
+ * request. This helper only mounts the canonical handler and never infers
34
+ * identity from request headers or body fields.
35
+ */
36
+ export interface DurableWaitHttpHostOptions extends DurableWaitHttpOptions {
37
+ readonly resolveContext: (request: IncomingMessage) => MaybePromise<TrustedDurableWaitRequestContext | null>;
38
+ }
39
+ /** A composable route handler. `false` means that another host route may handle it. */
40
+ export type DurableWaitHttpHandler = (request: IncomingMessage, response: ServerResponse, context: TrustedDurableWaitRequestContext | null) => Promise<boolean>;
41
+ export declare function createDurableWaitHttpHandler(options: DurableWaitHttpOptions): DurableWaitHttpHandler;
42
+ /**
43
+ * Create a startable Node HTTP host around the canonical handler.
44
+ *
45
+ * The caller owns `server.listen()` and the authentication implementation.
46
+ * A route outside the durable-wait base path is answered with a generic 404 so
47
+ * a standalone fixture cannot leave the request hanging; larger hosts can use
48
+ * `createDurableWaitHttpHandler` directly when they need route delegation.
49
+ */
50
+ export declare function createDurableWaitHttpHost(options: DurableWaitHttpHostOptions): Server;
51
+ export {};