@pareto4/pp-core 1.11.1 → 1.12.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 (255) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +31 -0
  3. package/agents/pp-code-fixer.md +1 -1
  4. package/agents/pp-debug-session-manager.md +1 -1
  5. package/agents/pp-debugger.md +1 -1
  6. package/agents/pp-executor.md +20 -10
  7. package/agents/pp-intel-updater.md +1 -1
  8. package/agents/pp-phase-researcher.md +1 -1
  9. package/agents/pp-plan-checker.md +5 -5
  10. package/agents/pp-planner.md +10 -24
  11. package/agents/pp-project-researcher.md +1 -1
  12. package/agents/pp-research-synthesizer.md +1 -1
  13. package/agents/pp-ui-researcher.md +1 -1
  14. package/agents/pp-verifier.md +8 -4
  15. package/bin/install.js +429 -82
  16. package/commands/pp-autonomous.md +8 -6
  17. package/commands/pp-discovery-to-build.md +1 -1
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-discuss-phase.md +2 -2
  20. package/commands/pp-execute-phase.md +3 -1
  21. package/commands/pp-graphify.md +5 -5
  22. package/commands/pp-import.md +1 -1
  23. package/commands/pp-map-codebase.md +1 -1
  24. package/commands/pp-ns-project.md +2 -3
  25. package/commands/pp-ns-workflow.md +1 -1
  26. package/commands/pp-plan-phase.md +2 -1
  27. package/commands/pp-verify-work.md +2 -1
  28. package/gemini-extension.json +1 -1
  29. package/hooks/dist/pp-cursor-session-start.js +2 -2
  30. package/hooks/dist/pp-session-state.sh +1 -1
  31. package/hooks/dist/pp-statusline.js +21 -2
  32. package/hooks/pp-cursor-session-start.js +2 -2
  33. package/hooks/pp-session-state.sh +1 -1
  34. package/hooks/pp-statusline.js +21 -2
  35. package/package.json +18 -5
  36. package/pareto-skills/pp-project-setup/SKILL.md +5 -63
  37. package/pp-core/bin/detect-build-commands.sh +182 -0
  38. package/pp-core/bin/lib/capability-registry.cjs +65 -7
  39. package/pp-core/bin/lib/check-command-router.cjs +79 -38
  40. package/pp-core/bin/lib/clusters.cjs +2 -3
  41. package/pp-core/bin/lib/command-aliases.cjs +36 -0
  42. package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
  43. package/pp-core/bin/lib/commands.cjs +21 -6
  44. package/pp-core/bin/lib/compose-bindings.cjs +70 -6
  45. package/pp-core/bin/lib/config-schema.cjs +16 -0
  46. package/pp-core/bin/lib/config.cjs +142 -27
  47. package/pp-core/bin/lib/core-utils.cjs +43 -7
  48. package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
  49. package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
  50. package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
  51. package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
  52. package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
  53. package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
  54. package/pp-core/bin/lib/discovery-table.cjs +98 -0
  55. package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
  56. package/pp-core/bin/lib/frontmatter.cjs +17 -2
  57. package/pp-core/bin/lib/init-command-router.cjs +8 -5
  58. package/pp-core/bin/lib/init-contract.cjs +93 -4
  59. package/pp-core/bin/lib/init.cjs +124 -134
  60. package/pp-core/bin/lib/install-profiles.cjs +5 -0
  61. package/pp-core/bin/lib/install-run.cjs +536 -0
  62. package/pp-core/bin/lib/io.cjs +15 -0
  63. package/pp-core/bin/lib/loop-resolver.cjs +24 -0
  64. package/pp-core/bin/lib/milestone.cjs +345 -48
  65. package/pp-core/bin/lib/phase-command-router.cjs +23 -1
  66. package/pp-core/bin/lib/phase-locator.cjs +15 -6
  67. package/pp-core/bin/lib/phase-scope.cjs +91 -0
  68. package/pp-core/bin/lib/phase-tier.cjs +467 -0
  69. package/pp-core/bin/lib/phase.cjs +247 -21
  70. package/pp-core/bin/lib/phases-command-router.cjs +2 -0
  71. package/pp-core/bin/lib/plan-graph.cjs +368 -0
  72. package/pp-core/bin/lib/planning-workspace.cjs +53 -0
  73. package/pp-core/bin/lib/platform-bindings.cjs +32 -2
  74. package/pp-core/bin/lib/profile-output.cjs +11 -3
  75. package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
  76. package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
  77. package/pp-core/bin/lib/roadmap.cjs +141 -39
  78. package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
  79. package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
  80. package/pp-core/bin/lib/state-command-router.cjs +82 -13
  81. package/pp-core/bin/lib/state-document.cjs +124 -7
  82. package/pp-core/bin/lib/state.cjs +784 -257
  83. package/pp-core/bin/lib/template.cjs +22 -6
  84. package/pp-core/bin/lib/vault-command-router.cjs +57 -6
  85. package/pp-core/bin/lib/vault-practices.cjs +6 -36
  86. package/pp-core/bin/lib/vault-write-back.cjs +20 -4
  87. package/pp-core/bin/lib/vault.cjs +555 -23
  88. package/pp-core/bin/lib/verify.cjs +53 -31
  89. package/pp-core/bin/lib/workstream.cjs +1 -1
  90. package/pp-core/bin/lib/worktree-safety.cjs +26 -15
  91. package/pp-core/bin/portable-timeout.sh +89 -0
  92. package/pp-core/bin/pp-tools.cjs +149 -9
  93. package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
  94. package/pp-core/bin/shared/config-schema.manifest.json +3 -0
  95. package/pp-core/references/autonomous-best-effort.md +461 -0
  96. package/pp-core/references/autonomous-lifecycle.md +146 -0
  97. package/pp-core/references/autonomous-smart-discuss.md +16 -12
  98. package/pp-core/references/checkpoints.md +1 -1
  99. package/pp-core/references/discovery-id-conventions.md +202 -0
  100. package/pp-core/references/discovery-rebaseline.md +254 -0
  101. package/pp-core/references/discovery-research-track.md +283 -0
  102. package/pp-core/references/discovery-traceability.md +400 -0
  103. package/pp-core/references/loop-hook-dispatch.md +7 -0
  104. package/pp-core/references/mandatory-initial-read.md +1 -1
  105. package/pp-core/references/planner-gap-closure.md +1 -0
  106. package/pp-core/references/planner-human-verify-mode.md +1 -1
  107. package/pp-core/references/planner-load-graph-context.md +1 -1
  108. package/pp-core/references/planner-plan-graph.md +66 -0
  109. package/pp-core/references/planner-tdd.md +1 -0
  110. package/pp-core/references/planning-config.md +8 -0
  111. package/pp-core/references/sketch-tooling.md +4 -0
  112. package/pp-core/references/tdd.md +1 -0
  113. package/pp-core/templates/claude-md.md +1 -1
  114. package/pp-core/templates/phase-prompt.md +6 -2
  115. package/pp-core/templates/prototype-review/review.js +303 -0
  116. package/pp-core/templates/state.md +6 -0
  117. package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
  118. package/pp-core/workflows/add-backlog.md +1 -1
  119. package/pp-core/workflows/add-phase.md +1 -1
  120. package/pp-core/workflows/add-tests.md +1 -1
  121. package/pp-core/workflows/add-todo.md +1 -1
  122. package/pp-core/workflows/ai-integration-phase.md +2 -2
  123. package/pp-core/workflows/audit-fix.md +12 -16
  124. package/pp-core/workflows/audit-milestone.md +1 -1
  125. package/pp-core/workflows/audit-uat.md +1 -1
  126. package/pp-core/workflows/autonomous.md +68 -306
  127. package/pp-core/workflows/check-todos.md +1 -1
  128. package/pp-core/workflows/cleanup.md +1 -1
  129. package/pp-core/workflows/code-review-fix.md +1 -1
  130. package/pp-core/workflows/code-review.md +19 -7
  131. package/pp-core/workflows/complete-milestone.md +41 -60
  132. package/pp-core/workflows/debug.md +1 -1
  133. package/pp-core/workflows/diagnose-issues.md +1 -1
  134. package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
  135. package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
  136. package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
  137. package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
  138. package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
  139. package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
  140. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
  141. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
  142. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
  143. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
  144. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
  145. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
  146. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
  147. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
  148. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
  149. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
  150. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
  151. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
  152. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
  153. package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
  154. package/pp-core/workflows/discovery-sprint.md +79 -1714
  155. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
  156. package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
  157. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
  158. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
  159. package/pp-core/workflows/discovery-to-build.md +21 -238
  160. package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
  161. package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
  162. package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
  163. package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
  164. package/pp-core/workflows/discuss-phase.md +9 -2
  165. package/pp-core/workflows/do.md +4 -4
  166. package/pp-core/workflows/docs-update.md +1 -1
  167. package/pp-core/workflows/edit-phase.md +1 -1
  168. package/pp-core/workflows/eval-review.md +1 -1
  169. package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
  170. package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
  171. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  172. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
  173. package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
  174. package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
  175. package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
  176. package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
  177. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
  178. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
  179. package/pp-core/workflows/execute-phase.md +102 -216
  180. package/pp-core/workflows/execute-plan.md +6 -2
  181. package/pp-core/workflows/explore.md +1 -1
  182. package/pp-core/workflows/extract-learnings.md +1 -1
  183. package/pp-core/workflows/forensics.md +4 -4
  184. package/pp-core/workflows/graduation.md +1 -1
  185. package/pp-core/workflows/health.md +1 -1
  186. package/pp-core/workflows/help/modes/brief.md +1 -0
  187. package/pp-core/workflows/help/modes/default.md +2 -1
  188. package/pp-core/workflows/help/modes/full.md +12 -5
  189. package/pp-core/workflows/import.md +1 -1
  190. package/pp-core/workflows/ingest-docs.md +1 -1
  191. package/pp-core/workflows/init.md +18 -2
  192. package/pp-core/workflows/insert-phase.md +1 -1
  193. package/pp-core/workflows/list-workspaces.md +1 -1
  194. package/pp-core/workflows/manager.md +3 -3
  195. package/pp-core/workflows/map-codebase.md +2 -2
  196. package/pp-core/workflows/milestone-summary.md +4 -4
  197. package/pp-core/workflows/mvp-phase.md +1 -1
  198. package/pp-core/workflows/new-milestone.md +7 -8
  199. package/pp-core/workflows/new-project.md +6 -1
  200. package/pp-core/workflows/new-workspace.md +1 -1
  201. package/pp-core/workflows/next.md +2 -2
  202. package/pp-core/workflows/note.md +2 -2
  203. package/pp-core/workflows/pause-work.md +1 -1
  204. package/pp-core/workflows/plan-milestone-gaps.md +1 -1
  205. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
  206. package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
  207. package/pp-core/workflows/plan-phase.md +43 -167
  208. package/pp-core/workflows/plan-review-convergence.md +2 -2
  209. package/pp-core/workflows/plant-seed.md +1 -1
  210. package/pp-core/workflows/pr-branch.md +1 -1
  211. package/pp-core/workflows/profile-user.md +1 -1
  212. package/pp-core/workflows/progress.md +3 -3
  213. package/pp-core/workflows/quick.md +3 -1
  214. package/pp-core/workflows/remove-phase.md +1 -1
  215. package/pp-core/workflows/remove-workspace.md +1 -1
  216. package/pp-core/workflows/resume-project.md +6 -12
  217. package/pp-core/workflows/review.md +1 -1
  218. package/pp-core/workflows/scan.md +1 -1
  219. package/pp-core/workflows/secure-phase.md +1 -1
  220. package/pp-core/workflows/settings-advanced.md +1 -1
  221. package/pp-core/workflows/settings-integrations.md +1 -1
  222. package/pp-core/workflows/settings.md +1 -1
  223. package/pp-core/workflows/ship.md +25 -8
  224. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  225. package/pp-core/workflows/sketch.md +2 -1
  226. package/pp-core/workflows/spec-phase.md +1 -1
  227. package/pp-core/workflows/spike-wrap-up.md +1 -1
  228. package/pp-core/workflows/spike.md +1 -1
  229. package/pp-core/workflows/stats.md +2 -2
  230. package/pp-core/workflows/thread.md +1 -1
  231. package/pp-core/workflows/transition.md +5 -9
  232. package/pp-core/workflows/ui-phase.md +23 -2
  233. package/pp-core/workflows/ui-review.md +1 -1
  234. package/pp-core/workflows/ultraplan-phase.md +2 -2
  235. package/pp-core/workflows/validate-phase.md +1 -1
  236. package/pp-core/workflows/vault-write-back.md +1 -1
  237. package/pp-core/workflows/verify-phase.md +29 -26
  238. package/pp-core/workflows/verify-work.md +14 -10
  239. package/scripts/base64-scan.sh +7 -41
  240. package/scripts/changeset/cli.cjs +25 -5
  241. package/scripts/dev-env.cjs +462 -0
  242. package/scripts/lib/allowlist-ratchet.cjs +103 -1
  243. package/scripts/lib/size-budget.cjs +311 -0
  244. package/scripts/lint-neutrality-names.cjs +694 -0
  245. package/scripts/lint-org-coordinates.allowlist.json +35 -0
  246. package/scripts/lint-org-coordinates.cjs +426 -0
  247. package/scripts/lint-skill-deps.cjs +48 -9
  248. package/scripts/lint-test-file-count.cjs +17 -2
  249. package/scripts/prompt-injection-scan.sh +41 -20
  250. package/scripts/sync-manifest-versions.cjs +41 -7
  251. package/scripts/sync-runtime-launcher.cjs +106 -1
  252. package/scripts/update-size-baseline.cjs +274 -30
  253. package/scripts/workflow-size.cjs +61 -9
  254. package/commands/pp-build-handoff.md +0 -46
  255. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
@@ -15,6 +15,11 @@ exports.stateReplaceFieldWithFallback = stateReplaceFieldWithFallback;
15
15
  exports.normalizeStateStatus = normalizeStateStatus;
16
16
  exports.computeProgressPercent = computeProgressPercent;
17
17
  exports.shouldPreserveExistingProgress = shouldPreserveExistingProgress;
18
+ exports.reconcileCompletedPhases = reconcileCompletedPhases;
19
+ exports.matchCurrentPositionSection = matchCurrentPositionSection;
20
+ exports.extractPositionalField = extractPositionalField;
21
+ exports.holdNewerActivityDate = holdNewerActivityDate;
22
+ exports.diffFrontmatterFields = diffFrontmatterFields;
18
23
  exports.normalizeProgressNumbers = normalizeProgressNumbers;
19
24
  exports.isStateTemplateDefault = isStateTemplateDefault;
20
25
  exports.stateReplaceFieldIfTemplate = stateReplaceFieldIfTemplate;
@@ -76,17 +81,20 @@ function stateExtractField(content, fieldName) {
76
81
  return tableMatch[4].trim();
77
82
  return null;
78
83
  }
84
+ // The value separator is `[ \t]*`, as in stateExtractField: `\s*` crossed a newline, so an
85
+ // EMPTY field consumed the next line. An empty field's prefix ends at the colon, so the
86
+ // writer supplies the single space the format puts there.
79
87
  function stateReplaceField(content, fieldName, newValue) {
80
88
  const escaped = escapeRegex(fieldName);
81
89
  // Bold inline format: **FieldName:** value
82
- const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
90
+ const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*[ \\t]*)(.*)`, 'i');
83
91
  if (boldPattern.test(content)) {
84
- return content.replace(boldPattern, (_match, prefix) => `${prefix}${newValue}`);
92
+ return content.replace(boldPattern, (_match, prefix) => `${prefix}${/[ \t]$/.test(prefix) ? '' : ' '}${newValue}`);
85
93
  }
86
94
  // Plain line-start format: FieldName: value
87
- const plainPattern = new RegExp(`(^${escaped}:\\s*)(.*)`, 'im');
95
+ const plainPattern = new RegExp(`(^${escaped}:[ \\t]*)(.*)`, 'im');
88
96
  if (plainPattern.test(content)) {
89
- return content.replace(plainPattern, (_match, prefix) => `${prefix}${newValue}`);
97
+ return content.replace(plainPattern, (_match, prefix) => `${prefix}${/[ \t]$/.test(prefix) ? '' : ' '}${newValue}`);
90
98
  }
91
99
  // Pipe-table format: | FieldName | value |
92
100
  // Preserve the surrounding pipe/whitespace structure; only swap the value cell.
@@ -146,6 +154,20 @@ function computeProgressPercent(completedPlans, totalPlans, completedPhases, tot
146
154
  const phaseFraction = hasPhaseData ? (completedPhases ?? 0) / (totalPhases ?? 1) : 1;
147
155
  return Math.min(100, Math.round(Math.min(planFraction, phaseFraction) * 100));
148
156
  }
157
+ /**
158
+ * Decide whether a stored `progress` block must survive a re-derivation on the
159
+ * read path (`state json`).
160
+ *
161
+ * #3242 Bug A: a curated cross-milestone aggregate legitimately counts phases and
162
+ * plans the local disk scan cannot see, and its signature is a larger TOTAL — so a
163
+ * block whose `total_phases` or `total_plans` exceeds the derived one is kept whole.
164
+ *
165
+ * 261007-ixt D-1: a block whose totals do NOT exceed the derived ones but whose
166
+ * `completed_*` / `percent` are higher is not curation — it is the inflated count
167
+ * the writer used to store (a phase whose plans all have summaries, counted
168
+ * Complete while its ROADMAP row read `Built`). Preserving it would re-report the
169
+ * false number on every read, so the derived value wins.
170
+ */
149
171
  function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
150
172
  if (!existingProgress || typeof existingProgress !== 'object')
151
173
  return false;
@@ -154,9 +176,104 @@ function shouldPreserveExistingProgress(existingProgress, derivedProgress) {
154
176
  const existing = existingProgress;
155
177
  const derived = derivedProgress;
156
178
  return (existingProgressExceedsDerived(existing, derived, 'total_phases') ||
157
- existingProgressExceedsDerived(existing, derived, 'completed_phases') ||
158
- existingProgressExceedsDerived(existing, derived, 'total_plans') ||
159
- existingProgressExceedsDerived(existing, derived, 'completed_plans'));
179
+ existingProgressExceedsDerived(existing, derived, 'total_plans'));
180
+ }
181
+ /**
182
+ * The ONE owner of `completed_phases` (261007-ixt D-1).
183
+ *
184
+ * The disk scan counts a phase complete when every PLAN has a SUMMARY — that is
185
+ * BUILT. The ROADMAP progress table's Status column says whether a phase is
186
+ * COMPLETE. When the table yields a count, the record may not claim more than it
187
+ * grants, so the result is the smaller of the two; when the table is silent
188
+ * (`null`), the disk count stands. A forged or stale table can only LOWER the
189
+ * count, never inflate it.
190
+ */
191
+ function reconcileCompletedPhases(diskCompletedPhases, roadmapCompletedPhases) {
192
+ if (roadmapCompletedPhases === null || !Number.isFinite(roadmapCompletedPhases))
193
+ return diskCompletedPhases;
194
+ return Math.min(diskCompletedPhases, roadmapCompletedPhases);
195
+ }
196
+ /**
197
+ * Match the `## Current Position` section of a STATE.md body. Group 1 is the
198
+ * section body (up to the next `##` heading or the end). The same pattern text
199
+ * `updateCurrentPositionFields` and the milestone/complete-phase writers use, so
200
+ * the reader and the writers agree on what the section is.
201
+ */
202
+ function matchCurrentPositionSection(body) {
203
+ return body.match(/##\s*Current Position\s*\n([\s\S]*?)(?=\n##|$)/i);
204
+ }
205
+ /**
206
+ * Read a POSITIONAL source field (`Phase`, `Status`, `Last activity`) for the
207
+ * frontmatter writer (261007-ixt D-2).
208
+ *
209
+ * `stateExtractField` prefers a bold `**Field:**` ANYWHERE in the body over a
210
+ * line-start `Field:`, so a `**Status:**` in a historical log entry shadowed the
211
+ * `## Current Position` line the engine itself maintains. The position section is
212
+ * where the current position lives: read it first, and fall back to the whole
213
+ * body only when the section is absent or does not carry the field (so documents
214
+ * without the section keep their reading).
215
+ */
216
+ function extractPositionalField(body, fieldName, fallbackFieldName) {
217
+ const section = matchCurrentPositionSection(body);
218
+ if (section) {
219
+ const scoped = stateExtractField(section[1], fieldName)
220
+ ?? (fallbackFieldName ? stateExtractField(section[1], fallbackFieldName) : null);
221
+ if (scoped !== null)
222
+ return scoped;
223
+ }
224
+ return stateExtractField(body, fieldName)
225
+ ?? (fallbackFieldName ? stateExtractField(body, fallbackFieldName) : null);
226
+ }
227
+ const ISO_DATE_PREFIX = /^(\d{4}-\d{2}-\d{2})/;
228
+ /**
229
+ * A record date never moves backwards (261007-ixt W3a). `last_activity` is re-derived
230
+ * from the body's `Last activity:` line on every write; when that line lags the
231
+ * frontmatter (measured on the pre-close v1.1 record: 2026-09-21 rewritten to
232
+ * 2026-09-20 by `state record-session`), the older date would overwrite the newer
233
+ * one. When both values start with an ISO date and the derived one is OLDER, the
234
+ * stored `last_activity` and its `last_activity_desc` are kept as a pair. Returns the
235
+ * frontmatter to write; the input is not modified.
236
+ */
237
+ function holdNewerActivityDate(existingFm, derivedFm) {
238
+ const stored = typeof existingFm['last_activity'] === 'string' ? existingFm['last_activity'].match(ISO_DATE_PREFIX) : null;
239
+ const derived = typeof derivedFm['last_activity'] === 'string' ? derivedFm['last_activity'].match(ISO_DATE_PREFIX) : null;
240
+ if (!stored || !derived || derived[1] >= stored[1])
241
+ return derivedFm;
242
+ const held = { ...derivedFm, last_activity: existingFm['last_activity'] };
243
+ if (existingFm['last_activity_desc'] !== undefined)
244
+ held['last_activity_desc'] = existingFm['last_activity_desc'];
245
+ else
246
+ delete held['last_activity_desc'];
247
+ return held;
248
+ }
249
+ function flattenFrontmatterForDiff(fm) {
250
+ const out = {};
251
+ if (!fm || typeof fm !== 'object')
252
+ return out;
253
+ for (const [key, value] of Object.entries(fm)) {
254
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
255
+ for (const [sub, subValue] of Object.entries(value)) {
256
+ out[`${key}.${sub}`] = subValue;
257
+ }
258
+ }
259
+ else {
260
+ out[key] = value;
261
+ }
262
+ }
263
+ return out;
264
+ }
265
+ /**
266
+ * The dotted frontmatter keys (one level of nesting flattened, e.g.
267
+ * `progress.percent`) whose value differs between two parsed frontmatter objects,
268
+ * sorted. Keys present on only one side are included; nothing is excluded —
269
+ * `last_updated` included — because a report that omits a key it changed is the
270
+ * defect 261007-ixt D-3 closes.
271
+ */
272
+ function diffFrontmatterFields(before, after) {
273
+ const a = flattenFrontmatterForDiff(before);
274
+ const b = flattenFrontmatterForDiff(after);
275
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
276
+ return [...keys].filter((key) => JSON.stringify(a[key]) !== JSON.stringify(b[key])).sort();
160
277
  }
161
278
  function normalizeProgressNumbers(progress) {
162
279
  if (!progress || typeof progress !== 'object')