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,175 @@
1
+ import { access, readdir, readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { canonicalDigest } from './canonical.js';
5
+ import { resolveOperatingPipelineRoot } from './protocol.js';
6
+ import { OperateError } from './types.js';
7
+ import { resolveContainedPath } from './workspace.js';
8
+ async function loadPortablePipelineModule() {
9
+ const root = resolveOperatingPipelineRoot();
10
+ if (!root) {
11
+ throw new OperateError('E_PIPELINE_NOT_INSTALLED', 'Operating Board routing requires the full planr-pipeline package.');
12
+ }
13
+ return import(pathToFileURL(path.join(root, 'lib', 'pipeline', 'index.mjs')).href);
14
+ }
15
+ export async function loadPipelinePoBridge() {
16
+ const module = (await loadPortablePipelineModule());
17
+ if (typeof module.preparePlan !== 'function' || typeof module.completePlan !== 'function') {
18
+ throw new OperateError('E_PIPELINE_NOT_INSTALLED', 'The installed pipeline does not expose the required PLAN engine API.');
19
+ }
20
+ return module;
21
+ }
22
+ export async function loadOperatingGeneratorBridge() {
23
+ const module = (await loadPortablePipelineModule());
24
+ if (typeof module.prepareOperatingArtifactGeneration !== 'function' ||
25
+ typeof module.renderOperatingArtifactTemplate !== 'function' ||
26
+ typeof module.startOperatingArtifactGeneration !== 'function' ||
27
+ typeof module.validateOperatingArtifactOutput !== 'function' ||
28
+ typeof module.commitOperatingArtifactGeneration !== 'function' ||
29
+ typeof module.failOperatingArtifactGeneration !== 'function' ||
30
+ typeof module.resumeOperatingArtifactGeneration !== 'function') {
31
+ throw new OperateError('E_PIPELINE_NOT_INSTALLED', 'The installed pipeline does not expose the Protocol v1.2 operating artifact generator.');
32
+ }
33
+ return module;
34
+ }
35
+ async function walkFiles(root, maximum = 2_000) {
36
+ const files = [];
37
+ const queue = [root];
38
+ while (queue.length > 0) {
39
+ const current = queue.shift();
40
+ for (const entry of await readdir(current, { withFileTypes: true }).catch(() => [])) {
41
+ if (files.length >= maximum) {
42
+ throw new OperateError('E_OPERATE_PLANNER_CONFLICT', 'Planning producer inspection exceeded its bounded file count.');
43
+ }
44
+ const candidate = path.join(current, entry.name);
45
+ if (entry.isSymbolicLink()) {
46
+ throw new OperateError('E_OPERATE_PATH_ESCAPE', 'Planning targets cannot contain symlinks.');
47
+ }
48
+ if (entry.isDirectory())
49
+ queue.push(candidate);
50
+ else if (entry.isFile())
51
+ files.push(candidate);
52
+ }
53
+ }
54
+ return files.sort();
55
+ }
56
+ export async function inspectPlanningProducer(input) {
57
+ const target = await resolveContainedPath(input.projectRoot, input.targetPath);
58
+ const present = await access(target).then(() => true, () => false);
59
+ const files = present
60
+ ? (await stat(target)).isDirectory()
61
+ ? await walkFiles(target)
62
+ : [target]
63
+ : [];
64
+ const relativeFiles = files.map((file) => path.relative(input.projectRoot, file).split(path.sep).join('/'));
65
+ const planningFiles = relativeFiles.filter((file) => /(?:^|\/)(?:stories|tasks)\/.+\.(?:md|feature)$/i.test(file));
66
+ const producers = new Set();
67
+ const provenancePath = path.join(input.projectRoot, '.planr', 'provenance.jsonl');
68
+ const provenance = await readFile(provenancePath, 'utf8').catch(() => '');
69
+ for (const line of provenance.split('\n')) {
70
+ if (!line.trim())
71
+ continue;
72
+ try {
73
+ const event = JSON.parse(line);
74
+ if (event.artifact_path &&
75
+ (event.artifact_path === input.targetPath ||
76
+ event.artifact_path.startsWith(`${input.targetPath.replace(/\/$/, '')}/`))) {
77
+ if (event.producer?.product === 'planr-pipeline' && event.producer.phase === 'po') {
78
+ producers.add('pipeline-po');
79
+ }
80
+ else if (event.producer?.product === 'openplanr') {
81
+ producers.add('openplanr');
82
+ }
83
+ }
84
+ }
85
+ catch {
86
+ // Invalid provenance is diagnosed elsewhere; it cannot prove producer ownership.
87
+ }
88
+ }
89
+ if (producers.size > 1) {
90
+ throw new OperateError('E_OPERATE_PLANNER_CONFLICT', 'The target contains provenance from more than one planning producer.', { producers: [...producers].sort(), files: relativeFiles });
91
+ }
92
+ const producer = [...producers][0];
93
+ return {
94
+ populated: planningFiles.length > 0,
95
+ ...(producer ? { producer } : {}),
96
+ files: relativeFiles,
97
+ };
98
+ }
99
+ export async function hasPipelinePoCompletionProvenance(input) {
100
+ const normalizedTarget = input.targetPath.replace(/\/$/, '');
101
+ const provenancePath = path.join(input.projectRoot, '.planr', 'provenance.jsonl');
102
+ const provenance = await readFile(provenancePath, 'utf8').catch(() => '');
103
+ for (const line of provenance.split('\n')) {
104
+ if (!line.trim())
105
+ continue;
106
+ try {
107
+ const event = JSON.parse(line);
108
+ const artifactPath = typeof event.artifact_path === 'string' ? event.artifact_path : '';
109
+ if (event.run_id === input.runId &&
110
+ event.operation === 'decomposed' &&
111
+ event.producer?.product === 'planr-pipeline' &&
112
+ event.producer.phase === 'po' &&
113
+ (artifactPath === normalizedTarget || artifactPath.startsWith(`${normalizedTarget}/`))) {
114
+ return true;
115
+ }
116
+ }
117
+ catch {
118
+ // Invalid provenance cannot prove completion.
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+ export async function assertPlanningProducer(input) {
124
+ const inspection = await inspectPlanningProducer(input);
125
+ if (inspection.populated && (!inspection.producer || inspection.producer !== input.selected)) {
126
+ throw new OperateError('E_OPERATE_PLANNER_CONFLICT', 'The target already contains planning artifacts from another or unknown producer.', {
127
+ selected: input.selected,
128
+ existing: inspection.producer ?? 'unknown',
129
+ files: inspection.files,
130
+ });
131
+ }
132
+ }
133
+ export async function preparePipelinePoHandoff(input) {
134
+ await assertPlanningProducer({
135
+ projectRoot: input.projectRoot,
136
+ targetPath: input.targetPath,
137
+ selected: 'pipeline-po',
138
+ });
139
+ const prepared = await input.bridge.preparePlan({
140
+ projectRoot: input.projectRoot,
141
+ feature: input.feature,
142
+ scaffold: false,
143
+ createStackTemplate: false,
144
+ });
145
+ return {
146
+ planningEngine: 'pipeline-po',
147
+ feature: input.feature,
148
+ runId: input.runId,
149
+ preparedDigest: canonicalDigest(prepared),
150
+ prepared,
151
+ invocation: `planr pipeline plan ${JSON.stringify(input.feature)} --runtime ${input.runtime}`,
152
+ state: 'awaiting-native-plan',
153
+ shipInvoked: false,
154
+ };
155
+ }
156
+ export async function completePipelinePoHandoff(input) {
157
+ if (input.nativePlanCompleted !== true ||
158
+ canonicalDigest(input.handoff.prepared) !== input.handoff.preparedDigest) {
159
+ throw new OperateError('E_OPERATE_PLANNER_CONFLICT', 'Pipeline PO completion requires the unchanged prepared handoff and explicit PLAN completion.');
160
+ }
161
+ const result = await input.bridge.completePlan({
162
+ projectRoot: input.projectRoot,
163
+ feature: input.handoff.feature,
164
+ runtime: input.runtime,
165
+ runId: input.handoff.runId,
166
+ });
167
+ return {
168
+ planningEngine: 'pipeline-po',
169
+ runId: input.handoff.runId,
170
+ state: 'plan-completed',
171
+ result,
172
+ shipInvoked: false,
173
+ };
174
+ }
175
+ //# sourceMappingURL=pipeline-handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline-handoff.js","sourceRoot":"","sources":["../../../src/services/operate/pipeline-handoff.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,4BAA4B,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAgC,MAAM,YAAY,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AA8FtD,KAAK,UAAU,0BAA0B;IACvC,MAAM,IAAI,GAAG,4BAA4B,EAAE,CAAC;IAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,YAAY,CACpB,0BAA0B,EAC1B,mEAAmE,CACpE,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACrF,CAAC;AAaD,MAAM,CAAC,KAAK,UAAU,oBAAoB;IACxC,MAAM,MAAM,GAAG,CAAC,MAAM,0BAA0B,EAAE,CAA8B,CAAC;IACjF,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,UAAU,IAAI,OAAO,MAAM,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QAC1F,MAAM,IAAI,YAAY,CACpB,0BAA0B,EAC1B,sEAAsE,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,MAA0B,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,4BAA4B;IAChD,MAAM,MAAM,GAAG,CAAC,MAAM,0BAA0B,EAAE,CAAsC,CAAC;IACzF,IACE,OAAO,MAAM,CAAC,kCAAkC,KAAK,UAAU;QAC/D,OAAO,MAAM,CAAC,+BAA+B,KAAK,UAAU;QAC5D,OAAO,MAAM,CAAC,gCAAgC,KAAK,UAAU;QAC7D,OAAO,MAAM,CAAC,+BAA+B,KAAK,UAAU;QAC5D,OAAO,MAAM,CAAC,iCAAiC,KAAK,UAAU;QAC9D,OAAO,MAAM,CAAC,+BAA+B,KAAK,UAAU;QAC5D,OAAO,MAAM,CAAC,iCAAiC,KAAK,UAAU,EAC9D,CAAC;QACD,MAAM,IAAI,YAAY,CACpB,0BAA0B,EAC1B,wFAAwF,CACzF,CAAC;IACJ,CAAC;IACD,OAAO,MAAkC,CAAC;AAC5C,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,IAAY,EAAE,OAAO,GAAG,KAAK;IACpD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC;IACrB,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,EAAY,CAAC;QACxC,KAAK,MAAM,KAAK,IAAI,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YACpF,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC5B,MAAM,IAAI,YAAY,CACpB,4BAA4B,EAC5B,+DAA+D,CAChE,CAAC;YACJ,CAAC;YACD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACjD,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;gBAC3B,MAAM,IAAI,YAAY,CACpB,uBAAuB,EACvB,2CAA2C,CAC5C,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;iBAC1C,IAAI,KAAK,CAAC,MAAM,EAAE;gBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,KAG7C;IAKC,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACvC,GAAG,EAAE,CAAC,IAAI,EACV,GAAG,EAAE,CAAC,KAAK,CACZ,CAAC;IACF,MAAM,KAAK,GAAG,OAAO;QACnB,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE;YAClC,CAAC,CAAC,MAAM,SAAS,CAAC,MAAM,CAAC;YACzB,CAAC,CAAC,CAAC,MAAM,CAAC;QACZ,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACvC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CACjE,CAAC;IACF,MAAM,aAAa,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAClD,iDAAiD,CAAC,IAAI,CAAC,IAAI,CAAC,CAC7D,CAAC;IAEF,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;IACrD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,kBAAkB,CAAC,CAAC;IAClF,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1E,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAG5B,CAAC;YACF,IACE,KAAK,CAAC,aAAa;gBACnB,CAAC,KAAK,CAAC,aAAa,KAAK,KAAK,CAAC,UAAU;oBACvC,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAC5E,CAAC;gBACD,IAAI,KAAK,CAAC,QAAQ,EAAE,OAAO,KAAK,gBAAgB,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;oBAClF,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;gBAC/B,CAAC;qBAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,OAAO,KAAK,WAAW,EAAE,CAAC;oBACnD,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBAC7B,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,iFAAiF;QACnF,CAAC;IACH,CAAC;IACD,IAAI,SAAS,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,YAAY,CACpB,4BAA4B,EAC5B,sEAAsE,EACtE,EAAE,SAAS,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,CAC3D,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACnC,OAAO;QACL,SAAS,EAAE,aAAa,CAAC,MAAM,GAAG,CAAC;QACnC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,KAAK,EAAE,aAAa;KACrB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iCAAiC,CAAC,KAIvD;IACC,MAAM,gBAAgB,GAAG,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7D,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,kBAAkB,CAAC,CAAC;IAClF,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC1E,KAAK,MAAM,IAAI,IAAI,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAK5B,CAAC;YACF,MAAM,YAAY,GAAG,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,IACE,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,KAAK;gBAC5B,KAAK,CAAC,SAAS,KAAK,YAAY;gBAChC,KAAK,CAAC,QAAQ,EAAE,OAAO,KAAK,gBAAgB;gBAC5C,KAAK,CAAC,QAAQ,CAAC,KAAK,KAAK,IAAI;gBAC7B,CAAC,YAAY,KAAK,gBAAgB,IAAI,YAAY,CAAC,UAAU,CAAC,GAAG,gBAAgB,GAAG,CAAC,CAAC,EACtF,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,8CAA8C;QAChD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,KAI5C;IACC,MAAM,UAAU,GAAG,MAAM,uBAAuB,CAAC,KAAK,CAAC,CAAC;IACxD,IAAI,UAAU,CAAC,SAAS,IAAI,CAAC,CAAC,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7F,MAAM,IAAI,YAAY,CACpB,4BAA4B,EAC5B,kFAAkF,EAClF;YACE,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,SAAS;YAC1C,KAAK,EAAE,UAAU,CAAC,KAAK;SACxB,CACF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAY,KAOzD;IACC,MAAM,sBAAsB,CAAC;QAC3B,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,QAAQ,EAAE,aAAa;KACxB,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC;QAC9C,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,QAAQ,EAAE,KAAK;QACf,mBAAmB,EAAE,KAAK;KAC3B,CAAC,CAAC;IACH,OAAO;QACL,cAAc,EAAE,aAAa;QAC7B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,cAAc,EAAE,eAAe,CAAC,QAAQ,CAAC;QACzC,QAAQ;QACR,UAAU,EAAE,uBAAuB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,cAAc,KAAK,CAAC,OAAO,EAAE;QAC7F,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EAAE,KAAK;KACnB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAwB,KAMtE;IAOC,IACE,KAAK,CAAC,mBAAmB,KAAK,IAAI;QAClC,eAAe,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,KAAK,CAAC,OAAO,CAAC,cAAc,EACxE,CAAC;QACD,MAAM,IAAI,YAAY,CACpB,4BAA4B,EAC5B,8FAA8F,CAC/F,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC;QAC7C,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO;QAC9B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK;KAC3B,CAAC,CAAC;IACH,OAAO;QACL,cAAc,EAAE,aAAa;QAC7B,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK;QAC1B,KAAK,EAAE,gBAAgB;QACvB,MAAM;QACN,WAAW,EAAE,KAAK;KACnB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { type OperatingEventHead, type OperatingState } from './types.js';
2
+ export type OperatingProjectionDriftStatus = 'absent' | 'current' | 'drift';
3
+ export interface OperatingProjectionDrift {
4
+ path: string;
5
+ status: OperatingProjectionDriftStatus;
6
+ expectedDigest: `sha256:${string}`;
7
+ actualDigest: `sha256:${string}` | null;
8
+ reason?: string;
9
+ }
10
+ export interface OperatingProjectionFile {
11
+ relativePath: string;
12
+ content: string;
13
+ markerName?: string;
14
+ managedContent?: string;
15
+ }
16
+ export interface OperatingProjectionPreview {
17
+ stateDigest: `sha256:${string}`;
18
+ eventHead: OperatingEventHead;
19
+ previewDigest: `sha256:${string}`;
20
+ files: OperatingProjectionFile[];
21
+ changedPaths: string[];
22
+ drift: OperatingProjectionDrift[];
23
+ }
24
+ export interface PersistOperatingProjectionResult extends OperatingProjectionPreview {
25
+ transactionId: string | null;
26
+ }
27
+ export declare function renderOperatingProjectionFiles(state: OperatingState): OperatingProjectionFile[];
28
+ export declare function inspectOperatingProjectionDrift(input: {
29
+ projectRoot: string;
30
+ state: OperatingState;
31
+ }): Promise<OperatingProjectionDrift[]>;
32
+ export declare function assertOperatingProjectionsCurrent(input: {
33
+ projectRoot: string;
34
+ state: OperatingState;
35
+ }): Promise<void>;
36
+ export declare function prepareOperatingProjectionPersistence(input: {
37
+ projectRoot: string;
38
+ state: OperatingState;
39
+ }): Promise<OperatingProjectionPreview>;
40
+ export declare function persistOperatingProjections(input: {
41
+ projectRoot: string;
42
+ state: OperatingState;
43
+ localRoot?: string;
44
+ transactionId?: string;
45
+ now?: string;
46
+ revalidateEventHead?: () => Promise<OperatingEventHead>;
47
+ }): Promise<PersistOperatingProjectionResult>;
48
+ export declare const OPERATING_PROJECTION_PATHS: {
49
+ readonly state: ".planr/operate/projections/state.json";
50
+ readonly register: ".planr/operate/projections/register.md";
51
+ readonly decisions: ".planr/operate/projections/decisions.md";
52
+ readonly dataGaps: ".planr/operate/projections/data-gaps.md";
53
+ readonly backlog: ".planr/operate/projections/backlog.md";
54
+ };
55
+ //# sourceMappingURL=projection-persistence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projection-persistence.d.ts","sourceRoot":"","sources":["../../../src/services/operate/projection-persistence.ts"],"names":[],"mappings":"AAWA,OAAO,EAAgB,KAAK,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AA4BxF,MAAM,MAAM,8BAA8B,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,8BAA8B,CAAC;IACvC,cAAc,EAAE,UAAU,MAAM,EAAE,CAAC;IACnC,YAAY,EAAE,UAAU,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,EAAE,UAAU,MAAM,EAAE,CAAC;IAChC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,aAAa,EAAE,UAAU,MAAM,EAAE,CAAC;IAClC,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,KAAK,EAAE,wBAAwB,EAAE,CAAC;CACnC;AAED,MAAM,WAAW,gCAAiC,SAAQ,0BAA0B;IAClF,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAwQD,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,cAAc,GAAG,uBAAuB,EAAE,CAY/F;AAkED,wBAAsB,+BAA+B,CAAC,KAAK,EAAE;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,cAAc,CAAC;CACvB,GAAG,OAAO,CAAC,wBAAwB,EAAE,CAAC,CAItC;AAED,wBAAsB,iCAAiC,CAAC,KAAK,EAAE;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,cAAc,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB;AAED,wBAAsB,qCAAqC,CAAC,KAAK,EAAE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,cAAc,CAAC;CACvB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAsDtC;AAED,wBAAsB,2BAA2B,CAAC,KAAK,EAAE;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,cAAc,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACzD,GAAG,OAAO,CAAC,gCAAgC,CAAC,CA6B5C;AAED,eAAO,MAAM,0BAA0B;;;;;;CAM7B,CAAC"}
@@ -0,0 +1,393 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
4
+ import { applyJournalTransaction, prepareJournalTransaction, } from './journal.js';
5
+ import { renderOperatingBrief, selectCycleState } from './projection.js';
6
+ import { assertOperatingArtifact } from './protocol.js';
7
+ import { operatingStalledItems } from './stalled-item-service.js';
8
+ import { OperateError } from './types.js';
9
+ const PROJECTION_ROOT = '.planr/operate/projections';
10
+ const STATE_PATH = `${PROJECTION_ROOT}/state.json`;
11
+ const MARKDOWN_PROJECTIONS = [
12
+ {
13
+ relativePath: `${PROJECTION_ROOT}/register.md`,
14
+ markerName: 'operate-findings-register',
15
+ render: renderFindingRegister,
16
+ },
17
+ {
18
+ relativePath: `${PROJECTION_ROOT}/decisions.md`,
19
+ markerName: 'operate-decisions-register',
20
+ render: renderDecisionRegister,
21
+ },
22
+ {
23
+ relativePath: `${PROJECTION_ROOT}/data-gaps.md`,
24
+ markerName: 'operate-data-gaps-register',
25
+ render: renderDataGapRegister,
26
+ },
27
+ {
28
+ relativePath: `${PROJECTION_ROOT}/backlog.md`,
29
+ markerName: 'operate-backlog-register',
30
+ render: renderBacklogRegister,
31
+ },
32
+ ];
33
+ const CYCLE_BRIEF_MARKER = 'operate-cycle-brief';
34
+ function value(record, key, fallback = '') {
35
+ const candidate = record[key];
36
+ if (typeof candidate === 'string' && candidate.trim()) {
37
+ return candidate.replace(/\s+/g, ' ').trim();
38
+ }
39
+ if (typeof candidate === 'number' || typeof candidate === 'boolean') {
40
+ return String(candidate);
41
+ }
42
+ return fallback;
43
+ }
44
+ function markdownCell(candidate, fallback = '—') {
45
+ const rendered = typeof candidate === 'string' || typeof candidate === 'number' || typeof candidate === 'boolean'
46
+ ? String(candidate)
47
+ : fallback;
48
+ const withoutControls = [...rendered]
49
+ .filter((character) => {
50
+ const codePoint = character.codePointAt(0) ?? 0;
51
+ return (codePoint === 9 ||
52
+ codePoint === 10 ||
53
+ codePoint === 13 ||
54
+ (codePoint >= 32 && codePoint !== 127));
55
+ })
56
+ .join('');
57
+ const normalized = withoutControls.replace(/\s+/g, ' ').trim();
58
+ return (normalized || fallback).replaceAll('\\', '\\\\').replaceAll('|', '\\|');
59
+ }
60
+ function evidenceRefs(record) {
61
+ if (!Array.isArray(record.evidenceRefs))
62
+ return '—';
63
+ const refs = [
64
+ ...new Set(record.evidenceRefs.filter((entry) => typeof entry === 'string')),
65
+ ].sort();
66
+ return refs.length > 0 ? refs.map((entry) => `\`${markdownCell(entry)}\``).join(', ') : '—';
67
+ }
68
+ function generatedHeader(state, title) {
69
+ return [
70
+ `# ${title}`,
71
+ '',
72
+ '> Generated by `planr operate` from canonical events. Edit operating state through the CLI.',
73
+ `> Event head: ${state.eventHead.sequence} / ${state.eventHead.hash ?? 'genesis'}`,
74
+ `> Projection time: ${state.generatedAt}`,
75
+ '',
76
+ ];
77
+ }
78
+ function renderFindingRegister(state) {
79
+ const findings = [...state.findings].sort((left, right) => left.id.localeCompare(right.id));
80
+ const stalledById = new Map(operatingStalledItems(state).map((item) => [item.id, item.stalledCycles]));
81
+ return [
82
+ ...generatedHeader(state, 'Operating Findings Register'),
83
+ '| Finding | Status | Lane | Owner | Severity | Score | Stalled cycles | Title | Evidence |',
84
+ '|---|---|---|---|---|---:|---:|---|---|',
85
+ ...(findings.length > 0
86
+ ? findings.map((finding) => `| ${markdownCell(finding.id)} | ${markdownCell(finding.status)} | ${markdownCell(finding.lane)} | ${markdownCell(finding.owner)} | ${markdownCell(finding.severity)} | ${markdownCell(finding.score, '0')} | ${markdownCell(stalledById.get(finding.id) ?? 0, '0')} | ${markdownCell(finding.title)} | ${evidenceRefs(finding)} |`)
87
+ : ['| — | — | — | — | — | 0 | 0 | No findings recorded. | — |']),
88
+ ].join('\n');
89
+ }
90
+ function renderDecisionRegister(state) {
91
+ const stalledById = new Map(operatingStalledItems(state).map((item) => [item.id, item.stalledCycles]));
92
+ const decisions = [...state.decisions].sort((left, right) => {
93
+ const deadlineDelta = value(left, 'deadline').localeCompare(value(right, 'deadline'));
94
+ return deadlineDelta || left.id.localeCompare(right.id);
95
+ });
96
+ return [
97
+ ...generatedHeader(state, 'Operating Decisions'),
98
+ '| Decision | Status | Owner | Deadline | Stalled cycles | Reversibility | Question | Recommendation |',
99
+ '|---|---|---|---|---:|---|---|---|',
100
+ ...(decisions.length > 0
101
+ ? decisions.map((decision) => `| ${markdownCell(decision.id)} | ${markdownCell(decision.status)} | ${markdownCell(decision.owner)} | ${markdownCell(decision.deadline)} | ${markdownCell(stalledById.get(decision.id) ?? 0, '0')} | ${markdownCell(decision.reversibility)} | ${markdownCell(decision.question)} | ${markdownCell(decision.recommendation)} |`)
102
+ : ['| — | — | — | — | 0 | — | No decisions recorded. | — |']),
103
+ ].join('\n');
104
+ }
105
+ function renderDataGapRegister(state) {
106
+ const gaps = [...state.dataGaps].sort((left, right) => left.id.localeCompare(right.id));
107
+ return [
108
+ ...generatedHeader(state, 'Operating Data Gaps'),
109
+ '| Gap | Status | Owner | Question | Reason | Unblocks |',
110
+ '|---|---|---|---|---|---|',
111
+ ...(gaps.length > 0
112
+ ? gaps.map((gap) => {
113
+ const unblocks = Array.isArray(gap.unblocks)
114
+ ? gap.unblocks
115
+ .filter((entry) => typeof entry === 'string')
116
+ .sort()
117
+ .join(', ')
118
+ : '';
119
+ return `| ${markdownCell(gap.id)} | ${markdownCell(gap.status)} | ${markdownCell(gap.owner)} | ${markdownCell(gap.question)} | ${markdownCell(gap.reason)} | ${markdownCell(unblocks)} |`;
120
+ })
121
+ : ['| — | — | — | No data gaps recorded. | — | — |']),
122
+ ].join('\n');
123
+ }
124
+ function renderBacklogRegister(state) {
125
+ const parked = state.findings
126
+ .filter((finding) => finding.parked === true)
127
+ .sort((left, right) => {
128
+ const scoreDelta = Number(right.score ?? 0) - Number(left.score ?? 0);
129
+ return scoreDelta || left.id.localeCompare(right.id);
130
+ });
131
+ return [
132
+ ...generatedHeader(state, 'Operating Backlog'),
133
+ '| Finding | Status | Owner | Score | Title | Parked reason | Evidence |',
134
+ '|---|---|---|---:|---|---|---|',
135
+ ...(parked.length > 0
136
+ ? parked.map((finding) => `| ${markdownCell(finding.id)} | ${markdownCell(finding.status)} | ${markdownCell(finding.owner)} | ${markdownCell(finding.score, '0')} | ${markdownCell(finding.title)} | ${markdownCell(finding.problem ?? finding.proposal, 'Deprioritized by the current attention cap.')} | ${evidenceRefs(finding)} |`)
137
+ : ['| — | — | — | 0 | No parked findings. | — | — |']),
138
+ ].join('\n');
139
+ }
140
+ function renderCycleBrief(state, cycleId) {
141
+ const selected = selectCycleState(state, cycleId);
142
+ const brief = renderOperatingBrief(selected);
143
+ const words = brief.split(/\s+/).filter(Boolean);
144
+ if (words.length > 900) {
145
+ throw new OperateError('E_OPERATE_PROJECTION_DRIFT', `Cycle brief ${cycleId} exceeds the 900-word projection limit.`, { cycleId, words: words.length });
146
+ }
147
+ return brief;
148
+ }
149
+ function markerExpression(markerName, edge) {
150
+ const escaped = markerName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
151
+ return edge === 'begin'
152
+ ? new RegExp(`^<!--\\s*##planr-${escaped}:begin##[^>]*-->[\\t ]*(?:\\r?\\n|$)`, 'gm')
153
+ : new RegExp(`^<!--\\s*##planr-${escaped}:end##\\s*-->[\\t ]*`, 'gm');
154
+ }
155
+ function managedSection(content, markerName) {
156
+ const beginMatches = [...content.matchAll(markerExpression(markerName, 'begin'))];
157
+ const endMatches = [...content.matchAll(markerExpression(markerName, 'end'))];
158
+ if (beginMatches.length === 0 && endMatches.length === 0)
159
+ return null;
160
+ if (beginMatches.length !== 1 || endMatches.length !== 1) {
161
+ return { content: '', malformed: true };
162
+ }
163
+ const begin = beginMatches[0];
164
+ const end = endMatches[0];
165
+ const beginEnd = (begin.index ?? 0) + begin[0].length;
166
+ const endStart = end.index ?? 0;
167
+ if (endStart < beginEnd)
168
+ return { content: '', malformed: true };
169
+ return {
170
+ content: content.slice(beginEnd, endStart).trimEnd(),
171
+ malformed: false,
172
+ };
173
+ }
174
+ function beginMarker(markerName) {
175
+ return `<!-- ##planr-${markerName}:begin## (managed by planr CLI; preserve hand-edits outside this block) -->`;
176
+ }
177
+ function endMarker(markerName) {
178
+ return `<!-- ##planr-${markerName}:end## -->`;
179
+ }
180
+ function spliceProjectionBlock(existing, markerName, managedContent) {
181
+ const beginMatches = [...existing.matchAll(markerExpression(markerName, 'begin'))];
182
+ const endMatches = [...existing.matchAll(markerExpression(markerName, 'end'))];
183
+ const block = [beginMarker(markerName), managedContent.trimEnd(), endMarker(markerName)].join('\n');
184
+ if (beginMatches.length === 0 && endMatches.length === 0) {
185
+ if (existing.length === 0)
186
+ return `${block}\n`;
187
+ const separator = existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
188
+ return `${existing}${separator}${block}\n`;
189
+ }
190
+ if (beginMatches.length !== 1 || endMatches.length !== 1) {
191
+ throw new OperateError('E_OPERATE_PROJECTION_DRIFT', `Projection markers for ${markerName} are malformed or duplicated.`, { markerName });
192
+ }
193
+ const begin = beginMatches[0];
194
+ const end = endMatches[0];
195
+ const beginStart = begin.index ?? 0;
196
+ const endStart = end.index ?? 0;
197
+ if (endStart < beginStart + begin[0].length) {
198
+ throw new OperateError('E_OPERATE_PROJECTION_DRIFT', `Projection markers for ${markerName} are out of order.`, { markerName });
199
+ }
200
+ const afterStart = endStart + end[0].length;
201
+ return `${existing.slice(0, beginStart)}${block}${existing.slice(afterStart)}`;
202
+ }
203
+ function markdownProjectionSpecs(state) {
204
+ const registers = MARKDOWN_PROJECTIONS.map((projection) => ({
205
+ relativePath: projection.relativePath,
206
+ markerName: projection.markerName,
207
+ managedContent: projection.render(state),
208
+ }));
209
+ const briefs = [...state.cycles]
210
+ .filter((cycle) => cycle.state === 'reviewable' || cycle.state === 'closed')
211
+ .sort((left, right) => left.id.localeCompare(right.id))
212
+ .map((cycle) => ({
213
+ relativePath: `.planr/operate/cycles/${cycle.id}/brief.md`,
214
+ markerName: CYCLE_BRIEF_MARKER,
215
+ managedContent: renderCycleBrief(state, cycle.id),
216
+ }));
217
+ return [...registers, ...briefs];
218
+ }
219
+ export function renderOperatingProjectionFiles(state) {
220
+ const stateFile = {
221
+ relativePath: STATE_PATH,
222
+ content: `${canonicalize(state)}\n`,
223
+ };
224
+ const markdownFiles = markdownProjectionSpecs(state).map((projection) => ({
225
+ relativePath: projection.relativePath,
226
+ markerName: projection.markerName,
227
+ managedContent: projection.managedContent,
228
+ content: spliceProjectionBlock('', projection.markerName, projection.managedContent),
229
+ }));
230
+ return [stateFile, ...markdownFiles];
231
+ }
232
+ async function inspectProjectionFile(projectRoot, expected) {
233
+ const expectedBytes = expected.markerName ? (expected.managedContent ?? '') : expected.content;
234
+ const expectedDigest = sha256Digest(expectedBytes);
235
+ const current = await readFile(path.join(projectRoot, expected.relativePath), 'utf8').catch((error) => {
236
+ if (error.code === 'ENOENT')
237
+ return null;
238
+ throw error;
239
+ });
240
+ if (current === null) {
241
+ return {
242
+ path: expected.relativePath,
243
+ status: 'absent',
244
+ expectedDigest,
245
+ actualDigest: null,
246
+ reason: 'Projection has not been generated.',
247
+ };
248
+ }
249
+ if (!expected.markerName) {
250
+ const actualDigest = sha256Digest(current);
251
+ return actualDigest === expectedDigest
252
+ ? { path: expected.relativePath, status: 'current', expectedDigest, actualDigest }
253
+ : {
254
+ path: expected.relativePath,
255
+ status: 'drift',
256
+ expectedDigest,
257
+ actualDigest,
258
+ reason: 'Canonical JSON bytes differ from event replay.',
259
+ };
260
+ }
261
+ const section = managedSection(current, expected.markerName);
262
+ if (!section) {
263
+ return {
264
+ path: expected.relativePath,
265
+ status: 'drift',
266
+ expectedDigest,
267
+ actualDigest: sha256Digest(current),
268
+ reason: 'Managed projection markers are missing.',
269
+ };
270
+ }
271
+ if (section.malformed) {
272
+ return {
273
+ path: expected.relativePath,
274
+ status: 'drift',
275
+ expectedDigest,
276
+ actualDigest: sha256Digest(current),
277
+ reason: 'Managed projection markers are malformed or duplicated.',
278
+ };
279
+ }
280
+ const actualDigest = sha256Digest(section.content);
281
+ return actualDigest === expectedDigest
282
+ ? { path: expected.relativePath, status: 'current', expectedDigest, actualDigest }
283
+ : {
284
+ path: expected.relativePath,
285
+ status: 'drift',
286
+ expectedDigest,
287
+ actualDigest,
288
+ reason: 'Generated projection rows differ from event replay.',
289
+ };
290
+ }
291
+ export async function inspectOperatingProjectionDrift(input) {
292
+ await assertOperatingArtifact('operating-state', input.state);
293
+ const expected = renderOperatingProjectionFiles(input.state);
294
+ return Promise.all(expected.map((file) => inspectProjectionFile(input.projectRoot, file)));
295
+ }
296
+ export async function assertOperatingProjectionsCurrent(input) {
297
+ const drift = await inspectOperatingProjectionDrift(input);
298
+ const mismatches = drift.filter((entry) => entry.status !== 'current');
299
+ if (mismatches.length > 0) {
300
+ throw new OperateError('E_OPERATE_PROJECTION_DRIFT', `${mismatches.length} Operating Board projection(s) differ from event replay.`, { projections: mismatches });
301
+ }
302
+ }
303
+ export async function prepareOperatingProjectionPersistence(input) {
304
+ await assertOperatingArtifact('operating-state', input.state);
305
+ const rendered = renderOperatingProjectionFiles(input.state);
306
+ const files = [];
307
+ for (const file of rendered) {
308
+ if (!file.markerName) {
309
+ files.push(file);
310
+ continue;
311
+ }
312
+ const current = await readFile(path.join(input.projectRoot, file.relativePath), 'utf8').catch((error) => {
313
+ if (error.code === 'ENOENT')
314
+ return '';
315
+ throw error;
316
+ });
317
+ files.push({
318
+ ...file,
319
+ content: spliceProjectionBlock(current, file.markerName, file.managedContent ?? ''),
320
+ });
321
+ }
322
+ const changedPaths = [];
323
+ for (const file of files) {
324
+ const current = await readFile(path.join(input.projectRoot, file.relativePath)).catch((error) => {
325
+ if (error.code === 'ENOENT')
326
+ return null;
327
+ throw error;
328
+ });
329
+ if (current === null || sha256Digest(current) !== sha256Digest(file.content)) {
330
+ changedPaths.push(file.relativePath);
331
+ }
332
+ }
333
+ const drift = await inspectOperatingProjectionDrift(input);
334
+ const stateDigest = canonicalDigest(input.state);
335
+ const previewDigest = canonicalDigest({
336
+ kind: 'operating-projection-preview',
337
+ stateDigest,
338
+ eventHead: input.state.eventHead,
339
+ writes: files
340
+ .filter((file) => changedPaths.includes(file.relativePath))
341
+ .map((file) => ({
342
+ path: file.relativePath,
343
+ digest: sha256Digest(file.content),
344
+ }))
345
+ .sort((left, right) => left.path.localeCompare(right.path)),
346
+ });
347
+ return {
348
+ stateDigest,
349
+ eventHead: structuredClone(input.state.eventHead),
350
+ previewDigest,
351
+ files,
352
+ changedPaths,
353
+ drift,
354
+ };
355
+ }
356
+ export async function persistOperatingProjections(input) {
357
+ const preview = await prepareOperatingProjectionPersistence(input);
358
+ if (preview.changedPaths.length === 0) {
359
+ return { ...preview, transactionId: null };
360
+ }
361
+ const changed = new Set(preview.changedPaths);
362
+ const writes = preview.files
363
+ .filter((file) => changed.has(file.relativePath))
364
+ .map((file) => ({
365
+ relativePath: file.relativePath,
366
+ content: file.content,
367
+ mode: '0600',
368
+ }));
369
+ const transaction = await prepareJournalTransaction(input.projectRoot, {
370
+ writes,
371
+ eventHead: preview.eventHead,
372
+ previewDigest: preview.previewDigest,
373
+ localRoot: input.localRoot,
374
+ transactionId: input.transactionId,
375
+ now: input.now,
376
+ });
377
+ await applyJournalTransaction(input.projectRoot, transaction, {
378
+ currentEventHead: preview.eventHead,
379
+ revalidateEventHead: input.revalidateEventHead,
380
+ });
381
+ return {
382
+ ...preview,
383
+ transactionId: transaction.record.transactionId,
384
+ };
385
+ }
386
+ export const OPERATING_PROJECTION_PATHS = {
387
+ state: STATE_PATH,
388
+ register: `${PROJECTION_ROOT}/register.md`,
389
+ decisions: `${PROJECTION_ROOT}/decisions.md`,
390
+ dataGaps: `${PROJECTION_ROOT}/data-gaps.md`,
391
+ backlog: `${PROJECTION_ROOT}/backlog.md`,
392
+ };
393
+ //# sourceMappingURL=projection-persistence.js.map