@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
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery requirements rollup engine (discovery-v2 Phase 1, REQM-04, D-05..D-08).
4
+ *
5
+ * Parses the per-engagement `.planning/discovery/_requirements.md` artifact
6
+ * (grammar single-source: pp-core/references/discovery-id-conventions.md) and
7
+ * computes per-prefix × per-disposition totals with explicit ID enumeration
8
+ * per bucket, plus the per-prefix next mintable ID (D-12: derived, never stored).
9
+ *
10
+ * Phase 2 (REQM-05, D-26/D-27): validates the Provenance cell — origin
11
+ * (accession cite / session ref / decision ID) plus an optional closed-verb
12
+ * relation suffix (`refines` / `absorbed into`) whose target requirement ID
13
+ * must exist in the same table. Grammar single-source: same reference file.
14
+ *
15
+ * Phase 3 (D-44), extended by Phase 4 (D-50): a Prefix Registry row may not
16
+ * claim one of the six reserved prefixes (`D-` `A-` `SP-` `DG-` `RS-` `SCR-`)
17
+ * — they all match the requirement-ID regex, so registering one collides with
18
+ * the decision / action / sparring / data-gathering / research / screen ID
19
+ * families. Reserved set single-source: same reference file.
20
+ *
21
+ * Fail-loud contract (D-08): any unparseable row, malformed/duplicate ID,
22
+ * unknown prefix, out-of-enum MoSCoW/Disposition, or strike/disposition
23
+ * inconsistency is a structured error naming the offending row. There is no
24
+ * warn-and-skip — a row the parser cannot count is drift by definition.
25
+ *
26
+ * The command is read-only (D-06): it emits JSON plus a rendered markdown
27
+ * totals table; the workflow replaces the `<!-- rollup:auto -->` block with
28
+ * that output verbatim. `--check` (D-07) recomputes and exits non-zero when
29
+ * the written block disagrees with the rows.
30
+ */
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ const node_fs_1 = __importDefault(require("node:fs"));
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const io = require("./io.cjs");
38
+ const { output, error, ERROR_REASON } = io;
39
+ // ─── Constants ────────────────────────────────────────────────────────────────
40
+ const DISPOSITIONS = Object.freeze({ MVP: 'MVP', LATER: 'Later', DROPPED: 'Dropped' });
41
+ const MOSCOW = Object.freeze({ MUST: 'Must', SHOULD: 'Should', COULD: 'Could', WONT: 'Wont' });
42
+ // D-44 (discovery-v2 Phase 3): these six all match the requirement-ID regex, so
43
+ // registering one as a domain prefix collides with the decision/action/research
44
+ // ID families. See references/discovery-id-conventions.md.
45
+ // D-50 (discovery-v2 Phase 4) added `SCR-`: screen IDs are a flat global
46
+ // `SCR-<nn>` sequence, so an engagement registering `SCR-` as a domain prefix
47
+ // would make `SCR-01` ambiguous between the screen inventory and the
48
+ // requirements table.
49
+ const RESERVED_PREFIXES = new Set(['D-', 'A-', 'SP-', 'DG-', 'RS-', 'SCR-']);
50
+ const ROW_ID_RE = /^([A-Z][A-Z0-9]*-)(\d+)$/;
51
+ const DROPPED_RE = /^Dropped \((D-\d{2}-\d+)\)$/;
52
+ const SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
53
+ const ROLLUP_OPEN = '<!-- rollup:auto -->';
54
+ const ROLLUP_CLOSE = '<!-- /rollup:auto -->';
55
+ const REGISTRY_PREFIX_RE = /^[A-Z][A-Z0-9]*-$/;
56
+ const STRIKE_RE = /^~~.+~~$/;
57
+ // Provenance cell grammar (REQM-05, D-26/D-27) — anchored, non-backtracking,
58
+ // applied to already-split single cells (T-01-02 idiom carried forward; the
59
+ // accession locator is bounded to 80 chars, T-02-02). Separator is
60
+ // space + middle dot U+00B7 + space.
61
+ const PROVENANCE_SEP = ' · ';
62
+ const ACCESSION_CITE_RE = /^\[(\d{2})( §[^\]]{1,80})?\]$/;
63
+ const SESSION_REF_RE = /^S\d+$/;
64
+ const PROVENANCE_DECISION_RE = /^D-\d{2}-\d+$/;
65
+ const RELATION_RE = /^(refines|absorbed into) ([A-Z][A-Z0-9]*-\d+)$/;
66
+ const MOSCOW_SET = new Set(Object.values(MOSCOW));
67
+ const REGISTRY_HEADING = '## Prefix Registry';
68
+ const REQUIREMENTS_HEADING = '## Requirements';
69
+ // ─── Parsing ──────────────────────────────────────────────────────────────────
70
+ /** Split a pipe-table line into trimmed cell strings (outer pipes stripped). */
71
+ function splitCells(line) {
72
+ const trimmed = line.trim();
73
+ const inner = trimmed.replace(/^\|/, '').replace(/\|$/, '');
74
+ return inner.split('|').map(c => c.trim());
75
+ }
76
+ /**
77
+ * Collect the table-row lines belonging to the section under `heading`,
78
+ * stopping at the next `## ` heading (or, for Requirements, the rollup
79
+ * marker — defensive; the canonical template puts markers under their own
80
+ * heading). Returns null when the heading itself is absent.
81
+ */
82
+ function sectionRows(lines, heading) {
83
+ const start = lines.findIndex(l => l.trim() === heading);
84
+ if (start === -1)
85
+ return null;
86
+ const rows = [];
87
+ for (let i = start + 1; i < lines.length; i += 1) {
88
+ const line = lines[i];
89
+ const trimmed = line.trim();
90
+ if (trimmed.startsWith('## ') || trimmed === ROLLUP_OPEN)
91
+ break;
92
+ if (!trimmed.includes('|'))
93
+ continue;
94
+ // Skip markdown table separator rows and header rows immediately preceding them.
95
+ if (SEPARATOR_ROW_RE.test(line))
96
+ continue;
97
+ if (i + 1 < lines.length && SEPARATOR_ROW_RE.test(lines[i + 1]))
98
+ continue;
99
+ rows.push({ line, num: i + 1 });
100
+ }
101
+ return rows;
102
+ }
103
+ /**
104
+ * Parse a `_requirements.md` artifact into registry entries and requirement
105
+ * rows. Fail-loud (D-08): ALL grammar violations are collected before the
106
+ * ok:false return — never warn-and-skip.
107
+ */
108
+ function parseRequirementsArtifact(content) {
109
+ const errors = [];
110
+ if (!content || typeof content !== 'string') {
111
+ return { ok: false, errors: [{ row: 0, cell: '', reason: 'missing_section' }] };
112
+ }
113
+ const lines = content.split(/\r?\n/);
114
+ const registryRows = sectionRows(lines, REGISTRY_HEADING);
115
+ const requirementRows = sectionRows(lines, REQUIREMENTS_HEADING);
116
+ if (registryRows === null)
117
+ errors.push({ row: 0, cell: REGISTRY_HEADING, reason: 'missing_section' });
118
+ if (requirementRows === null)
119
+ errors.push({ row: 0, cell: REQUIREMENTS_HEADING, reason: 'missing_section' });
120
+ if (registryRows === null || requirementRows === null)
121
+ return { ok: false, errors };
122
+ const registry = [];
123
+ // Registered prefixes match ^[A-Z][A-Z0-9]*-$ (validated below), which
124
+ // excludes __proto__/constructor-shaped keys; lookups still go through a
125
+ // Set/Map, never a plain {} keyed by parsed content (engine convention).
126
+ const registeredPrefixes = new Set();
127
+ for (const { line, num } of registryRows) {
128
+ const cells = splitCells(line);
129
+ const prefix = cells[0] ?? '';
130
+ if (!REGISTRY_PREFIX_RE.test(prefix)) {
131
+ errors.push({ row: num, cell: prefix, reason: 'bad_registry_row' });
132
+ continue;
133
+ }
134
+ // D-44: ordering is load-bearing — a reserved prefix must never reach
135
+ // registeredPrefixes, or its requirement rows parse cleanly and only the
136
+ // registry row errors, which reads confusingly.
137
+ if (RESERVED_PREFIXES.has(prefix)) {
138
+ errors.push({ row: num, cell: prefix, reason: 'reserved_prefix' });
139
+ continue;
140
+ }
141
+ registry.push({ prefix, domain: cells[1] ?? '' });
142
+ registeredPrefixes.add(prefix);
143
+ }
144
+ const rows = [];
145
+ const seenIds = new Set();
146
+ for (const { line, num } of requirementRows) {
147
+ const cells = splitCells(line);
148
+ const idCell = cells[0] ?? '';
149
+ const reqCell = cells[1] ?? '';
150
+ const moscowCell = cells[2] ?? '';
151
+ const dispositionCell = cells[3] ?? '';
152
+ let rowErrors = 0;
153
+ // ID chain: grammar → registry membership → uniqueness. First failure
154
+ // wins for this axis; the row is still checked on the other axes.
155
+ let prefix = '';
156
+ let seq = 0;
157
+ const idMatch = ROW_ID_RE.exec(idCell);
158
+ if (!idMatch) {
159
+ errors.push({ row: num, cell: idCell, reason: 'bad_id' });
160
+ rowErrors += 1;
161
+ }
162
+ else if (!registeredPrefixes.has(idMatch[1])) {
163
+ errors.push({ row: num, cell: idCell, reason: 'unknown_prefix' });
164
+ rowErrors += 1;
165
+ }
166
+ else if (seenIds.has(idCell)) {
167
+ errors.push({ row: num, cell: idCell, reason: 'duplicate_id' });
168
+ rowErrors += 1;
169
+ }
170
+ else {
171
+ seenIds.add(idCell);
172
+ prefix = idMatch[1];
173
+ seq = parseInt(idMatch[2], 10);
174
+ }
175
+ // MoSCoW: closed enum, case-normalized on parse (D-03).
176
+ const moscow = moscowCell
177
+ ? moscowCell.charAt(0).toUpperCase() + moscowCell.slice(1).toLowerCase()
178
+ : '';
179
+ if (!MOSCOW_SET.has(moscow)) {
180
+ errors.push({ row: num, cell: moscowCell, reason: 'bad_moscow' });
181
+ rowErrors += 1;
182
+ }
183
+ // Disposition: exactly MVP, Later, or Dropped (D-<ss>-<n>). Bare
184
+ // `Dropped` without a killing decision ID is an error (D-03, D-04).
185
+ let disposition = '';
186
+ let droppedDecisionId = null;
187
+ if (dispositionCell === DISPOSITIONS.MVP || dispositionCell === DISPOSITIONS.LATER) {
188
+ disposition = dispositionCell;
189
+ }
190
+ else {
191
+ const dm = DROPPED_RE.exec(dispositionCell);
192
+ if (dm) {
193
+ disposition = DISPOSITIONS.DROPPED;
194
+ droppedDecisionId = dm[1];
195
+ }
196
+ else {
197
+ errors.push({ row: num, cell: dispositionCell, reason: 'bad_disposition' });
198
+ rowErrors += 1;
199
+ }
200
+ }
201
+ // Strike consistency: ~~...~~ if-and-only-if Dropped (checked only when
202
+ // the disposition itself parsed — a bad disposition already errored).
203
+ if (disposition) {
204
+ const struck = STRIKE_RE.test(reqCell);
205
+ const dropped = disposition === DISPOSITIONS.DROPPED;
206
+ if (struck !== dropped) {
207
+ errors.push({ row: num, cell: reqCell, reason: 'strike_mismatch' });
208
+ rowErrors += 1;
209
+ }
210
+ }
211
+ // Provenance (REQM-05, D-26): origin plus an optional single relation
212
+ // suffix, split on the exact ` · ` separator. An empty or unparseable
213
+ // cell is a structured error — every requirement answers "where did you
214
+ // come from"; no warn-and-skip (D-08). Header text is not parsed
215
+ // (positional contract), so pre-rename artifacts whose fifth cells hold
216
+ // valid decision/session refs keep parsing.
217
+ const provenanceCell = (cells[4] ?? '').trim();
218
+ let provenance = null;
219
+ const parts = provenanceCell.split(PROVENANCE_SEP);
220
+ if (parts.length > 2 || parts[0] === '') {
221
+ errors.push({ row: num, cell: provenanceCell, reason: 'bad_provenance' });
222
+ rowErrors += 1;
223
+ }
224
+ else {
225
+ const origin = parts[0];
226
+ let kind = null;
227
+ if (ACCESSION_CITE_RE.test(origin))
228
+ kind = 'accession';
229
+ else if (SESSION_REF_RE.test(origin))
230
+ kind = 'session';
231
+ else if (PROVENANCE_DECISION_RE.test(origin))
232
+ kind = 'decision';
233
+ let relation = null;
234
+ let relationOk = true;
235
+ if (parts.length === 2) {
236
+ const rm = RELATION_RE.exec(parts[1]);
237
+ if (rm) {
238
+ relation = { verb: rm[1] === 'refines' ? 'refines' : 'absorbed into', target: rm[2] };
239
+ }
240
+ else {
241
+ relationOk = false;
242
+ }
243
+ }
244
+ if (kind === null || !relationOk) {
245
+ errors.push({ row: num, cell: provenanceCell, reason: 'bad_provenance' });
246
+ rowErrors += 1;
247
+ }
248
+ else {
249
+ provenance = { origin, kind, relation };
250
+ }
251
+ }
252
+ if (rowErrors === 0 && provenance !== null) {
253
+ rows.push({
254
+ id: idCell,
255
+ prefix,
256
+ seq,
257
+ requirement: reqCell,
258
+ moscow,
259
+ disposition,
260
+ droppedDecisionId,
261
+ provenance,
262
+ line: num,
263
+ });
264
+ }
265
+ }
266
+ // Relation-target existence post-pass (D-27): a refines/absorbed-into
267
+ // target must be an ID present in the same table. Any disposition counts —
268
+ // existence is the only check.
269
+ for (const row of rows) {
270
+ const relation = row.provenance.relation;
271
+ if (relation && !seenIds.has(relation.target)) {
272
+ errors.push({
273
+ row: row.line,
274
+ cell: `${row.provenance.origin}${PROVENANCE_SEP}${relation.verb} ${relation.target}`,
275
+ reason: 'unknown_relation_target',
276
+ });
277
+ }
278
+ }
279
+ if (errors.length > 0)
280
+ return { ok: false, errors };
281
+ return { ok: true, registry, rows };
282
+ }
283
+ // ─── Rollup computation ───────────────────────────────────────────────────────
284
+ /**
285
+ * Bucket parsed rows per registry prefix × disposition. Buckets are keyed in
286
+ * registry order; IDs sort ascending by numeric sequence. next_id per prefix
287
+ * is max sequence + 1 (1 for a prefix with zero rows) — D-12: the table IS
288
+ * the counter.
289
+ */
290
+ function computeRollup(rows, registry) {
291
+ const bucketMap = new Map();
292
+ for (const entry of registry) {
293
+ bucketMap.set(entry.prefix, { MVP: [], Later: [], Dropped: [] });
294
+ }
295
+ for (const row of rows) {
296
+ let bucket = bucketMap.get(row.prefix);
297
+ if (!bucket) {
298
+ // Defensive: parse guarantees registry membership, but keep unknown
299
+ // prefixes countable rather than silently dropping rows.
300
+ bucket = { MVP: [], Later: [], Dropped: [] };
301
+ bucketMap.set(row.prefix, bucket);
302
+ }
303
+ if (row.disposition === DISPOSITIONS.MVP)
304
+ bucket.MVP.push(row);
305
+ else if (row.disposition === DISPOSITIONS.LATER)
306
+ bucket.Later.push(row);
307
+ else
308
+ bucket.Dropped.push(row);
309
+ }
310
+ const counts = {};
311
+ const nextId = {};
312
+ const totals = { MVP: 0, Later: 0, Dropped: 0, total: 0 };
313
+ const bySeq = (a, b) => a.seq - b.seq;
314
+ for (const [prefix, bucket] of bucketMap) {
315
+ counts[prefix] = {
316
+ MVP: bucket.MVP.slice().sort(bySeq).map(r => r.id),
317
+ Later: bucket.Later.slice().sort(bySeq).map(r => r.id),
318
+ Dropped: bucket.Dropped.slice().sort(bySeq).map(r => r.id),
319
+ };
320
+ totals.MVP += bucket.MVP.length;
321
+ totals.Later += bucket.Later.length;
322
+ totals.Dropped += bucket.Dropped.length;
323
+ const maxSeq = [...bucket.MVP, ...bucket.Later, ...bucket.Dropped]
324
+ .reduce((max, r) => (r.seq > max ? r.seq : max), 0);
325
+ nextId[prefix] = maxSeq + 1;
326
+ }
327
+ totals.total = totals.MVP + totals.Later + totals.Dropped;
328
+ return { counts, totals, next_id: nextId };
329
+ }
330
+ // ─── Rendering ────────────────────────────────────────────────────────────────
331
+ function renderCell(ids) {
332
+ return ids.length > 0 ? `${ids.length} (${ids.join(', ')})` : '0';
333
+ }
334
+ /**
335
+ * Render the machine-owned totals table (D-06). Deterministic: registry
336
+ * order, then a bold Total row — the workflow pastes this verbatim between
337
+ * the rollup:auto markers.
338
+ */
339
+ function renderRollupTable(rollup) {
340
+ const lines = [
341
+ '| Prefix | MVP | Later | Dropped | Total |',
342
+ '|--------|-----|-------|---------|-------|',
343
+ ];
344
+ for (const prefix of Object.keys(rollup.counts)) {
345
+ const bucket = rollup.counts[prefix];
346
+ const rowTotal = bucket.MVP.length + bucket.Later.length + bucket.Dropped.length;
347
+ lines.push(`| ${prefix} | ${renderCell(bucket.MVP)} | ${renderCell(bucket.Later)} | ${renderCell(bucket.Dropped)} | ${rowTotal} |`);
348
+ }
349
+ const t = rollup.totals;
350
+ lines.push(`| **Total** | ${t.MVP} | ${t.Later} | ${t.Dropped} | ${t.total} |`);
351
+ return lines.join('\n');
352
+ }
353
+ // ─── Check (D-07) ─────────────────────────────────────────────────────────────
354
+ /** Whitespace-normalize for block comparison: trim lines, collapse runs, drop blanks. */
355
+ function normalizeBlock(text) {
356
+ return text
357
+ .split(/\r?\n/)
358
+ .map(l => l.trim().replace(/\s+/g, ' '))
359
+ .filter(l => l.length > 0)
360
+ .join('\n');
361
+ }
362
+ function countOccurrences(haystack, needle) {
363
+ return haystack.split(needle).length - 1;
364
+ }
365
+ /**
366
+ * Verify the written rollup:auto block against totals recomputed from the
367
+ * rows (D-07). Missing/duplicated/out-of-order markers and parse failures
368
+ * are structured failures too — a block that cannot be located or a table
369
+ * that cannot be counted is drift by definition.
370
+ */
371
+ function checkRollup(content) {
372
+ const parsed = parseRequirementsArtifact(content);
373
+ if (!parsed.ok)
374
+ return { ok: false, reason: 'parse_error', errors: parsed.errors };
375
+ const openCount = countOccurrences(content, ROLLUP_OPEN);
376
+ const closeCount = countOccurrences(content, ROLLUP_CLOSE);
377
+ if (openCount !== 1 || closeCount !== 1) {
378
+ return {
379
+ ok: false,
380
+ reason: 'rollup_markers',
381
+ detail: `expected exactly one ${ROLLUP_OPEN} and one ${ROLLUP_CLOSE} (found ${openCount}/${closeCount})`,
382
+ };
383
+ }
384
+ const openIdx = content.indexOf(ROLLUP_OPEN);
385
+ const closeIdx = content.indexOf(ROLLUP_CLOSE);
386
+ if (closeIdx < openIdx) {
387
+ return { ok: false, reason: 'rollup_markers', detail: 'close marker precedes open marker' };
388
+ }
389
+ const rollup = computeRollup(parsed.rows, parsed.registry);
390
+ const expected = normalizeBlock(renderRollupTable(rollup));
391
+ const actual = normalizeBlock(content.slice(openIdx + ROLLUP_OPEN.length, closeIdx));
392
+ if (expected !== actual) {
393
+ return { ok: false, reason: 'rollup_mismatch', expected, actual };
394
+ }
395
+ return { ok: true, totals: rollup.totals };
396
+ }
397
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
398
+ function formatRollupErrors(errors) {
399
+ return errors.map(e => ` row ${e.row}: ${e.reason}${e.cell ? ` (${e.cell})` : ''}`).join('\n');
400
+ }
401
+ /**
402
+ * CLI adapter for `pp-tools discovery rollup <path> [--check]`.
403
+ *
404
+ * Exit semantics (D-06/D-07/D-08): plain mode is the recompute source — it
405
+ * exits 0 with JSON + rendered table even when the written block is stale,
406
+ * but fails loud (non-zero) on any parse error. `--check` exits non-zero on
407
+ * drift, marker problems, or parse errors, via error() which honors
408
+ * --json-errors.
409
+ */
410
+ function cmdDiscoveryRollup(cwd, args, raw) {
411
+ const check = args.includes('--check');
412
+ for (const a of args.slice(2)) {
413
+ if (a.startsWith('--') && a !== '--check') {
414
+ error(`discovery rollup: unknown flag ${a}`, ERROR_REASON.USAGE);
415
+ }
416
+ }
417
+ const target = args.slice(2).find(a => !a.startsWith('--'));
418
+ if (!target) {
419
+ error('Usage: pp-tools discovery rollup <path> [--check]', ERROR_REASON.USAGE);
420
+ return; // unreachable — error() exits
421
+ }
422
+ // Path-traversal guard: the resolved target must stay under the project
423
+ // root (threat T-01-01; mirrors the workstream-inventory guard convention).
424
+ const root = node_path_1.default.resolve(cwd);
425
+ const resolved = node_path_1.default.resolve(cwd, target);
426
+ if (!resolved.startsWith(root + node_path_1.default.sep)) {
427
+ error('discovery rollup: path escapes project root', ERROR_REASON.USAGE);
428
+ }
429
+ let content = '';
430
+ try {
431
+ content = node_fs_1.default.readFileSync(resolved, 'utf-8');
432
+ }
433
+ catch {
434
+ error(`discovery rollup: cannot read ${target}`, ERROR_REASON.USAGE);
435
+ }
436
+ if (check) {
437
+ const result = checkRollup(content);
438
+ if (!result.ok) {
439
+ if (result.reason === 'parse_error') {
440
+ error(`discovery rollup --check: parse failed:\n${formatRollupErrors(result.errors)}`, ERROR_REASON.DISCOVERY_ROLLUP_PARSE_FAILED);
441
+ return; // unreachable — error() exits; return keeps tsc narrowing sound
442
+ }
443
+ const detail = result.reason === 'rollup_markers'
444
+ ? result.detail
445
+ : `expected:\n${result.expected}\nactual:\n${result.actual}`;
446
+ error(`discovery rollup --check: ${result.reason}\n${detail}`, ERROR_REASON.DISCOVERY_ROLLUP_MISMATCH);
447
+ return; // unreachable — error() exits
448
+ }
449
+ output({ ok: true, checked: true, totals: result.totals }, raw);
450
+ return;
451
+ }
452
+ const parsed = parseRequirementsArtifact(content);
453
+ if (!parsed.ok) {
454
+ error(`discovery rollup: parse failed:\n${formatRollupErrors(parsed.errors)}`, ERROR_REASON.DISCOVERY_ROLLUP_PARSE_FAILED);
455
+ return; // unreachable — error() exits
456
+ }
457
+ const rollup = computeRollup(parsed.rows, parsed.registry);
458
+ const table = renderRollupTable(rollup);
459
+ output({
460
+ ok: true,
461
+ registry: parsed.registry,
462
+ counts: rollup.counts,
463
+ totals: rollup.totals,
464
+ next_id: rollup.next_id,
465
+ table,
466
+ }, raw, table);
467
+ }
468
+ module.exports = {
469
+ DISPOSITIONS,
470
+ MOSCOW,
471
+ parseRequirementsArtifact,
472
+ computeRollup,
473
+ renderRollupTable,
474
+ checkRollup,
475
+ cmdDiscoveryRollup,
476
+ };