@hassangameryt/freeflow 0.5.0 → 0.6.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 (284) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +148 -154
  5. package/capabilities/cognitive-routing/SKILL.md +229 -0
  6. package/capabilities/context-virtualization/SKILL.md +103 -0
  7. package/capabilities/conversation-history/SKILL.md +53 -0
  8. package/command-surface.json +6 -13
  9. package/hooks/freeflow-runtime-context.mjs +35 -91
  10. package/hooks/tests/check-runtime-context-hook.sh +56 -39
  11. package/package.json +24 -16
  12. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  13. package/pi-extension/dist/cognitive-routing/config.js +236 -0
  14. package/pi-extension/dist/cognitive-routing/controller.js +862 -0
  15. package/pi-extension/dist/cognitive-routing/history.js +439 -0
  16. package/pi-extension/dist/cognitive-routing/host.js +126 -0
  17. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  18. package/pi-extension/dist/cognitive-routing/tool.js +211 -0
  19. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  20. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  21. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  22. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  23. package/pi-extension/dist/context-virtualization/state.js +57 -0
  24. package/pi-extension/dist/context-virtualization/types.js +10 -0
  25. package/pi-extension/dist/conversation-history/passages.js +66 -0
  26. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  27. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  28. package/pi-extension/dist/conversation-history/search.js +56 -0
  29. package/pi-extension/dist/conversation-history/types.js +1 -0
  30. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  31. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  32. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  33. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  34. package/pi-extension/dist/freeflow-context/types.js +12 -0
  35. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  36. package/pi-extension/dist/index.js +516 -98
  37. package/pi-extension/dist/runtime/runtime-context.js +318 -271
  38. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  39. package/pi-extension/dist/settings/settings-tui.js +78 -0
  40. package/pi-extension/dist/settings/settings-ui.js +593 -563
  41. package/pi-extension/dist/ui/text-component.js +57 -0
  42. package/pi-extension/src/cognitive-routing/commands.ts +119 -0
  43. package/pi-extension/src/cognitive-routing/config.ts +307 -0
  44. package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
  45. package/pi-extension/src/cognitive-routing/history.ts +638 -0
  46. package/pi-extension/src/cognitive-routing/host.ts +172 -0
  47. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  48. package/pi-extension/src/cognitive-routing/tool.ts +272 -0
  49. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  50. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  51. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  52. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  53. package/pi-extension/src/context-virtualization/state.ts +79 -0
  54. package/pi-extension/src/context-virtualization/types.ts +92 -0
  55. package/pi-extension/src/conversation-history/passages.ts +81 -0
  56. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  57. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  58. package/pi-extension/src/conversation-history/search.ts +79 -0
  59. package/pi-extension/src/conversation-history/types.ts +31 -0
  60. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  61. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  62. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  63. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  64. package/pi-extension/src/freeflow-context/types.ts +70 -0
  65. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  66. package/pi-extension/src/index.ts +553 -101
  67. package/pi-extension/src/runtime/runtime-context.ts +339 -281
  68. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  69. package/pi-extension/src/settings/settings-tui.ts +93 -0
  70. package/pi-extension/src/settings/settings-ui.ts +700 -613
  71. package/pi-extension/src/ui/text-component.ts +66 -0
  72. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  73. package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
  74. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
  75. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  76. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  77. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  78. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
  79. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  80. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  81. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  82. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  83. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  84. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  85. package/pi-extension/tests/integration/pi-extension.test.js +927 -2048
  86. package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
  87. package/runtime/prompts/cognitive-routing.md +9 -0
  88. package/runtime/prompts/context-virtualization.md +3 -0
  89. package/runtime/prompts/conversation-history.md +3 -0
  90. package/runtime/prompts/core.md +13 -0
  91. package/runtime/prompts/interaction-contract.md +7 -0
  92. package/runtime/prompts/skills.md +65 -0
  93. package/skills/action-selection/SKILL.md +91 -0
  94. package/skills/bypass/SKILL.md +3 -2
  95. package/skills/commit-work/SKILL.md +1 -1
  96. package/skills/commit-work/references/staging-decisions.md +1 -1
  97. package/skills/decision-gate/SKILL.md +1 -1
  98. package/skills/design-for-depth/SKILL.md +14 -33
  99. package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
  100. package/skills/design-for-depth/references/interface-design-loop.md +1 -1
  101. package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
  102. package/skills/diagnose-failure/SKILL.md +20 -14
  103. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
  104. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
  105. package/skills/discuss/SKILL.md +34 -22
  106. package/skills/discuss/references/discussion-continuity.md +71 -0
  107. package/skills/evaluate-skill/SKILL.md +5 -5
  108. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
  109. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  110. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  111. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  112. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  113. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  114. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  115. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  116. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  117. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  118. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  119. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  120. package/skills/execute-work/SKILL.md +18 -15
  121. package/skills/execute-work/references/code-practices.md +1 -1
  122. package/skills/execute-work/references/execute-work-edges.md +42 -0
  123. package/skills/finish-branch/SKILL.md +2 -2
  124. package/skills/finish-branch/references/integration-options.md +1 -1
  125. package/skills/handoff/SKILL.md +3 -3
  126. package/skills/launch-work/SKILL.md +1 -1
  127. package/skills/launch-work/references/launch-readiness.md +2 -2
  128. package/skills/migration-work/SKILL.md +2 -2
  129. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  130. package/skills/mode-contract/SKILL.md +4 -4
  131. package/skills/release-work/SKILL.md +1 -1
  132. package/skills/release-work/references/release-evidence.md +1 -1
  133. package/skills/review-artifact/SKILL.md +28 -26
  134. package/skills/review-work/SKILL.md +28 -26
  135. package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
  136. package/skills/review-work/references/security-risk-lens.md +1 -1
  137. package/skills/setup-freeflow/SKILL.md +3 -1
  138. package/skills/setup-freeflow/references/activation-contract.md +2 -2
  139. package/skills/setup-freeflow/references/host-setup.md +2 -2
  140. package/skills/simplify-code/SKILL.md +1 -1
  141. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  142. package/skills/tdd/SKILL.md +18 -38
  143. package/skills/tdd/references/test-design.md +2 -2
  144. package/skills/track-work/SKILL.md +362 -58
  145. package/skills/track-work/scripts/lib/codec.mjs +570 -0
  146. package/skills/track-work/scripts/lib/commands.mjs +568 -0
  147. package/skills/track-work/scripts/lib/contract.mjs +303 -0
  148. package/skills/track-work/scripts/lib/model.mjs +506 -0
  149. package/skills/track-work/scripts/lib/operations.mjs +965 -0
  150. package/skills/track-work/scripts/lib/record-store.mjs +264 -0
  151. package/skills/track-work/scripts/lib/result.mjs +37 -0
  152. package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
  153. package/skills/track-work/scripts/lib/schema.mjs +277 -0
  154. package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
  155. package/skills/track-work/scripts/lib/views.mjs +443 -0
  156. package/skills/track-work/scripts/lib/workspace.mjs +134 -0
  157. package/skills/track-work/scripts/working-record.mjs +122 -0
  158. package/skills/verify-work/SKILL.md +14 -2
  159. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  160. package/skills/verify-work/references/integration-evidence.md +1 -1
  161. package/skills/verify-work/references/performance-evidence.md +1 -1
  162. package/skills/workflow/SKILL.md +105 -53
  163. package/skills/workflow/references/domain-skill-composition.md +22 -42
  164. package/skills/write-plan/SKILL.md +1 -1
  165. package/skills/write-skill/SKILL.md +3 -2
  166. package/skills/write-skill/references/progressive-disclosure.md +17 -3
  167. package/capabilities/output-router/SKILL.md +0 -206
  168. package/capabilities/output-router/references/safety-policy.md +0 -40
  169. package/capabilities/output-router/references/setup.md +0 -110
  170. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  171. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  172. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  173. package/pi-extension/dist/output-router/renderers.js +0 -1023
  174. package/pi-extension/dist/output-router/router-tools.js +0 -554
  175. package/pi-extension/dist/output-router/schemas.js +0 -383
  176. package/pi-extension/dist/output-router/status.js +0 -744
  177. package/pi-extension/dist/output-router/utils.js +0 -502
  178. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  179. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  180. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  181. package/pi-extension/src/output-router/renderers.ts +0 -1108
  182. package/pi-extension/src/output-router/router-tools.ts +0 -579
  183. package/pi-extension/src/output-router/schemas.ts +0 -387
  184. package/pi-extension/src/output-router/status.ts +0 -791
  185. package/pi-extension/src/output-router/utils.ts +0 -554
  186. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  187. package/pi-extension/tests/output-router/search.test.js +0 -722
  188. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  189. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  190. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  191. package/router/dist/benchmarks/benchmarks.js +0 -979
  192. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  193. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  194. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  195. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  196. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  197. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  198. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  199. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  200. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  201. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  202. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  203. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  204. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  205. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  206. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  207. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  208. package/router/dist/config/config.d.ts +0 -103
  209. package/router/dist/config/config.js +0 -632
  210. package/router/dist/config/router-contract.d.ts +0 -10
  211. package/router/dist/config/router-contract.js +0 -54
  212. package/router/dist/config/schema.d.ts +0 -27
  213. package/router/dist/config/schema.js +0 -648
  214. package/router/dist/config/types.d.ts +0 -573
  215. package/router/dist/config/types.js +0 -41
  216. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  217. package/router/dist/evidence/bounded-evidence.js +0 -323
  218. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  219. package/router/dist/evidence/evidence-range-selector.js +0 -232
  220. package/router/dist/evidence/evidence-search.d.ts +0 -27
  221. package/router/dist/evidence/evidence-search.js +0 -490
  222. package/router/dist/evidence/evidence.d.ts +0 -38
  223. package/router/dist/evidence/evidence.js +0 -200
  224. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  225. package/router/dist/evidence/failure-contracts.js +0 -166
  226. package/router/dist/evidence/line-ranges.d.ts +0 -24
  227. package/router/dist/evidence/line-ranges.js +0 -22
  228. package/router/dist/experiments/local-index.d.ts +0 -64
  229. package/router/dist/experiments/local-index.js +0 -581
  230. package/router/dist/index.d.ts +0 -30
  231. package/router/dist/index.js +0 -32
  232. package/router/dist/local/local-traversal.d.ts +0 -18
  233. package/router/dist/local/local-traversal.js +0 -334
  234. package/router/dist/processing/engine.d.ts +0 -132
  235. package/router/dist/processing/engine.js +0 -533
  236. package/router/dist/processing/reducers.d.ts +0 -280
  237. package/router/dist/processing/reducers.js +0 -1673
  238. package/router/dist/processing/renderers.d.ts +0 -31
  239. package/router/dist/processing/renderers.js +0 -195
  240. package/router/dist/processing/scripts.d.ts +0 -91
  241. package/router/dist/processing/scripts.js +0 -386
  242. package/router/dist/repo/repo-traversal.d.ts +0 -18
  243. package/router/dist/repo/repo-traversal.js +0 -245
  244. package/router/dist/routing/observed-reducers.d.ts +0 -41
  245. package/router/dist/routing/observed-reducers.js +0 -661
  246. package/router/dist/routing/observed-routing.d.ts +0 -26
  247. package/router/dist/routing/observed-routing.js +0 -378
  248. package/router/dist/routing/parsers.d.ts +0 -16
  249. package/router/dist/routing/parsers.js +0 -521
  250. package/router/dist/routing/run-filters.d.ts +0 -60
  251. package/router/dist/routing/run-filters.js +0 -238
  252. package/router/dist/routing/run-reducers.d.ts +0 -49
  253. package/router/dist/routing/run-reducers.js +0 -122
  254. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  255. package/router/dist/sandbox/adapter-roots.js +0 -66
  256. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  257. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  258. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  259. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  260. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  261. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  262. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  263. package/router/dist/sandbox/script-sandbox.js +0 -337
  264. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  265. package/router/dist/setup/script-transform-adapters.js +0 -278
  266. package/router/dist/tools/batch.d.ts +0 -33
  267. package/router/dist/tools/batch.js +0 -586
  268. package/router/dist/tools/run.d.ts +0 -62
  269. package/router/dist/tools/run.js +0 -1304
  270. package/router/dist/tools/search.d.ts +0 -66
  271. package/router/dist/tools/search.js +0 -1364
  272. package/router/dist/transform/engine.d.ts +0 -168
  273. package/router/dist/transform/engine.js +0 -2072
  274. package/router/dist/vault/vault-index.d.ts +0 -125
  275. package/router/dist/vault/vault-index.js +0 -416
  276. package/router/dist/vault/vault.d.ts +0 -138
  277. package/router/dist/vault/vault.js +0 -630
  278. package/runtime/interaction-contract.md +0 -16
  279. package/skills/discuss/references/checkpoints.md +0 -66
  280. package/skills/execute-work/references/execution-loop.md +0 -77
  281. package/skills/track-work/references/working-record-schema.md +0 -378
  282. package/skills/workflow/references/workflow-loop.md +0 -92
  283. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
  284. /package/skills/review-artifact/references/{reviewer-prompt.md → independent-artifact-reviewer-contract.md} +0 -0
@@ -0,0 +1,965 @@
1
+ import {
2
+ HISTORICAL_SLICE_STATES,
3
+ TASK_TRANSITIONS,
4
+ SLICE_TYPES,
5
+ TASK_STATES,
6
+ assertCompleteSlice,
7
+ clone,
8
+ compactObject,
9
+ ensureArray,
10
+ ensureString,
11
+ fail,
12
+ nextId,
13
+ normalizeLineEndings,
14
+ normalizeDecision,
15
+ normalizeCheckpoint,
16
+ normalizeProposal,
17
+ normalizeSlice,
18
+ } from "./model.mjs";
19
+ import {
20
+ CHECKPOINT_RESULTS,
21
+ COMMAND_INPUT_KEYS,
22
+ CONTEXT_PATCH_FIELDS,
23
+ DECISION_CONTROLLED_FIELDS,
24
+ DECISION_ID_PATTERN,
25
+ DECISION_OPERATIONS,
26
+ EDIT_MODE_INPUT_KEYS,
27
+ hasDecisionContent,
28
+ isSingleLineTitle,
29
+ PRECISE_FORBIDDEN_FIELDS,
30
+ PRECISE_LIST_FIELDS,
31
+ PRECISE_STRING_FIELDS,
32
+ PROPOSAL_OPERATIONS,
33
+ NOTE_OPERATIONS,
34
+ SLICE_LIST_FIELDS,
35
+ UPDATE_INPUT_KEYS,
36
+ WORK_PATCH_FIELDS,
37
+ isSingleLineList,
38
+ } from "./contract.mjs";
39
+
40
+ function assertSingleLineTitle(value, path) {
41
+ if (!isSingleLineTitle(value)) fail("invalid-title", "Title must be a non-empty single-line string", { path });
42
+ }
43
+
44
+ function activeDecisionSync(data) {
45
+ delete data.currentContext.activeDecisions;
46
+ }
47
+
48
+ function findById(entities, id) {
49
+ return entities.find((entity) => entity.id === id);
50
+ }
51
+
52
+ function assertDecisionReference(value, field, path) {
53
+ if (value === undefined || value === "") return;
54
+ if (typeof value !== "string" || !DECISION_ID_PATTERN.test(value))
55
+ fail("invalid-decision-reference", `Decision ${field} requires a D-NNN string`, { path });
56
+ }
57
+
58
+ function assertDecisionReferences(change, path) {
59
+ assertDecisionReference(change.supersedes, "supersedes", `${path}.supersedes`);
60
+ assertDecisionReference(change.supersededBy, "supersededBy", `${path}.supersededBy`);
61
+ if (change.replacement) {
62
+ assertDecisionReference(change.replacement.supersedes, "supersedes", `${path}.replacement.supersedes`);
63
+ assertDecisionReference(change.replacement.supersededBy, "supersededBy", `${path}.replacement.supersededBy`);
64
+ }
65
+ }
66
+
67
+ function assertKnownCommandInput(input, command) {
68
+ for (const key of Object.keys(input))
69
+ if (!COMMAND_INPUT_KEYS[command]?.has(key))
70
+ fail("unknown-input-field", `Unknown ${command} input field: ${key}`, { path: `${command}.${key}` });
71
+ }
72
+
73
+ function assertSliceListInputs(input, path) {
74
+ for (const field of SLICE_LIST_FIELDS) {
75
+ if (input[field] === undefined) continue;
76
+ if (!Array.isArray(input[field]) || input[field].some((item) => typeof item !== "string"))
77
+ fail("invalid-input-type", `Field requires an array of strings: ${path}.${field}`, {
78
+ path: `${path}.${field}`,
79
+ expected: "string[]",
80
+ });
81
+ if (!isSingleLineList(input[field]))
82
+ fail("invalid-list-member", `List members must be single-line strings: ${path}.${field}`, {
83
+ path: `${path}.${field}`,
84
+ });
85
+ }
86
+ }
87
+
88
+ function assertRequiredText(value, code, message, path) {
89
+ if (typeof value !== "string" || !value.trim()) fail(code, message, { path });
90
+ }
91
+
92
+ function assertDecisionContent(decision, path) {
93
+ if (!hasDecisionContent(decision))
94
+ fail("missing-decision-content", "Decision requires decision, rationale, or consequences", { path });
95
+ }
96
+
97
+ function assertProposalContent(proposal, path) {
98
+ assertRequiredText(
99
+ proposal.intendedResult,
100
+ "missing-proposal-content",
101
+ "Proposal intended result is required",
102
+ `${path}.intendedResult`,
103
+ );
104
+ assertRequiredText(
105
+ proposal.expectedEvidence,
106
+ "missing-proposal-content",
107
+ "Proposal expected evidence is required",
108
+ `${path}.expectedEvidence`,
109
+ );
110
+ }
111
+
112
+ function assertNoCallerControlledDecisionFields(change, path, allowed = new Set()) {
113
+ for (const field of DECISION_CONTROLLED_FIELDS)
114
+ if (Object.hasOwn(change, field) && !allowed.has(field))
115
+ fail("caller-controlled-decision-field", `Decision field is controlled by its lifecycle: ${field}`, {
116
+ path: `${path}.${field}`,
117
+ });
118
+ }
119
+
120
+ function patchObject(target, patch) {
121
+ for (const [key, value] of Object.entries(patch ?? {})) {
122
+ if (value !== undefined && key !== "currentSlice" && key !== "taskState") target[key] = clone(value);
123
+ }
124
+ }
125
+
126
+ function applyTaskState(data, input) {
127
+ if (input.taskState === undefined) return;
128
+ const next = input.taskState;
129
+ if (!TASK_STATES.has(next)) fail("invalid-task-state", `Invalid task state: ${next}`);
130
+ const current = data.taskState;
131
+ const authority = input.taskStateAuthority ?? input.authoritySource ?? input.userDirection;
132
+ if (current !== next && !authority)
133
+ fail(
134
+ "missing-authority",
135
+ "Explicit task-state changes require taskStateAuthority, authoritySource, or userDirection",
136
+ );
137
+ if (!TASK_TRANSITIONS.get(current)?.has(next))
138
+ fail("invalid-transition", `Task state transition ${current} → ${next} is not allowed`);
139
+ if ((current === "Completed" || current === "Abandoned") && next === "Active" && data.currentWork.currentSlice)
140
+ fail("invalid-transition", "A reopened terminal task cannot retain a Current Slice");
141
+ if ((next === "Completed" || next === "Abandoned") && data.currentWork.currentSlice)
142
+ fail("terminal-current-slice", "Clear the Current Slice before setting a terminal task state");
143
+ data.taskState = next;
144
+ }
145
+
146
+ function assertUpdateValue(value, type, path) {
147
+ if (type === "string" && typeof value !== "string")
148
+ fail("invalid-input-type", `Field requires a string: ${path}`, { path, expected: "string" });
149
+ if (type === "array") {
150
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
151
+ fail("invalid-input-type", `Field requires an array of strings: ${path}`, { path, expected: "string[]" });
152
+ if (!isSingleLineList(value))
153
+ fail("invalid-list-member", `List members must be single-line strings: ${path}`, { path });
154
+ }
155
+ }
156
+
157
+ function assertUpdatePatch(patch, fields, path) {
158
+ if (patch === undefined) return {};
159
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
160
+ fail("invalid-input-type", `Patch requires an object: ${path}`, { path, expected: "object" });
161
+ for (const [key, value] of Object.entries(patch)) {
162
+ const type = fields[key];
163
+ if (!type) fail("unknown-input-field", `Unknown update field: ${key}`, { path: `${path}.${key}` });
164
+ if (type === "forbidden")
165
+ fail("invalid-operation", "Use start, block, resume, or close to change Current Slice ownership", {
166
+ path: `${path}.${key}`,
167
+ });
168
+ assertUpdateValue(value, type, `${path}.${key}`);
169
+ }
170
+ return patch;
171
+ }
172
+
173
+ function applyContextPatch(data, input) {
174
+ const contextPatch = { ...assertUpdatePatch(input.currentContext, CONTEXT_PATCH_FIELDS, "input.currentContext") };
175
+ const aliases = {
176
+ goal: input.goal,
177
+ whatDefinesTask: input.whatDefinesTask,
178
+ settled: input.settled,
179
+ tentative: input.tentative,
180
+ open: input.open,
181
+ currentDirection: input.currentDirection,
182
+ boundaries: input.boundaries,
183
+ };
184
+ for (const [key, value] of Object.entries(aliases)) {
185
+ if (value !== undefined) {
186
+ assertUpdateValue(value, CONTEXT_PATCH_FIELDS[key], `input.${key}`);
187
+ contextPatch[key] = value;
188
+ }
189
+ }
190
+ patchObject(data.currentContext, contextPatch);
191
+
192
+ const currentWorkPatch = assertUpdatePatch(input.currentWork, WORK_PATCH_FIELDS, "input.currentWork");
193
+ patchObject(data.currentWork, currentWorkPatch);
194
+
195
+ const workAliases = {
196
+ route: input.currentRoute ?? input.route,
197
+ nextAction: input.nextAction ?? input.nextUsefulAction,
198
+ blockers: input.blockers,
199
+ upcomingCheckpoints: input.upcomingCheckpoints,
200
+ };
201
+ for (const [key, value] of Object.entries(workAliases)) {
202
+ if (value !== undefined) {
203
+ assertUpdateValue(value, WORK_PATCH_FIELDS[key], `input.${key}`);
204
+ data.currentWork[key] = clone(value);
205
+ }
206
+ }
207
+ }
208
+
209
+ function applyDecisionChange(data, input) {
210
+ if (Array.isArray(input.decisions)) {
211
+ for (const decision of input.decisions) applyDecisionChange(data, { decision });
212
+ return;
213
+ }
214
+ const change = input.decision ?? (input.decisions && !Array.isArray(input.decisions) ? input.decisions : null);
215
+ if (!change) return;
216
+ const operation = change.operation ?? change.op ?? "add";
217
+ if (!DECISION_OPERATIONS.has(operation))
218
+ fail("unsupported-decision-operation", `Unsupported decision operation: ${operation}`, {
219
+ path: "decision.operation",
220
+ });
221
+ assertDecisionReferences(change, "decision");
222
+ assertNoCallerControlledDecisionFields(
223
+ change,
224
+ "decision",
225
+ operation === "supersede" ? new Set(["supersededBy"]) : undefined,
226
+ );
227
+ if (operation === "add") {
228
+ if (change.id !== undefined) fail("caller-supplied-id", "The script assigns decision IDs");
229
+ assertSingleLineTitle(change.title ?? change.name ?? "Untitled decision", "decision.title");
230
+ assertDecisionContent(change, "decision");
231
+ const decision = normalizeDecision(change, nextId(data, "D"));
232
+ if (findById(data.history.decisions, decision.id))
233
+ fail("duplicate-id", `Decision ID already exists: ${decision.id}`);
234
+ data.history.decisions.push(decision);
235
+ } else {
236
+ const existing = findById(data.history.decisions, change.id);
237
+ if (!existing) fail("missing-entity", `Decision does not exist: ${change.id}`);
238
+ if (operation === "remove" || operation === "retire") {
239
+ existing.state = "Retired";
240
+ } else if (operation === "supersede") {
241
+ let replacementId = change.supersededBy;
242
+ let replacement = replacementId ? findById(data.history.decisions, replacementId) : null;
243
+ if (change.replacement) {
244
+ if (change.replacement.id !== undefined) fail("caller-supplied-id", "The script assigns decision IDs");
245
+ assertNoCallerControlledDecisionFields(change.replacement, "decision.replacement");
246
+ assertSingleLineTitle(
247
+ change.replacement.title ?? change.replacement.name ?? "Untitled decision",
248
+ "decision.replacement.title",
249
+ );
250
+ assertDecisionContent(change.replacement, "decision.replacement");
251
+ replacement = normalizeDecision(change.replacement, nextId(data, "D"));
252
+ replacementId = replacement.id;
253
+ data.history.decisions.push(replacement);
254
+ }
255
+ if (!replacementId) fail("missing-superseded-by", "Supersede requires supersededBy or replacement");
256
+ if (replacementId === existing.id) fail("invalid-supersession", "A decision cannot supersede itself");
257
+ if (!replacement) fail("missing-entity", `Replacement decision does not exist: ${replacementId}`);
258
+ existing.state = "Superseded";
259
+ existing.supersededBy = replacementId;
260
+ replacement.supersedes = existing.id;
261
+ } else {
262
+ const updated = normalizeDecision({ ...existing, ...change }, existing.id);
263
+ assertDecisionContent(updated, "decision");
264
+ Object.assign(existing, updated);
265
+ }
266
+ }
267
+ activeDecisionSync(data);
268
+ }
269
+
270
+ function applyProposalChange(data, input) {
271
+ const change = input.proposal ?? (input.proposals && !Array.isArray(input.proposals) ? input.proposals : null);
272
+ if (!change && !Array.isArray(input.proposals)) return;
273
+ if (Array.isArray(input.proposals)) {
274
+ data.proposals = input.proposals.map(normalizeProposal);
275
+ return;
276
+ }
277
+ const operation = change.operation ?? change.op ?? "add";
278
+ if (!PROPOSAL_OPERATIONS.has(operation))
279
+ fail("unsupported-proposal-operation", `Unsupported proposal operation: ${operation}`, {
280
+ path: "proposal.operation",
281
+ });
282
+ assertSliceListInputs(change, "proposal");
283
+ const title = change.title ?? change.name;
284
+ const index = data.proposals.findIndex((proposal) => proposal.title === title);
285
+ if (operation === "add") {
286
+ if (index >= 0) fail("duplicate-proposal", `Proposal already exists: ${title}`);
287
+ assertSingleLineTitle(title, "proposal.title");
288
+ const proposal = normalizeProposal(change);
289
+ if (!proposal.title) fail("missing-proposal-title", "Proposal title is required");
290
+ assertProposalContent(proposal, "proposal");
291
+ data.proposals.push(proposal);
292
+ } else if (index < 0) fail("missing-proposal", `Proposal does not exist: ${title}`);
293
+ else if (operation === "remove") data.proposals.splice(index, 1);
294
+ else data.proposals[index] = normalizeProposal({ ...data.proposals[index], ...change });
295
+ }
296
+
297
+ function applyNoteChange(data, input) {
298
+ const change = input.note ?? (input.notes && !Array.isArray(input.notes) ? input.notes : null);
299
+ if (!change && !Array.isArray(input.notes)) return;
300
+ if (Array.isArray(input.notes)) {
301
+ data.notes = clone(input.notes);
302
+ return;
303
+ }
304
+ const operation = change.operation ?? change.op ?? "add";
305
+ if (!NOTE_OPERATIONS.has(operation))
306
+ fail("unsupported-note-operation", `Unsupported Note operation: ${operation}`, {
307
+ path: "note.operation",
308
+ });
309
+ const title = change.title;
310
+ const index = data.notes.findIndex((note) => note.title === title);
311
+ if (operation === "add") {
312
+ if (index >= 0) fail("duplicate-note", `Note already exists: ${title}`);
313
+ assertSingleLineTitle(title, "note.title");
314
+ data.notes.push({ title, source: ensureString(change.source), body: ensureString(change.body) });
315
+ } else if (index < 0) fail("missing-note", `Note does not exist: ${title}`);
316
+ else if (operation === "remove") data.notes.splice(index, 1);
317
+ else {
318
+ if (change.source !== undefined && typeof change.source !== "string")
319
+ fail("invalid-input-type", "Note source requires a string", { path: "note.source" });
320
+ if (change.body !== undefined && typeof change.body !== "string")
321
+ fail("invalid-input-type", "Note body requires a string", { path: "note.body" });
322
+ data.notes[index] = {
323
+ title,
324
+ source: change.source === undefined ? data.notes[index].source : change.source,
325
+ body: change.body === undefined ? data.notes[index].body : change.body,
326
+ };
327
+ }
328
+ }
329
+
330
+ function applyCurrentSlicePatch(data, input) {
331
+ const patch = input.currentSlice;
332
+ if (patch === undefined) return;
333
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
334
+ fail("invalid-input-type", "currentSlice patch requires an object");
335
+ if (!data.currentWork.currentSlice) fail("invalid-operation", "Use start to create a Current Slice");
336
+ assertSliceListInputs(patch, "currentSlice");
337
+ for (const [field, value] of Object.entries(patch)) {
338
+ if (field === "id") fail("caller-supplied-id", "Current Slice IDs are script-owned");
339
+ if (field === "state") {
340
+ if (value !== data.currentWork.currentSlice.state)
341
+ fail("invalid-transition", "Use block or resume for Current Slice state transitions");
342
+ continue;
343
+ }
344
+ assertPreciseValue(field, value, `currentSlice.${field}`);
345
+ }
346
+ if (patch.state && patch.state !== data.currentWork.currentSlice.state)
347
+ fail("invalid-transition", "Use block or resume for Current Slice state transitions");
348
+ const current = data.currentWork.currentSlice;
349
+ const normalized = normalizeSlice({ ...current, ...patch, id: current.id }, current.id);
350
+ normalized.id = current.id;
351
+ normalized.state = current.state;
352
+ assertCompleteSlice(normalized, "update");
353
+ data.currentWork.currentSlice = normalized;
354
+ }
355
+
356
+ function preciseFieldType(field) {
357
+ if (PRECISE_LIST_FIELDS.has(field)) return "array";
358
+ if (PRECISE_STRING_FIELDS.has(field)) return "string";
359
+ if (field === "blocker") return "object-or-string";
360
+ return null;
361
+ }
362
+
363
+ function assertPreciseValue(field, value, path) {
364
+ if (PRECISE_FORBIDDEN_FIELDS.has(field))
365
+ fail("immutable-edit-field", `Field ${field} is controlled by a dedicated transition`, { path });
366
+ const type = preciseFieldType(field);
367
+ if (!type) fail("unknown-edit-field", `Unknown editable field: ${field}`, { path });
368
+ if (type === "string" && typeof value !== "string")
369
+ fail("invalid-edit-type", `Field ${field} requires a string`, { path, expected: "string" });
370
+ if (type === "array" && (!Array.isArray(value) || value.some((item) => typeof item !== "string")))
371
+ fail("invalid-edit-type", `Field ${field} requires an array of strings`, { path, expected: "string[]" });
372
+ if (field === "type" && !SLICE_TYPES.has(value)) fail("invalid-slice-type", `Invalid slice type: ${value}`, { path });
373
+ if (
374
+ field === "blocker" &&
375
+ value !== null &&
376
+ typeof value !== "string" &&
377
+ (typeof value !== "object" || Array.isArray(value))
378
+ )
379
+ fail("invalid-edit-type", "Field blocker requires an object, string, or null", { path });
380
+ }
381
+
382
+ function entityCollections(data, kind) {
383
+ if (kind === "proposal") return [{ collection: data.proposals, label: "proposals" }];
384
+ if (kind === "decision") return [{ collection: data.history.decisions, label: "history.decisions" }];
385
+ if (kind === "checkpoint") return [{ collection: data.history.checkpoints, label: "history.checkpoints" }];
386
+ if (kind === "note") return [{ collection: data.notes, label: "notes" }];
387
+ if (kind === "slice")
388
+ return [{ collection: data.history.slices, label: "history.slices" }].concat(
389
+ data.currentWork.currentSlice ? [{ collection: [data.currentWork.currentSlice], label: "currentSlice" }] : [],
390
+ );
391
+ if (kind === "currentSlice")
392
+ return data.currentWork.currentSlice
393
+ ? [{ collection: [data.currentWork.currentSlice], label: "currentSlice" }]
394
+ : [];
395
+ return null;
396
+ }
397
+
398
+ function selectPreciseEntity(data, target, path) {
399
+ if (!target || typeof target !== "object" || Array.isArray(target))
400
+ fail("invalid-edit-target", "Entity edits require an object target", { path });
401
+ const kind = target.kind;
402
+ const collections = entityCollections(data, kind);
403
+ if (!collections) fail("invalid-edit-target", `Unknown entity kind: ${kind}`, { path });
404
+ if (kind === "currentSlice" && !target.id && !target.title)
405
+ return { entity: collections[0]?.collection[0], label: "currentSlice" };
406
+ const matches = [];
407
+ for (const { collection, label } of collections) {
408
+ for (const entity of collection) {
409
+ if (
410
+ (target.id !== undefined && entity.id === target.id) ||
411
+ (target.title !== undefined && entity.title === target.title)
412
+ )
413
+ matches.push({ entity, label });
414
+ }
415
+ }
416
+ if (!matches.length) fail("missing-entity", `No ${kind} matched the requested selector`, { path, target });
417
+ if (matches.length > 1)
418
+ fail("ambiguous-entity", `Entity selector matched multiple ${kind} entities`, { path, target });
419
+ return matches[0];
420
+ }
421
+
422
+ function fieldPathTarget(data, target, path) {
423
+ if (typeof target !== "string") return null;
424
+ const parts = target.split(".");
425
+ if (parts.length !== 2) fail("invalid-edit-target", `Field target must use section.field: ${target}`, { path });
426
+ const [section, field] = parts;
427
+ const owners = {
428
+ currentContext: data.currentContext,
429
+ currentWork: data.currentWork,
430
+ currentSlice: data.currentWork.currentSlice,
431
+ };
432
+ const owner = owners[section];
433
+ if (!owner) fail("invalid-edit-target", `Unknown field target section: ${section}`, { path });
434
+ if (section === "currentSlice" && !owner) fail("missing-entity", "There is no Current Slice to edit", { path });
435
+ return { owner, field, path: target };
436
+ }
437
+
438
+ function setPreciseField(owner, field, value, path) {
439
+ assertPreciseValue(field, value, path);
440
+ owner[field] = clone(value);
441
+ }
442
+
443
+ function clearPreciseField(owner, field, path) {
444
+ if (PRECISE_FORBIDDEN_FIELDS.has(field))
445
+ fail("immutable-edit-field", `Field ${field} is controlled by a dedicated transition`, { path });
446
+ if (!preciseFieldType(field)) fail("unknown-edit-field", `Unknown editable field: ${field}`, { path });
447
+ delete owner[field];
448
+ }
449
+
450
+ function replacePreciseText(owner, field, replacement, path) {
451
+ if (
452
+ !replacement ||
453
+ typeof replacement !== "object" ||
454
+ typeof replacement.old !== "string" ||
455
+ typeof replacement.new !== "string"
456
+ )
457
+ fail("invalid-edit-type", "replaceText requires { old, new } strings", { path });
458
+ const current = owner[field];
459
+ if (typeof current !== "string") fail("invalid-edit-type", `replaceText requires a string field: ${field}`, { path });
460
+ const oldText = normalizeLineEndings(replacement.old);
461
+ const newText = normalizeLineEndings(replacement.new);
462
+ const matches = current.split(oldText).length - 1;
463
+ if (matches !== 1)
464
+ fail("ambiguous-text-replacement", `Expected exactly one match in ${field}, found ${matches}`, { path });
465
+ owner[field] = current.replace(oldText, newText);
466
+ }
467
+
468
+ function applyListChange(owner, operation, field, values, path) {
469
+ if (!PRECISE_LIST_FIELDS.has(field)) fail("invalid-list-field", `Field ${field} is not an editable list`, { path });
470
+ if (!Array.isArray(values) || values.some((item) => typeof item !== "string"))
471
+ fail("invalid-edit-type", `List operation ${operation} requires an array of strings`, { path });
472
+ if (!isSingleLineList(values)) fail("invalid-list-member", "List members must be single-line strings", { path });
473
+ const current = Array.isArray(owner[field]) ? [...owner[field]] : [];
474
+ if (operation === "add") {
475
+ for (const value of values) {
476
+ if (current.includes(value)) fail("duplicate-list-member", `List ${field} already contains: ${value}`, { path });
477
+ current.push(value);
478
+ }
479
+ } else {
480
+ for (const value of values) {
481
+ const index = current.indexOf(value);
482
+ if (index < 0) fail("missing-list-member", `List ${field} does not contain: ${value}`, { path });
483
+ current.splice(index, 1);
484
+ }
485
+ }
486
+ owner[field] = current;
487
+ }
488
+
489
+ function applyFieldMap(owner, fields, operation, path) {
490
+ if (fields === undefined) return;
491
+ if (!fields || typeof fields !== "object" || Array.isArray(fields))
492
+ fail("invalid-edit-type", `${operation} must be an object of fields`, { path });
493
+ for (const [field, value] of Object.entries(fields)) {
494
+ const fieldPath = `${path}.${field}`;
495
+ if (operation === "set") setPreciseField(owner, field, value, fieldPath);
496
+ else if (operation === "clear") clearPreciseField(owner, field, fieldPath);
497
+ else if (operation === "replaceText") replacePreciseText(owner, field, value, fieldPath);
498
+ else applyListChange(owner, operation, field, value, fieldPath);
499
+ }
500
+ }
501
+
502
+ function moveProposal(data, title, destination, direction, path) {
503
+ const from = data.proposals.findIndex((proposal) => proposal.title === title);
504
+ if (from < 0) fail("missing-proposal", `Proposal does not exist: ${title}`, { path });
505
+ const to = data.proposals.findIndex((proposal) => proposal.title === destination);
506
+ if (to < 0) fail("missing-proposal", `Destination proposal does not exist: ${destination}`, { path });
507
+ const [proposal] = data.proposals.splice(from, 1);
508
+ const adjusted = from < to ? to - 1 : to;
509
+ data.proposals.splice(direction === "before" ? adjusted : adjusted + 1, 0, proposal);
510
+ }
511
+
512
+ function applyPreciseEntityEdit(data, edit, path) {
513
+ const allowed = new Set([
514
+ "target",
515
+ "set",
516
+ "clear",
517
+ "replaceText",
518
+ "add",
519
+ "remove",
520
+ "rename",
521
+ "moveBefore",
522
+ "moveAfter",
523
+ ]);
524
+ for (const key of Object.keys(edit))
525
+ if (!allowed.has(key)) fail("unknown-edit-operation", `Unknown edit operation: ${key}`, { path });
526
+ const selected = selectPreciseEntity(data, edit.target, `${path}.target`);
527
+ const entity = selected.entity;
528
+ if (!entity) fail("missing-entity", "There is no Current Slice to edit", { path });
529
+ if (edit.clear !== undefined && !Array.isArray(edit.clear))
530
+ fail("invalid-edit-type", "clear requires an array of field names", { path: `${path}.clear` });
531
+ if (edit.rename !== undefined) {
532
+ assertSingleLineTitle(edit.rename, `${path}.rename`);
533
+ entity.title = edit.rename;
534
+ }
535
+ applyFieldMap(entity, edit.set, "set", `${path}.set`);
536
+ if (Array.isArray(edit.clear)) for (const field of edit.clear) clearPreciseField(entity, field, `${path}.clear`);
537
+ applyFieldMap(entity, edit.replaceText, "replaceText", `${path}.replaceText`);
538
+ applyFieldMap(entity, edit.add, "add", `${path}.add`);
539
+ applyFieldMap(entity, edit.remove, "remove", `${path}.remove`);
540
+ if (edit.moveBefore !== undefined || edit.moveAfter !== undefined) {
541
+ if (edit.target.kind !== "proposal") fail("invalid-edit-operation", "Only proposals can be reordered", { path });
542
+ moveProposal(
543
+ data,
544
+ entity.title,
545
+ edit.moveBefore ?? edit.moveAfter,
546
+ edit.moveBefore === undefined ? "after" : "before",
547
+ path,
548
+ );
549
+ }
550
+ }
551
+
552
+ function applyPreciseFieldEdit(data, edit, path) {
553
+ const allowed = new Set(["target", "set", "clear", "replaceText"]);
554
+ for (const key of Object.keys(edit))
555
+ if (!allowed.has(key)) fail("unknown-edit-operation", `Unknown edit operation: ${key}`, { path });
556
+ const target = fieldPathTarget(data, edit.target, `${path}.target`);
557
+ if (!target) fail("invalid-edit-target", "Unsupported edit target", { path });
558
+ const operations = [edit.set !== undefined, edit.clear !== undefined, edit.replaceText !== undefined].filter(Boolean);
559
+ if (operations.length > 1) fail("multiple-edit-operations", "A field edit must choose one operation", { path });
560
+ if (edit.clear !== undefined && edit.clear !== true)
561
+ fail("invalid-edit-type", "clear requires the boolean true", { path: `${path}.clear` });
562
+ if (edit.set !== undefined) {
563
+ if (typeof edit.set === "object" && !Array.isArray(edit.set))
564
+ fail("invalid-edit-type", "Field target set requires one scalar value", { path });
565
+ setPreciseField(target.owner, target.field, edit.set, `${path}.set`);
566
+ }
567
+ if (edit.clear === true) clearPreciseField(target.owner, target.field, `${path}.clear`);
568
+ if (edit.replaceText !== undefined)
569
+ replacePreciseText(target.owner, target.field, edit.replaceText, `${path}.replaceText`);
570
+ }
571
+
572
+ function applyPreciseCollectionEdit(data, edit, path) {
573
+ const affectedIds = [];
574
+ const allowed = new Set(["target", "addEntity", "removeEntity"]);
575
+ for (const key of Object.keys(edit))
576
+ if (!allowed.has(key)) fail("unknown-edit-operation", `Unknown edit operation: ${key}`, { path });
577
+ const hasAdd = edit.addEntity !== undefined;
578
+ const hasRemove = edit.removeEntity !== undefined;
579
+ if (hasAdd === hasRemove)
580
+ fail("multiple-edit-operations", "A collection edit must choose exactly one operation", { path });
581
+ const collectionName = edit.target?.kind ?? edit.target?.collection;
582
+ if (
583
+ collectionName !== "proposals" &&
584
+ collectionName !== "decisions" &&
585
+ collectionName !== "checkpoints" &&
586
+ collectionName !== "notes"
587
+ )
588
+ fail("invalid-edit-target", `Unknown editable collection: ${collectionName}`, { path });
589
+ const collection =
590
+ collectionName === "proposals"
591
+ ? data.proposals
592
+ : collectionName === "decisions"
593
+ ? data.history.decisions
594
+ : collectionName === "checkpoints"
595
+ ? data.history.checkpoints
596
+ : data.notes;
597
+ if (edit.addEntity !== undefined) {
598
+ const value = edit.addEntity;
599
+ if (!value || typeof value !== "object" || Array.isArray(value))
600
+ fail("invalid-edit-type", "addEntity requires an object", { path });
601
+ if (collectionName === "proposals") {
602
+ assertSingleLineTitle(value.title, `${path}.addEntity.title`);
603
+ if (collection.some((item) => item.title === value.title))
604
+ fail("duplicate-proposal", `Proposal already exists: ${value.title}`, { path });
605
+ if (!SLICE_TYPES.has(value.type)) fail("invalid-proposal-type", `Invalid proposal type: ${value.type}`, { path });
606
+ assertPreciseValue("intendedResult", value.intendedResult, `${path}.addEntity.intendedResult`);
607
+ assertPreciseValue("expectedEvidence", value.expectedEvidence, `${path}.addEntity.expectedEvidence`);
608
+ assertSliceListInputs(value, `${path}.addEntity`);
609
+ const proposal = normalizeProposal(value);
610
+ assertProposalContent(proposal, `${path}.addEntity`);
611
+ collection.push(proposal);
612
+ } else if (collectionName === "decisions") {
613
+ if (value.id !== undefined) fail("caller-supplied-id", "The script assigns decision IDs", { path });
614
+ assertNoCallerControlledDecisionFields(value, `${path}.addEntity`);
615
+ assertSingleLineTitle(value.title, `${path}.addEntity.title`);
616
+ assertDecisionContent(value, `${path}.addEntity`);
617
+ assertDecisionReferences(value, `${path}.addEntity`);
618
+ const decision = normalizeDecision(value, nextId(data, "D"));
619
+ collection.push(decision);
620
+ affectedIds.push(decision.id);
621
+ } else if (collectionName === "checkpoints") {
622
+ assertSingleLineTitle(value.title, `${path}.addEntity.title`);
623
+ if (typeof value.type !== "string" || !value.type.trim())
624
+ fail("missing-checkpoint-field", "Checkpoint type is required", { path: `${path}.addEntity.type` });
625
+ for (const field of ["selectedBy", "condition", "result"])
626
+ assertPreciseValue(field, value[field], `${path}.addEntity.${field}`);
627
+ if (collection.some((item) => item.title === value.title))
628
+ fail("duplicate-checkpoint", `Checkpoint already exists: ${value.title}`, { path });
629
+ collection.push(normalizeCheckpoint(value));
630
+ } else {
631
+ assertSingleLineTitle(value.title, `${path}.addEntity.title`);
632
+ if (typeof value.body !== "string") fail("invalid-edit-type", "Note body requires a string", { path });
633
+ collection.push({
634
+ title: value.title,
635
+ source: typeof value.source === "string" ? value.source : "",
636
+ body: value.body,
637
+ });
638
+ }
639
+ }
640
+ if (edit.removeEntity !== undefined) {
641
+ if (collectionName === "decisions")
642
+ fail("decision-removal-requires-lifecycle", "Retire or supersede decisions through the decision operation");
643
+ const selector = edit.removeEntity;
644
+ if (!selector || typeof selector !== "object")
645
+ fail("invalid-edit-target", "removeEntity requires an id or title selector", { path });
646
+ const matches = collection
647
+ .map((item, index) => ({ item, index }))
648
+ .filter(
649
+ ({ item }) =>
650
+ (selector.id !== undefined && item.id === selector.id) ||
651
+ (selector.title !== undefined && item.title === selector.title),
652
+ );
653
+ if (!matches.length) fail("missing-entity", `No ${collectionName} matched the remove selector`, { path });
654
+ if (matches.length > 1) fail("ambiguous-entity", `Remove selector matched multiple ${collectionName}`, { path });
655
+ if (matches[0].item.id) affectedIds.push(matches[0].item.id);
656
+ collection.splice(matches[0].index, 1);
657
+ }
658
+ return affectedIds;
659
+ }
660
+
661
+ function applyPreciseEdits(data, edits) {
662
+ const affectedIds = [];
663
+ if (!Array.isArray(edits) || edits.length === 0) fail("missing-edits", "update requires a non-empty edits array");
664
+ for (const [index, edit] of edits.entries()) {
665
+ if (!edit || typeof edit !== "object" || Array.isArray(edit))
666
+ fail("invalid-edit", `Edit ${index} must be an object`);
667
+ if (edit.addEntity !== undefined || edit.removeEntity !== undefined)
668
+ affectedIds.push(...applyPreciseCollectionEdit(data, edit, `edits[${index}]`));
669
+ else if (typeof edit.target === "string") applyPreciseFieldEdit(data, edit, `edits[${index}]`);
670
+ else applyPreciseEntityEdit(data, edit, `edits[${index}]`);
671
+ if (edit.target?.id) affectedIds.push(edit.target.id);
672
+ }
673
+ return { affectedIds: [...new Set(affectedIds)] };
674
+ }
675
+
676
+ function assertKnownUpdateInput(input) {
677
+ for (const key of Object.keys(input))
678
+ if (!UPDATE_INPUT_KEYS.has(key))
679
+ fail("unknown-input-field", `Unknown update input field: ${key}`, { path: `input.${key}` });
680
+ }
681
+
682
+ function assertNoMixedEditInput(input) {
683
+ const mixedFields = Object.keys(input).filter((key) => !EDIT_MODE_INPUT_KEYS.has(key));
684
+ if (mixedFields.length)
685
+ fail("mixed-update-input", "The edits form cannot be combined with direct update fields", {
686
+ fields: mixedFields,
687
+ });
688
+ }
689
+
690
+ function applyUpdate(data, input) {
691
+ assertKnownUpdateInput(input);
692
+ if (input.edits !== undefined) {
693
+ assertNoMixedEditInput(input);
694
+ const operation = applyPreciseEdits(data, input.edits);
695
+ applyTaskState(data, input);
696
+ return operation;
697
+ }
698
+ if (Array.isArray(input.proposals))
699
+ fail(
700
+ "collection-replacement-requires-edits",
701
+ "Use explicit edits with a target and operation instead of replacing proposals",
702
+ );
703
+ if (Array.isArray(input.notes))
704
+ fail(
705
+ "collection-replacement-requires-edits",
706
+ "Use explicit edits with a target and operation instead of replacing notes",
707
+ );
708
+ applyContextPatch(data, input);
709
+ applyCurrentSlicePatch(data, input);
710
+ applyDecisionChange(data, input);
711
+ applyProposalChange(data, input);
712
+ applyNoteChange(data, input);
713
+ applyTaskState(data, input);
714
+ if (input.start || input.close || input.block || input.resume)
715
+ fail("invalid-operation", "Use the dedicated transition command instead of update");
716
+ return { affectedIds: [] };
717
+ }
718
+
719
+ function assertActiveTask(data, operation) {
720
+ if (data.taskState !== "Active")
721
+ fail("invalid-transition", `${operation} requires an Active task; current state is ${data.taskState}`);
722
+ }
723
+
724
+ function assertExpectedCurrentSlice(data, input) {
725
+ const current = data.currentWork.currentSlice;
726
+ if (!current) fail("missing-current-slice", "No Current Slice exists");
727
+ const id = input.sliceId ?? input.id ?? input.currentSliceId;
728
+ if (id && current.id !== id) fail("slice-mismatch", `Requested slice ${id} is not the Current Slice ${current.id}`);
729
+ return current;
730
+ }
731
+
732
+ function applyStart(data, input) {
733
+ assertKnownCommandInput(input, "start");
734
+ assertSliceListInputs(input, "start");
735
+ assertActiveTask(data, "start");
736
+ if (data.currentWork.currentSlice) fail("existing-current-slice", "A Current Slice already exists");
737
+ if (input.id !== undefined) fail("caller-supplied-id", "The script assigns Current Slice IDs");
738
+ const proposalTitle = input.proposalTitle ?? input.proposal;
739
+ let source = {};
740
+ if (proposalTitle) {
741
+ const index = data.proposals.findIndex((proposal) => proposal.title === proposalTitle);
742
+ if (index < 0) fail("missing-proposal", `Proposal does not exist: ${proposalTitle}`);
743
+ source = data.proposals[index];
744
+ data.proposals.splice(index, 1);
745
+ }
746
+ const slice = normalizeSlice(
747
+ {
748
+ ...source,
749
+ ...input,
750
+ title: input.title ?? source.title,
751
+ authoritySource: input.authoritySource ?? input.authority,
752
+ },
753
+ nextId(data, "S"),
754
+ );
755
+ assertSingleLineTitle(slice.title, "start.title");
756
+ if (!slice.authoritySource) fail("missing-authority", "start requires authoritySource");
757
+ if (!SLICE_TYPES.has(slice.type)) fail("invalid-slice-type", `Invalid slice type: ${slice.type}`);
758
+ assertCompleteSlice(slice, "start");
759
+ slice.state = "In progress";
760
+ data.currentWork.currentSlice = slice;
761
+ return { affectedIds: [slice.id] };
762
+ }
763
+
764
+ function applyBlock(data, input) {
765
+ assertKnownCommandInput(input, "block");
766
+ const slice = assertExpectedCurrentSlice(data, input);
767
+ if (slice.state !== "In progress") fail("invalid-transition", "Only an In progress slice can be blocked");
768
+ const blocker = input.blocker ?? {
769
+ blocker: input.reason ?? input.what,
770
+ why: input.why,
771
+ required: input.required ?? input.requiredResolution,
772
+ };
773
+ if (!blocker || typeof blocker !== "object" || !blocker.blocker || !blocker.why || !blocker.required)
774
+ fail("missing-blocker", "block requires blocker, why, and required resolution");
775
+ slice.state = "Blocked";
776
+ slice.blocker = clone(blocker);
777
+ slice.resumeWhen = ensureString(input.resumeWhen);
778
+ data.currentWork.blockers = [blocker.blocker];
779
+ return { affectedIds: [slice.id] };
780
+ }
781
+
782
+ function applyResume(data, input) {
783
+ assertKnownCommandInput(input, "resume");
784
+ assertSliceListInputs(input, "resume");
785
+ assertActiveTask(data, "resume");
786
+ const slice = assertExpectedCurrentSlice(data, input);
787
+ if (slice.state !== "Blocked") fail("invalid-transition", "Only a Blocked slice can be resumed");
788
+ const resolution = input.resolutionSource ?? input.resolvedBy;
789
+ if (!resolution) fail("missing-resolution", "resume requires resolutionSource");
790
+ slice.blockerHistory = [
791
+ ...ensureArray(slice.blockerHistory),
792
+ JSON.stringify({ blocker: slice.blocker, resolutionSource: resolution }),
793
+ ];
794
+ slice.blocker = null;
795
+ slice.resumeWhen = "";
796
+ slice.state = "In progress";
797
+ data.currentWork.blockers = [];
798
+ if (input.evidence !== undefined) slice.evidence = ensureArray(input.evidence);
799
+ return { affectedIds: [slice.id] };
800
+ }
801
+
802
+ function applyReopen(data, input) {
803
+ assertKnownCommandInput(input, "reopen");
804
+ assertSliceListInputs(input, "reopen");
805
+ assertActiveTask(data, "reopen");
806
+ if (data.currentWork.currentSlice) fail("existing-current-slice", "A Current Slice already exists");
807
+ const sliceId = input.sliceId ?? input.id ?? input.currentSliceId;
808
+ if (!sliceId) fail("missing-slice-id", "reopen requires sliceId");
809
+ const historyIndex = data.history.slices.findIndex((slice) => slice.id === sliceId);
810
+ if (historyIndex < 0) fail("missing-historical-slice", `No historical slice exists with ID ${sliceId}`);
811
+ const historical = data.history.slices[historyIndex];
812
+ if (!HISTORICAL_SLICE_STATES.has(historical.state))
813
+ fail("invalid-transition", "Only a historical slice can be reopened");
814
+ const authority = input.authoritySource ?? input.authority ?? input.userDirection;
815
+ if (!authority) fail("missing-authority", "reopen requires authoritySource");
816
+ const reason = input.reopenReason ?? input.reason;
817
+ if (!reason) fail("missing-reopen-reason", "reopen requires reopenReason");
818
+ const reasonAndScope = input.reasonAndScope ?? input.scopeChange ?? input.scope;
819
+ if (!reasonAndScope) fail("missing-reopen-scope", "reopen requires reasonAndScope");
820
+ const expectedEvidence = input.expectedEvidence;
821
+ if (!expectedEvidence) fail("missing-reopen-evidence", "reopen requires expectedEvidence");
822
+ const stopCondition = input.stopCondition;
823
+ if (!stopCondition) fail("missing-reopen-stop-condition", "reopen requires stopCondition");
824
+ const reopenHistory = [
825
+ ...ensureArray(historical.reopenHistory),
826
+ JSON.stringify({
827
+ priorState: historical.state,
828
+ priorOutcome: historical.outcome,
829
+ priorEvidence: historical.evidence,
830
+ priorBlocker: historical.blocker,
831
+ priorTaskEffect: historical.taskEffect,
832
+ priorAbandonmentReason: historical.abandonmentReason,
833
+ authoritySource: authority,
834
+ reopenReason: reason,
835
+ }),
836
+ ];
837
+ const reopened = normalizeSlice(
838
+ {
839
+ id: historical.id,
840
+ title: historical.title,
841
+ state: "In progress",
842
+ type: historical.type,
843
+ intendedResult: historical.intendedResult,
844
+ authoritySource: authority,
845
+ reasonAndScope,
846
+ expectedEvidence,
847
+ stopCondition,
848
+ startingState: input.startingState,
849
+ acceptedExtensions: historical.acceptedExtensions,
850
+ dependencies: historical.dependencies,
851
+ selectedCheckpoints: input.selectedCheckpoints,
852
+ reopenHistory,
853
+ currentResult: input.currentResult,
854
+ evidence: input.evidence,
855
+ reviewSummary: input.reviewSummary,
856
+ taskEffect: input.taskEffect,
857
+ pendingBoundaries: input.pendingBoundaries,
858
+ pendingReviews: input.pendingReviews,
859
+ },
860
+ historical.id,
861
+ );
862
+ assertSingleLineTitle(reopened.title, "reopen.title");
863
+ assertCompleteSlice(reopened, "reopen");
864
+ data.history.slices.splice(historyIndex, 1);
865
+ data.currentWork.currentSlice = reopened;
866
+ data.currentWork.blockers = [];
867
+ if (input.currentRoute !== undefined || input.route !== undefined)
868
+ data.currentWork.route = input.currentRoute ?? input.route;
869
+ if (input.nextAction !== undefined || input.nextUsefulAction !== undefined)
870
+ data.currentWork.nextAction = input.nextAction ?? input.nextUsefulAction;
871
+ return { affectedIds: [reopened.id] };
872
+ }
873
+
874
+ function unresolvedSelectedCheckpoints(data, slice) {
875
+ const resolved = new Set(
876
+ data.history.checkpoints
877
+ .filter((checkpoint) => CHECKPOINT_RESULTS.has(checkpoint.result))
878
+ .map((checkpoint) => checkpoint.title),
879
+ );
880
+ return ensureArray(slice.selectedCheckpoints).filter((title) => !resolved.has(title));
881
+ }
882
+
883
+ function applyClose(data, input) {
884
+ assertKnownCommandInput(input, "close");
885
+ assertSliceListInputs(input, "close");
886
+ const slice = assertExpectedCurrentSlice(data, input);
887
+ const finalState = input.finalState ?? input.state;
888
+ if (!HISTORICAL_SLICE_STATES.has(finalState)) fail("invalid-close-state", `Invalid final slice state: ${finalState}`);
889
+ if (finalState === "Blocked" && slice.state !== "Blocked")
890
+ fail("invalid-transition", "Historical Blocked close requires a currently Blocked slice");
891
+ if (finalState === "Blocked" && !(input.authoritySource ?? input.parkAuthority))
892
+ fail("missing-authority", "Historical Blocked close requires deliberate park authority");
893
+ if (finalState === "Abandoned" && !(input.authoritySource ?? input.abandonmentAuthority))
894
+ fail("missing-authority", "Abandoned close requires authoritySource");
895
+ const outcome = ensureString(input.outcome ?? input.result ?? slice.result);
896
+ const evidence = ensureArray(input.evidence ?? slice.evidence);
897
+ if (!outcome.trim()) fail("missing-outcome", "close requires a compact settled outcome");
898
+ if (finalState === "Abandoned" && !(input.abandonmentReason ?? input.reason))
899
+ fail("missing-abandonment-reason", "Abandoned close requires abandonmentReason");
900
+ if (finalState === "Abandoned" && !(input.residualEffects ?? input.taskEffect))
901
+ fail("missing-residual-effects", "Abandoned close requires residualEffects or taskEffect");
902
+ if (!evidence.length) fail("missing-evidence", "close requires the strongest available evidence boundary");
903
+ if (finalState === "Completed") {
904
+ const pending = [
905
+ ...ensureArray(slice.pendingBoundaries),
906
+ ...ensureArray(slice.pendingReviews),
907
+ ...unresolvedSelectedCheckpoints(data, slice),
908
+ ];
909
+ if (pending.length)
910
+ fail("unresolved-settlement", "Completed close requires all pending boundaries to be reconciled", { pending });
911
+ if (slice.state === "Blocked")
912
+ fail("blocked-close", "A Blocked slice must be parked as historical Blocked or explicitly abandoned");
913
+ }
914
+ const entry = compactObject({
915
+ id: slice.id,
916
+ title: slice.title,
917
+ state: finalState,
918
+ type: slice.type,
919
+ intendedResult: slice.intendedResult,
920
+ authoritySource: slice.authoritySource,
921
+ acceptedExtensions: slice.acceptedExtensions.length ? slice.acceptedExtensions : undefined,
922
+ dependencies: slice.dependencies.length ? slice.dependencies : undefined,
923
+ outcome,
924
+ evidence,
925
+ reviewSummary: ensureString(input.reviewSummary ?? slice.reviewSummary) || undefined,
926
+ taskEffect: ensureString(input.residualEffects ?? input.taskEffect ?? slice.taskEffect) || undefined,
927
+ abandonmentReason:
928
+ finalState === "Abandoned" ? ensureString(input.abandonmentReason ?? input.reason) || undefined : undefined,
929
+ reopenHistory: slice.reopenHistory.length ? slice.reopenHistory : undefined,
930
+ blocker:
931
+ finalState === "Blocked"
932
+ ? { blocker: slice.blocker, resumeWhen: slice.resumeWhen, required: slice.blocker?.required }
933
+ : undefined,
934
+ });
935
+ data.history.slices.push(entry);
936
+ data.currentWork.currentSlice = null;
937
+ data.currentWork.blockers = [];
938
+ if (input.currentContext) patchObject(data.currentContext, input.currentContext);
939
+ if (input.currentRoute !== undefined || input.route !== undefined)
940
+ data.currentWork.route = input.currentRoute ?? input.route;
941
+ if (input.nextAction !== undefined || input.nextUsefulAction !== undefined)
942
+ data.currentWork.nextAction = input.nextAction ?? input.nextUsefulAction;
943
+ if (input.taskState !== undefined) {
944
+ if (!input.taskStateAuthority && !input.authoritySource)
945
+ fail("missing-authority", "Post-close task-state changes require explicit authority");
946
+ if (!TASK_STATES.has(input.taskState)) fail("invalid-task-state", `Invalid task state: ${input.taskState}`);
947
+ if (
948
+ (finalState === "Completed" && input.taskState !== "Completed") ||
949
+ (finalState === "Abandoned" && input.taskState !== "Abandoned")
950
+ )
951
+ fail("inconsistent-task-state", "Post-close terminal task state must agree with the settled slice state");
952
+ data.taskState = input.taskState;
953
+ }
954
+ return { affectedIds: [slice.id] };
955
+ }
956
+
957
+ export function applyOperation(data, command, input) {
958
+ if (command === "update") return applyUpdate(data, input);
959
+ if (command === "start") return applyStart(data, input);
960
+ if (command === "block") return applyBlock(data, input);
961
+ if (command === "resume") return applyResume(data, input);
962
+ if (command === "reopen") return applyReopen(data, input);
963
+ if (command === "close") return applyClose(data, input);
964
+ fail("unknown-command", `Unsupported mutation command: ${command}`);
965
+ }