@dsh-enhanced/assistant-recovery 0.1.12

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/cordis.patch.yml +14 -0
  4. package/lib/attestation.d.ts +18 -0
  5. package/lib/attestation.d.ts.map +1 -0
  6. package/lib/attestation.js +80 -0
  7. package/lib/attestation.js.map +1 -0
  8. package/lib/automation-executor.d.ts +21 -0
  9. package/lib/automation-executor.d.ts.map +1 -0
  10. package/lib/automation-executor.js +141 -0
  11. package/lib/automation-executor.js.map +1 -0
  12. package/lib/catalog.d.ts +16 -0
  13. package/lib/catalog.d.ts.map +1 -0
  14. package/lib/catalog.js +49 -0
  15. package/lib/catalog.js.map +1 -0
  16. package/lib/config.d.ts +34 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +101 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/executor.d.ts +50 -0
  21. package/lib/executor.d.ts.map +1 -0
  22. package/lib/executor.js +413 -0
  23. package/lib/executor.js.map +1 -0
  24. package/lib/index.d.ts +21 -0
  25. package/lib/index.d.ts.map +1 -0
  26. package/lib/index.js +26 -0
  27. package/lib/index.js.map +1 -0
  28. package/lib/port.d.ts +125 -0
  29. package/lib/port.d.ts.map +1 -0
  30. package/lib/port.js +706 -0
  31. package/lib/port.js.map +1 -0
  32. package/lib/service.d.ts +46 -0
  33. package/lib/service.d.ts.map +1 -0
  34. package/lib/service.js +549 -0
  35. package/lib/service.js.map +1 -0
  36. package/lib/sqlite.d.ts +9 -0
  37. package/lib/sqlite.d.ts.map +1 -0
  38. package/lib/sqlite.js +247 -0
  39. package/lib/sqlite.js.map +1 -0
  40. package/lib/store.d.ts +79 -0
  41. package/lib/store.d.ts.map +1 -0
  42. package/lib/store.js +806 -0
  43. package/lib/store.js.map +1 -0
  44. package/lib/types.d.ts +157 -0
  45. package/lib/types.d.ts.map +1 -0
  46. package/lib/types.js +3 -0
  47. package/lib/types.js.map +1 -0
  48. package/lib/version.d.ts +2 -0
  49. package/lib/version.d.ts.map +1 -0
  50. package/lib/version.js +2 -0
  51. package/lib/version.js.map +1 -0
  52. package/package.json +96 -0
package/lib/port.js ADDED
@@ -0,0 +1,706 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonicalEvaluationHostScope, } from '@dsh-enhanced/assistant-evaluation';
3
+ import { canonicalEvolutionHostScope, } from '@dsh-enhanced/assistant-evolution';
4
+ import { canonicalPreferenceHostScope, } from '@dsh-enhanced/preference-learning';
5
+ import { RecoveryPortError } from './executor.js';
6
+ import { RECOVERY_RUNBOOK_VERSION, } from './types.js';
7
+ export const RECOVERY_SYSTEM_OWNER = 'dsh-enhanced-assistant-recovery';
8
+ const STABLE_CODE = /^[a-z\d][a-z\d.-]{0,63}$/u;
9
+ function canonicalJson(value) {
10
+ if (value === null || typeof value !== 'object')
11
+ return JSON.stringify(value) ?? 'null';
12
+ if (Array.isArray(value))
13
+ return `[${value.map(canonicalJson).join(',')}]`;
14
+ const record = value;
15
+ return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
16
+ }
17
+ function digest(value) {
18
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
19
+ }
20
+ function operationId(context, stepId, phase) {
21
+ return `recovery:${phase}:${RECOVERY_RUNBOOK_VERSION}:${context.occurrenceId}:${stepId}`;
22
+ }
23
+ function throwIfAborted(signal) {
24
+ if (signal.aborted)
25
+ throw new RecoveryPortError('execution-cancelled', 'none');
26
+ }
27
+ function serviceCode(error, fallback) {
28
+ const candidate = typeof error === 'object' && error !== null && 'code' in error
29
+ ? error.code
30
+ : undefined;
31
+ if (typeof candidate !== 'string')
32
+ return fallback;
33
+ const normalized = candidate.normalize('NFC').trim().toLowerCase();
34
+ return STABLE_CODE.test(normalized) ? normalized : fallback;
35
+ }
36
+ function portFailure(error, fallback, possibleEffect) {
37
+ if (error instanceof RecoveryPortError)
38
+ throw error;
39
+ const code = serviceCode(error, fallback);
40
+ const provenPreEffect = [
41
+ 'conflict', 'disabled', 'disposed', 'forbidden', 'idempotency-conflict',
42
+ 'invalid-input', 'invalid-scope', 'missing-binding', 'missing-principal', 'not-found',
43
+ 'policy-denied', 'unauthorized-principal', 'unattested-signal', 'version-conflict',
44
+ ].includes(code);
45
+ throw new RecoveryPortError(code, possibleEffect && !provenPreEffect ? 'possible' : 'none');
46
+ }
47
+ function exactJob(context, jobs, activationPlanDigests) {
48
+ const job = jobs.get(context.automationId);
49
+ if (job === undefined)
50
+ throw new RecoveryPortError('automation-not-configured', 'none');
51
+ const planDigest = activationPlanDigests.get(context.automationId);
52
+ if (job.workspace !== context.targetScope.workspace
53
+ || job.preset !== context.targetScope.preset
54
+ || job.principal !== context.principal
55
+ || job.ownerRouteId !== context.ownerRouteId
56
+ || job.activationNonce !== context.activationNonce
57
+ || planDigest === undefined
58
+ || planDigest !== context.activationPlanDigest
59
+ || job.catalogDigest !== context.catalogDigest) {
60
+ throw new RecoveryPortError('authority-scope-mismatch', 'none');
61
+ }
62
+ if (context.executionMode === 'production' && job.activationState !== 'active') {
63
+ throw new RecoveryPortError('production-not-active', 'none');
64
+ }
65
+ if (context.executionMode === 'preview' && job.activationState !== 'preview') {
66
+ throw new RecoveryPortError('preview-not-enabled', 'none');
67
+ }
68
+ return job;
69
+ }
70
+ function projectionState(projection) {
71
+ return Object.freeze({
72
+ owner: projection.owner,
73
+ automationId: projection.automationId,
74
+ automationStatus: projection.automationStatus,
75
+ definitionHash: projection.definitionHash,
76
+ definitionVersion: projection.definitionVersion,
77
+ currentCircuit: projection.currentCircuit === undefined ? null : {
78
+ definitionHash: projection.currentCircuit.definitionHash,
79
+ state: projection.currentCircuit.state,
80
+ failureClass: projection.currentCircuit.failureClass,
81
+ failurePhase: projection.currentCircuit.failurePhase,
82
+ failureCode: projection.currentCircuit.failureCode,
83
+ version: projection.currentCircuit.version,
84
+ },
85
+ });
86
+ }
87
+ function healthState(report) {
88
+ return Object.freeze({
89
+ ready: report.ready,
90
+ severity: report.severity,
91
+ providers: report.providers.map(provider => ({
92
+ id: provider.id,
93
+ status: provider.status,
94
+ metrics: provider.metrics,
95
+ })),
96
+ assessments: report.assessments.map(value => ({
97
+ providerId: value.providerId,
98
+ severity: value.severity,
99
+ code: value.code,
100
+ })),
101
+ });
102
+ }
103
+ function assertRequiredProviders(report, phase) {
104
+ const byId = new Map(report.providers.map(provider => [provider.id, provider.status]));
105
+ const required = [
106
+ 'assistantAutomations', 'assistantEvaluation', 'preferenceLearning',
107
+ 'assistantEvolution', 'assistantRecovery',
108
+ ];
109
+ const admissionRepairable = new Set([
110
+ 'assistantAutomations:open-circuit-backlog',
111
+ 'assistantAutomations:open-incident-backlog',
112
+ 'assistantEvaluation:projection-retry-backlog',
113
+ 'assistantRecovery:bootstrap-in-progress',
114
+ 'assistantRecovery:incomplete-recovery',
115
+ ]);
116
+ const unexpectedAssessment = report.assessments.find(assessment => {
117
+ if (assessment.severity !== 'degraded')
118
+ return true;
119
+ const key = `${assessment.providerId}:${assessment.code}`;
120
+ return phase === 'admission'
121
+ ? !admissionRepairable.has(key)
122
+ : key !== 'assistantRecovery:bootstrap-in-progress';
123
+ });
124
+ const internallyInconsistentDegraded = report.severity === 'degraded'
125
+ && report.assessments.length === 0;
126
+ if (!report.ready
127
+ || report.severity === 'unhealthy'
128
+ || unexpectedAssessment !== undefined
129
+ || internallyInconsistentDegraded
130
+ || required.some(id => byId.get(id) !== 'ready')) {
131
+ throw new RecoveryPortError('health-not-ready', 'none');
132
+ }
133
+ }
134
+ function candidateState(candidates) {
135
+ return candidates.map(candidate => Object.freeze({
136
+ kind: candidate.kind,
137
+ situation: candidate.situation,
138
+ ruleId: candidate.ruleId ?? null,
139
+ failures: candidate.stats.failures,
140
+ total: candidate.stats.total,
141
+ evidenceDigest: candidate.evidenceDigest,
142
+ evidenceTotal: candidate.evidenceTotal,
143
+ }));
144
+ }
145
+ function ruleState(rules) {
146
+ return rules.map(rule => Object.freeze({
147
+ id: rule.id,
148
+ situation: rule.situation,
149
+ generation: rule.generation,
150
+ status: rule.status,
151
+ version: rule.version,
152
+ }));
153
+ }
154
+ function validateOwnerRoute(context, delivery, expectedAuthorityHash) {
155
+ const receipt = delivery.validateOwnerRoute({
156
+ authorityId: context.ownerRouteId,
157
+ principalId: context.principal,
158
+ workspace: context.targetScope.workspace,
159
+ agentPreset: context.targetScope.preset,
160
+ });
161
+ const principalRecordIdValid = typeof receipt.principalRecordId === 'string'
162
+ && receipt.principalRecordId === receipt.principalRecordId.normalize('NFC').trim()
163
+ && receipt.principalRecordId !== ''
164
+ && Buffer.byteLength(receipt.principalRecordId, 'utf8') <= 500
165
+ && ![...receipt.principalRecordId].some((character) => {
166
+ const point = character.codePointAt(0);
167
+ return point <= 0x1f || point === 0x7f;
168
+ });
169
+ if (receipt.receiptVersion !== 2
170
+ || receipt.authorityId !== context.ownerRouteId
171
+ || receipt.principalId !== context.principal
172
+ || !principalRecordIdValid
173
+ || !Number.isSafeInteger(receipt.principalVersion) || receipt.principalVersion < 1
174
+ || receipt.workspace !== context.targetScope.workspace
175
+ || receipt.agentPreset !== context.targetScope.preset
176
+ || !Number.isSafeInteger(receipt.bindingVersion) || receipt.bindingVersion < 1
177
+ || !Number.isSafeInteger(receipt.generation) || receipt.generation < 1
178
+ || typeof receipt.authorityHash !== 'string' || !/^[a-f\d]{64}$/u.test(receipt.authorityHash)) {
179
+ throw new RecoveryPortError('owner-route-receipt-invalid', 'none');
180
+ }
181
+ if (!/^[a-f\d]{64}$/u.test(expectedAuthorityHash)
182
+ || receipt.authorityHash !== expectedAuthorityHash) {
183
+ throw new RecoveryPortError('owner-route-authority-mismatch', 'none');
184
+ }
185
+ return receipt;
186
+ }
187
+ function routePrincipalLineage(route) {
188
+ return Object.freeze({
189
+ principalRecordId: route.principalRecordId,
190
+ principalVersion: route.principalVersion,
191
+ });
192
+ }
193
+ function exactMaintenanceAction(action) {
194
+ const raw = action;
195
+ const lineage = raw.principalLineage;
196
+ if (!Number.isSafeInteger(raw.ownerGeneration) || raw.ownerGeneration < 1
197
+ || typeof lineage !== 'object' || lineage === null
198
+ || typeof lineage.principalRecordId !== 'string'
199
+ || !Number.isSafeInteger(lineage.principalVersion) || lineage.principalVersion < 1) {
200
+ throw new RecoveryPortError('preference-maintenance-action-unfenced', 'none');
201
+ }
202
+ return action;
203
+ }
204
+ function exactPreferenceOwnerFence(value, principalLineage) {
205
+ const raw = value;
206
+ if (raw === null || typeof raw !== 'object'
207
+ || !Number.isSafeInteger(raw.ownerGeneration) || raw.ownerGeneration < 1
208
+ || raw.principalLineage === null || typeof raw.principalLineage !== 'object'
209
+ || raw.principalLineage.principalRecordId !== principalLineage.principalRecordId
210
+ || raw.principalLineage.principalVersion !== principalLineage.principalVersion) {
211
+ throw new RecoveryPortError('preference-maintenance-fence-receipt-invalid', 'none');
212
+ }
213
+ return value;
214
+ }
215
+ function exactMaintenanceReceipt(value, action) {
216
+ const raw = value;
217
+ if (raw === null || typeof raw !== 'object'
218
+ || !Number.isSafeInteger(raw.deletedSignals) || raw.deletedSignals < 0
219
+ || raw.deletedSignals > action.limit
220
+ || raw.ownerGeneration !== action.ownerGeneration
221
+ || raw.principalLineageId !== action.principalLineage.principalRecordId
222
+ || raw.principalLineageVersion !== action.principalLineage.principalVersion
223
+ || typeof raw.replayed !== 'boolean') {
224
+ throw new RecoveryPortError('preference-maintenance-receipt-invalid', 'possible');
225
+ }
226
+ return value;
227
+ }
228
+ /**
229
+ * Concrete, model-free adapter over the narrow Host seams of the learning
230
+ * services. Planning may inspect only the configured scope; mutations are
231
+ * exact CAS/idempotent operations selected before Recovery persists intent.
232
+ */
233
+ export class HostRecoveryRunbookPort {
234
+ runtime;
235
+ jobs;
236
+ activationPlanDigests;
237
+ authorityHashes;
238
+ constructor(jobs, runtime, activationPlanDigests, authorityHashes) {
239
+ this.runtime = runtime;
240
+ this.jobs = new Map(jobs);
241
+ this.activationPlanDigests = new Map(activationPlanDigests);
242
+ this.authorityHashes = new Map(authorityHashes);
243
+ }
244
+ validateOwnerRoute(context) {
245
+ const expectedAuthorityHash = this.authorityHashes.get(context.automationId);
246
+ if (expectedAuthorityHash === undefined) {
247
+ throw new RecoveryPortError('owner-route-authority-missing', 'none');
248
+ }
249
+ return validateOwnerRoute(context, this.runtime.delivery, expectedAuthorityHash);
250
+ }
251
+ async plan(context, stepId, signal) {
252
+ throwIfAborted(signal);
253
+ exactJob(context, this.jobs, this.activationPlanDigests);
254
+ switch (stepId) {
255
+ case 'authority-admission': return this.planAuthority(context, signal);
256
+ case 'ledger-reconcile': return this.planLedger(context);
257
+ case 'retention-maintenance': return this.planRetention(context);
258
+ case 't1-effects': return this.planPreferenceActivation(context);
259
+ case 'regression-rollback': return this.planEvolutionRollback(context);
260
+ case 'incident-review': return this.planCircuitProbe(context);
261
+ case 'verification': return this.planVerification(context, signal);
262
+ }
263
+ }
264
+ async execute(context, stepId, action, idempotencyKey, signal) {
265
+ throwIfAborted(signal);
266
+ exactJob(context, this.jobs, this.activationPlanDigests);
267
+ switch (action.kind) {
268
+ case 'verify-authority': return this.verifyAuthority(context, idempotencyKey, signal);
269
+ case 'project-evaluation': return this.projectEvaluation(context, action, idempotencyKey, signal);
270
+ case 'maintain-preferences': return this.maintainPreferences(context, action, idempotencyKey, signal);
271
+ case 'activate-preference': return this.activatePreference(context, action, idempotencyKey, signal);
272
+ case 'rollback-evolution': return this.rollbackEvolution(context, action, idempotencyKey, signal);
273
+ case 'probe-automation-circuit': return this.probeCircuit(context, action, idempotencyKey, signal);
274
+ case 'verify-health': return this.verifyHealth(context, idempotencyKey, signal);
275
+ case 'noop': return Object.freeze({
276
+ status: 'noop', resultCode: action.reasonCode, afterDigest: digest({ stepId, action }),
277
+ });
278
+ }
279
+ }
280
+ planAuthority(context, signal) {
281
+ try {
282
+ const projection = this.runtime.automations.inspectSystemOwned({
283
+ owner: RECOVERY_SYSTEM_OWNER,
284
+ automationId: context.automationId,
285
+ });
286
+ if (projection.definitionHash !== context.definitionHash || projection.automationStatus !== 'active') {
287
+ throw new RecoveryPortError('automation-definition-mismatch', 'none');
288
+ }
289
+ const report = this.runtime.health.hostGlobalSnapshot({
290
+ principal: context.principal,
291
+ operationId: operationId(context, 'authority-admission', 'plan'),
292
+ });
293
+ const route = this.validateOwnerRoute(context);
294
+ throwIfAborted(signal);
295
+ assertRequiredProviders(report, 'admission');
296
+ return Object.freeze({
297
+ action: { kind: 'verify-authority' },
298
+ beforeDigest: digest({
299
+ job: exactJob(context, this.jobs, this.activationPlanDigests),
300
+ projection: projectionState(projection),
301
+ health: healthState(report),
302
+ route,
303
+ }),
304
+ });
305
+ }
306
+ catch (error) {
307
+ portFailure(error, 'authority-admission-failed', false);
308
+ }
309
+ }
310
+ planLedger(context) {
311
+ try {
312
+ const capable = typeof this.runtime.evaluation.peekPendingProjection === 'function'
313
+ && typeof this.runtime.evaluation.reconcileProjection === 'function';
314
+ const target = capable
315
+ ? this.runtime.evaluation.peekPendingProjection({
316
+ scope: canonicalEvaluationHostScope(context.targetScope),
317
+ })
318
+ : undefined;
319
+ const action = !capable
320
+ ? { kind: 'noop', reasonCode: 'projection-seam-unavailable' }
321
+ : target === undefined
322
+ ? { kind: 'noop', reasonCode: 'no-quality-projection' }
323
+ : { kind: 'project-evaluation', evaluationId: target.evaluationId };
324
+ return Object.freeze({
325
+ action,
326
+ beforeDigest: digest({
327
+ scope: context.targetScope,
328
+ evaluation: this.runtime.evaluation.health(),
329
+ target: target ?? null,
330
+ capable,
331
+ }),
332
+ });
333
+ }
334
+ catch (error) {
335
+ portFailure(error, 'projection-plan-failed', false);
336
+ }
337
+ }
338
+ planRetention(context) {
339
+ try {
340
+ const route = this.validateOwnerRoute(context);
341
+ const principalLineage = routePrincipalLineage(route);
342
+ const ownerFence = exactPreferenceOwnerFence(this.runtime.preference.hostOwnerFence({
343
+ scope: canonicalPreferenceHostScope(context.targetScope),
344
+ principal: context.principal,
345
+ principalLineage,
346
+ operationId: operationId(context, 'retention-maintenance', 'plan'),
347
+ }), principalLineage);
348
+ return Object.freeze({
349
+ action: {
350
+ kind: 'maintain-preferences',
351
+ limit: 1,
352
+ ownerGeneration: ownerFence.ownerGeneration,
353
+ principalLineage,
354
+ },
355
+ beforeDigest: digest({
356
+ scope: context.targetScope,
357
+ health: this.runtime.preference.health(),
358
+ route,
359
+ ownerFence,
360
+ }),
361
+ });
362
+ }
363
+ catch (error) {
364
+ portFailure(error, 'preference-maintenance-plan-failed', false);
365
+ }
366
+ }
367
+ planPreferenceActivation(context) {
368
+ try {
369
+ const route = this.validateOwnerRoute(context);
370
+ const principalLineage = routePrincipalLineage(route);
371
+ const candidate = this.runtime.preference.hostActivationCandidate({
372
+ scope: canonicalPreferenceHostScope(context.targetScope),
373
+ principal: context.principal,
374
+ principalLineage,
375
+ operationId: operationId(context, 't1-effects', 'plan'),
376
+ });
377
+ let action;
378
+ if (candidate === undefined) {
379
+ action = { kind: 'noop', reasonCode: 'no-preference-candidate' };
380
+ }
381
+ else {
382
+ if (!Number.isSafeInteger(candidate.ownerGeneration) || candidate.ownerGeneration < 1
383
+ || candidate.principalLineage.principalRecordId !== principalLineage.principalRecordId
384
+ || candidate.principalLineage.principalVersion !== principalLineage.principalVersion) {
385
+ throw new RecoveryPortError('preference-activation-candidate-receipt-invalid', 'none');
386
+ }
387
+ action = {
388
+ kind: 'activate-preference',
389
+ hypothesisId: candidate.hypothesisId,
390
+ expectedVersion: candidate.expectedVersion,
391
+ ownerGeneration: candidate.ownerGeneration,
392
+ principalLineage,
393
+ };
394
+ }
395
+ return Object.freeze({
396
+ action,
397
+ beforeDigest: digest({ scope: context.targetScope, route, candidate: candidate ?? null }),
398
+ });
399
+ }
400
+ catch (error) {
401
+ portFailure(error, 'preference-plan-failed', false);
402
+ }
403
+ }
404
+ planEvolutionRollback(context) {
405
+ try {
406
+ const host = {
407
+ scope: canonicalEvolutionHostScope(context.targetScope),
408
+ principal: context.principal,
409
+ operationId: operationId(context, 'regression-rollback', 'plan'),
410
+ };
411
+ const candidates = this.runtime.evolution.hostCandidates(host);
412
+ const rules = this.runtime.evolution.hostListRules({ ...host, status: 'active' });
413
+ const target = candidates
414
+ .filter(candidate => candidate.kind === 'retire' && candidate.ruleId !== undefined)
415
+ .sort((left, right) => left.ruleId.localeCompare(right.ruleId))[0];
416
+ const rule = target === undefined ? undefined : rules.find(value => value.id === target.ruleId);
417
+ const action = rule === undefined
418
+ ? { kind: 'noop', reasonCode: 'no-regression-candidate' }
419
+ : { kind: 'rollback-evolution', ruleId: rule.id, expectedVersion: rule.version };
420
+ return Object.freeze({
421
+ action,
422
+ beforeDigest: digest({
423
+ scope: context.targetScope,
424
+ candidates: candidateState(candidates),
425
+ rules: ruleState(rules),
426
+ }),
427
+ });
428
+ }
429
+ catch (error) {
430
+ portFailure(error, 'evolution-plan-failed', false);
431
+ }
432
+ }
433
+ planCircuitProbe(context) {
434
+ try {
435
+ if (typeof this.runtime.automations.probeCircuitAndScheduleCanary !== 'function') {
436
+ return Object.freeze({
437
+ action: { kind: 'noop', reasonCode: 'circuit-canary-seam-unavailable' },
438
+ beforeDigest: digest({ capable: false }),
439
+ });
440
+ }
441
+ const candidates = [...this.jobs.keys()]
442
+ .filter(automationId => automationId !== context.automationId)
443
+ .sort()
444
+ .map(automationId => this.runtime.automations.inspectSystemOwned({
445
+ owner: RECOVERY_SYSTEM_OWNER,
446
+ automationId,
447
+ }))
448
+ .filter(projection => projection.automationStatus === 'active'
449
+ && projection.currentCircuit?.state === 'open')
450
+ .map(projection => ({ projection, circuit: projection.currentCircuit }));
451
+ const target = candidates[0];
452
+ const action = target === undefined
453
+ ? { kind: 'noop', reasonCode: 'no-circuit-candidate' }
454
+ : {
455
+ kind: 'probe-automation-circuit',
456
+ automationId: target.projection.automationId,
457
+ definitionHash: target.circuit.definitionHash,
458
+ expectedVersion: target.circuit.version,
459
+ };
460
+ return Object.freeze({
461
+ action,
462
+ beforeDigest: digest(candidates.map(value => projectionState(value.projection))),
463
+ });
464
+ }
465
+ catch (error) {
466
+ portFailure(error, 'circuit-plan-failed', false);
467
+ }
468
+ }
469
+ planVerification(context, signal) {
470
+ try {
471
+ const report = this.runtime.health.hostGlobalSnapshot({
472
+ principal: context.principal,
473
+ operationId: operationId(context, 'verification', 'plan'),
474
+ });
475
+ const route = this.validateOwnerRoute(context);
476
+ throwIfAborted(signal);
477
+ return Object.freeze({
478
+ action: { kind: 'verify-health' },
479
+ beforeDigest: digest({ health: healthState(report), route }),
480
+ });
481
+ }
482
+ catch (error) {
483
+ portFailure(error, 'health-plan-failed', false);
484
+ }
485
+ }
486
+ verifyAuthority(context, idempotencyKey, signal) {
487
+ try {
488
+ const projection = this.runtime.automations.inspectSystemOwned({
489
+ owner: RECOVERY_SYSTEM_OWNER,
490
+ automationId: context.automationId,
491
+ });
492
+ if (projection.definitionHash !== context.definitionHash || projection.automationStatus !== 'active') {
493
+ throw new RecoveryPortError('automation-definition-mismatch', 'none');
494
+ }
495
+ const report = this.runtime.health.hostGlobalSnapshot({
496
+ principal: context.principal,
497
+ operationId: idempotencyKey,
498
+ });
499
+ const route = this.validateOwnerRoute(context);
500
+ throwIfAborted(signal);
501
+ assertRequiredProviders(report, 'admission');
502
+ return Object.freeze({
503
+ status: 'succeeded',
504
+ resultCode: 'authority-verified',
505
+ afterDigest: digest({ projection: projectionState(projection), health: healthState(report), route }),
506
+ });
507
+ }
508
+ catch (error) {
509
+ portFailure(error, 'authority-verification-failed', false);
510
+ }
511
+ }
512
+ async projectEvaluation(context, action, idempotencyKey, _signal) {
513
+ if (typeof this.runtime.evaluation.reconcileProjection !== 'function') {
514
+ throw new RecoveryPortError('projection-seam-unavailable', 'none');
515
+ }
516
+ try {
517
+ this.validateOwnerRoute(context);
518
+ const result = await this.runtime.evaluation.reconcileProjection({
519
+ scope: canonicalEvaluationHostScope(context.targetScope),
520
+ evaluationId: action.evaluationId,
521
+ operationId: idempotencyKey,
522
+ });
523
+ if (result.evaluationId !== action.evaluationId
524
+ || !Number.isSafeInteger(result.attemptCount) || result.attemptCount < 0
525
+ || (result.status !== 'recorded' && result.status !== 'deferred')) {
526
+ throw new RecoveryPortError('projection-receipt-invalid', 'possible');
527
+ }
528
+ if (result.status === 'deferred') {
529
+ throw new RecoveryPortError('projection-deferred', 'possible');
530
+ }
531
+ return Object.freeze({
532
+ status: 'succeeded',
533
+ resultCode: 'quality-projected',
534
+ afterDigest: digest(result),
535
+ });
536
+ }
537
+ catch (error) {
538
+ portFailure(error, 'projection-reconcile-failed', true);
539
+ }
540
+ }
541
+ maintainPreferences(context, rawAction, idempotencyKey, _signal) {
542
+ try {
543
+ const action = exactMaintenanceAction(rawAction);
544
+ const route = this.validateOwnerRoute(context);
545
+ if (action.principalLineage.principalRecordId !== route.principalRecordId
546
+ || action.principalLineage.principalVersion !== route.principalVersion) {
547
+ throw new RecoveryPortError('owner-route-lineage-mismatch', 'none');
548
+ }
549
+ const result = exactMaintenanceReceipt(this.runtime.preference.hostMaintainOne({
550
+ scope: canonicalPreferenceHostScope(context.targetScope),
551
+ principal: context.principal,
552
+ principalLineage: action.principalLineage,
553
+ ownerGeneration: action.ownerGeneration,
554
+ operationId: idempotencyKey,
555
+ }), action);
556
+ return Object.freeze({
557
+ status: result.deletedSignals === 0 ? 'noop' : 'succeeded',
558
+ resultCode: result.deletedSignals === 0 ? 'no-expired-preference' : 'preference-retained',
559
+ afterDigest: digest(result),
560
+ });
561
+ }
562
+ catch (error) {
563
+ portFailure(error, 'preference-maintenance-failed', true);
564
+ }
565
+ }
566
+ activatePreference(context, action, idempotencyKey, _signal) {
567
+ try {
568
+ const scope = canonicalPreferenceHostScope(context.targetScope);
569
+ const route = this.validateOwnerRoute(context);
570
+ if (action.principalLineage.principalRecordId !== route.principalRecordId
571
+ || action.principalLineage.principalVersion !== route.principalVersion) {
572
+ throw new RecoveryPortError('owner-route-lineage-mismatch', 'none');
573
+ }
574
+ // Do not re-peek here. On crash-after-commit the candidate is no longer
575
+ // pending, while hostActivateOne's operation receipt is exactly what can
576
+ // prove and replay the prior mutation.
577
+ const result = this.runtime.preference.hostActivateOne({
578
+ scope,
579
+ principal: context.principal,
580
+ principalLineage: action.principalLineage,
581
+ ownerGeneration: action.ownerGeneration,
582
+ operationId: idempotencyKey,
583
+ hypothesisId: action.hypothesisId,
584
+ expectedVersion: action.expectedVersion,
585
+ });
586
+ if (result.hypothesisId !== action.hypothesisId
587
+ || result.expectedVersion !== action.expectedVersion
588
+ || result.resultVersion !== action.expectedVersion + 1
589
+ || result.ownerGeneration !== action.ownerGeneration
590
+ || result.principalLineageId !== action.principalLineage.principalRecordId
591
+ || result.principalLineageVersion !== action.principalLineage.principalVersion
592
+ || typeof result.replayed !== 'boolean') {
593
+ throw new RecoveryPortError('preference-activation-receipt-invalid', 'possible');
594
+ }
595
+ return Object.freeze({
596
+ status: 'succeeded', resultCode: 'preference-activated', afterDigest: digest(result),
597
+ });
598
+ }
599
+ catch (error) {
600
+ portFailure(error, 'preference-activation-failed', true);
601
+ }
602
+ }
603
+ rollbackEvolution(context, action, idempotencyKey, _signal) {
604
+ try {
605
+ const scope = canonicalEvolutionHostScope(context.targetScope);
606
+ this.validateOwnerRoute(context);
607
+ // The Evolution store recomputes evidence/CAS on first execution and
608
+ // returns its immutable rollback receipt on replay. A fresh candidate
609
+ // lookup here would incorrectly reject a successful prior retirement.
610
+ const result = this.runtime.evolution.hostRollbackOne({
611
+ scope,
612
+ principal: context.principal,
613
+ operationId: idempotencyKey,
614
+ ruleId: action.ruleId,
615
+ expectedVersion: action.expectedVersion,
616
+ });
617
+ if (result.rollback.ruleId !== action.ruleId
618
+ || result.rollback.expectedVersion !== action.expectedVersion
619
+ || result.rollback.resultVersion !== action.expectedVersion + 1
620
+ || result.rule.id !== action.ruleId
621
+ || result.rule.version !== result.rollback.resultVersion
622
+ || result.rule.status !== 'retired'
623
+ || typeof result.replayed !== 'boolean'
624
+ || !/^[a-f\d]{64}$/u.test(result.rollback.evidence.digest)) {
625
+ throw new RecoveryPortError('evolution-rollback-receipt-invalid', 'possible');
626
+ }
627
+ return Object.freeze({
628
+ status: 'succeeded', resultCode: 'evolution-rolled-back', afterDigest: digest({
629
+ ruleId: result.rule.id,
630
+ version: result.rule.version,
631
+ replayed: result.replayed,
632
+ evidenceDigest: result.rollback.evidence.digest,
633
+ }),
634
+ });
635
+ }
636
+ catch (error) {
637
+ portFailure(error, 'evolution-rollback-failed', true);
638
+ }
639
+ }
640
+ probeCircuit(context, action, idempotencyKey, _signal) {
641
+ if (typeof this.runtime.automations.probeCircuitAndScheduleCanary !== 'function') {
642
+ throw new RecoveryPortError('circuit-canary-seam-unavailable', 'none');
643
+ }
644
+ try {
645
+ this.validateOwnerRoute(context);
646
+ // The sink atomically arms the exact circuit and durably schedules a
647
+ // production canary. Replaying this operation never re-inspects `open`.
648
+ const result = this.runtime.automations.probeCircuitAndScheduleCanary({
649
+ owner: RECOVERY_SYSTEM_OWNER,
650
+ operationId: idempotencyKey,
651
+ automationId: action.automationId,
652
+ definitionHash: action.definitionHash,
653
+ expectedCircuitVersion: action.expectedVersion,
654
+ });
655
+ if (result.operationId !== idempotencyKey
656
+ || result.circuit.automationId !== action.automationId
657
+ || result.circuit.definitionHash !== action.definitionHash
658
+ || result.circuit.state !== 'half-open'
659
+ || result.circuit.version !== action.expectedVersion + 1
660
+ || result.executionMode !== 'production'
661
+ || typeof result.occurrenceId !== 'string' || result.occurrenceId === ''
662
+ || typeof result.taskId !== 'string' || result.taskId === ''
663
+ || typeof result.replayed !== 'boolean') {
664
+ throw new RecoveryPortError('circuit-canary-receipt-invalid', 'possible');
665
+ }
666
+ return Object.freeze({
667
+ status: 'succeeded', resultCode: 'circuit-canary-scheduled', afterDigest: digest({
668
+ operationId: result.operationId,
669
+ automationId: result.circuit.automationId,
670
+ definitionHash: result.circuit.definitionHash,
671
+ state: result.circuit.state,
672
+ version: result.circuit.version,
673
+ canary: {
674
+ occurrenceId: result.occurrenceId,
675
+ taskId: result.taskId,
676
+ executionMode: result.executionMode,
677
+ },
678
+ replayed: result.replayed,
679
+ }),
680
+ });
681
+ }
682
+ catch (error) {
683
+ portFailure(error, 'circuit-probe-failed', true);
684
+ }
685
+ }
686
+ verifyHealth(context, idempotencyKey, signal) {
687
+ try {
688
+ const report = this.runtime.health.hostGlobalSnapshot({
689
+ principal: context.principal,
690
+ operationId: idempotencyKey,
691
+ });
692
+ const route = this.validateOwnerRoute(context);
693
+ throwIfAborted(signal);
694
+ assertRequiredProviders(report, 'verification');
695
+ return Object.freeze({
696
+ status: 'succeeded', resultCode: 'health-verified', afterDigest: digest({
697
+ health: healthState(report), route,
698
+ }),
699
+ });
700
+ }
701
+ catch (error) {
702
+ portFailure(error, 'health-verification-failed', false);
703
+ }
704
+ }
705
+ }
706
+ //# sourceMappingURL=port.js.map