@hassangameryt/freeflow 0.5.0 → 0.7.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 (301) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +4 -5
  4. package/README.md +141 -163
  5. package/capabilities/cognitive-routing/SKILL.md +387 -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 -37
  9. package/hooks/freeflow-runtime-context.mjs +50 -541
  10. package/hooks/hooks.json +1 -26
  11. package/hooks/tests/check-runtime-context-hook.sh +114 -308
  12. package/package.json +27 -17
  13. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  14. package/pi-extension/dist/cognitive-routing/config.js +232 -0
  15. package/pi-extension/dist/cognitive-routing/controller.js +1292 -0
  16. package/pi-extension/dist/cognitive-routing/history.js +451 -0
  17. package/pi-extension/dist/cognitive-routing/host.js +188 -0
  18. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
  19. package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
  20. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  21. package/pi-extension/dist/cognitive-routing/tool.js +212 -0
  22. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  23. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  24. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  25. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  26. package/pi-extension/dist/context-virtualization/state.js +57 -0
  27. package/pi-extension/dist/context-virtualization/types.js +10 -0
  28. package/pi-extension/dist/conversation-history/passages.js +66 -0
  29. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  30. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  31. package/pi-extension/dist/conversation-history/search.js +56 -0
  32. package/pi-extension/dist/conversation-history/types.js +1 -0
  33. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  34. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  35. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  36. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  37. package/pi-extension/dist/freeflow-context/types.js +12 -0
  38. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  39. package/pi-extension/dist/index.js +564 -129
  40. package/pi-extension/dist/runtime/runtime-context.js +357 -470
  41. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  42. package/pi-extension/dist/settings/settings-tui.js +78 -0
  43. package/pi-extension/dist/settings/settings-ui.js +572 -826
  44. package/pi-extension/dist/ui/text-component.js +57 -0
  45. package/pi-extension/src/cognitive-routing/commands.ts +129 -0
  46. package/pi-extension/src/cognitive-routing/config.ts +303 -0
  47. package/pi-extension/src/cognitive-routing/controller.ts +1628 -0
  48. package/pi-extension/src/cognitive-routing/history.ts +650 -0
  49. package/pi-extension/src/cognitive-routing/host.ts +266 -0
  50. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
  51. package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
  52. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  53. package/pi-extension/src/cognitive-routing/tool.ts +273 -0
  54. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  55. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  56. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  57. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  58. package/pi-extension/src/context-virtualization/state.ts +79 -0
  59. package/pi-extension/src/context-virtualization/types.ts +92 -0
  60. package/pi-extension/src/conversation-history/passages.ts +81 -0
  61. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  62. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  63. package/pi-extension/src/conversation-history/search.ts +79 -0
  64. package/pi-extension/src/conversation-history/types.ts +31 -0
  65. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  66. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  67. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  68. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  69. package/pi-extension/src/freeflow-context/types.ts +70 -0
  70. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  71. package/pi-extension/src/index.ts +597 -135
  72. package/pi-extension/src/runtime/runtime-context.ts +373 -491
  73. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  74. package/pi-extension/src/settings/settings-tui.ts +93 -0
  75. package/pi-extension/src/settings/settings-ui.ts +679 -910
  76. package/pi-extension/src/ui/text-component.ts +66 -0
  77. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  78. package/pi-extension/tests/cognitive-routing/controller.test.js +1128 -0
  79. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1142 -0
  80. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  81. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  82. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  83. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
  84. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
  85. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
  86. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +279 -0
  87. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  88. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  89. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  90. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  91. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  92. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  93. package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
  94. package/pi-extension/tests/integration/pi-extension.test.js +532 -3701
  95. package/pi-extension/tests/integration/prompt-architecture.test.js +350 -0
  96. package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
  97. package/runtime/prompts/cognitive-routing.md +10 -0
  98. package/runtime/prompts/context-virtualization.md +3 -0
  99. package/runtime/prompts/conversation-history.md +3 -0
  100. package/runtime/prompts/core.md +84 -0
  101. package/runtime/prompts/interaction-contract.md +11 -0
  102. package/skills/action-selection/SKILL.md +140 -0
  103. package/skills/action-selection/references/trajectory-stalls.md +71 -0
  104. package/skills/bypass/SKILL.md +109 -38
  105. package/skills/commit-work/SKILL.md +94 -55
  106. package/skills/commit-work/references/git-state-edges.md +94 -0
  107. package/skills/commit-work/references/simple-push.md +92 -0
  108. package/skills/commit-work/references/staging-decisions.md +79 -46
  109. package/skills/decision-gate/SKILL.md +80 -22
  110. package/skills/design-for-depth/SKILL.md +206 -60
  111. package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
  112. package/skills/design-for-depth/references/interface-design-loop.md +77 -97
  113. package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
  114. package/skills/design-for-depth/references/software-design-philosophy.md +128 -174
  115. package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
  116. package/skills/diagnose-failure/SKILL.md +200 -48
  117. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +18 -6
  118. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +13 -5
  119. package/skills/discuss/SKILL.md +119 -65
  120. package/skills/discuss/references/discussion-continuity.md +123 -0
  121. package/skills/evaluate-skill/SKILL.md +166 -52
  122. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +43 -3
  123. package/skills/evaluate-skill/references/evaluation-design.md +56 -30
  124. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  125. package/skills/evaluate-skill/references/review-and-revision.md +62 -21
  126. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  127. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  128. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  129. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  130. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  131. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  132. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  133. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  134. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  135. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  136. package/skills/execute-work/SKILL.md +99 -57
  137. package/skills/execute-work/references/code-practices.md +4 -4
  138. package/skills/execute-work/references/execute-work-edges.md +62 -0
  139. package/skills/finish-branch/SKILL.md +2 -2
  140. package/skills/finish-branch/references/integration-options.md +1 -1
  141. package/skills/handoff/SKILL.md +3 -3
  142. package/skills/launch-work/SKILL.md +1 -1
  143. package/skills/launch-work/references/launch-readiness.md +2 -2
  144. package/skills/migration-work/SKILL.md +2 -2
  145. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  146. package/skills/release-work/SKILL.md +1 -1
  147. package/skills/release-work/references/release-evidence.md +1 -1
  148. package/skills/review-artifact/SKILL.md +129 -66
  149. package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
  150. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +159 -0
  151. package/skills/review-work/SKILL.md +113 -68
  152. package/skills/review-work/references/adjudicate-work-review.md +97 -0
  153. package/skills/review-work/references/independent-work-reviewer-contract.md +139 -0
  154. package/skills/review-work/references/security-risk-lens.md +18 -16
  155. package/skills/setup-freeflow/SKILL.md +16 -17
  156. package/skills/setup-freeflow/references/activation-contract.md +24 -30
  157. package/skills/setup-freeflow/references/host-setup.md +6 -7
  158. package/skills/simplify-code/SKILL.md +97 -39
  159. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  160. package/skills/tdd/SKILL.md +100 -53
  161. package/skills/tdd/references/test-design.md +3 -3
  162. package/skills/track-work/SKILL.md +195 -58
  163. package/skills/track-work/references/working-record-format.md +515 -0
  164. package/skills/track-work/scripts/lib/document.mjs +713 -0
  165. package/skills/track-work/scripts/lib/format.mjs +166 -0
  166. package/skills/track-work/scripts/lib/store.mjs +233 -0
  167. package/skills/track-work/scripts/lib/transitions.mjs +790 -0
  168. package/skills/track-work/scripts/lib/views.mjs +40 -0
  169. package/skills/track-work/scripts/working-record.mjs +316 -0
  170. package/skills/verify-work/SKILL.md +57 -19
  171. package/skills/verify-work/references/browser-runtime-evidence.md +2 -2
  172. package/skills/verify-work/references/integration-evidence.md +1 -7
  173. package/skills/verify-work/references/performance-evidence.md +2 -2
  174. package/skills/workflow/SKILL.md +137 -55
  175. package/skills/workflow/references/domain-skill-composition.md +26 -40
  176. package/skills/write-plan/SKILL.md +1 -1
  177. package/skills/write-skill/SKILL.md +218 -61
  178. package/skills/write-skill/references/activation-boundaries.md +63 -21
  179. package/skills/write-skill/references/agent-first-instructions.md +61 -27
  180. package/skills/write-skill/references/progressive-disclosure.md +97 -33
  181. package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
  182. package/capabilities/output-router/SKILL.md +0 -206
  183. package/capabilities/output-router/references/safety-policy.md +0 -40
  184. package/capabilities/output-router/references/setup.md +0 -110
  185. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  186. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  187. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  188. package/pi-extension/dist/output-router/renderers.js +0 -1023
  189. package/pi-extension/dist/output-router/router-tools.js +0 -554
  190. package/pi-extension/dist/output-router/schemas.js +0 -383
  191. package/pi-extension/dist/output-router/status.js +0 -744
  192. package/pi-extension/dist/output-router/utils.js +0 -502
  193. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  194. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  195. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  196. package/pi-extension/src/output-router/renderers.ts +0 -1108
  197. package/pi-extension/src/output-router/router-tools.ts +0 -579
  198. package/pi-extension/src/output-router/schemas.ts +0 -387
  199. package/pi-extension/src/output-router/status.ts +0 -791
  200. package/pi-extension/src/output-router/utils.ts +0 -554
  201. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  202. package/pi-extension/tests/output-router/search.test.js +0 -722
  203. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  204. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  205. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  206. package/router/dist/benchmarks/benchmarks.js +0 -979
  207. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  208. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  209. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  210. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  211. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  212. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  213. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  214. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  215. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  216. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  217. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  218. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  219. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  220. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  221. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  222. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  223. package/router/dist/config/config.d.ts +0 -103
  224. package/router/dist/config/config.js +0 -632
  225. package/router/dist/config/router-contract.d.ts +0 -10
  226. package/router/dist/config/router-contract.js +0 -54
  227. package/router/dist/config/schema.d.ts +0 -27
  228. package/router/dist/config/schema.js +0 -648
  229. package/router/dist/config/types.d.ts +0 -573
  230. package/router/dist/config/types.js +0 -41
  231. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  232. package/router/dist/evidence/bounded-evidence.js +0 -323
  233. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  234. package/router/dist/evidence/evidence-range-selector.js +0 -232
  235. package/router/dist/evidence/evidence-search.d.ts +0 -27
  236. package/router/dist/evidence/evidence-search.js +0 -490
  237. package/router/dist/evidence/evidence.d.ts +0 -38
  238. package/router/dist/evidence/evidence.js +0 -200
  239. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  240. package/router/dist/evidence/failure-contracts.js +0 -166
  241. package/router/dist/evidence/line-ranges.d.ts +0 -24
  242. package/router/dist/evidence/line-ranges.js +0 -22
  243. package/router/dist/experiments/local-index.d.ts +0 -64
  244. package/router/dist/experiments/local-index.js +0 -581
  245. package/router/dist/index.d.ts +0 -30
  246. package/router/dist/index.js +0 -32
  247. package/router/dist/local/local-traversal.d.ts +0 -18
  248. package/router/dist/local/local-traversal.js +0 -334
  249. package/router/dist/processing/engine.d.ts +0 -132
  250. package/router/dist/processing/engine.js +0 -533
  251. package/router/dist/processing/reducers.d.ts +0 -280
  252. package/router/dist/processing/reducers.js +0 -1673
  253. package/router/dist/processing/renderers.d.ts +0 -31
  254. package/router/dist/processing/renderers.js +0 -195
  255. package/router/dist/processing/scripts.d.ts +0 -91
  256. package/router/dist/processing/scripts.js +0 -386
  257. package/router/dist/repo/repo-traversal.d.ts +0 -18
  258. package/router/dist/repo/repo-traversal.js +0 -245
  259. package/router/dist/routing/observed-reducers.d.ts +0 -41
  260. package/router/dist/routing/observed-reducers.js +0 -661
  261. package/router/dist/routing/observed-routing.d.ts +0 -26
  262. package/router/dist/routing/observed-routing.js +0 -378
  263. package/router/dist/routing/parsers.d.ts +0 -16
  264. package/router/dist/routing/parsers.js +0 -521
  265. package/router/dist/routing/run-filters.d.ts +0 -60
  266. package/router/dist/routing/run-filters.js +0 -238
  267. package/router/dist/routing/run-reducers.d.ts +0 -49
  268. package/router/dist/routing/run-reducers.js +0 -122
  269. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  270. package/router/dist/sandbox/adapter-roots.js +0 -66
  271. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  272. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  273. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  274. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  275. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  276. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  277. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  278. package/router/dist/sandbox/script-sandbox.js +0 -337
  279. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  280. package/router/dist/setup/script-transform-adapters.js +0 -278
  281. package/router/dist/tools/batch.d.ts +0 -33
  282. package/router/dist/tools/batch.js +0 -586
  283. package/router/dist/tools/run.d.ts +0 -62
  284. package/router/dist/tools/run.js +0 -1304
  285. package/router/dist/tools/search.d.ts +0 -66
  286. package/router/dist/tools/search.js +0 -1364
  287. package/router/dist/transform/engine.d.ts +0 -168
  288. package/router/dist/transform/engine.js +0 -2072
  289. package/router/dist/vault/vault-index.d.ts +0 -125
  290. package/router/dist/vault/vault-index.js +0 -416
  291. package/router/dist/vault/vault.d.ts +0 -138
  292. package/router/dist/vault/vault.js +0 -630
  293. package/runtime/interaction-contract.md +0 -16
  294. package/skills/discuss/references/checkpoints.md +0 -66
  295. package/skills/execute-work/references/execution-loop.md +0 -77
  296. package/skills/mode-contract/SKILL.md +0 -104
  297. package/skills/review-artifact/references/reviewer-prompt.md +0 -148
  298. package/skills/review-work/references/reviewer-prompt.md +0 -134
  299. package/skills/track-work/references/working-record-schema.md +0 -378
  300. package/skills/workflow/references/workflow-loop.md +0 -92
  301. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
@@ -0,0 +1,713 @@
1
+ import {
2
+ ACTIVE_SLICE_FIELDS,
3
+ CHECKPOINT_TYPES,
4
+ CURRENT_SLICE_STATES,
5
+ DECISION_FIELDS,
6
+ DECISION_STATES,
7
+ FUTURE_CHECKPOINT_FIELDS,
8
+ FUTURE_CHECKPOINT_STATES,
9
+ HISTORICAL_CHECKPOINT_FIELDS,
10
+ HISTORICAL_CHECKPOINT_STATES,
11
+ HISTORICAL_SLICE_FIELDS,
12
+ HISTORICAL_SLICE_STATES,
13
+ HISTORY_HEADINGS,
14
+ PROPOSED_SLICE_FIELDS,
15
+ SCHEMA_VERSION,
16
+ SLICE_TYPES,
17
+ TASK_STATES,
18
+ TOP_LEVEL_SECTIONS,
19
+ joinLines,
20
+ nextId,
21
+ parseId,
22
+ splitLines,
23
+ } from "./format.mjs";
24
+
25
+ export class DocumentError extends Error {
26
+ constructor(code, message, details = {}) {
27
+ super(message);
28
+ this.name = "DocumentError";
29
+ this.code = code;
30
+ this.details = details;
31
+ }
32
+ }
33
+
34
+ function fail(code, message, details = {}) {
35
+ throw new DocumentError(code, message, details);
36
+ }
37
+
38
+ function lineHeading(line) {
39
+ const match = /^(#{1,6})[ \t]+(.+?)\s*$/.exec(line);
40
+ return match ? { level: match[1].length, title: match[2] } : null;
41
+ }
42
+
43
+ function isBlank(line) {
44
+ return line.trim() === "";
45
+ }
46
+
47
+ function isUtcTimestamp(value) {
48
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) && !Number.isNaN(Date.parse(value));
49
+ }
50
+
51
+ function fieldLine(line) {
52
+ const compact = /^- ([A-Za-z][A-Za-z -]*):(?:[ \t]*(.*))?$/.exec(line);
53
+ if (compact) return { name: compact[1], inline: compact[2] ?? "", style: "compact" };
54
+ const expanded = /^([A-Za-z][A-Za-z -]*):(?:[ \t]*(.*))?$/.exec(line);
55
+ return expanded ? { name: expanded[1], inline: expanded[2] ?? "", style: "expanded" } : null;
56
+ }
57
+
58
+ function nonBlankLines(lines) {
59
+ return lines.filter((line) => !isBlank(line));
60
+ }
61
+
62
+ function hasMeaningfulContent(lines, start, end) {
63
+ return lines.slice(start, end).some((line) => line.replace(/^\s*[-*+]\s*/, "").trim() !== "");
64
+ }
65
+
66
+ function trimValueLines(lines) {
67
+ let start = 0;
68
+ let end = lines.length;
69
+ while (start < end && isBlank(lines[start])) start += 1;
70
+ while (end > start && isBlank(lines[end - 1])) end -= 1;
71
+ return lines.slice(start, end);
72
+ }
73
+
74
+ function valueText(field) {
75
+ if (!field) return "";
76
+ return trimValueLines(field.valueLines).join("\n").trim();
77
+ }
78
+
79
+ function scalarValue(block, name) {
80
+ return valueText(block.fields.get(name));
81
+ }
82
+
83
+ function requiredValue(block, name) {
84
+ const value = scalarValue(block, name);
85
+ if (!value) fail("missing-field", `Missing or empty field ${name} in ${block.path}`);
86
+ return value;
87
+ }
88
+
89
+ function fieldNames(allowed) {
90
+ return [...allowed].sort().join(", ");
91
+ }
92
+
93
+ function detectFieldStyle(lines, start, end) {
94
+ for (let index = start; index < end; index += 1) {
95
+ if (isBlank(lines[index])) continue;
96
+ const foundField = fieldLine(lines[index]);
97
+ return foundField?.style ?? "expanded";
98
+ }
99
+ return "expanded";
100
+ }
101
+
102
+ function parseFields(lines, start, end, allowed, path, allowedNestedHeadings = new Set()) {
103
+ const fields = new Map();
104
+ const style = detectFieldStyle(lines, start, end);
105
+ let current = null;
106
+ let inNestedBlock = false;
107
+ for (let index = start; index < end; index += 1) {
108
+ const line = lines[index];
109
+ const foundHeading = lineHeading(line);
110
+ if (foundHeading) {
111
+ if (foundHeading.level <= 4) fail("malformed-structure", `Unexpected heading in ${path}: ${line}`);
112
+ if (!allowedNestedHeadings.has(foundHeading.title))
113
+ fail("unknown-heading", `Unknown nested heading ${foundHeading.title} in ${path}`);
114
+ current = null;
115
+ inNestedBlock = true;
116
+ continue;
117
+ }
118
+ if (inNestedBlock) continue;
119
+ const foundField = fieldLine(line);
120
+ if (foundField && foundField.style !== style) {
121
+ if (style === "expanded" && current && foundField.style === "compact") {
122
+ current.valueLines.push(line);
123
+ continue;
124
+ }
125
+ fail("mixed-field-style", `Mixed structured-field styles in ${path}: ${line}`);
126
+ }
127
+ if (foundField) {
128
+ if (!allowed.has(foundField.name))
129
+ fail("unknown-field", `Unknown field ${foundField.name} in ${path}; allowed fields: ${fieldNames(allowed)}`);
130
+ if (fields.has(foundField.name)) fail("duplicate-field", `Duplicate field ${foundField.name} in ${path}`);
131
+ current = {
132
+ name: foundField.name,
133
+ start: index,
134
+ valueLines: foundField.inline === "" ? [] : [foundField.inline],
135
+ };
136
+ fields.set(foundField.name, current);
137
+ continue;
138
+ }
139
+ if (current) {
140
+ if (style === "compact" && !isBlank(line)) {
141
+ if (!line.startsWith(" "))
142
+ fail("malformed-field", `Compact field values must be indented in ${path}: ${line}`);
143
+ current.valueLines.push(line.slice(2));
144
+ } else {
145
+ current.valueLines.push(line);
146
+ }
147
+ } else if (!isBlank(line) && style === "compact") {
148
+ fail("unowned-content", `Unowned content in ${path}: ${line}`);
149
+ } else if (!isBlank(line)) {
150
+ fail("unowned-content", `Unowned content in ${path}: ${line}`);
151
+ }
152
+ }
153
+ return { fields, style };
154
+ }
155
+
156
+ function parseBlock(lines, start, end, kind, title, id, allowed, path) {
157
+ const nested =
158
+ path === "Current Slice"
159
+ ? new Set(["Material updates"])
160
+ : path.startsWith("History/")
161
+ ? new Set(["Corrections"])
162
+ : new Set();
163
+ const parsed = parseFields(lines, start + 1, end, allowed, path, nested);
164
+ return { kind, title, id: id ?? null, start, end, fields: parsed.fields, fieldStyle: parsed.style, path };
165
+ }
166
+
167
+ function findHeadings(lines, start, end, level) {
168
+ const result = [];
169
+ for (let index = start; index < end; index += 1) {
170
+ const found = lineHeading(lines[index]);
171
+ if (found?.level === level) result.push({ index, title: found.title });
172
+ }
173
+ return result;
174
+ }
175
+
176
+ function blockEnd(lines, headingIndex, sectionEnd, level) {
177
+ for (let index = headingIndex + 1; index < sectionEnd; index += 1) {
178
+ const found = lineHeading(lines[index]);
179
+ if (found && found.level <= level) return index;
180
+ }
181
+ return sectionEnd;
182
+ }
183
+
184
+ function parseHeader(lines, firstSectionIndex) {
185
+ if (!/^# Working Record:\s*.+$/.test(lines[0] ?? ""))
186
+ fail("malformed-header", "The record must begin with '# Working Record: <task name>'");
187
+ const name = lines[0].slice("# Working Record:".length).trim();
188
+ const schemaLines = [];
189
+ const stateLines = [];
190
+ const updatedLines = [];
191
+ for (let index = 1; index < firstSectionIndex; index += 1) {
192
+ if (isBlank(lines[index])) continue;
193
+ const schema = /^Schema:\s*(\d+)\s*$/.exec(lines[index]);
194
+ if (schema) {
195
+ schemaLines.push({ index, value: Number(schema[1]) });
196
+ continue;
197
+ }
198
+ const state = /^State:\s*(\S+)\s*$/.exec(lines[index]);
199
+ if (state) {
200
+ stateLines.push({ index, value: state[1] });
201
+ continue;
202
+ }
203
+ const updated = /^Last updated:\s*(\S+)\s*$/.exec(lines[index]);
204
+ if (updated) {
205
+ updatedLines.push({ index, value: updated[1] });
206
+ continue;
207
+ }
208
+ fail("unknown-header-field", `Unknown header content: ${lines[index]}`);
209
+ }
210
+ if (schemaLines.length !== 1) fail("missing-field", "Header must contain exactly one Schema field");
211
+ if (stateLines.length !== 1) fail("missing-field", "Header must contain exactly one State field");
212
+ if (updatedLines.length !== 1) fail("missing-field", "Header must contain exactly one Last updated field");
213
+ if (!isUtcTimestamp(updatedLines[0].value))
214
+ fail("invalid-timestamp", `Invalid Last updated value: ${updatedLines[0].value}`);
215
+ if (schemaLines[0].value !== SCHEMA_VERSION)
216
+ fail("unsupported-schema", `Expected Schema ${SCHEMA_VERSION}, found Schema ${schemaLines[0].value}`);
217
+ if (!TASK_STATES.has(stateLines[0].value)) fail("invalid-state", `Invalid task State: ${stateLines[0].value}`);
218
+ return {
219
+ name,
220
+ schema: schemaLines[0].value,
221
+ state: stateLines[0].value,
222
+ stateLine: stateLines[0].index,
223
+ lastUpdated: updatedLines[0].value,
224
+ lastUpdatedLine: updatedLines[0].index,
225
+ };
226
+ }
227
+
228
+ function parseTopLevelSections(lines) {
229
+ const headings = [];
230
+ for (let index = 0; index < lines.length; index += 1) {
231
+ const found = lineHeading(lines[index]);
232
+ if (found?.level === 2) {
233
+ headings.push({ index, title: found.title });
234
+ if (found.title === "Notes") break;
235
+ }
236
+ }
237
+ if (headings.length !== TOP_LEVEL_SECTIONS.length)
238
+ fail("invalid-sections", `Expected exactly ${TOP_LEVEL_SECTIONS.length} top-level sections`);
239
+ for (let index = 0; index < TOP_LEVEL_SECTIONS.length; index += 1) {
240
+ if (headings[index].title !== TOP_LEVEL_SECTIONS[index])
241
+ fail(
242
+ "invalid-sections",
243
+ `Expected top-level section ${TOP_LEVEL_SECTIONS[index]}, found ${headings[index].title}`,
244
+ );
245
+ }
246
+ return Object.fromEntries(
247
+ headings.map((item, index) => [
248
+ item.title,
249
+ { headingIndex: item.index, start: item.index + 1, end: headings[index + 1]?.index ?? lines.length },
250
+ ]),
251
+ );
252
+ }
253
+
254
+ function parseContext(lines, section) {
255
+ const headings = findHeadings(lines, section.start, section.end, 3);
256
+ if (headings.length !== 7)
257
+ fail("invalid-context", "Current Context must contain exactly the seven canonical headings");
258
+ const statements = new Map();
259
+ for (let index = 0; index < headings.length; index += 1) {
260
+ const expected = [
261
+ "Goal",
262
+ "What defines this task",
263
+ "Settled",
264
+ "Tentative",
265
+ "Open",
266
+ "Current direction",
267
+ "Boundaries",
268
+ ][index];
269
+ if (headings[index].title !== expected) fail("invalid-context", `Expected Current Context heading ${expected}`);
270
+ const end = headings[index + 1]?.index ?? section.end;
271
+ statements.set(expected, { headingIndex: headings[index].index, start: headings[index].index + 1, end });
272
+ }
273
+ return { ...section, headings: statements };
274
+ }
275
+
276
+ function parseCurrentWork(lines, section) {
277
+ const headings = findHeadings(lines, section.start, section.end, 3);
278
+ if (headings.length !== 2 || headings[0].title !== "Current Slice" || headings[1].title !== "Next useful action")
279
+ fail("invalid-current-work", "Current Work must contain Current Slice followed by Next useful action");
280
+ const sliceContentStart = headings[0].index + 1;
281
+ const sliceContentEnd = headings[1].index;
282
+ const content = nonBlankLines(lines.slice(sliceContentStart, sliceContentEnd));
283
+ let currentSlice = null;
284
+ if (content.length === 1 && content[0] === "None") {
285
+ currentSlice = null;
286
+ } else {
287
+ const entries = findHeadings(lines, sliceContentStart, sliceContentEnd, 4);
288
+ if (entries.length !== 1)
289
+ fail("invalid-current-slice", "Current Slice must contain exactly one S-NNN block or None");
290
+ const found = /^S-(\d{3,})\s+—\s+(.+)$/.exec(entries[0].title);
291
+ if (!found) fail("invalid-current-slice", `Invalid Current Slice heading: ${lines[entries[0].index]}`);
292
+ const end = blockEnd(lines, entries[0].index, sliceContentEnd, 4);
293
+ currentSlice = parseBlock(
294
+ lines,
295
+ entries[0].index,
296
+ end,
297
+ "slice",
298
+ found[2],
299
+ `S-${found[1]}`,
300
+ ACTIVE_SLICE_FIELDS,
301
+ "Current Slice",
302
+ );
303
+ }
304
+ return {
305
+ ...section,
306
+ sliceHeadingIndex: headings[0].index,
307
+ nextActionHeadingIndex: headings[1].index,
308
+ currentSlice,
309
+ nextAction: { start: headings[1].index + 1, end: section.end },
310
+ };
311
+ }
312
+
313
+ function parseFuture(lines, section) {
314
+ const headings = findHeadings(lines, section.start, section.end, 3);
315
+ const items = [];
316
+ for (const itemHeading of headings) {
317
+ let match = /^Slice\s+—\s+(.+)$/.exec(itemHeading.title);
318
+ let kind = "slice";
319
+ let id = null;
320
+ let title;
321
+ if (match) {
322
+ title = match[1];
323
+ } else {
324
+ match = /^Checkpoint(?:\s+(C-\d{3,}))?\s+—\s+(.+)$/.exec(itemHeading.title);
325
+ if (!match) fail("invalid-future-item", `Unknown Future Work heading: ${lines[itemHeading.index]}`);
326
+ kind = "checkpoint";
327
+ id = match[1] ?? null;
328
+ title = match[2];
329
+ }
330
+ const end = blockEnd(lines, itemHeading.index, section.end, 3);
331
+ const allowed = kind === "slice" ? PROPOSED_SLICE_FIELDS : FUTURE_CHECKPOINT_FIELDS;
332
+ items.push(parseBlock(lines, itemHeading.index, end, kind, title, id, allowed, `Future Work/${title}`));
333
+ }
334
+ return { ...section, items };
335
+ }
336
+
337
+ function parseHistorySubsection(lines, section, title, kind, idPattern, allowed) {
338
+ const heading = findHeadings(lines, section.start, section.end, 3).find((item) => item.title === title);
339
+ if (!heading) fail("invalid-history", `History is missing the ${title} subsection`);
340
+ const subsectionEnd = blockEnd(lines, heading.index, section.end, 3);
341
+ const entries = findHeadings(lines, heading.index + 1, subsectionEnd, 4);
342
+ const blocks = [];
343
+ for (const entry of entries) {
344
+ const match = idPattern.exec(entry.title);
345
+ if (!match) fail("invalid-history-entry", `Invalid ${title} heading: ${lines[entry.index]}`);
346
+ const end = blockEnd(lines, entry.index, subsectionEnd, 4);
347
+ blocks.push(parseBlock(lines, entry.index, end, kind, match[2], match[1], allowed, `History/${title}/${match[1]}`));
348
+ }
349
+ return { headingIndex: heading.index, start: heading.index + 1, end: subsectionEnd, blocks };
350
+ }
351
+
352
+ function parseHistory(lines, section) {
353
+ const headings = findHeadings(lines, section.start, section.end, 3);
354
+ if (
355
+ headings.length !== HISTORY_HEADINGS.length ||
356
+ headings.some((item, index) => item.title !== HISTORY_HEADINGS[index])
357
+ )
358
+ fail("invalid-history", "History must contain Decisions, Checkpoints, and Slices in that order");
359
+ return {
360
+ ...section,
361
+ decisions: parseHistorySubsection(
362
+ lines,
363
+ section,
364
+ "Decisions",
365
+ "decision",
366
+ /^(D-\d{3,})\s+—\s+(.+)$/,
367
+ DECISION_FIELDS,
368
+ ),
369
+ checkpoints: parseHistorySubsection(
370
+ lines,
371
+ section,
372
+ "Checkpoints",
373
+ "checkpoint",
374
+ /^(C-\d{3,})\s+—\s+(.+)$/,
375
+ HISTORICAL_CHECKPOINT_FIELDS,
376
+ ),
377
+ slices: parseHistorySubsection(
378
+ lines,
379
+ section,
380
+ "Slices",
381
+ "slice",
382
+ /^(S-\d{3,})\s+—\s+(.+)$/,
383
+ HISTORICAL_SLICE_FIELDS,
384
+ ),
385
+ };
386
+ }
387
+
388
+ function validateChoice(block, field, choices, required = true) {
389
+ const value = scalarValue(block, field);
390
+ if (!value && !required) return "";
391
+ if (!value) requiredValue(block, field);
392
+ if (!choices.has(value)) fail("invalid-state", `Invalid ${field} in ${block.path}: ${value}`);
393
+ return value;
394
+ }
395
+
396
+ function validateFields(block, required, conditional = {}) {
397
+ for (const field of required) requiredValue(block, field);
398
+ for (const [field, condition] of Object.entries(conditional)) {
399
+ if (condition(block)) requiredValue(block, field);
400
+ }
401
+ }
402
+
403
+ function allBlocks(document) {
404
+ return [
405
+ ...(document.current.currentSlice ? [document.current.currentSlice] : []),
406
+ ...document.future.items,
407
+ ...document.history.decisions.blocks,
408
+ ...document.history.checkpoints.blocks,
409
+ ...document.history.slices.blocks,
410
+ ];
411
+ }
412
+
413
+ function validateReferences(document) {
414
+ const currentSlice = document.current.currentSlice;
415
+ const historicalSlices = document.history.slices.blocks;
416
+ const sliceIds = new Set([...(currentSlice ? [currentSlice] : []), ...historicalSlices].map((block) => block.id));
417
+ const historicalSliceIds = new Set(historicalSlices.map((block) => block.id));
418
+ const futureSliceTitles = new Set(
419
+ document.future.items.filter((item) => item.kind === "slice").map((item) => item.title),
420
+ );
421
+ const checkpointIds = new Set([
422
+ ...document.future.items.filter((item) => item.kind === "checkpoint" && item.id).map((item) => item.id),
423
+ ...document.history.checkpoints.blocks.map((item) => item.id),
424
+ ]);
425
+ const decisionIds = new Set(document.history.decisions.blocks.map((item) => item.id));
426
+
427
+ for (const checkpoint of document.future.items.filter((item) => item.kind === "checkpoint")) {
428
+ const appliesTo = requiredValue(checkpoint, "Applies to");
429
+ if (!sliceIds.has(appliesTo) && !futureSliceTitles.has(appliesTo))
430
+ fail("invalid-reference", `Checkpoint ${checkpoint.title} applies to unknown Slice ${appliesTo}`);
431
+ }
432
+ for (const checkpoint of document.history.checkpoints.blocks) {
433
+ const appliesTo = requiredValue(checkpoint, "Applies to");
434
+ if (!sliceIds.has(appliesTo))
435
+ fail("invalid-reference", `Checkpoint ${checkpoint.id} applies to unknown Slice ${appliesTo}`);
436
+ if (scalarValue(checkpoint, "State") === "replaced") {
437
+ const replacement = requiredValue(checkpoint, "Replaced by");
438
+ if (!checkpointIds.has(replacement) || replacement === checkpoint.id)
439
+ fail("invalid-reference", `Invalid replacement Checkpoint ${replacement}`);
440
+ }
441
+ }
442
+ for (const decision of document.history.decisions.blocks) {
443
+ const state = scalarValue(decision, "State");
444
+ if (state === "superseded") {
445
+ const replacement = requiredValue(decision, "Superseded by");
446
+ if (!decisionIds.has(replacement) || replacement === decision.id)
447
+ fail("invalid-reference", `Invalid replacement Decision ${replacement}`);
448
+ }
449
+ }
450
+ if (currentSlice) {
451
+ const continues = scalarValue(currentSlice, "Reopened from");
452
+ if (continues && (!historicalSliceIds.has(continues) || continues === currentSlice.id))
453
+ fail("invalid-reference", `Current Slice continues unknown historical Slice ${continues}`);
454
+ }
455
+ for (const slice of historicalSlices) {
456
+ const continues = scalarValue(slice, "Reopened from");
457
+ if (continues && (!historicalSliceIds.has(continues) || continues === slice.id))
458
+ fail("invalid-reference", `Historical Slice ${slice.id} continues unknown historical Slice ${continues}`);
459
+ }
460
+ }
461
+
462
+ export function validateDocument(document) {
463
+ const { header, context, current, future, history } = document;
464
+ if (!header.name) fail("malformed-header", "Task name cannot be empty");
465
+ const goal = context.headings.get("Goal");
466
+ if (!hasMeaningfulContent(document.lines, goal.start, goal.end))
467
+ fail("missing-field", "Current Context Goal must contain meaningful content");
468
+ if (!hasMeaningfulContent(document.lines, current.nextAction.start, current.nextAction.end))
469
+ fail("missing-field", "Next useful action must contain meaningful content");
470
+ for (const block of allBlocks(document)) {
471
+ if (block.id && !parseId(block.id, block.kind)) fail("invalid-id", `Invalid ${block.kind} ID ${block.id}`);
472
+ }
473
+
474
+ const ids = new Set();
475
+ for (const block of allBlocks(document)) {
476
+ if (block.id) {
477
+ if (ids.has(block.id)) fail("duplicate-id", `Duplicate durable ID ${block.id}`);
478
+ ids.add(block.id);
479
+ }
480
+ }
481
+
482
+ for (const item of future.items) {
483
+ if (!item.title.trim()) fail("invalid-title", "Future Work item titles cannot be empty");
484
+ if (item.kind === "slice") {
485
+ validateFields(item, ["State", "Intended result"], {});
486
+ validateChoice(item, "State", new Set(["proposed"]));
487
+ const type = scalarValue(item, "Type");
488
+ if (type && !SLICE_TYPES.has(type)) fail("invalid-type", `Invalid Slice Type ${type}`);
489
+ } else {
490
+ validateFields(item, ["State"], {});
491
+ validateChoice(item, "State", FUTURE_CHECKPOINT_STATES);
492
+ validateChoice(item, "Type", CHECKPOINT_TYPES);
493
+ validateFields(item, ["Condition", "Applies to"]);
494
+ if (item.id && !parseId(item.id, "checkpoint")) fail("invalid-id", `Invalid Checkpoint ID ${item.id}`);
495
+ const checkpointState = scalarValue(item, "State");
496
+ if (checkpointState === "proposed" && item.id)
497
+ fail("invalid-future-item", "A proposed Checkpoint cannot have a durable ID");
498
+ if (checkpointState !== "proposed" && !item.id)
499
+ fail("invalid-future-item", "A pending or deferred Checkpoint requires a durable ID");
500
+ }
501
+ }
502
+
503
+ const futureTitles = new Set();
504
+ for (const item of future.items) {
505
+ if (futureTitles.has(item.title)) fail("duplicate-title", `Duplicate Future Work title: ${item.title}`);
506
+ futureTitles.add(item.title);
507
+ }
508
+
509
+ if (current.currentSlice) {
510
+ validateChoice(current.currentSlice, "State", CURRENT_SLICE_STATES);
511
+ validateFields(current.currentSlice, [
512
+ "Intended result",
513
+ "Authority source",
514
+ "Scope",
515
+ "Expected evidence",
516
+ "Stop condition",
517
+ "Starting state",
518
+ ]);
519
+ const type = scalarValue(current.currentSlice, "Type");
520
+ if (type && !SLICE_TYPES.has(type)) fail("invalid-type", `Invalid Current Slice Type ${type}`);
521
+ }
522
+
523
+ for (const decision of history.decisions.blocks) {
524
+ validateChoice(decision, "State", DECISION_STATES);
525
+ validateFields(decision, ["Decision", "Established by", "Rationale", "Consequences", "Revisit when"]);
526
+ const state = scalarValue(decision, "State");
527
+ if (state === "superseded") requiredValue(decision, "Superseded by");
528
+ if (state === "retired") requiredValue(decision, "Retired because");
529
+ }
530
+
531
+ for (const checkpoint of history.checkpoints.blocks) {
532
+ validateChoice(checkpoint, "State", HISTORICAL_CHECKPOINT_STATES);
533
+ validateChoice(checkpoint, "Type", CHECKPOINT_TYPES);
534
+ validateFields(checkpoint, ["Condition", "Applies to", "Result", "Task effect"]);
535
+ const state = scalarValue(checkpoint, "State");
536
+ if (state === "cancelled" || state === "replaced") requiredValue(checkpoint, "Reason");
537
+ if (state === "replaced") requiredValue(checkpoint, "Replaced by");
538
+ }
539
+
540
+ for (const slice of history.slices.blocks) {
541
+ validateChoice(slice, "State", HISTORICAL_SLICE_STATES);
542
+ validateFields(slice, ["Intended result", "Result", "Evidence and limits", "Task effect"]);
543
+ const state = scalarValue(slice, "State");
544
+ if (state === "blocked") requiredValue(slice, "Resume when");
545
+ if (state === "abandoned") requiredValue(slice, "Reason");
546
+ }
547
+
548
+ if (header.state === "paused" && current.currentSlice && scalarValue(current.currentSlice, "State") !== "paused")
549
+ fail("invalid-task-state", "A paused task must have a paused Current Slice");
550
+ if ((header.state === "completed" || header.state === "abandoned") && current.currentSlice)
551
+ fail("invalid-task-state", `A ${header.state} task cannot have a Current Slice`);
552
+ if (
553
+ (header.state === "completed" || header.state === "abandoned") &&
554
+ future.items.some(
555
+ (item) => item.kind === "checkpoint" && ["pending", "deferred"].includes(scalarValue(item, "State")),
556
+ )
557
+ )
558
+ fail("invalid-task-state", `A ${header.state} task cannot have a pending or deferred Checkpoint`);
559
+
560
+ validateReferences(document);
561
+ return document;
562
+ }
563
+
564
+ export function parseDocument(text, { validate = true } = {}) {
565
+ if (typeof text !== "string" || !text) fail("empty-record", "Record is empty");
566
+ const { lines, newline } = splitLines(text);
567
+ const topLevel = parseTopLevelSections(lines);
568
+ const header = parseHeader(lines, topLevel[TOP_LEVEL_SECTIONS[0]].headingIndex);
569
+ const context = parseContext(lines, topLevel["Current Context"]);
570
+ const current = parseCurrentWork(lines, topLevel["Current Work"]);
571
+ const future = parseFuture(lines, topLevel["Future Work"]);
572
+ const history = parseHistory(lines, topLevel.History);
573
+ const document = {
574
+ text,
575
+ lines,
576
+ newline,
577
+ header,
578
+ sections: topLevel,
579
+ context,
580
+ current,
581
+ future,
582
+ history,
583
+ notes: topLevel.Notes,
584
+ };
585
+ return validate ? validateDocument(document) : document;
586
+ }
587
+
588
+ export function parseFragment(text, allowed, path = "input") {
589
+ if (typeof text !== "string") fail("invalid-input", `Input for ${path} must be text`);
590
+ const { lines } = splitLines(text);
591
+ const parsed = parseFields(lines, 0, lines.length, allowed, path);
592
+ return { fields: parsed.fields, fieldStyle: parsed.style, lines };
593
+ }
594
+
595
+ export function fieldValue(block, name) {
596
+ return scalarValue(block, name);
597
+ }
598
+
599
+ export function fieldLines(block, name) {
600
+ return block.fields.has(name) ? trimValueLines(block.fields.get(name).valueLines) : null;
601
+ }
602
+
603
+ export function hasField(block, name) {
604
+ return block.fields.has(name) && Boolean(valueText(block.fields.get(name)));
605
+ }
606
+
607
+ export function formatField(name, linesOrValue, style = "expanded") {
608
+ const raw = Array.isArray(linesOrValue) ? linesOrValue : String(linesOrValue ?? "").split("\n");
609
+ const lines = trimValueLines(raw);
610
+ if (style === "compact") {
611
+ if (lines.length === 0) return [`- ${name}:`];
612
+ if (lines.length === 1) {
613
+ const value = lines[0].startsWith("- ") ? lines[0].slice(2) : lines[0];
614
+ return [`- ${name}: ${value}`];
615
+ }
616
+ return [`- ${name}:`, ...lines.map((line) => (line === "" ? "" : ` ${line}`))];
617
+ }
618
+ if (lines.length === 0) return [`${name}:`];
619
+ if (lines.length === 1 && !lines[0].startsWith("- ")) return [`${name}: ${lines[0]}`];
620
+ return [`${name}:`, ...lines];
621
+ }
622
+
623
+ export function renderBlock(blockHeading, orderedFields, values, style = "compact") {
624
+ const lines = [blockHeading];
625
+ let emittedField = false;
626
+ for (const field of orderedFields) {
627
+ if (!Object.hasOwn(values, field) || values[field] === undefined || values[field] === null) continue;
628
+ const value = Array.isArray(values[field]) ? values[field] : String(values[field]).split("\n");
629
+ if (value.length === 0 || (value.length === 1 && value[0] === "")) continue;
630
+ if (emittedField && style !== "compact") lines.push("");
631
+ lines.push(...formatField(field, value, style));
632
+ emittedField = true;
633
+ }
634
+ lines.push("");
635
+ return lines;
636
+ }
637
+
638
+ export function blockLines(document, block) {
639
+ return document.lines.slice(block.start, block.end);
640
+ }
641
+
642
+ export function applyLineReplacements(document, replacements) {
643
+ const sorted = [...replacements].sort((left, right) => right.start - left.start);
644
+ let previousStart = Number.POSITIVE_INFINITY;
645
+ for (const replacement of sorted) {
646
+ if (
647
+ !Number.isInteger(replacement.start) ||
648
+ !Number.isInteger(replacement.end) ||
649
+ replacement.start > replacement.end
650
+ )
651
+ fail("invalid-edit", "Invalid source range");
652
+ if (replacement.end > previousStart) fail("overlapping-edit", "Overlapping source edits are not supported");
653
+ previousStart = replacement.start;
654
+ const replacementLines = Array.isArray(replacement.lines)
655
+ ? replacement.lines
656
+ : String(replacement.text ?? "").split(/\r?\n/);
657
+ document.lines.splice(replacement.start, replacement.end - replacement.start, ...replacementLines);
658
+ }
659
+ return joinLines(document.lines, document.newline);
660
+ }
661
+
662
+ export function insertLines(document, index, lines) {
663
+ return applyLineReplacements(document, [{ start: index, end: index, lines }]);
664
+ }
665
+
666
+ export function replaceBlock(document, block, lines) {
667
+ return applyLineReplacements(document, [{ start: block.start, end: block.end, lines }]);
668
+ }
669
+
670
+ export function replaceFieldInBlock(document, block, fieldName, value) {
671
+ const field = block.fields.get(fieldName);
672
+ if (!field) fail("missing-field", `Missing field ${fieldName} in ${block.path}`);
673
+ const fieldEnd =
674
+ [...block.fields.values()]
675
+ .filter((candidate) => candidate.start > field.start)
676
+ .sort((left, right) => left.start - right.start)[0]?.start ?? block.end;
677
+ const style = block.fieldStyle ?? "expanded";
678
+ const lines = formatField(fieldName, value, style);
679
+ if (style !== "compact") lines.push("");
680
+ return applyLineReplacements(document, [{ start: field.start, end: fieldEnd, lines }]);
681
+ }
682
+
683
+ export function nextEntityId(document, kind) {
684
+ return nextId(
685
+ allBlocks(document)
686
+ .map((block) => block.id)
687
+ .filter(Boolean),
688
+ kind,
689
+ );
690
+ }
691
+
692
+ export function findFutureItem(document, title, kind = null) {
693
+ const matches = document.future.items.filter((item) => item.title === title && (!kind || item.kind === kind));
694
+ if (matches.length === 0) fail("missing-item", `No Future Work item matched: ${title}`);
695
+ if (matches.length > 1) fail("ambiguous-item", `Future Work title matched more than once: ${title}`);
696
+ return matches[0];
697
+ }
698
+
699
+ export function findEntity(document, id, kind = null) {
700
+ const candidates = allBlocks(document).filter((block) => block.id === id && (!kind || block.kind === kind));
701
+ if (candidates.length === 0) fail("missing-entity", `No entity matched ${id}`);
702
+ if (candidates.length > 1) fail("ambiguous-entity", `Entity ID matched more than once: ${id}`);
703
+ return candidates[0];
704
+ }
705
+
706
+ export function sectionContentLines(document, sectionName) {
707
+ const section = document.sections[sectionName];
708
+ return document.lines.slice(section.headingIndex, section.end);
709
+ }
710
+
711
+ export function activeDecisionBlocks(document) {
712
+ return document.history.decisions.blocks.filter((block) => fieldValue(block, "State") === "active");
713
+ }