@pareto4/pp-core 1.18.0 → 1.20.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 (194) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-code-fixer.md +9 -5
  3. package/agents/pp-code-reviewer.md +1 -0
  4. package/agents/pp-codebase-mapper.md +3 -0
  5. package/agents/pp-debugger.md +3 -2
  6. package/agents/pp-executor.md +16 -7
  7. package/agents/pp-integration-checker.md +1 -0
  8. package/agents/pp-phase-researcher.md +13 -1
  9. package/agents/pp-plan-checker.md +3 -0
  10. package/agents/pp-planner.md +20 -1
  11. package/agents/pp-research-synthesizer.md +1 -1
  12. package/agents/pp-ui-auditor.md +1 -0
  13. package/agents/pp-ui-researcher.md +3 -1
  14. package/agents/pp-vault-reviewer.md +2 -0
  15. package/agents/pp-verifier.md +22 -10
  16. package/commands/pp-add-tests.md +1 -1
  17. package/commands/pp-debug.md +1 -0
  18. package/commands/pp-discovery.md +3 -2
  19. package/commands/pp-init.md +4 -3
  20. package/commands/pp-quick.md +3 -0
  21. package/commands/pp-review-backlog.md +2 -1
  22. package/gemini-extension.json +1 -1
  23. package/package.json +6 -3
  24. package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
  25. package/pareto-skills/pp-project-setup/SKILL.md +18 -343
  26. package/pp-core/bin/lib/capability-registry.cjs +3 -3
  27. package/pp-core/bin/lib/check-command-router.cjs +60 -20
  28. package/pp-core/bin/lib/commands.cjs +79 -4
  29. package/pp-core/bin/lib/compose-bindings.cjs +97 -14
  30. package/pp-core/bin/lib/discovery-access.cjs +276 -0
  31. package/pp-core/bin/lib/discovery-charter.cjs +664 -0
  32. package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
  33. package/pp-core/bin/lib/discovery-entities.cjs +323 -0
  34. package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
  35. package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
  36. package/pp-core/bin/lib/discovery-migration.cjs +246 -0
  37. package/pp-core/bin/lib/discovery-phases.cjs +233 -0
  38. package/pp-core/bin/lib/discovery-processes.cjs +355 -0
  39. package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
  40. package/pp-core/bin/lib/discovery-rollup.cjs +12 -8
  41. package/pp-core/bin/lib/discovery-rules.cjs +216 -0
  42. package/pp-core/bin/lib/discovery-screens.cjs +21 -3
  43. package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
  44. package/pp-core/bin/lib/discovery-stories.cjs +273 -0
  45. package/pp-core/bin/lib/discovery-sweep.cjs +13 -4
  46. package/pp-core/bin/lib/discovery-table.cjs +55 -0
  47. package/pp-core/bin/lib/discovery-vault-check.cjs +37 -3
  48. package/pp-core/bin/lib/init-contract.cjs +48 -7
  49. package/pp-core/bin/lib/io.cjs +22 -0
  50. package/pp-core/bin/lib/vault-coordinates.cjs +1 -1
  51. package/pp-core/bin/lib/vault-index.cjs +18 -5
  52. package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
  53. package/pp-core/bin/lib/vault-update.cjs +4 -0
  54. package/pp-core/bin/lib/vault.cjs +2 -2
  55. package/pp-core/bin/pp-tools.cjs +153 -5
  56. package/pp-core/org-profile.json +1 -1
  57. package/pp-core/references/autonomous-best-effort.md +3 -2
  58. package/pp-core/references/autonomous-lifecycle.md +1 -0
  59. package/pp-core/references/autonomous-smart-discuss.md +5 -1
  60. package/pp-core/references/discovery-access.md +112 -0
  61. package/pp-core/references/discovery-build-consumption.md +256 -0
  62. package/pp-core/references/discovery-charter.md +127 -0
  63. package/pp-core/references/discovery-data-model.md +145 -0
  64. package/pp-core/references/discovery-id-conventions.md +77 -8
  65. package/pp-core/references/discovery-integrations.md +151 -0
  66. package/pp-core/references/discovery-processes.md +190 -0
  67. package/pp-core/references/discovery-rebaseline.md +2 -2
  68. package/pp-core/references/discovery-research-track.md +5 -6
  69. package/pp-core/references/discovery-session-output.md +70 -0
  70. package/pp-core/references/discovery-standards.md +107 -0
  71. package/pp-core/references/discovery-traceability.md +2 -2
  72. package/pp-core/references/execute-mvp-tdd.md +3 -3
  73. package/pp-core/references/git-integration.md +57 -33
  74. package/pp-core/references/git-planning-commit.md +31 -9
  75. package/pp-core/references/phase-argument-parsing.md +1 -0
  76. package/pp-core/references/planner-discovery-extract.md +35 -0
  77. package/pp-core/references/planner-gap-closure.md +1 -0
  78. package/pp-core/references/planner-revision.md +3 -1
  79. package/pp-core/references/planner-tdd.md +4 -4
  80. package/pp-core/references/planning-config.md +3 -3
  81. package/pp-core/references/shell-portability.md +28 -0
  82. package/pp-core/references/tdd.md +20 -16
  83. package/pp-core/references/verifier-discovery-conformance.md +36 -0
  84. package/pp-core/references/worktree-path-safety.md +1 -0
  85. package/pp-core/templates/milestone.md +5 -5
  86. package/pp-core/templates/project-charter.md +161 -100
  87. package/pp-core/templates/project.md +3 -3
  88. package/pp-core/templates/requirements.md +18 -0
  89. package/pp-core/templates/verification-report.md +16 -0
  90. package/pp-core/workflows/add-backlog.md +1 -1
  91. package/pp-core/workflows/add-tests.md +5 -2
  92. package/pp-core/workflows/add-todo.md +3 -1
  93. package/pp-core/workflows/ai-integration-phase.md +2 -1
  94. package/pp-core/workflows/audit-fix.md +4 -3
  95. package/pp-core/workflows/audit-milestone.md +1 -0
  96. package/pp-core/workflows/autonomous.md +6 -0
  97. package/pp-core/workflows/check-todos.md +2 -1
  98. package/pp-core/workflows/cleanup.md +3 -1
  99. package/pp-core/workflows/code-review-fix.md +12 -5
  100. package/pp-core/workflows/code-review.md +15 -1
  101. package/pp-core/workflows/complete-milestone.md +13 -3
  102. package/pp-core/workflows/debug.md +2 -0
  103. package/pp-core/workflows/diagnose-issues.md +1 -1
  104. package/pp-core/workflows/discovery-sprint/charter.md +114 -0
  105. package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
  106. package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
  107. package/pp-core/workflows/discovery-sprint/session-13-gates.md +11 -1
  108. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +54 -7
  109. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +31 -7
  110. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +64 -37
  111. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +12 -3
  112. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +45 -22
  113. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +30 -15
  114. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +43 -25
  115. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +63 -24
  116. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +28 -13
  117. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +35 -16
  118. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +32 -8
  119. package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +21 -18
  120. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +29 -12
  121. package/pp-core/workflows/discovery-sprint/vault-context.md +14 -3
  122. package/pp-core/workflows/discovery-sprint.md +23 -23
  123. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +81 -29
  124. package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
  125. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
  126. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
  127. package/pp-core/workflows/discovery-to-build.md +70 -34
  128. package/pp-core/workflows/discuss-phase/discovery.md +83 -0
  129. package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
  130. package/pp-core/workflows/discuss-phase-assumptions.md +5 -3
  131. package/pp-core/workflows/discuss-phase.md +18 -10
  132. package/pp-core/workflows/docs-update.md +1 -0
  133. package/pp-core/workflows/eval-review.md +2 -1
  134. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -6
  135. package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +1 -0
  136. package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -0
  137. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +12 -0
  138. package/pp-core/workflows/execute-phase.md +19 -7
  139. package/pp-core/workflows/execute-plan.md +18 -7
  140. package/pp-core/workflows/explore.md +1 -1
  141. package/pp-core/workflows/forensics.md +4 -2
  142. package/pp-core/workflows/health.md +1 -0
  143. package/pp-core/workflows/import.md +2 -1
  144. package/pp-core/workflows/ingest-docs.md +1 -0
  145. package/pp-core/workflows/init/steps/client-context-capture.md +1 -0
  146. package/pp-core/workflows/init/steps/github-provisioning.md +145 -0
  147. package/pp-core/workflows/init/steps/gitlab-provisioning.md +18 -4
  148. package/pp-core/workflows/init/steps/reachability-outcome.md +3 -1
  149. package/pp-core/workflows/init.md +56 -66
  150. package/pp-core/workflows/map-codebase.md +5 -1
  151. package/pp-core/workflows/milestone-summary.md +2 -1
  152. package/pp-core/workflows/mvp-phase.md +1 -0
  153. package/pp-core/workflows/new-milestone.md +9 -5
  154. package/pp-core/workflows/new-project.md +12 -11
  155. package/pp-core/workflows/new-workspace.md +1 -0
  156. package/pp-core/workflows/next.md +2 -1
  157. package/pp-core/workflows/pause-work.md +6 -4
  158. package/pp-core/workflows/plan-milestone-gaps.md +3 -1
  159. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +1 -1
  160. package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
  161. package/pp-core/workflows/plan-phase.md +36 -16
  162. package/pp-core/workflows/plan-review-convergence.md +2 -0
  163. package/pp-core/workflows/plant-seed.md +4 -2
  164. package/pp-core/workflows/pr-branch.md +2 -0
  165. package/pp-core/workflows/progress.md +5 -0
  166. package/pp-core/workflows/quick.md +11 -4
  167. package/pp-core/workflows/reapply-patches.md +3 -0
  168. package/pp-core/workflows/remove-phase.md +1 -1
  169. package/pp-core/workflows/resume-project.md +3 -0
  170. package/pp-core/workflows/review.md +7 -1
  171. package/pp-core/workflows/secure-phase.md +2 -1
  172. package/pp-core/workflows/session-report.md +1 -0
  173. package/pp-core/workflows/ship.md +8 -1
  174. package/pp-core/workflows/sketch-wrap-up.md +1 -1
  175. package/pp-core/workflows/sketch.md +3 -2
  176. package/pp-core/workflows/spec-phase.md +2 -1
  177. package/pp-core/workflows/spike-wrap-up.md +1 -1
  178. package/pp-core/workflows/spike.md +4 -3
  179. package/pp-core/workflows/sync-skills.md +5 -2
  180. package/pp-core/workflows/thread.md +3 -2
  181. package/pp-core/workflows/transition.md +6 -0
  182. package/pp-core/workflows/ui-phase.md +3 -1
  183. package/pp-core/workflows/ui-review.md +2 -1
  184. package/pp-core/workflows/ultraplan-phase.md +1 -0
  185. package/pp-core/workflows/undo.md +14 -10
  186. package/pp-core/workflows/update.md +5 -0
  187. package/pp-core/workflows/validate-phase.md +6 -3
  188. package/pp-core/workflows/vault-review.md +7 -1
  189. package/pp-core/workflows/vault-write-back.md +5 -0
  190. package/pp-core/workflows/verify-phase.md +5 -0
  191. package/pp-core/workflows/verify-work.md +8 -1
  192. package/scripts/prompt-injection-scan.sh +1 -1
  193. package/scripts/sync-runtime-launcher.cjs +115 -5
  194. package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -266
@@ -0,0 +1,664 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery project charter check (Discovery Sprint improvement plan, Batch 1).
4
+ *
5
+ * The Project Charter is the one document every stakeholder signs at the end
6
+ * of a Discovery Sprint. Session 13 drafts it from the session record into
7
+ * `docs/discovery/PROJECT-CHARTER.md` (client-visible, like `docs/architecture/`),
8
+ * in the sprint language, from the fixed-shape template in
9
+ * `pp-core/templates/project-charter.md`. This module checks the structure of
10
+ * that document — never its judgement:
11
+ *
12
+ * - the frontmatter carries every machine field, with `status`, `go_no_go`,
13
+ * `version`, `date` and `language` inside their closed grammars;
14
+ * - every one of the twenty sections is present exactly once, in template
15
+ * order, identified by its `<!-- charter:<key> -->` heading key, so the
16
+ * same shape is recognised whatever language the headings are rendered in;
17
+ * - every ID a table cell cites resolves: requirement IDs against
18
+ * `_requirements.md` (through the rollup parser, the owner of that file),
19
+ * every other discovery family (`D-`, `A-`, `OI-`, `R-`, `CON-`, `SCR-`,
20
+ * `E-`, `US-`, `BO-`, `ADR-`, `IF-`, `CR-`, `RS-`, `DG-`, `DEV-`) against the
21
+ * IDs the session files and registers under `.planning/discovery/` mint
22
+ * in their first table column;
23
+ * - the requirement counts line equals the roll-up, and every `Dropped`
24
+ * requirement is listed under § Conditions;
25
+ * - the Go / No-Go table and the frontmatter verdict equal Session 13's
26
+ * assessment as recorded — the charter copies it, never re-judges it;
27
+ * - every Session 13 open item marked "must resolve before build" is closed
28
+ * or listed under § Conditions;
29
+ * - `DEV-` IDs resolve only against `_deviations.md`, and every row of that
30
+ * register appears in § Deviations (improvement plan, Batch 3);
31
+ * - the sign-off rows agree with `status`, and the version history ends at
32
+ * the frontmatter version.
33
+ *
34
+ * Reporting posture (D-43/D-53): the check fails only on structural faults.
35
+ * At Session 13 it reports; at `/pp-discovery-to-build` the same check runs with
36
+ * `--require-signed`, and a charter whose status is not `Signed` or `Signed with
37
+ * conditions` exits non-zero on its own identity so the handoff can hard-block
38
+ * on it and `--unsigned` can record the gap instead.
39
+ *
40
+ * Content-free contract (the T-02-05 precedent): output names the file, line
41
+ * numbers, validated IDs, section keys, field names and reason codes — never
42
+ * charter prose.
43
+ *
44
+ * Traversal: the check takes no positional path. The charter and every source
45
+ * resolve from fixed convention paths under the working directory (the T-02-08
46
+ * precedent).
47
+ */
48
+ var __importDefault = (this && this.__importDefault) || function (mod) {
49
+ return (mod && mod.__esModule) ? mod : { "default": mod };
50
+ };
51
+ const node_fs_1 = __importDefault(require("node:fs"));
52
+ const node_path_1 = __importDefault(require("node:path"));
53
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
54
+ const io = require("./io.cjs");
55
+ const { output, error, ERROR_REASON } = io;
56
+ // `_requirements.md` is owned by `discovery rollup`; this gate consumes its
57
+ // parser and re-implements none of its grammar.
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
59
+ const rollup = require("./discovery-rollup.cjs");
60
+ // ─── Constants ────────────────────────────────────────────────────────────────
61
+ const CHARTER_FILE = 'docs/discovery/PROJECT-CHARTER.md';
62
+ const DISCOVERY_DIR = '.planning/discovery';
63
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
64
+ const SESSION_13_FILE = `${DISCOVERY_DIR}/session-13.md`;
65
+ /** The twenty sections, in template order. Keys are the heading's `<!-- charter:<key> -->`. */
66
+ const SECTION_KEYS = Object.freeze([
67
+ 'purpose',
68
+ 'objectives',
69
+ 'scope',
70
+ 'stakeholders',
71
+ 'approvals',
72
+ 'solution',
73
+ 'deviations',
74
+ 'data',
75
+ 'quality',
76
+ 'validated',
77
+ 'delivery',
78
+ 'team',
79
+ 'budget',
80
+ 'risks',
81
+ 'open-items',
82
+ 'conditions',
83
+ 'go-no-go',
84
+ 'decisions',
85
+ 'sign-off',
86
+ 'history',
87
+ ]);
88
+ /** Keys the parser reads a table from; a section without one is a structural fault. */
89
+ const TABLE_REQUIRED = Object.freeze(['deviations', 'open-items', 'conditions', 'go-no-go', 'sign-off', 'history']);
90
+ /** The one non-section key: marks the requirement-counts line in § Scope. */
91
+ const COUNTS_KEY = 'counts';
92
+ const DOCUMENT_KIND = 'project-charter';
93
+ const REQUIRED_FIELDS = Object.freeze([
94
+ 'document', 'project', 'client', 'version', 'status', 'date', 'language',
95
+ 'discovery_sessions_completed', 'go_no_go',
96
+ ]);
97
+ const STATUS_VALUES = Object.freeze(['Draft', 'For sign-off', 'Signed', 'Signed with conditions', 'Rejected']);
98
+ const SIGNED_VALUES = Object.freeze(['Signed', 'Signed with conditions']);
99
+ const GO_NO_GO_VALUES = Object.freeze(['Pending', 'Go', 'Go with conditions', 'No-Go']);
100
+ const SIGNOFF_DECISIONS = Object.freeze(['Approve', 'Approve with conditions', 'Reject']);
101
+ const VERSION_RE = /^\d+\.\d+$/;
102
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
103
+ /** BCP-47-shaped: `nl`, `en`, `en-GB`, `nl-BE`. */
104
+ const LANGUAGE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
105
+ const KEY_RE = /<!--\s*charter:([a-z-]+)\s*-->/;
106
+ const HEADING_RE = /^##\s/;
107
+ const TABLE_SEPARATOR_RE = /^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
108
+ /**
109
+ * Session-table and register families whose IDs the charter may cite, all
110
+ * resolved against the first table column of the files under
111
+ * `.planning/discovery/`. Decisions and actions carry a session component.
112
+ */
113
+ const TWO_PART_FAMILIES = Object.freeze(['D', 'A']);
114
+ const ONE_PART_FAMILIES = Object.freeze(['OI', 'R', 'CON', 'SCR', 'E', 'US', 'BO', 'ADR', 'IF', 'CR', 'RS', 'DG', 'DEV']);
115
+ /** The one family with a single owning register: a `DEV-` ID is minted by `_deviations.md` alone (Batch 3). */
116
+ const DEVIATION_FAMILY = 'DEV';
117
+ const DEVIATIONS_FILE = `${DISCOVERY_DIR}/_deviations.md`;
118
+ const ID_TOKEN_RE = /(?<![A-Za-z0-9_`-])([A-Z][A-Z0-9]*)-((?:P?\d+)(?:-\d+)?)(?![A-Za-z0-9_-])/g;
119
+ const FIRST_CELL_ID_RE = /^\|\s*\**\s*([A-Z][A-Z0-9]*-(?:P?\d+)(?:-\d+)?)\s*\**\s*\|/;
120
+ const SESSION_13_GO_NO_GO = 'Go / No-Go Assessment';
121
+ const SESSION_13_OPEN_ITEMS = 'Open Items Register';
122
+ /** Session 13's eight Go / No-Go rows: seven criteria, then the overall recommendation. */
123
+ const GO_NO_GO_ROWS = 8;
124
+ const MUST_RESOLVE_RE = /^yes\b/i;
125
+ const CLOSED_RE = /^(closed|resolved|done)\b/i;
126
+ const USAGE = 'Usage: pp-tools discovery charter-check [--require-signed]';
127
+ // ─── Parsing ──────────────────────────────────────────────────────────────────
128
+ function splitCells(line) {
129
+ const trimmed = line.trim().replace(/^\|/, '').replace(/\|$/, '');
130
+ return trimmed.split('|').map((c) => c.trim());
131
+ }
132
+ function stripEmphasis(cell) {
133
+ return cell.replace(/\*\*/g, '').replace(/^_|_$/g, '').trim();
134
+ }
135
+ function parseFrontmatter(lines) {
136
+ if (lines.length === 0 || lines[0].trim() !== '---')
137
+ return null;
138
+ const fields = new Map();
139
+ const fieldLines = new Map();
140
+ for (let i = 1; i < lines.length; i += 1) {
141
+ if (lines[i].trim() === '---')
142
+ return { fields, lines: fieldLines, endLine: i + 1 };
143
+ const m = /^([a-z_]+):\s*(.*)$/.exec(lines[i]);
144
+ if (m) {
145
+ fields.set(m[1], m[2].trim().replace(/^"(.*)"$/, '$1'));
146
+ fieldLines.set(m[1], i + 1);
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+ /**
152
+ * Every pipe table in a run of lines: the header is the row before the first
153
+ * separator, data rows follow until the first non-pipe line. A second table
154
+ * starts at the next separator.
155
+ */
156
+ function parseTables(body) {
157
+ const tables = [];
158
+ for (let i = 1; i < body.length; i += 1) {
159
+ if (!TABLE_SEPARATOR_RE.test(body[i].text) || !body[i - 1].text.trim().startsWith('|'))
160
+ continue;
161
+ const table = { line: body[i - 1].n, header: splitCells(body[i - 1].text), rows: [] };
162
+ let j = i + 1;
163
+ for (; j < body.length; j += 1) {
164
+ const text = body[j].text.trim();
165
+ if (!text.startsWith('|'))
166
+ break;
167
+ if (TABLE_SEPARATOR_RE.test(text))
168
+ break;
169
+ table.rows.push({ line: body[j].n, cells: splitCells(text) });
170
+ }
171
+ tables.push(table);
172
+ i = j - 1;
173
+ }
174
+ return tables;
175
+ }
176
+ /** Sections by heading key, in file order, with their bodies and tables. */
177
+ function parseSections(lines, from) {
178
+ const sections = [];
179
+ const unknown = [];
180
+ let counts = null;
181
+ let current = null;
182
+ const bodies = [];
183
+ for (let i = from; i < lines.length; i += 1) {
184
+ const text = lines[i];
185
+ const n = i + 1;
186
+ const key = KEY_RE.exec(text);
187
+ if (key && key[1] === COUNTS_KEY) {
188
+ counts = { n, text };
189
+ }
190
+ else if (key && !HEADING_RE.test(text)) {
191
+ unknown.push({ n, key: key[1] });
192
+ }
193
+ if (HEADING_RE.test(text)) {
194
+ if (current)
195
+ bodies.push(current.body);
196
+ if (key && key[1] !== COUNTS_KEY) {
197
+ current = { key: key[1], line: n, tables: [], body: [] };
198
+ sections.push(current);
199
+ }
200
+ else {
201
+ current = null;
202
+ }
203
+ continue;
204
+ }
205
+ if (current)
206
+ current.body.push({ n, text });
207
+ }
208
+ for (const s of sections)
209
+ s.tables = parseTables(s.body);
210
+ return { sections, counts, unknown };
211
+ }
212
+ function readIfPresent(abs) {
213
+ if (!node_fs_1.default.existsSync(abs))
214
+ return { kind: 'absent' };
215
+ try {
216
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
217
+ }
218
+ catch {
219
+ return { kind: 'unreadable' };
220
+ }
221
+ }
222
+ /**
223
+ * Every ID minted in the first column of any table under `.planning/discovery/*.md`,
224
+ * plus the `DEV-` IDs `_deviations.md` mints on its own (`deviations` is null when
225
+ * that register does not exist).
226
+ */
227
+ function collectMintedIds(cwd) {
228
+ const ids = new Set();
229
+ let deviations = null;
230
+ const unreadable = [];
231
+ const dir = node_path_1.default.join(cwd, DISCOVERY_DIR);
232
+ let names = [];
233
+ try {
234
+ names = node_fs_1.default.readdirSync(dir).filter((f) => f.endsWith('.md') && f !== '_requirements.md').sort();
235
+ }
236
+ catch {
237
+ return { ids, deviations, unreadable };
238
+ }
239
+ for (const name of names) {
240
+ const read = readIfPresent(node_path_1.default.join(dir, name));
241
+ if (read.kind === 'absent')
242
+ continue;
243
+ if (read.kind === 'unreadable') {
244
+ unreadable.push(`${DISCOVERY_DIR}/${name}`);
245
+ continue;
246
+ }
247
+ const isDeviations = `${DISCOVERY_DIR}/${name}` === DEVIATIONS_FILE;
248
+ if (isDeviations)
249
+ deviations = new Set();
250
+ for (const line of read.content.split(/\r?\n/)) {
251
+ const m = FIRST_CELL_ID_RE.exec(line);
252
+ if (!m)
253
+ continue;
254
+ if (m[1].startsWith(`${DEVIATION_FAMILY}-`)) {
255
+ if (isDeviations)
256
+ deviations.add(m[1]);
257
+ }
258
+ else {
259
+ ids.add(m[1]);
260
+ }
261
+ }
262
+ }
263
+ return { ids, deviations, unreadable };
264
+ }
265
+ /** `_requirements.md` through its owner's parser: absent → null, unparseable → 'unparseable'. */
266
+ function readRequirements(cwd) {
267
+ const read = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
268
+ if (read.kind === 'absent')
269
+ return null;
270
+ if (read.kind === 'unreadable')
271
+ return 'unreadable';
272
+ const parsed = rollup.parseRequirementsArtifact(read.content);
273
+ if (!parsed.ok)
274
+ return 'unparseable';
275
+ const prefixes = new Set(parsed.registry.map((r) => r.prefix));
276
+ const ids = new Set();
277
+ const liveByPrefix = new Map();
278
+ for (const p of prefixes)
279
+ liveByPrefix.set(p, 0);
280
+ const dropped = [];
281
+ for (const row of parsed.rows) {
282
+ ids.add(row.id);
283
+ if (row.disposition === 'Dropped')
284
+ dropped.push(row.id);
285
+ else
286
+ liveByPrefix.set(row.prefix, (liveByPrefix.get(row.prefix) ?? 0) + 1);
287
+ }
288
+ return { prefixes, ids, liveByPrefix, dropped };
289
+ }
290
+ /** A Notes Template table in a session file: the first table after the named label (bold or heading). */
291
+ function sessionTable(lines, label) {
292
+ const norm = (s) => s.replace(/^#+\s*/, '').replace(/\*\*/g, '').trim();
293
+ const at = lines.findIndex((l) => norm(l) === label);
294
+ if (at === -1)
295
+ return null;
296
+ const body = [];
297
+ for (let i = at + 1; i < lines.length; i += 1) {
298
+ if (/^#{1,3}\s/.test(lines[i]) || /^\*\*[^*]+\*\*$/.test(lines[i].trim()))
299
+ break;
300
+ body.push({ n: i + 1, text: lines[i] });
301
+ }
302
+ const tables = parseTables(body);
303
+ return tables.length > 0 ? tables[0] : null;
304
+ }
305
+ function readSession13(cwd) {
306
+ const read = readIfPresent(node_path_1.default.join(cwd, SESSION_13_FILE));
307
+ if (read.kind === 'absent')
308
+ return null;
309
+ if (read.kind === 'unreadable')
310
+ return 'unreadable';
311
+ const lines = read.content.split(/\r?\n/);
312
+ const go = sessionTable(lines, SESSION_13_GO_NO_GO);
313
+ const open = sessionTable(lines, SESSION_13_OPEN_ITEMS);
314
+ return {
315
+ goNoGo: go ? go.rows.map((r) => ({ status: stripEmphasis(r.cells[1] ?? '') })) : null,
316
+ openItems: open
317
+ ? open.rows
318
+ .filter((r) => /^OI-\d+$/.test(stripEmphasis(r.cells[0] ?? '')))
319
+ .map((r) => ({
320
+ id: stripEmphasis(r.cells[0] ?? ''),
321
+ mustResolve: MUST_RESOLVE_RE.test(stripEmphasis(r.cells[4] ?? '')),
322
+ closed: CLOSED_RE.test(stripEmphasis(r.cells[7] ?? '')),
323
+ }))
324
+ : null,
325
+ };
326
+ }
327
+ /** Check one charter text against its sources. Pure over its inputs. */
328
+ function checkCharter(content, sources, requireSigned = false) {
329
+ const violations = [];
330
+ const lines = content.split(/\r?\n/);
331
+ // 1 ─ Frontmatter
332
+ const fm = parseFrontmatter(lines);
333
+ let status = null;
334
+ let goNoGo = null;
335
+ let version = null;
336
+ let language = null;
337
+ if (fm === null) {
338
+ violations.push({ reason: 'frontmatter_missing', line: 1 });
339
+ }
340
+ else {
341
+ for (const f of REQUIRED_FIELDS) {
342
+ if (!fm.fields.has(f) || fm.fields.get(f) === '')
343
+ violations.push({ reason: 'field_missing', id: f });
344
+ }
345
+ const at = (f) => fm.lines.get(f);
346
+ const v = (f) => fm.fields.get(f) ?? '';
347
+ if (fm.fields.has('document') && v('document') !== DOCUMENT_KIND)
348
+ violations.push({ reason: 'bad_document', line: at('document') });
349
+ if (fm.fields.has('status')) {
350
+ if (STATUS_VALUES.includes(v('status')))
351
+ status = v('status');
352
+ else
353
+ violations.push({ reason: 'bad_status', line: at('status') });
354
+ }
355
+ if (fm.fields.has('go_no_go')) {
356
+ if (GO_NO_GO_VALUES.includes(v('go_no_go')))
357
+ goNoGo = v('go_no_go');
358
+ else
359
+ violations.push({ reason: 'bad_go_no_go', line: at('go_no_go') });
360
+ }
361
+ if (fm.fields.has('version')) {
362
+ if (VERSION_RE.test(v('version')))
363
+ version = v('version');
364
+ else
365
+ violations.push({ reason: 'bad_version', line: at('version') });
366
+ }
367
+ if (fm.fields.has('date') && !DATE_RE.test(v('date')))
368
+ violations.push({ reason: 'bad_date', line: at('date') });
369
+ if (fm.fields.has('language')) {
370
+ if (LANGUAGE_RE.test(v('language')))
371
+ language = v('language');
372
+ else
373
+ violations.push({ reason: 'bad_language', line: at('language') });
374
+ }
375
+ }
376
+ // 2 ─ Sections: present once each, in order, nothing unknown
377
+ const { sections, counts, unknown } = parseSections(lines, fm ? fm.endLine : 0);
378
+ for (const u of unknown)
379
+ violations.push({ reason: 'section_unknown', line: u.n, id: u.key });
380
+ const seen = new Map();
381
+ for (const s of sections) {
382
+ if (!SECTION_KEYS.includes(s.key)) {
383
+ violations.push({ reason: 'section_unknown', line: s.line, id: s.key });
384
+ continue;
385
+ }
386
+ if (seen.has(s.key))
387
+ violations.push({ reason: 'section_duplicate', line: s.line, id: s.key });
388
+ else
389
+ seen.set(s.key, s);
390
+ }
391
+ for (const k of SECTION_KEYS) {
392
+ if (!seen.has(k))
393
+ violations.push({ reason: 'section_missing', id: k });
394
+ }
395
+ const order = sections.filter((s) => seen.get(s.key) === s).map((s) => s.key);
396
+ const expected = SECTION_KEYS.filter((k) => seen.has(k));
397
+ for (let i = 0; i < order.length; i += 1) {
398
+ if (order[i] !== expected[i]) {
399
+ violations.push({ reason: 'section_order', line: seen.get(order[i])?.line, id: order[i] });
400
+ break;
401
+ }
402
+ }
403
+ for (const k of TABLE_REQUIRED) {
404
+ const s = seen.get(k);
405
+ if (s && s.tables.length === 0)
406
+ violations.push({ reason: 'table_missing', line: s.line, id: k });
407
+ }
408
+ // 3 ─ Every ID a table cell cites resolves
409
+ let idsChecked = 0;
410
+ const requirements = sources.requirements;
411
+ for (const s of seen.values()) {
412
+ if (s.key === 'history')
413
+ continue;
414
+ for (const t of s.tables) {
415
+ for (const row of t.rows) {
416
+ for (const cell of row.cells) {
417
+ for (const m of cell.matchAll(ID_TOKEN_RE)) {
418
+ const prefix = m[1];
419
+ const token = `${prefix}-${m[2]}`;
420
+ const twoPart = /^P?\d+-\d+$/.test(m[2]);
421
+ if (TWO_PART_FAMILIES.includes(prefix)) {
422
+ idsChecked += 1;
423
+ if (!/^(?:\d{2}|P\d{2})-\d+$/.test(m[2]) || !sources.minted.has(token))
424
+ violations.push({ reason: 'unknown_id', line: row.line, id: token });
425
+ }
426
+ else if (prefix === DEVIATION_FAMILY) {
427
+ idsChecked += 1;
428
+ if (twoPart || sources.deviations === null || !sources.deviations.has(token))
429
+ violations.push({ reason: 'unknown_id', line: row.line, id: token });
430
+ }
431
+ else if (ONE_PART_FAMILIES.includes(prefix)) {
432
+ idsChecked += 1;
433
+ if (twoPart || !sources.minted.has(token))
434
+ violations.push({ reason: 'unknown_id', line: row.line, id: token });
435
+ }
436
+ else if (requirements && requirements !== 'unparseable' && requirements.prefixes.has(`${prefix}-`)) {
437
+ idsChecked += 1;
438
+ if (twoPart || !requirements.ids.has(token))
439
+ violations.push({ reason: 'unknown_id', line: row.line, id: token });
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ }
446
+ // 3b ─ Every deviation on file is listed in § Deviations (Batch 3)
447
+ const deviationsSection = seen.get('deviations');
448
+ if (sources.deviations !== null && deviationsSection) {
449
+ const listed = new Set();
450
+ for (const t of deviationsSection.tables)
451
+ for (const row of t.rows)
452
+ for (const m of (row.cells[0] ?? '').matchAll(ID_TOKEN_RE))
453
+ listed.add(`${m[1]}-${m[2]}`);
454
+ for (const id of sources.deviations)
455
+ if (!listed.has(id))
456
+ violations.push({ reason: 'deviation_missing', line: deviationsSection.line, id });
457
+ }
458
+ // 4 ─ Requirement counts and dropped rows, when the register is on file
459
+ const conditions = seen.get('conditions');
460
+ const conditionsText = conditions ? conditions.tables.flatMap((t) => t.rows.flatMap((r) => r.cells)).join(' ') : '';
461
+ if (requirements === 'unparseable') {
462
+ violations.push({ reason: 'requirements_unparseable' });
463
+ }
464
+ else if (requirements) {
465
+ if (!counts) {
466
+ violations.push({ reason: 'counts_missing', line: seen.get('scope')?.line });
467
+ }
468
+ else {
469
+ const found = new Map();
470
+ for (const m of counts.text.replace(KEY_RE, '').matchAll(/([A-Z][A-Z0-9]*-)\s+(\d+)/g))
471
+ found.set(m[1], Number(m[2]));
472
+ const droppedM = /Dropped\s+(\d+)/.exec(counts.text);
473
+ let mismatch = droppedM === null || Number(droppedM[1]) !== requirements.dropped.length;
474
+ if (found.size !== requirements.liveByPrefix.size)
475
+ mismatch = true;
476
+ for (const [p, n] of requirements.liveByPrefix)
477
+ if (found.get(p) !== n)
478
+ mismatch = true;
479
+ if (mismatch)
480
+ violations.push({ reason: 'counts_mismatch', line: counts.n });
481
+ }
482
+ if (conditions) {
483
+ for (const id of requirements.dropped) {
484
+ if (!new RegExp(`(?<![A-Z0-9-])${id}(?![0-9-])`).test(conditionsText))
485
+ violations.push({ reason: 'dropped_missing', id });
486
+ }
487
+ }
488
+ }
489
+ // 5 ─ Go / No-Go: recorded, not re-judged
490
+ if (goNoGo === 'Pending' && status !== null && status !== 'Draft')
491
+ violations.push({ reason: 'go_no_go_pending', line: fm?.lines.get('go_no_go') });
492
+ const go = seen.get('go-no-go');
493
+ const goTable = go && go.tables.length > 0 ? go.tables[0] : null;
494
+ if (goTable && goTable.rows.length !== GO_NO_GO_ROWS)
495
+ violations.push({ reason: 'go_no_go_mismatch', line: goTable.line, id: 'rows' });
496
+ if (goTable && goTable.rows.length === GO_NO_GO_ROWS && goNoGo !== null && goNoGo !== 'Pending') {
497
+ const overall = stripEmphasis(goTable.rows[GO_NO_GO_ROWS - 1].cells[1] ?? '');
498
+ if (overall !== goNoGo)
499
+ violations.push({ reason: 'go_no_go_mismatch', line: goTable.rows[GO_NO_GO_ROWS - 1].line, id: 'overall' });
500
+ }
501
+ const s13 = sources.session13;
502
+ if (s13 && s13.goNoGo && s13.goNoGo.length === GO_NO_GO_ROWS && goTable && goTable.rows.length === GO_NO_GO_ROWS) {
503
+ const recordedOverall = s13.goNoGo[GO_NO_GO_ROWS - 1].status;
504
+ const recorded = recordedOverall !== '';
505
+ if (recorded) {
506
+ for (let i = 0; i < GO_NO_GO_ROWS; i += 1) {
507
+ if (stripEmphasis(goTable.rows[i].cells[1] ?? '') !== s13.goNoGo[i].status) {
508
+ violations.push({ reason: 'go_no_go_mismatch', line: goTable.rows[i].line, id: `row-${i + 1}` });
509
+ }
510
+ }
511
+ if (goNoGo !== null && goNoGo !== recordedOverall)
512
+ violations.push({ reason: 'go_no_go_mismatch', line: fm?.lines.get('go_no_go'), id: 'frontmatter' });
513
+ }
514
+ }
515
+ // 6 ─ Must-resolve open items are closed or conditions
516
+ if (s13 && s13.openItems && conditions) {
517
+ for (const item of s13.openItems) {
518
+ if (item.mustResolve && !item.closed && !new RegExp(`(?<![A-Z0-9-])${item.id}(?![0-9-])`).test(conditionsText)) {
519
+ violations.push({ reason: 'must_resolve_open', id: item.id });
520
+ }
521
+ }
522
+ }
523
+ // 7 ─ Sign-off rows agree with status
524
+ const signoff = seen.get('sign-off');
525
+ const signTable = signoff && signoff.tables.length > 0 ? signoff.tables[0] : null;
526
+ if (signTable) {
527
+ const decisions = [];
528
+ for (const row of signTable.rows) {
529
+ const d = stripEmphasis(row.cells[2] ?? '');
530
+ if (d !== '' && !SIGNOFF_DECISIONS.includes(d))
531
+ violations.push({ reason: 'bad_signoff_decision', line: row.line });
532
+ else
533
+ decisions.push(d);
534
+ }
535
+ const all = (pred) => decisions.length > 0 && decisions.every(pred);
536
+ let consistent = true;
537
+ if (status === 'Signed')
538
+ consistent = all((d) => d === 'Approve');
539
+ else if (status === 'Signed with conditions')
540
+ consistent = all((d) => d === 'Approve' || d === 'Approve with conditions') && decisions.includes('Approve with conditions');
541
+ else if (status === 'Rejected')
542
+ consistent = decisions.includes('Reject');
543
+ if (!consistent)
544
+ violations.push({ reason: 'signoff_status_mismatch', line: signTable.line });
545
+ }
546
+ // 8 ─ Version history ends at the frontmatter version
547
+ const history = seen.get('history');
548
+ const histTable = history && history.tables.length > 0 ? history.tables[0] : null;
549
+ if (histTable) {
550
+ if (histTable.rows.length === 0)
551
+ violations.push({ reason: 'history_missing', line: histTable.line });
552
+ else {
553
+ for (const row of histTable.rows) {
554
+ if (!VERSION_RE.test(stripEmphasis(row.cells[0] ?? '')))
555
+ violations.push({ reason: 'bad_version', line: row.line });
556
+ }
557
+ const last = stripEmphasis(histTable.rows[histTable.rows.length - 1].cells[0] ?? '');
558
+ if (version !== null && VERSION_RE.test(last) && last !== version)
559
+ violations.push({ reason: 'history_version_mismatch', line: histTable.rows[histTable.rows.length - 1].line });
560
+ }
561
+ }
562
+ // 9 ─ The handoff arm
563
+ if (requireSigned && (status === null || !SIGNED_VALUES.includes(status))) {
564
+ violations.push({ reason: 'charter_unsigned', line: fm?.lines.get('status') });
565
+ }
566
+ return {
567
+ ok: violations.length === 0,
568
+ status,
569
+ go_no_go: goNoGo,
570
+ version,
571
+ language,
572
+ sections: seen.size,
573
+ ids_checked: idsChecked,
574
+ violations,
575
+ };
576
+ }
577
+ /** Check the charter of the project at `cwd`. */
578
+ function checkProjectCharter(cwd, requireSigned = false) {
579
+ const empty = { status: null, go_no_go: null, version: null, language: null, sections: 0, ids_checked: 0 };
580
+ const read = readIfPresent(node_path_1.default.join(cwd, CHARTER_FILE));
581
+ if (read.kind === 'absent')
582
+ return { ok: false, file: CHARTER_FILE, ...empty, violations: [{ reason: 'charter_missing' }] };
583
+ if (read.kind === 'unreadable')
584
+ return { ok: false, file: CHARTER_FILE, ...empty, violations: [{ reason: 'artifact_unreadable', id: CHARTER_FILE }] };
585
+ const content = read.content;
586
+ const minted = collectMintedIds(cwd);
587
+ const requirements = readRequirements(cwd);
588
+ const session13 = readSession13(cwd);
589
+ const sources = {
590
+ minted: minted.ids,
591
+ deviations: minted.deviations,
592
+ requirements: requirements === 'unreadable' ? null : requirements,
593
+ session13: session13 === 'unreadable' ? null : session13,
594
+ };
595
+ const result = checkCharter(content, sources, requireSigned);
596
+ for (const p of minted.unreadable)
597
+ result.violations.push({ reason: 'artifact_unreadable', id: p });
598
+ if (requirements === 'unreadable')
599
+ result.violations.push({ reason: 'artifact_unreadable', id: REQUIREMENTS_FILE });
600
+ if (session13 === 'unreadable')
601
+ result.violations.push({ reason: 'artifact_unreadable', id: SESSION_13_FILE });
602
+ result.ok = result.violations.length === 0;
603
+ return { file: CHARTER_FILE, ...result };
604
+ }
605
+ function formatViolation(v) {
606
+ const where = v.line !== undefined ? `line ${v.line}: ` : '';
607
+ const what = v.id !== undefined ? ` (${v.id.slice(0, 80)})` : '';
608
+ return ` - ${where}${v.reason}${what}`;
609
+ }
610
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
611
+ /**
612
+ * CLI adapter for `pp-tools discovery charter-check [--require-signed]`
613
+ * (dotted form `pp-tools query discovery.charter-check`).
614
+ *
615
+ * Exit semantics: a structural fault exits non-zero via
616
+ * `ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED`; a structurally sound charter
617
+ * that `--require-signed` finds unsigned exits via
618
+ * `ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED`. The payload is emitted on stdout
619
+ * first on both failure paths, so the status survives alongside the error.
620
+ */
621
+ function cmdDiscoveryCharterCheck(cwd, args, raw) {
622
+ // args arrive shaped ['discovery', 'charter-check', ...flags].
623
+ const rest = args.slice(2);
624
+ const requireSigned = rest.includes('--require-signed');
625
+ if (rest.some((a) => a !== '--require-signed')) {
626
+ error(USAGE, ERROR_REASON.USAGE);
627
+ return; // unreachable — error() exits
628
+ }
629
+ const result = checkProjectCharter(cwd, requireSigned);
630
+ const payload = {
631
+ ok: result.ok,
632
+ file: result.file,
633
+ status: result.status,
634
+ go_no_go: result.go_no_go,
635
+ version: result.version,
636
+ language: result.language,
637
+ sections: result.sections,
638
+ ids_checked: result.ids_checked,
639
+ };
640
+ if (!result.ok) {
641
+ output({ ...payload, violations: result.violations.length }, raw);
642
+ const onlyUnsigned = result.violations.every((v) => v.reason === 'charter_unsigned');
643
+ error(`discovery charter-check: ${result.violations.length} violation(s) in ${result.file}:\n` +
644
+ result.violations.map(formatViolation).join('\n'), onlyUnsigned ? ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED : ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED);
645
+ return; // unreachable — error() exits
646
+ }
647
+ output(payload, raw);
648
+ }
649
+ module.exports = {
650
+ CHARTER_FILE,
651
+ SECTION_KEYS,
652
+ TABLE_REQUIRED,
653
+ COUNTS_KEY,
654
+ STATUS_VALUES,
655
+ SIGNED_VALUES,
656
+ GO_NO_GO_VALUES,
657
+ SIGNOFF_DECISIONS,
658
+ CHARTER_CHECK_FAILED: ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED,
659
+ CHARTER_UNSIGNED: ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED,
660
+ parseTables,
661
+ checkCharter,
662
+ checkProjectCharter,
663
+ cmdDiscoveryCharterCheck,
664
+ };