openplanr 1.13.2 → 1.14.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 (159) hide show
  1. package/README.md +46 -4
  2. package/dist/cli/commands/artifact.d.ts.map +1 -1
  3. package/dist/cli/commands/artifact.js +17 -6
  4. package/dist/cli/commands/artifact.js.map +1 -1
  5. package/dist/cli/commands/operate.d.ts +3 -0
  6. package/dist/cli/commands/operate.d.ts.map +1 -0
  7. package/dist/cli/commands/operate.js +568 -0
  8. package/dist/cli/commands/operate.js.map +1 -0
  9. package/dist/cli/index.js +42 -2
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/services/ai-service.d.ts +9 -2
  12. package/dist/services/ai-service.d.ts.map +1 -1
  13. package/dist/services/ai-service.js +14 -6
  14. package/dist/services/ai-service.js.map +1 -1
  15. package/dist/services/artifact-pipeline-service.d.ts +1 -0
  16. package/dist/services/artifact-pipeline-service.d.ts.map +1 -1
  17. package/dist/services/artifact-pipeline-service.js.map +1 -1
  18. package/dist/services/delta-apply-service.js +1 -1
  19. package/dist/services/delta-apply-service.js.map +1 -1
  20. package/dist/services/operate/advisors.d.ts +219 -0
  21. package/dist/services/operate/advisors.d.ts.map +1 -0
  22. package/dist/services/operate/advisors.js +745 -0
  23. package/dist/services/operate/advisors.js.map +1 -0
  24. package/dist/services/operate/artifact-route-generation.d.ts +111 -0
  25. package/dist/services/operate/artifact-route-generation.d.ts.map +1 -0
  26. package/dist/services/operate/artifact-route-generation.js +459 -0
  27. package/dist/services/operate/artifact-route-generation.js.map +1 -0
  28. package/dist/services/operate/artifacts.d.ts +26 -0
  29. package/dist/services/operate/artifacts.d.ts.map +1 -0
  30. package/dist/services/operate/artifacts.js +92 -0
  31. package/dist/services/operate/artifacts.js.map +1 -0
  32. package/dist/services/operate/canonical.d.ts +18 -0
  33. package/dist/services/operate/canonical.d.ts.map +1 -0
  34. package/dist/services/operate/canonical.js +157 -0
  35. package/dist/services/operate/canonical.js.map +1 -0
  36. package/dist/services/operate/config.d.ts +56 -0
  37. package/dist/services/operate/config.d.ts.map +1 -0
  38. package/dist/services/operate/config.js +644 -0
  39. package/dist/services/operate/config.js.map +1 -0
  40. package/dist/services/operate/consolidation.d.ts +18 -0
  41. package/dist/services/operate/consolidation.d.ts.map +1 -0
  42. package/dist/services/operate/consolidation.js +539 -0
  43. package/dist/services/operate/consolidation.js.map +1 -0
  44. package/dist/services/operate/doctor.d.ts +12 -0
  45. package/dist/services/operate/doctor.d.ts.map +1 -0
  46. package/dist/services/operate/doctor.js +307 -0
  47. package/dist/services/operate/doctor.js.map +1 -0
  48. package/dist/services/operate/engine.d.ts +53 -0
  49. package/dist/services/operate/engine.d.ts.map +1 -0
  50. package/dist/services/operate/engine.js +1097 -0
  51. package/dist/services/operate/engine.js.map +1 -0
  52. package/dist/services/operate/event-store.d.ts +57 -0
  53. package/dist/services/operate/event-store.d.ts.map +1 -0
  54. package/dist/services/operate/event-store.js +232 -0
  55. package/dist/services/operate/event-store.js.map +1 -0
  56. package/dist/services/operate/evidence-cache.d.ts +17 -0
  57. package/dist/services/operate/evidence-cache.d.ts.map +1 -0
  58. package/dist/services/operate/evidence-cache.js +96 -0
  59. package/dist/services/operate/evidence-cache.js.map +1 -0
  60. package/dist/services/operate/evidence-import.d.ts +40 -0
  61. package/dist/services/operate/evidence-import.d.ts.map +1 -0
  62. package/dist/services/operate/evidence-import.js +340 -0
  63. package/dist/services/operate/evidence-import.js.map +1 -0
  64. package/dist/services/operate/evidence-readiness.d.ts +8 -0
  65. package/dist/services/operate/evidence-readiness.d.ts.map +1 -0
  66. package/dist/services/operate/evidence-readiness.js +78 -0
  67. package/dist/services/operate/evidence-readiness.js.map +1 -0
  68. package/dist/services/operate/evidence.d.ts +71 -0
  69. package/dist/services/operate/evidence.d.ts.map +1 -0
  70. package/dist/services/operate/evidence.js +1022 -0
  71. package/dist/services/operate/evidence.js.map +1 -0
  72. package/dist/services/operate/ids.d.ts +7 -0
  73. package/dist/services/operate/ids.d.ts.map +1 -0
  74. package/dist/services/operate/ids.js +23 -0
  75. package/dist/services/operate/ids.js.map +1 -0
  76. package/dist/services/operate/index.d.ts +14 -0
  77. package/dist/services/operate/index.d.ts.map +1 -0
  78. package/dist/services/operate/index.js +881 -0
  79. package/dist/services/operate/index.js.map +1 -0
  80. package/dist/services/operate/journal.d.ts +34 -0
  81. package/dist/services/operate/journal.d.ts.map +1 -0
  82. package/dist/services/operate/journal.js +229 -0
  83. package/dist/services/operate/journal.js.map +1 -0
  84. package/dist/services/operate/legacy-import-service.d.ts +58 -0
  85. package/dist/services/operate/legacy-import-service.d.ts.map +1 -0
  86. package/dist/services/operate/legacy-import-service.js +922 -0
  87. package/dist/services/operate/legacy-import-service.js.map +1 -0
  88. package/dist/services/operate/lifecycle.d.ts +88 -0
  89. package/dist/services/operate/lifecycle.d.ts.map +1 -0
  90. package/dist/services/operate/lifecycle.js +556 -0
  91. package/dist/services/operate/lifecycle.js.map +1 -0
  92. package/dist/services/operate/lock-service.d.ts +37 -0
  93. package/dist/services/operate/lock-service.d.ts.map +1 -0
  94. package/dist/services/operate/lock-service.js +208 -0
  95. package/dist/services/operate/lock-service.js.map +1 -0
  96. package/dist/services/operate/maintenance.d.ts +38 -0
  97. package/dist/services/operate/maintenance.d.ts.map +1 -0
  98. package/dist/services/operate/maintenance.js +699 -0
  99. package/dist/services/operate/maintenance.js.map +1 -0
  100. package/dist/services/operate/outcomes.d.ts +48 -0
  101. package/dist/services/operate/outcomes.d.ts.map +1 -0
  102. package/dist/services/operate/outcomes.js +312 -0
  103. package/dist/services/operate/outcomes.js.map +1 -0
  104. package/dist/services/operate/pipeline-handoff.d.ts +138 -0
  105. package/dist/services/operate/pipeline-handoff.d.ts.map +1 -0
  106. package/dist/services/operate/pipeline-handoff.js +175 -0
  107. package/dist/services/operate/pipeline-handoff.js.map +1 -0
  108. package/dist/services/operate/projection-persistence.d.ts +55 -0
  109. package/dist/services/operate/projection-persistence.d.ts.map +1 -0
  110. package/dist/services/operate/projection-persistence.js +393 -0
  111. package/dist/services/operate/projection-persistence.js.map +1 -0
  112. package/dist/services/operate/projection.d.ts +4 -0
  113. package/dist/services/operate/projection.d.ts.map +1 -0
  114. package/dist/services/operate/projection.js +128 -0
  115. package/dist/services/operate/projection.js.map +1 -0
  116. package/dist/services/operate/protocol.d.ts +60 -0
  117. package/dist/services/operate/protocol.d.ts.map +1 -0
  118. package/dist/services/operate/protocol.js +62 -0
  119. package/dist/services/operate/protocol.js.map +1 -0
  120. package/dist/services/operate/read-only-providers.d.ts +38 -0
  121. package/dist/services/operate/read-only-providers.d.ts.map +1 -0
  122. package/dist/services/operate/read-only-providers.js +286 -0
  123. package/dist/services/operate/read-only-providers.js.map +1 -0
  124. package/dist/services/operate/redaction.d.ts +47 -0
  125. package/dist/services/operate/redaction.d.ts.map +1 -0
  126. package/dist/services/operate/redaction.js +279 -0
  127. package/dist/services/operate/redaction.js.map +1 -0
  128. package/dist/services/operate/reducer.d.ts +10 -0
  129. package/dist/services/operate/reducer.d.ts.map +1 -0
  130. package/dist/services/operate/reducer.js +11 -0
  131. package/dist/services/operate/reducer.js.map +1 -0
  132. package/dist/services/operate/routes.d.ts +49 -0
  133. package/dist/services/operate/routes.d.ts.map +1 -0
  134. package/dist/services/operate/routes.js +996 -0
  135. package/dist/services/operate/routes.js.map +1 -0
  136. package/dist/services/operate/shipment-observer.d.ts +14 -0
  137. package/dist/services/operate/shipment-observer.d.ts.map +1 -0
  138. package/dist/services/operate/shipment-observer.js +321 -0
  139. package/dist/services/operate/shipment-observer.js.map +1 -0
  140. package/dist/services/operate/stalled-item-service.d.ts +18 -0
  141. package/dist/services/operate/stalled-item-service.d.ts.map +1 -0
  142. package/dist/services/operate/stalled-item-service.js +121 -0
  143. package/dist/services/operate/stalled-item-service.js.map +1 -0
  144. package/dist/services/operate/subprocess-env.d.ts +4 -0
  145. package/dist/services/operate/subprocess-env.d.ts.map +1 -0
  146. package/dist/services/operate/subprocess-env.js +45 -0
  147. package/dist/services/operate/subprocess-env.js.map +1 -0
  148. package/dist/services/operate/types.d.ts +729 -0
  149. package/dist/services/operate/types.d.ts.map +1 -0
  150. package/dist/services/operate/types.js +13 -0
  151. package/dist/services/operate/types.js.map +1 -0
  152. package/dist/services/operate/workspace.d.ts +59 -0
  153. package/dist/services/operate/workspace.d.ts.map +1 -0
  154. package/dist/services/operate/workspace.js +316 -0
  155. package/dist/services/operate/workspace.js.map +1 -0
  156. package/dist/services/runtime-manager-service.d.ts.map +1 -1
  157. package/dist/services/runtime-manager-service.js +71 -2
  158. package/dist/services/runtime-manager-service.js.map +1 -1
  159. package/package.json +10 -4
@@ -0,0 +1,1097 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { loadConfig } from '../config-service.js';
5
+ import { advisorFailureGaps, buildAdvisorOperatingContext, configuredAdvisorProviderPolicy, createConfiguredStructuredAdapter, createOfflineAdvisorAdapter, dispatchOperatingAdvisors, ensureOperatingProviderConsent, } from './advisors.js';
6
+ import { canonicalDigest, canonicalize } from './canonical.js';
7
+ import { operatingProjectKey, validateOperatingConfiguration } from './config.js';
8
+ import { consolidateOperatingResults } from './consolidation.js';
9
+ import { OperatingEventStore } from './event-store.js';
10
+ import { collectOperatingEvidence, evidenceProjectionSources, unqualifiedCommercialEvidenceGaps, } from './evidence.js';
11
+ import { OperatingEvidenceCache } from './evidence-cache.js';
12
+ import { evaluateEvidenceReadiness } from './evidence-readiness.js';
13
+ import { nextCycleId } from './ids.js';
14
+ import { applyJournalTransaction, prepareJournalTransaction, readJournal, rollbackJournalTransaction, } from './journal.js';
15
+ import { reconcileOperatingDecisionDeadlines } from './lifecycle.js';
16
+ import { withOperatingLock } from './lock-service.js';
17
+ import { reconcileOperatingOutcomeFiles } from './outcomes.js';
18
+ import { persistOperatingProjections } from './projection-persistence.js';
19
+ import { assertOperatingArtifact, loadOperatingProtocol } from './protocol.js';
20
+ import { maximumSensitivity } from './redaction.js';
21
+ import { createOperatingRoutePlan, nextOperatingSpecOrdinal } from './routes.js';
22
+ import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
23
+ import { assertOperatingProject, refreshOperatingWorkspaceManifest, resolveOperatingPaths, } from './workspace.js';
24
+ const PRODUCT_VERSION = '1.14.0';
25
+ function normalizeFocus(focus) {
26
+ const supported = new Set(['strategy', 'product', 'growth', 'operations', 'technology', 'all']);
27
+ const normalized = [...new Set((focus ?? []).map(String))]
28
+ .filter((value) => supported.has(value))
29
+ .sort();
30
+ return normalized.length > 0 ? normalized : ['all'];
31
+ }
32
+ function loadJson(target) {
33
+ return readFile(target, 'utf8').then((raw) => JSON.parse(raw));
34
+ }
35
+ async function snapshotWorkspace(projectRoot, localRoot) {
36
+ return refreshOperatingWorkspaceManifest(projectRoot, { localRoot });
37
+ }
38
+ async function readPreferences(projectRoot, localRoot) {
39
+ const paths = resolveOperatingPaths(projectRoot, { localRoot });
40
+ return loadJson(path.join(paths.localRoot, 'preferences.json')).catch(() => ({
41
+ runtime: 'auto',
42
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
43
+ sensitivityCeiling: 'internal',
44
+ evidenceTtlMs: 7 * 24 * 60 * 60 * 1_000,
45
+ enabledSources: [],
46
+ }));
47
+ }
48
+ function activeCycle(state) {
49
+ return (state.cycles.find((cycle) => !['closed', 'cancelled', 'failed'].includes(cycle.state)) ?? null);
50
+ }
51
+ function maximumOrdinal(records, prefix) {
52
+ return records.reduce((maximum, record) => {
53
+ const match = String(record.id ?? '').match(new RegExp(`^${prefix}-(\\d+)$`));
54
+ return Math.max(maximum, match ? Number(match[1]) : 0);
55
+ }, 0);
56
+ }
57
+ async function remapReadinessGaps(readiness, state) {
58
+ let ordinal = maximumOrdinal(state.dataGaps, 'GAP');
59
+ const remapped = {
60
+ ...readiness,
61
+ roles: readiness.roles.map((role) => ({
62
+ ...role,
63
+ gapId: role.gapId
64
+ ? (state.dataGaps.find((gap) => gap.cycleId === readiness.cycleId &&
65
+ !['closed', 'superseded'].includes(String(gap.status)) &&
66
+ Array.isArray(gap.affectedRoles) &&
67
+ gap.affectedRoles.includes(role.roleId))?.id ?? `GAP-${String(++ordinal).padStart(3, '0')}`)
68
+ : null,
69
+ })),
70
+ };
71
+ return assertOperatingArtifact('operating-evidence-readiness', remapped);
72
+ }
73
+ function readinessGaps(readiness, decisionOwner, now) {
74
+ return readiness.roles
75
+ .filter((role) => role.gapId)
76
+ .map((role) => ({
77
+ kind: 'operating-data-gap',
78
+ schemaVersion: OPERATE_SCHEMA_VERSION,
79
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
80
+ id: role.gapId,
81
+ cycleId: readiness.cycleId,
82
+ question: `What evidence is required for ${role.roleId}?`,
83
+ reason: role.missingEvidence.join('; ') || 'Minimum evidence was not available.',
84
+ unblocks: [],
85
+ affectedRoles: [role.roleId],
86
+ status: 'open',
87
+ owner: decisionOwner,
88
+ evidenceRefs: [...role.evidenceRefs],
89
+ createdAt: now,
90
+ updatedAt: now,
91
+ }));
92
+ }
93
+ function requiredOpenEvidenceRefs(state) {
94
+ const records = [
95
+ ...state.findings.filter((finding) => !['done', 'rejected', 'superseded'].includes(String(finding.status))),
96
+ ...state.decisions.filter((decision) => !['closed', 'superseded'].includes(String(decision.status))),
97
+ ...state.outcomes.filter((outcome) => ['pending', 'observing', 'inconclusive'].includes(String(outcome.status))),
98
+ ];
99
+ return [
100
+ ...new Set(records.flatMap((record) => Array.isArray(record.evidenceRefs)
101
+ ? record.evidenceRefs.filter((reference) => typeof reference === 'string')
102
+ : [])),
103
+ ].sort();
104
+ }
105
+ export function buildChairEvidence(evidence, results, now) {
106
+ const evidenceById = new Map(evidence.items.map((item) => [item.id, item]));
107
+ const synthetic = results.flatMap((result) => {
108
+ const evidenceRefs = [
109
+ ...new Set(result.proposals.flatMap((proposal) => proposal.evidenceRefs)),
110
+ ].sort();
111
+ const missingRefs = evidenceRefs.filter((reference) => !evidenceById.has(reference));
112
+ if (missingRefs.length > 0) {
113
+ throw new OperateError('E_OPERATE_EVIDENCE_REJECTED', `${result.roleId} advisor result cites unavailable evidence: ${missingRefs.join(', ')}.`);
114
+ }
115
+ const sensitivity = maximumSensitivity([
116
+ 'internal',
117
+ ...evidenceRefs.map((reference) => evidenceById.get(reference)?.sensitivity ?? 'public'),
118
+ ]);
119
+ const bodies = [
120
+ {
121
+ id: `EVD-advisor-results-${result.roleId}-context`,
122
+ location: `${result.roleId}/context`,
123
+ body: {
124
+ roleId: result.roleId,
125
+ outcome: result.outcome,
126
+ gaps: [...new Set(result.gaps)].sort(),
127
+ conflicts: [...new Set(result.conflicts)].sort(),
128
+ },
129
+ },
130
+ ...result.proposals.map((proposal) => ({
131
+ id: `EVD-advisor-results-${result.roleId}-${proposal.proposalKey}`,
132
+ location: `${result.roleId}/proposals/${proposal.proposalKey}`,
133
+ body: {
134
+ roleId: result.roleId,
135
+ proposal: structuredClone(proposal),
136
+ },
137
+ })),
138
+ ];
139
+ return bodies.map(({ id, location, body }) => ({
140
+ id,
141
+ source: 'advisor-results',
142
+ location,
143
+ digest: canonicalDigest(body),
144
+ collectedAt: now,
145
+ observedFrom: now,
146
+ observedTo: now,
147
+ freshness: 'fresh',
148
+ sensitivity,
149
+ claimTypes: ['verified-advisor-result'],
150
+ summary: canonicalize(body),
151
+ }));
152
+ });
153
+ return {
154
+ ...evidence,
155
+ items: [...evidence.items, ...synthetic],
156
+ fingerprint: canonicalDigest({
157
+ source: evidence.fingerprint,
158
+ advisors: synthetic
159
+ .map(({ id, digest, sensitivity }) => ({ id, digest, sensitivity }))
160
+ .sort((left, right) => left.id.localeCompare(right.id)),
161
+ }),
162
+ };
163
+ }
164
+ const SENSITIVE_ROUTE_LANGUAGE = /\b(?:payment|billing|security|privacy|legal|tenant|isolation|destructive|delete|purge|credential|secret)\b/i;
165
+ function containsSensitiveRouteProposal(results) {
166
+ return results.some((result) => result.proposals.some((proposal) => SENSITIVE_ROUTE_LANGUAGE.test(`${proposal.type} ${proposal.title} ${proposal.problem} ${proposal.proposal}`)));
167
+ }
168
+ async function createCycleManifest(input) {
169
+ return assertOperatingArtifact('operating-cycle-manifest', {
170
+ kind: 'operating-cycle-manifest',
171
+ schemaVersion: OPERATE_SCHEMA_VERSION,
172
+ protocolVersion: OPERATE_PROTOCOL_VERSION,
173
+ id: input.id,
174
+ state: input.state,
175
+ depth: input.depth,
176
+ focus: input.focus,
177
+ inputDigest: input.inputDigest,
178
+ enabledRoles: [...input.enabledRoles],
179
+ enabledProviders: [...input.enabledProviders],
180
+ createdAt: input.now,
181
+ updatedAt: input.now,
182
+ producer: {
183
+ product: 'openplanr',
184
+ version: PRODUCT_VERSION,
185
+ runtime: input.runtime,
186
+ },
187
+ });
188
+ }
189
+ async function appendLocked(store, lock, head, input) {
190
+ const event = await store.append({ ...input, expectedHead: head.hash });
191
+ const next = { sequence: event.sequence, hash: event.eventHash };
192
+ await lock.advanceEventHead(head, next);
193
+ return { event, head: next };
194
+ }
195
+ function remapConsolidation(state, findings, parked, decisions, gaps) {
196
+ let findingOrdinal = maximumOrdinal(state.findings, 'FND');
197
+ let decisionOrdinal = maximumOrdinal(state.decisions, 'DEC');
198
+ let gapOrdinal = maximumOrdinal(state.dataGaps, 'GAP');
199
+ const findingIds = new Map();
200
+ for (const finding of [...findings, ...parked]) {
201
+ findingIds.set(finding.id, `FND-${String(++findingOrdinal).padStart(3, '0')}`);
202
+ }
203
+ const remapFinding = (finding) => ({
204
+ ...finding,
205
+ id: findingIds.get(finding.id),
206
+ dependsOn: finding.dependsOn.map((dependency) => findingIds.get(dependency) ?? dependency),
207
+ });
208
+ return {
209
+ findings: findings.map(remapFinding),
210
+ parked: parked.map(remapFinding),
211
+ decisions: decisions.map((decision) => ({
212
+ ...decision,
213
+ id: `DEC-${String(++decisionOrdinal).padStart(3, '0')}`,
214
+ })),
215
+ gaps: gaps.map((gap) => ({
216
+ ...gap,
217
+ id: `GAP-${String(++gapOrdinal).padStart(3, '0')}`,
218
+ })),
219
+ };
220
+ }
221
+ function findingIdentity(value) {
222
+ const finding = value;
223
+ if (typeof finding.fingerprint === 'string' &&
224
+ /^sha256:[a-f0-9]{64}$/.test(finding.fingerprint)) {
225
+ return finding.fingerprint;
226
+ }
227
+ return canonicalDigest({
228
+ title: String(finding.title ?? '')
229
+ .trim()
230
+ .toLowerCase(),
231
+ lane: String(finding.lane ?? ''),
232
+ owner: String(finding.owner ?? ''),
233
+ });
234
+ }
235
+ function suppressRepeatedFindings(state, findings, cycleId) {
236
+ const priorByIdentity = new Map(state.findings
237
+ .filter((finding) => finding.cycleId !== cycleId)
238
+ .map((finding) => [findingIdentity(finding), finding]));
239
+ let suppressed = 0;
240
+ const retained = [];
241
+ for (const finding of findings) {
242
+ const prior = priorByIdentity.get(findingIdentity(finding));
243
+ if (!prior) {
244
+ retained.push(finding);
245
+ continue;
246
+ }
247
+ const priorEvidence = Array.isArray(prior.evidenceRefs)
248
+ ? [...prior.evidenceRefs].sort()
249
+ : [];
250
+ const currentEvidence = [...finding.evidenceRefs].sort();
251
+ if (canonicalDigest(priorEvidence) === canonicalDigest(currentEvidence)) {
252
+ suppressed += 1;
253
+ continue;
254
+ }
255
+ retained.push({
256
+ ...finding,
257
+ dependsOn: [...new Set([...finding.dependsOn, String(prior.id)])].sort(),
258
+ });
259
+ }
260
+ return { findings: retained, suppressed };
261
+ }
262
+ function committedCycleFindings(events, cycleId) {
263
+ return events
264
+ .filter((event) => event.type === 'finding.proposed' &&
265
+ event.cycleId === cycleId &&
266
+ event.payload.record &&
267
+ typeof event.payload.record === 'object')
268
+ .map((event) => structuredClone(event.payload.record));
269
+ }
270
+ function reconcileCycleFindings(events, cycleId, findings) {
271
+ const existing = committedCycleFindings(events, cycleId);
272
+ const existingByIdentity = new Map(existing.map((finding) => [findingIdentity(finding), finding]));
273
+ return findings.map((finding) => {
274
+ const prior = existingByIdentity.get(findingIdentity(finding));
275
+ if (!prior)
276
+ return finding;
277
+ return canonicalDigest([...prior.evidenceRefs].sort()) ===
278
+ canonicalDigest([...finding.evidenceRefs].sort())
279
+ ? prior
280
+ : finding;
281
+ });
282
+ }
283
+ function recordContent(value) {
284
+ return structuredClone(value);
285
+ }
286
+ async function persistedCycleEvidence(store, events, cycleId) {
287
+ const event = [...events]
288
+ .reverse()
289
+ .find((candidate) => candidate.cycleId === cycleId &&
290
+ candidate.type === 'evidence.collected' &&
291
+ typeof candidate.payload.recordDigest === 'string');
292
+ if (!event)
293
+ return null;
294
+ const record = await store.readRecord(event.payload.recordDigest);
295
+ if (record.recordType !== 'evidence-metadata') {
296
+ throw new OperateError('E_OPERATE_STATE_INVALID', `Cycle ${cycleId} evidence event references a ${record.recordType} record.`);
297
+ }
298
+ return assertOperatingArtifact('operating-evidence', record.content);
299
+ }
300
+ async function persistedCycleRoleResults(store, events, cycleId) {
301
+ const byRole = new Map();
302
+ for (const event of events) {
303
+ if (event.cycleId !== cycleId ||
304
+ event.type !== 'advisory.recorded' ||
305
+ typeof event.payload.recordDigest !== 'string') {
306
+ continue;
307
+ }
308
+ const record = await store.readRecord(event.payload.recordDigest);
309
+ if (record.recordType !== 'advisor-result') {
310
+ throw new OperateError('E_OPERATE_STATE_INVALID', `Cycle ${cycleId} advisory event references a ${record.recordType} record.`);
311
+ }
312
+ const result = await assertOperatingArtifact('operating-role-result', record.content);
313
+ (await loadOperatingProtocol()).validateOperatingRoleResultDigest(result);
314
+ if (result.cycleId !== cycleId) {
315
+ throw new OperateError('E_OPERATE_STATE_INVALID', `Advisor result ${result.roleId} belongs to another cycle.`);
316
+ }
317
+ byRole.set(result.roleId, result);
318
+ }
319
+ return [...byRole.values()].sort((left, right) => left.roleId.localeCompare(right.roleId));
320
+ }
321
+ /**
322
+ * Full evidence → advisors → deterministic consolidation lifecycle. All
323
+ * mutations are serialized under the process-identity lease; preview and
324
+ * dry-run never enter the mutating branch.
325
+ */
326
+ export async function runOperatingCycle(input) {
327
+ const projectRoot = await assertOperatingProject(input.projectRoot);
328
+ const config = await validateOperatingConfiguration(projectRoot);
329
+ const paths = resolveOperatingPaths(projectRoot, { localRoot: input.localRoot });
330
+ const initializedWorkspace = await loadJson(paths.workspace);
331
+ await assertOperatingArtifact('operating-workspace-manifest', initializedWorkspace);
332
+ const preferences = await readPreferences(projectRoot, input.localRoot);
333
+ const store = new OperatingEventStore(projectRoot, { localRoot: input.localRoot });
334
+ const now = input.now ?? new Date();
335
+ const deadlineReconciliation = !input.preview && !input.dryRun
336
+ ? await reconcileOperatingDecisionDeadlines({
337
+ projectRoot,
338
+ localRoot: input.localRoot,
339
+ now,
340
+ })
341
+ : { transitioned: 0 };
342
+ const initial = await store.replay();
343
+ const initialState = await store.state();
344
+ const current = activeCycle(initialState);
345
+ const timestamp = now.toISOString();
346
+ if (input.reviewOnly) {
347
+ const persisted = initialState.cycles.at(-1);
348
+ if (!persisted) {
349
+ throw new OperateError('E_OPERATE_STATE_INVALID', 'Review-only mode requires at least one committed operating cycle.');
350
+ }
351
+ const persistedCycle = await assertOperatingArtifact('operating-cycle-manifest', persisted);
352
+ const reconciled = input.dryRun
353
+ ? { reconciled: 0, shipObserved: 0, state: initialState }
354
+ : await reconcileOperatingOutcomeFiles({
355
+ projectRoot,
356
+ localRoot: input.localRoot,
357
+ });
358
+ return {
359
+ preview: false,
360
+ dryRun: Boolean(input.dryRun),
361
+ cycle: persistedCycle,
362
+ provider: null,
363
+ state: reconciled.state,
364
+ modelCalls: 0,
365
+ warnings: [
366
+ `Review-only mode reconciled ${reconciled.reconciled} outcome observation(s) without evidence or model calls.`,
367
+ ...(deadlineReconciliation.transitioned > 0
368
+ ? [
369
+ `Marked ${deadlineReconciliation.transitioned} elapsed decision deadline(s) default-due without executing an action.`,
370
+ ]
371
+ : []),
372
+ ],
373
+ };
374
+ }
375
+ const resumableStates = new Set([
376
+ 'preparing',
377
+ 'collecting',
378
+ 'advising',
379
+ 'consolidating',
380
+ 'blocked',
381
+ ]);
382
+ if (current && !resumableStates.has(current.state)) {
383
+ throw new OperateError('E_OPERATE_CYCLE_ACTIVE', `Operating cycle ${current.id} is already ${current.state}; close or cancel it before starting another.`);
384
+ }
385
+ const depth = input.depth ?? current?.depth ?? 'standard';
386
+ const focus = normalizeFocus(input.focus ?? current?.focus);
387
+ const resolvedRuntime = input.runtime && input.runtime !== 'auto' ? input.runtime : preferences.runtime;
388
+ const cycleStart = current
389
+ ? initial.events.find((event) => event.type === 'cycle.preparing' &&
390
+ event.cycleId === current.id &&
391
+ event.entityId === current.id)
392
+ : null;
393
+ if (current && !cycleStart) {
394
+ throw new OperateError('E_OPERATE_STATE_INVALID', `Operating cycle ${current.id} has no canonical preparing event.`);
395
+ }
396
+ const inputHead = cycleStart
397
+ ? {
398
+ sequence: cycleStart.sequence - 1,
399
+ hash: cycleStart.previousEventHash,
400
+ }
401
+ : initial.eventHead;
402
+ // Preview is intentionally machine-state-free: it does not require local
403
+ // component-root mappings and binds only a prospective digest. A committed
404
+ // cycle always snapshots the live roots at its actual start.
405
+ const workspace = input.preview
406
+ ? initializedWorkspace
407
+ : await snapshotWorkspace(projectRoot, input.localRoot);
408
+ const inputDigest = canonicalDigest({
409
+ workspaceDigest: workspace.workspaceDigest,
410
+ config: canonicalDigest(config),
411
+ eventHead: inputHead,
412
+ depth,
413
+ focus,
414
+ runtime: resolvedRuntime,
415
+ });
416
+ if (current && current.inputDigest !== inputDigest) {
417
+ throw new OperateError('E_OPERATE_CYCLE_INPUT_CONFLICT', `Operating cycle ${current.id} can resume only with its original workspace, configuration, depth, and focus.`, {
418
+ cycleId: current.id,
419
+ expectedInputDigest: current.inputDigest,
420
+ actualInputDigest: inputDigest,
421
+ });
422
+ }
423
+ const cycle = current
424
+ ? await assertOperatingArtifact('operating-cycle-manifest', current)
425
+ : await createCycleManifest({
426
+ id: nextCycleId(initialState),
427
+ state: 'preparing',
428
+ depth,
429
+ focus,
430
+ inputDigest,
431
+ enabledRoles: config.enabledRoles,
432
+ enabledProviders: config.enabledProviders,
433
+ runtime: resolvedRuntime,
434
+ now: timestamp,
435
+ });
436
+ if (input.preview) {
437
+ return {
438
+ preview: true,
439
+ dryRun: false,
440
+ cycle,
441
+ provider: null,
442
+ modelCalls: 0,
443
+ warnings: [],
444
+ };
445
+ }
446
+ const assertCycleWorkspace = async () => {
447
+ const observed = await snapshotWorkspace(projectRoot, input.localRoot);
448
+ if (observed.workspaceDigest !== workspace.workspaceDigest) {
449
+ throw new OperateError('E_OPERATE_HEAD_DIVERGED', 'Workspace revisions, branches, remotes, or material dirty fingerprints changed during the operating cycle.', {
450
+ expectedWorkspaceDigest: workspace.workspaceDigest,
451
+ actualWorkspaceDigest: observed.workspaceDigest,
452
+ });
453
+ }
454
+ };
455
+ const existingEvidence = current && current.state !== 'blocked'
456
+ ? await persistedCycleEvidence(store, initial.events, cycle.id)
457
+ : null;
458
+ const existingRoleResults = current
459
+ ? await persistedCycleRoleResults(store, initial.events, cycle.id)
460
+ : [];
461
+ const advisorContext = await buildAdvisorOperatingContext({
462
+ charterPath: paths.charter,
463
+ state: initialState,
464
+ cycleId: cycle.id,
465
+ });
466
+ const evaluate = async (persist) => {
467
+ await assertCycleWorkspace();
468
+ const evidence = existingEvidence ??
469
+ (await collectOperatingEvidence({
470
+ projectRoot,
471
+ cycleId: cycle.id,
472
+ workspace,
473
+ providers: config.enabledProviders,
474
+ sensitivityCeiling: preferences.sensitivityCeiling,
475
+ budgets: {
476
+ ...config.budgets,
477
+ maxItemBytes: Math.min(config.budgets.maxBytes, 256 * 1024),
478
+ },
479
+ localRoot: input.localRoot,
480
+ now,
481
+ incremental: depth === 'standard',
482
+ persistIncremental: persist && depth === 'standard',
483
+ requiredEvidenceRefs: requiredOpenEvidenceRefs(initialState),
484
+ }));
485
+ const evidenceGaps = await unqualifiedCommercialEvidenceGaps({
486
+ cycleId: cycle.id,
487
+ evidence,
488
+ owner: config.decisionOwner,
489
+ now: timestamp,
490
+ });
491
+ const nonChair = config.enabledRoles.filter((role) => role !== 'chair');
492
+ let readiness = await evaluateEvidenceReadiness({
493
+ cycleId: cycle.id,
494
+ evidence,
495
+ enabledRoles: nonChair,
496
+ now,
497
+ });
498
+ readiness = await remapReadinessGaps(readiness, initialState);
499
+ let adapter = input.adapter ?? null;
500
+ let provider = null;
501
+ const resolveAdapter = async () => {
502
+ if (adapter)
503
+ return adapter;
504
+ if (input.offline) {
505
+ adapter = createOfflineAdvisorAdapter();
506
+ return adapter;
507
+ }
508
+ adapter = await createConfiguredStructuredAdapter(projectRoot, {
509
+ quiet: input.quiet,
510
+ });
511
+ const openPlanrConfig = await loadConfig(projectRoot);
512
+ const consent = await ensureOperatingProviderConsent({
513
+ projectRoot,
514
+ provider: configuredAdvisorProviderPolicy({
515
+ config: openPlanrConfig,
516
+ adapterId: adapter.id,
517
+ runtime: resolvedRuntime,
518
+ }),
519
+ confirmed: Boolean(input.confirmed),
520
+ persist,
521
+ now: timestamp,
522
+ });
523
+ provider = consent.manifest;
524
+ return adapter;
525
+ };
526
+ const existingByRole = new Map(existingRoleResults.map((result) => [result.roleId, result]));
527
+ const missingNonChair = readiness.roles.filter((role) => role.roleId !== 'chair' && !existingByRole.has(role.roleId));
528
+ const runnableMissingNonChair = missingNonChair.filter((role) => role.modelCallAllowed);
529
+ if (input.deferAdvisors && runnableMissingNonChair.length > 0) {
530
+ return {
531
+ preview: false,
532
+ dryRun: false,
533
+ cycle,
534
+ evidence,
535
+ readiness,
536
+ roleResults: existingRoleResults.filter((result) => result.roleId !== 'chair'),
537
+ findings: [],
538
+ decisions: [],
539
+ gaps: readinessGaps(readiness, config.decisionOwner, timestamp),
540
+ routes: [],
541
+ provider: null,
542
+ modelCalls: 0,
543
+ warnings: ['Native advisor execution is required before consolidation.'],
544
+ nativeHandoff: {
545
+ phase: 'advisors',
546
+ cycleId: cycle.id,
547
+ evidenceDigest: evidence.fingerprint,
548
+ roles: runnableMissingNonChair.map((role) => role.roleId).sort(),
549
+ },
550
+ };
551
+ }
552
+ await assertCycleWorkspace();
553
+ const first = missingNonChair.length > 0
554
+ ? await dispatchOperatingAdvisors({
555
+ cycleId: cycle.id,
556
+ evidence,
557
+ readiness: { ...readiness, roles: missingNonChair },
558
+ context: advisorContext,
559
+ adapter: await resolveAdapter(),
560
+ depth,
561
+ runtime: resolvedRuntime,
562
+ })
563
+ : { results: [], modelCalls: 0, blocked: false, skipped: [], failed: [] };
564
+ let roleResults = [
565
+ ...existingRoleResults.filter((result) => result.roleId !== 'chair'),
566
+ ...first.results,
567
+ ];
568
+ let modelCalls = first.modelCalls;
569
+ let blocked = first.blocked;
570
+ const skipped = [...first.skipped];
571
+ const failed = [...first.failed];
572
+ let consolidationEvidence = evidence;
573
+ if (config.enabledRoles.includes('chair')) {
574
+ const chairEvidence = buildChairEvidence(evidence, roleResults, timestamp);
575
+ consolidationEvidence = chairEvidence;
576
+ let chairReadiness = await evaluateEvidenceReadiness({
577
+ cycleId: cycle.id,
578
+ evidence: chairEvidence,
579
+ enabledRoles: ['chair'],
580
+ now,
581
+ });
582
+ chairReadiness = await remapReadinessGaps(chairReadiness, {
583
+ ...initialState,
584
+ dataGaps: [
585
+ ...initialState.dataGaps,
586
+ ...readiness.roles
587
+ .filter((role) => role.gapId)
588
+ .map((role) => ({ id: role.gapId, status: 'open' })),
589
+ ],
590
+ });
591
+ readiness = {
592
+ ...readiness,
593
+ roles: [...readiness.roles, ...chairReadiness.roles],
594
+ };
595
+ await assertOperatingArtifact('operating-evidence-readiness', readiness);
596
+ await assertCycleWorkspace();
597
+ const persistedChair = existingByRole.get('chair');
598
+ const runnableChair = chairReadiness.roles.filter((role) => role.modelCallAllowed && !persistedChair);
599
+ if (input.deferAdvisors && runnableChair.length > 0) {
600
+ return {
601
+ preview: false,
602
+ dryRun: false,
603
+ cycle,
604
+ evidence,
605
+ readiness,
606
+ roleResults,
607
+ findings: [],
608
+ decisions: [],
609
+ gaps: readinessGaps(readiness, config.decisionOwner, timestamp),
610
+ routes: [],
611
+ provider: null,
612
+ modelCalls,
613
+ warnings: ['Native Chair execution is required before consolidation.'],
614
+ nativeHandoff: {
615
+ phase: 'chair',
616
+ cycleId: cycle.id,
617
+ evidenceDigest: evidence.fingerprint,
618
+ roles: ['chair'],
619
+ },
620
+ };
621
+ }
622
+ const chair = persistedChair
623
+ ? {
624
+ results: [persistedChair],
625
+ modelCalls: 0,
626
+ blocked: false,
627
+ skipped: [],
628
+ failed: [],
629
+ }
630
+ : await dispatchOperatingAdvisors({
631
+ cycleId: cycle.id,
632
+ evidence: chairEvidence,
633
+ readiness: chairReadiness,
634
+ context: advisorContext,
635
+ adapter: await resolveAdapter(),
636
+ depth,
637
+ runtime: resolvedRuntime,
638
+ });
639
+ roleResults = [...roleResults, ...chair.results];
640
+ modelCalls += chair.modelCalls;
641
+ skipped.push(...chair.skipped);
642
+ failed.push(...chair.failed);
643
+ blocked ||= chair.blocked || chairReadiness.roles.some((role) => !role.modelCallAllowed);
644
+ }
645
+ const technologyRiskReady = readiness.roles.some((role) => role.roleId === 'technology-risk' && role.modelCallAllowed);
646
+ const technologyRiskFailed = failed.some((entry) => entry.roleId === 'technology-risk');
647
+ const sensitiveRouteBlocked = containsSensitiveRouteProposal(roleResults) && (!technologyRiskReady || technologyRiskFailed);
648
+ blocked ||= sensitiveRouteBlocked;
649
+ const consolidated = await consolidateOperatingResults({
650
+ cycleId: cycle.id,
651
+ results: roleResults,
652
+ evidence: consolidationEvidence,
653
+ config,
654
+ now: timestamp,
655
+ existingGapCount: initialState.dataGaps.length,
656
+ });
657
+ const failureGaps = await advisorFailureGaps({
658
+ cycleId: cycle.id,
659
+ failed,
660
+ readiness,
661
+ owner: config.decisionOwner,
662
+ now: timestamp,
663
+ });
664
+ const surfacedDedup = suppressRepeatedFindings(initialState, consolidated.findings, cycle.id);
665
+ const overflowDedup = suppressRepeatedFindings(initialState, consolidated.criticalOverflow, cycle.id);
666
+ const parkedDedup = suppressRepeatedFindings(initialState, consolidated.parked, cycle.id);
667
+ const suppressedFindings = surfacedDedup.suppressed + overflowDedup.suppressed + parkedDedup.suppressed;
668
+ const criticalOverflowCount = overflowDedup.findings.length;
669
+ blocked ||= criticalOverflowCount > 0;
670
+ const remapped = remapConsolidation({
671
+ ...initialState,
672
+ dataGaps: [
673
+ ...initialState.dataGaps,
674
+ ...readiness.roles
675
+ .filter((role) => role.gapId)
676
+ .map((role) => ({ id: role.gapId, status: 'open' })),
677
+ ],
678
+ }, [...surfacedDedup.findings, ...overflowDedup.findings], parkedDedup.findings, consolidated.decisions, [...evidenceGaps, ...consolidated.gaps, ...failureGaps]);
679
+ const warnings = [
680
+ ...evidence.warnings,
681
+ ...(evidenceGaps.length > 0
682
+ ? [
683
+ `${evidenceGaps.length} numeric commercial evidence item(s) require query and observation-window identity.`,
684
+ ]
685
+ : []),
686
+ ...skipped.map((entry) => `${entry.roleId} not evaluated: ${entry.reason}`),
687
+ ...failed.map((entry) => `${entry.roleId} failed: ${entry.message}`),
688
+ ...(sensitiveRouteBlocked
689
+ ? [
690
+ 'Sensitive payment/security/privacy/legal/tenant/destructive routing is blocked until technology-risk completes.',
691
+ ]
692
+ : []),
693
+ ...(criticalOverflowCount > 0
694
+ ? [
695
+ `${criticalOverflowCount} verified critical finding(s) exceeded a configured cap; the cycle is blocked instead of parking them.`,
696
+ ]
697
+ : []),
698
+ ...(suppressedFindings > 0
699
+ ? [
700
+ `${suppressedFindings} unchanged cross-cycle finding(s) were suppressed instead of duplicated.`,
701
+ ]
702
+ : []),
703
+ ];
704
+ if (blocked)
705
+ warnings.push('The cycle is blocked by required advisor evidence or failure.');
706
+ const reconciledFindings = reconcileCycleFindings(initial.events, cycle.id, remapped.findings);
707
+ const reconciledParked = reconcileCycleFindings(initial.events, cycle.id, remapped.parked);
708
+ return {
709
+ preview: false,
710
+ dryRun: !persist,
711
+ cycle,
712
+ evidence,
713
+ readiness,
714
+ roleResults,
715
+ findings: [...reconciledFindings, ...reconciledParked],
716
+ decisions: remapped.decisions,
717
+ gaps: [...readinessGaps(readiness, config.decisionOwner, timestamp), ...remapped.gaps],
718
+ routes: [],
719
+ provider,
720
+ modelCalls,
721
+ warnings,
722
+ };
723
+ };
724
+ if (input.dryRun)
725
+ return evaluate(false);
726
+ return withOperatingLock(projectRoot, {
727
+ projectKey: operatingProjectKey(projectRoot),
728
+ expectedEventHead: initial.eventHead,
729
+ currentEventHead: initial.eventHead,
730
+ localRoot: input.localRoot,
731
+ }, async (lock) => {
732
+ const latest = await store.replay();
733
+ if (latest.eventHead.sequence !== initial.eventHead.sequence ||
734
+ latest.eventHead.hash !== initial.eventHead.hash) {
735
+ throw new OperateError('E_OPERATE_HEAD_DIVERGED', 'Operating state changed while preparing the cycle.');
736
+ }
737
+ let head = initial.eventHead;
738
+ let started = Boolean(current);
739
+ let phase = current?.state ?? null;
740
+ const correlationId = randomUUID();
741
+ try {
742
+ await assertCycleWorkspace();
743
+ if (!current) {
744
+ const prepared = await appendLocked(store, lock, head, {
745
+ type: 'cycle.preparing',
746
+ cycleId: cycle.id,
747
+ entityId: cycle.id,
748
+ correlationId,
749
+ payload: { record: cycle },
750
+ });
751
+ started = true;
752
+ head = prepared.head;
753
+ phase = 'preparing';
754
+ }
755
+ if (phase === 'preparing' || phase === 'blocked') {
756
+ const collecting = await appendLocked(store, lock, head, {
757
+ type: 'cycle.collecting',
758
+ cycleId: cycle.id,
759
+ entityId: cycle.id,
760
+ correlationId,
761
+ payload: {},
762
+ });
763
+ head = collecting.head;
764
+ phase = 'collecting';
765
+ }
766
+ const evaluated = await evaluate(true);
767
+ await assertCycleWorkspace();
768
+ const evidence = evaluated.evidence;
769
+ if (phase === 'collecting') {
770
+ if (!existingEvidence) {
771
+ const evidenceRecord = await store.putRecord('evidence-metadata', recordContent(evidence), { correlationId, createdAt: timestamp });
772
+ const cache = new OperatingEvidenceCache(resolveOperatingPaths(projectRoot, { localRoot: input.localRoot }).evidence, preferences.sensitivityCeiling);
773
+ await cache.put(evidence.fingerprint.slice('sha256:'.length), evidence, preferences.evidenceTtlMs, now);
774
+ const evidenceEvent = await appendLocked(store, lock, head, {
775
+ type: 'evidence.collected',
776
+ cycleId: cycle.id,
777
+ entityId: cycle.id,
778
+ correlationId,
779
+ evidenceRefs: evidence.items.map((item) => item.id),
780
+ payload: {
781
+ recordDigest: evidenceRecord.digest,
782
+ sources: evidenceProjectionSources(evidence),
783
+ },
784
+ });
785
+ head = evidenceEvent.head;
786
+ }
787
+ const advising = await appendLocked(store, lock, head, {
788
+ type: 'cycle.advising',
789
+ cycleId: cycle.id,
790
+ entityId: cycle.id,
791
+ correlationId,
792
+ payload: {},
793
+ });
794
+ head = advising.head;
795
+ phase = 'advising';
796
+ }
797
+ if (phase === 'advising' && evaluated.nativeHandoff) {
798
+ const state = await store.state();
799
+ await store.writeCheckpoint(state);
800
+ await persistOperatingProjections({
801
+ projectRoot,
802
+ localRoot: input.localRoot,
803
+ state,
804
+ revalidateEventHead: async () => (await store.replay()).eventHead,
805
+ });
806
+ return { ...evaluated, state };
807
+ }
808
+ if (phase === 'advising') {
809
+ const recordedRoles = new Set(existingRoleResults.map((result) => result.roleId));
810
+ for (const result of evaluated.roleResults ?? []) {
811
+ if (recordedRoles.has(result.roleId))
812
+ continue;
813
+ const record = await store.putRecord('advisor-result', recordContent(result), {
814
+ correlationId,
815
+ createdAt: timestamp,
816
+ });
817
+ const appended = await appendLocked(store, lock, head, {
818
+ type: 'advisory.recorded',
819
+ cycleId: cycle.id,
820
+ entityId: `${cycle.id}-${result.roleId}`,
821
+ correlationId,
822
+ evidenceRefs: result.proposals.flatMap((proposal) => proposal.evidenceRefs),
823
+ payload: { recordDigest: record.digest },
824
+ });
825
+ head = appended.head;
826
+ }
827
+ const existingGapIds = new Set(initialState.dataGaps.filter((gap) => gap.cycleId === cycle.id).map((gap) => gap.id));
828
+ for (const gap of evaluated.gaps ?? []) {
829
+ if (existingGapIds.has(gap.id))
830
+ continue;
831
+ await assertOperatingArtifact('operating-data-gap', gap);
832
+ await store.putRecord('data-gap', recordContent(gap), {
833
+ correlationId,
834
+ createdAt: timestamp,
835
+ });
836
+ const appended = await appendLocked(store, lock, head, {
837
+ type: 'gap.open',
838
+ cycleId: cycle.id,
839
+ entityId: gap.id,
840
+ correlationId,
841
+ evidenceRefs: gap.evidenceRefs,
842
+ payload: { record: gap },
843
+ });
844
+ head = appended.head;
845
+ }
846
+ }
847
+ if (phase === 'advising' &&
848
+ evaluated.warnings.some((warning) => warning.includes('cycle is blocked'))) {
849
+ const blocked = await appendLocked(store, lock, head, {
850
+ type: 'cycle.blocked',
851
+ cycleId: cycle.id,
852
+ entityId: cycle.id,
853
+ correlationId,
854
+ payload: { patch: { health: 'blocked' }, warnings: evaluated.warnings },
855
+ });
856
+ head = blocked.head;
857
+ const state = await store.state();
858
+ await store.writeCheckpoint(state);
859
+ await persistOperatingProjections({
860
+ projectRoot,
861
+ localRoot: input.localRoot,
862
+ state,
863
+ revalidateEventHead: async () => (await store.replay()).eventHead,
864
+ });
865
+ return { ...evaluated, state };
866
+ }
867
+ if (phase === 'advising') {
868
+ const consolidating = await appendLocked(store, lock, head, {
869
+ type: 'cycle.consolidating',
870
+ cycleId: cycle.id,
871
+ entityId: cycle.id,
872
+ correlationId,
873
+ payload: {},
874
+ });
875
+ head = consolidating.head;
876
+ phase = 'consolidating';
877
+ }
878
+ const existingFindingIds = new Set(initialState.findings
879
+ .filter((finding) => finding.cycleId === cycle.id)
880
+ .map((finding) => finding.id));
881
+ for (const finding of evaluated.findings ?? []) {
882
+ if (existingFindingIds.has(finding.id))
883
+ continue;
884
+ await store.putRecord('finding', recordContent(finding), {
885
+ correlationId,
886
+ createdAt: timestamp,
887
+ });
888
+ const appended = await appendLocked(store, lock, head, {
889
+ type: 'finding.proposed',
890
+ cycleId: cycle.id,
891
+ entityId: finding.id,
892
+ correlationId,
893
+ evidenceRefs: finding.evidenceRefs,
894
+ payload: { record: finding },
895
+ });
896
+ head = appended.head;
897
+ }
898
+ const existingDecisionQuestions = new Set(initialState.decisions
899
+ .filter((decision) => decision.cycleId === cycle.id)
900
+ .map((decision) => String(decision.question ?? '')));
901
+ for (const decision of evaluated.decisions ?? []) {
902
+ if (existingDecisionQuestions.has(decision.question))
903
+ continue;
904
+ await store.putRecord('decision', recordContent(decision), {
905
+ correlationId,
906
+ createdAt: timestamp,
907
+ });
908
+ const appended = await appendLocked(store, lock, head, {
909
+ type: 'decision.open',
910
+ cycleId: cycle.id,
911
+ entityId: decision.id,
912
+ correlationId,
913
+ evidenceRefs: decision.evidenceRefs,
914
+ payload: { record: decision },
915
+ });
916
+ head = appended.head;
917
+ }
918
+ const routeBase = await store.state();
919
+ const providerDigest = evaluated.provider?.policyDigest ?? canonicalDigest({ provider: 'offline' });
920
+ const routes = [];
921
+ let routeOrdinal = maximumOrdinal(routeBase.routes, 'ACT');
922
+ let specOrdinal = await nextOperatingSpecOrdinal(projectRoot);
923
+ const existingRoutedFindingIds = new Set(initial.events
924
+ .filter((event) => event.type === 'route.proposed' &&
925
+ event.payload.record &&
926
+ typeof event.payload.record === 'object')
927
+ .flatMap((event) => {
928
+ const record = event.payload.record;
929
+ return (record.actions ?? [])
930
+ .map((action) => action.findingId)
931
+ .filter((findingId) => Boolean(findingId));
932
+ }));
933
+ for (const finding of (evaluated.findings ?? []).filter((candidate) => !candidate.parked && !existingRoutedFindingIds.has(candidate.id))) {
934
+ const isDev = finding.lane === 'DEV';
935
+ routes.push(await createOperatingRoutePlan({
936
+ projectRoot,
937
+ cycleId: cycle.id,
938
+ finding,
939
+ config,
940
+ workspace,
941
+ eventHead: head,
942
+ evidenceDigest: evidence.fingerprint,
943
+ providerDigest,
944
+ sequence: ++routeOrdinal,
945
+ localRoot: input.localRoot,
946
+ ...(isDev ? { specId: `SPEC-${String(specOrdinal++).padStart(3, '0')}` } : {}),
947
+ now: timestamp,
948
+ }));
949
+ }
950
+ if (routes.length > 0) {
951
+ await assertCycleWorkspace();
952
+ for (const route of routes) {
953
+ const relativePath = `.planr/operate/routes/${route.id}.json`;
954
+ const content = `${canonicalize(route)}\n`;
955
+ const transactionId = `TXN-${cycle.id}-${route.id}-proposal`;
956
+ const transactionRoot = path.join(resolveOperatingPaths(projectRoot, { localRoot: input.localRoot }).transactions, transactionId);
957
+ const manifestPath = path.join(transactionRoot, 'journal.json');
958
+ const existingBytes = await readFile(path.join(projectRoot, relativePath), 'utf8').catch((error) => {
959
+ if (error.code === 'ENOENT')
960
+ return null;
961
+ throw error;
962
+ });
963
+ const journal = existingBytes === null
964
+ ? await prepareJournalTransaction(projectRoot, {
965
+ writes: [
966
+ {
967
+ relativePath,
968
+ operation: 'create',
969
+ content,
970
+ },
971
+ ],
972
+ eventHead: head,
973
+ previewDigest: route.previewDigest,
974
+ transactionId,
975
+ localRoot: input.localRoot,
976
+ })
977
+ : {
978
+ root: transactionRoot,
979
+ manifestPath,
980
+ record: await readJournal(manifestPath),
981
+ };
982
+ if (existingBytes !== null) {
983
+ if (existingBytes !== content ||
984
+ journal.record.state !== 'committed' ||
985
+ journal.record.previewDigest !== route.previewDigest) {
986
+ throw new OperateError('E_OPERATE_TRANSACTION_INVALID', `Orphaned route proposal ${route.id} does not match its committed journal.`);
987
+ }
988
+ }
989
+ else {
990
+ await applyJournalTransaction(projectRoot, journal, {
991
+ currentEventHead: head,
992
+ revalidateEventHead: async () => (await store.replay()).eventHead,
993
+ });
994
+ }
995
+ try {
996
+ await store.putRecord('route', recordContent(route), {
997
+ correlationId,
998
+ createdAt: timestamp,
999
+ });
1000
+ const appended = await appendLocked(store, lock, head, {
1001
+ type: 'route.proposed',
1002
+ cycleId: cycle.id,
1003
+ entityId: route.id,
1004
+ correlationId,
1005
+ evidenceRefs: route.actions.flatMap((action) => action.evidenceRefs),
1006
+ payload: { record: route },
1007
+ });
1008
+ head = appended.head;
1009
+ }
1010
+ catch (error) {
1011
+ await rollbackJournalTransaction(projectRoot, journal).catch(() => undefined);
1012
+ throw error;
1013
+ }
1014
+ }
1015
+ }
1016
+ const quiet = (evaluated.findings?.length ?? 0) === 0 &&
1017
+ (evaluated.decisions?.length ?? 0) === 0 &&
1018
+ (evaluated.gaps?.length ?? 0) === 0;
1019
+ const health = quiet ? 'quiet' : evaluated.warnings.length > 0 ? 'partial' : 'normal';
1020
+ const reviewable = await appendLocked(store, lock, head, {
1021
+ type: 'cycle.reviewable',
1022
+ cycleId: cycle.id,
1023
+ entityId: cycle.id,
1024
+ correlationId,
1025
+ payload: {
1026
+ patch: {
1027
+ health,
1028
+ },
1029
+ warnings: evaluated.warnings,
1030
+ },
1031
+ });
1032
+ head = reviewable.head;
1033
+ if (quiet) {
1034
+ const closed = await appendLocked(store, lock, head, {
1035
+ type: 'cycle.closed',
1036
+ cycleId: cycle.id,
1037
+ entityId: cycle.id,
1038
+ correlationId,
1039
+ payload: {
1040
+ patch: {
1041
+ health,
1042
+ completedAt: timestamp,
1043
+ },
1044
+ },
1045
+ });
1046
+ head = closed.head;
1047
+ }
1048
+ const state = await store.state();
1049
+ await store.writeCheckpoint(state);
1050
+ await persistOperatingProjections({
1051
+ projectRoot,
1052
+ localRoot: input.localRoot,
1053
+ state,
1054
+ revalidateEventHead: async () => (await store.replay()).eventHead,
1055
+ });
1056
+ await mkdir(resolveOperatingPaths(projectRoot).cycles, {
1057
+ recursive: true,
1058
+ mode: 0o700,
1059
+ });
1060
+ return { ...evaluated, routes, state };
1061
+ }
1062
+ catch (error) {
1063
+ if (started) {
1064
+ const currentHead = await store.replay();
1065
+ try {
1066
+ const blockedByCriticalCap = error instanceof OperateError && error.code === 'E_OPERATE_CRITICAL_CAP';
1067
+ const failed = await appendLocked(store, lock, currentHead.eventHead, {
1068
+ type: blockedByCriticalCap ? 'cycle.blocked' : 'cycle.failed',
1069
+ cycleId: cycle.id,
1070
+ entityId: cycle.id,
1071
+ correlationId,
1072
+ payload: {
1073
+ patch: { health: 'blocked' },
1074
+ errorCode: error instanceof OperateError ? error.code : 'E_OPERATE_STATE_INVALID',
1075
+ ...(blockedByCriticalCap ? { details: error.details } : {}),
1076
+ },
1077
+ });
1078
+ head = failed.head;
1079
+ const failedState = await store.state();
1080
+ await store.writeCheckpoint(failedState);
1081
+ await persistOperatingProjections({
1082
+ projectRoot,
1083
+ localRoot: input.localRoot,
1084
+ state: failedState,
1085
+ revalidateEventHead: async () => (await store.replay()).eventHead,
1086
+ });
1087
+ }
1088
+ catch {
1089
+ // Preserve the original failure; integrity diagnostics will expose
1090
+ // any inability to append the terminal audit event.
1091
+ }
1092
+ }
1093
+ throw error;
1094
+ }
1095
+ });
1096
+ }
1097
+ //# sourceMappingURL=engine.js.map