@pareto4/pp-core 1.19.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 (79) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-phase-researcher.md +10 -0
  3. package/agents/pp-planner.md +15 -0
  4. package/agents/pp-vault-reviewer.md +2 -0
  5. package/agents/pp-verifier.md +12 -8
  6. package/commands/pp-discovery.md +3 -2
  7. package/gemini-extension.json +1 -1
  8. package/package.json +6 -3
  9. package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
  10. package/pareto-skills/pp-project-setup/SKILL.md +18 -343
  11. package/pp-core/bin/lib/capability-registry.cjs +1 -1
  12. package/pp-core/bin/lib/discovery-access.cjs +276 -0
  13. package/pp-core/bin/lib/discovery-charter.cjs +664 -0
  14. package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
  15. package/pp-core/bin/lib/discovery-entities.cjs +323 -0
  16. package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
  17. package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
  18. package/pp-core/bin/lib/discovery-migration.cjs +246 -0
  19. package/pp-core/bin/lib/discovery-phases.cjs +233 -0
  20. package/pp-core/bin/lib/discovery-processes.cjs +355 -0
  21. package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
  22. package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
  23. package/pp-core/bin/lib/discovery-rules.cjs +216 -0
  24. package/pp-core/bin/lib/discovery-screens.cjs +1 -1
  25. package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
  26. package/pp-core/bin/lib/discovery-stories.cjs +273 -0
  27. package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
  28. package/pp-core/bin/lib/discovery-table.cjs +55 -0
  29. package/pp-core/bin/lib/io.cjs +22 -0
  30. package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
  31. package/pp-core/bin/pp-tools.cjs +143 -1
  32. package/pp-core/references/discovery-access.md +112 -0
  33. package/pp-core/references/discovery-build-consumption.md +256 -0
  34. package/pp-core/references/discovery-charter.md +127 -0
  35. package/pp-core/references/discovery-data-model.md +145 -0
  36. package/pp-core/references/discovery-id-conventions.md +50 -14
  37. package/pp-core/references/discovery-integrations.md +151 -0
  38. package/pp-core/references/discovery-processes.md +190 -0
  39. package/pp-core/references/discovery-rebaseline.md +2 -2
  40. package/pp-core/references/discovery-research-track.md +5 -6
  41. package/pp-core/references/discovery-session-output.md +70 -0
  42. package/pp-core/references/discovery-standards.md +107 -0
  43. package/pp-core/references/discovery-traceability.md +2 -2
  44. package/pp-core/references/planner-discovery-extract.md +35 -0
  45. package/pp-core/references/verifier-discovery-conformance.md +36 -0
  46. package/pp-core/templates/project-charter.md +161 -100
  47. package/pp-core/templates/project.md +3 -3
  48. package/pp-core/templates/requirements.md +18 -0
  49. package/pp-core/templates/verification-report.md +16 -0
  50. package/pp-core/workflows/discovery-sprint/charter.md +114 -0
  51. package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
  52. package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
  53. package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
  54. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
  55. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
  56. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
  57. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
  58. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
  59. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
  60. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
  61. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
  62. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
  63. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
  64. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
  65. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
  66. package/pp-core/workflows/discovery-sprint.md +12 -15
  67. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
  68. package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
  69. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
  70. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
  71. package/pp-core/workflows/discovery-to-build.md +70 -34
  72. package/pp-core/workflows/discuss-phase/discovery.md +83 -0
  73. package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
  74. package/pp-core/workflows/discuss-phase.md +10 -7
  75. package/pp-core/workflows/new-project.md +3 -3
  76. package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
  77. package/pp-core/workflows/plan-phase.md +15 -10
  78. package/pp-core/workflows/vault-review.md +5 -0
  79. package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
@@ -0,0 +1,628 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery extract per build phase (Discovery Sprint improvement plan,
4
+ * Batch 4 — §5.5 R1 and R3).
5
+ *
6
+ * A build phase is keyed by its requirement IDs (`**Requirements**: [K-03,
7
+ * N-02]` in ROADMAP.md; the `_phases.md` row before the handoff). `for-phase
8
+ * <N>` collects every row of every canonical discovery artifact that cites one
9
+ * of those IDs and renders them as ONE markdown extract between
10
+ * `<!-- discovery:auto -->` markers, which `/pp-discuss-phase` writes into
11
+ * CONTEXT.md and `/pp-plan-phase` hands the researcher and the planner. No
12
+ * agent opens a `session-NN.md`; nothing is copied by hand, so nothing drifts.
13
+ *
14
+ * What is collected, per artifact present under `.planning/discovery/`:
15
+ *
16
+ * - `_requirements.md` § Requirements — the rows whose ID is in the set;
17
+ * - `_stories.md` § Stories — rows whose `Requirements` cites one;
18
+ * - `_processes.md` § Steps — step rows whose `Requirements` cites one,
19
+ * under their `### MP-nn — <name>` heading, plus the § To-be row of each
20
+ * main process touched;
21
+ * - `_entities.md` — the entities named as data objects by those steps:
22
+ * their § Entities row, attribute table, relationships and lifecycles;
23
+ * - `_rules.md` § Rules — rows whose `Requirements` cites one;
24
+ * - `_screens.md` § Screens rows citing one and § No screen rows keyed by one;
25
+ * - `_roles.md` — the § Permissions rows citing one; the § Roles row and
26
+ * the § Screens rows of each role those permissions name, plus § Screens
27
+ * rows of the selected screens (Batch 3 grammar,
28
+ * `references/discovery-access.md`);
29
+ * - `_integrations.md` — the § Interfaces rows citing one or named by a
30
+ * selected step's `Integration` cell (`IF-nn`), with their § Contract and
31
+ * § Errors rows (`references/discovery-integrations.md`);
32
+ * - `_migration.md`, `_deviations.md` — generic: every table carrying a
33
+ * `Requirements` column contributes the rows citing one; a table without
34
+ * that column is reported as unfilterable, never silently dropped.
35
+ *
36
+ * Structure fails loud (`discovery_for_phase_failed`): a present artifact
37
+ * whose owning section or header is off its template, or a `_requirements.md`
38
+ * the rollup cannot parse. An absent artifact is skipped. With no
39
+ * `.planning/discovery/` at all the extract is empty and the exit is 0 — a
40
+ * project that never ran the sprint sees nothing (the vault's posture).
41
+ *
42
+ * R3 — `--conformance-template` renders the "Discovery conformance" block the
43
+ * verifier fills (one row per extract row, status `honoured | amended
44
+ * (D-P<nn>-<n>) | missed`); `--verify <VERIFICATION.md>` checks that block
45
+ * covers every extract row exactly once with a legal status. A `missed` row is
46
+ * the verifier's finding, not this command's fault.
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
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
57
+ const table = require("./discovery-table.cjs");
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
59
+ const rollup = require("./discovery-rollup.cjs");
60
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
61
+ const phases = require("./discovery-phases.cjs");
62
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
63
+ const roadmapParser = require("./roadmap-parser.cjs");
64
+ // ─── Constants ────────────────────────────────────────────────────────────────
65
+ const DISCOVERY_DIR = '.planning/discovery';
66
+ const ROADMAP_FILE = '.planning/ROADMAP.md';
67
+ const ART = Object.freeze({
68
+ requirements: `${DISCOVERY_DIR}/_requirements.md`,
69
+ stories: `${DISCOVERY_DIR}/_stories.md`,
70
+ processes: `${DISCOVERY_DIR}/_processes.md`,
71
+ entities: `${DISCOVERY_DIR}/_entities.md`,
72
+ rules: `${DISCOVERY_DIR}/_rules.md`,
73
+ screens: `${DISCOVERY_DIR}/_screens.md`,
74
+ roles: `${DISCOVERY_DIR}/_roles.md`,
75
+ integrations: `${DISCOVERY_DIR}/_integrations.md`,
76
+ migration: `${DISCOVERY_DIR}/_migration.md`,
77
+ deviations: `${DISCOVERY_DIR}/_deviations.md`,
78
+ phases: `${DISCOVERY_DIR}/_phases.md`,
79
+ });
80
+ /** The generic registers: filtered by a `Requirements` column wherever one exists. */
81
+ const GENERIC = Object.freeze([
82
+ { file: ART.migration, title: 'Migration' },
83
+ { file: ART.deviations, title: 'Deviations' },
84
+ ]);
85
+ const MARKER_OPEN = '<!-- discovery:auto -->';
86
+ const MARKER_CLOSE = '<!-- /discovery:auto -->';
87
+ const CONFORMANCE_OPEN = '<!-- discovery:conformance -->';
88
+ const CONFORMANCE_CLOSE = '<!-- /discovery:conformance -->';
89
+ const CONFORMANCE_HEADING = '### Discovery conformance';
90
+ const CONFORMANCE_HEADER = Object.freeze(['Artifact', 'Row', 'Status', 'Evidence']);
91
+ const STATUS_RE = /^(honoured|missed|amended \(D-P\d{2}-\d+\))$/;
92
+ const DEFAULT_STATUS = 'missed';
93
+ const REQUIREMENTS_HEADER = Object.freeze(['ID', 'Requirement', 'MoSCoW', 'Disposition', 'Provenance', 'Acceptance Criteria']);
94
+ const STORIES_HEADER = Object.freeze(['ID', 'EPIC', 'Requirements', 'Story', 'Acceptance criteria', 'Source step']);
95
+ const TO_BE_HEADER = Object.freeze(['ID', 'Main process', 'Replaces', 'Trigger', 'End state', 'Primary actor', 'EPICs']);
96
+ const STEPS_HEADER = Object.freeze(['#', 'Step', 'Actor', 'UI', 'Data objects', 'Integration', 'Exception', 'Requirements']);
97
+ const ENTITIES_HEADER = Object.freeze(['Entity', 'Identifier', 'Status lifecycle', 'Classification', 'Retention', 'Owner module', 'Source']);
98
+ const ATTRIBUTES_HEADER = Object.freeze(['Attribute', 'Type', 'Required', 'Bounds', 'Notes']);
99
+ const RELATIONSHIPS_HEADER = Object.freeze(['From', 'Cardinality', 'To', 'Name']);
100
+ const LIFECYCLES_HEADER = Object.freeze(['Entity', 'State', 'Transition', 'Allowed by']);
101
+ const RULES_HEADER = Object.freeze(['ID', 'Rule', 'Applies to', 'Checked at', 'Requirements', 'Source']);
102
+ const SCREENS_HEADER = Object.freeze(['ID', 'Role', 'Purpose', 'Surface', 'Requirements']);
103
+ const NO_SCREEN_HEADER = Object.freeze(['Requirement', 'Reason']);
104
+ const NO_SCREEN_SECTION = 'No screen (still MVP)';
105
+ const ROLES_HEADER = Object.freeze(['Role', 'Description', 'Scope', 'Recovery path', 'Source']);
106
+ const PERMISSIONS_HEADER = Object.freeze(['Role', 'Entity', 'Operation', 'Condition', 'Requirements']);
107
+ const ROLE_SCREENS_HEADER = Object.freeze(['Role', 'Screen']);
108
+ const INTERFACES_HEADER = Object.freeze(['ID', 'System', 'Direction', 'Pattern', 'Protocol', 'Format', 'Idempotency', 'Owner', 'Requirements']);
109
+ const CONTRACT_HEADER = Object.freeze(['Interface', 'Field', 'Type', 'Required', 'Mapping', 'Validation']);
110
+ const ERRORS_HEADER = Object.freeze(['Interface', 'Failure', 'Handling', 'Retry', 'Dead letter', 'Alert']);
111
+ const PHASE_ARG_RE = /^\d+(\.\d+)?$/;
112
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
113
+ const REQUIREMENTS_LINE_RE = /^\*\*Requirements\*\*:?\s*(.*)$/im;
114
+ const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
115
+ const DATA_OBJECT_RE = /^([A-Z][A-Za-z0-9]*) \[[CRUD](?:\/[CRUD])*\]$/;
116
+ const INTEGRATION_ID_RE = /^IF-\d{2}$/;
117
+ const REQUIREMENT_COLUMN_NAMES = Object.freeze(['requirements', 'requirement ids', 'requirement']);
118
+ const USAGE = 'Usage: pp-tools discovery for-phase <N> [--markdown | --conformance-template | --verify <VERIFICATION.md>]';
119
+ function readIfPresent(abs) {
120
+ if (!node_fs_1.default.existsSync(abs))
121
+ return { kind: 'absent' };
122
+ try {
123
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
124
+ }
125
+ catch {
126
+ return { kind: 'unreadable' };
127
+ }
128
+ }
129
+ function sameHeader(actual, expected) {
130
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
131
+ }
132
+ function splitList(cell) {
133
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
134
+ }
135
+ function cites(cell, ids) {
136
+ return splitList(cell ?? '').some((t) => ids.has(t));
137
+ }
138
+ function escapeCell(cell) {
139
+ return cell.replace(/\|/g, '\\|');
140
+ }
141
+ function renderTable(header, rows) {
142
+ const out = [`| ${header.join(' | ')} |`, `|${header.map(() => '---').join('|')}|`];
143
+ for (const r of rows)
144
+ out.push(`| ${header.map((_, i) => escapeCell(r.cells[i] ?? '')).join(' | ')} |`);
145
+ return out;
146
+ }
147
+ /** Sections of a file at heading level 2. */
148
+ function sectionsOf(content) {
149
+ return table.splitSections(content.split(/\r?\n/), 2);
150
+ }
151
+ /** The first table under a `## <heading>`, with its header checked against `expected`. */
152
+ function tableUnder(content, heading, expected, artifact, violations) {
153
+ const section = sectionsOf(content).find((s) => s.heading === heading);
154
+ if (!section) {
155
+ violations.push({ artifact, reason: 'missing_section', id: heading });
156
+ return null;
157
+ }
158
+ const tables = table.parseTables(section.body);
159
+ if (tables.length === 0) {
160
+ violations.push({ artifact, reason: 'missing_section', line: section.line, id: heading });
161
+ return null;
162
+ }
163
+ if (!sameHeader(tables[0].header, expected)) {
164
+ violations.push({ artifact, reason: 'bad_table_shape', line: tables[0].line, id: heading });
165
+ return null;
166
+ }
167
+ return tables[0];
168
+ }
169
+ // ─── Requirement-ID resolution ────────────────────────────────────────────────
170
+ function parseRequirementsLine(section) {
171
+ const m = REQUIREMENTS_LINE_RE.exec(section);
172
+ if (!m)
173
+ return [];
174
+ return m[1]
175
+ .replace(/[[\]]/g, '')
176
+ .split(/[,\s]+/)
177
+ .map((t) => t.trim())
178
+ .filter((t) => REQUIREMENT_ID_RE.test(t));
179
+ }
180
+ function resolveRequirementIds(cwd, phase, violations) {
181
+ if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, ROADMAP_FILE))) {
182
+ const found = roadmapParser.getRoadmapPhaseInternal(cwd, phase);
183
+ if (found && found.found) {
184
+ const ids = parseRequirementsLine(found.section);
185
+ if (ids.length > 0)
186
+ return { ids, source: 'ROADMAP.md' };
187
+ }
188
+ }
189
+ const plan = readIfPresent(node_path_1.default.join(cwd, ART.phases));
190
+ if (plan.kind === 'unreadable')
191
+ violations.push({ artifact: ART.phases, reason: 'artifact_unreadable' });
192
+ if (plan.kind === 'ok' && /^\d+$/.test(phase)) {
193
+ const parsed = phases.parsePhasePlan(plan.content);
194
+ for (const v of parsed.violations)
195
+ if (v.reason === 'missing_section' || v.reason === 'bad_table_shape')
196
+ violations.push(v);
197
+ const row = parsed.rows.find((r) => r.phase === Number(phase));
198
+ if (row && row.requirementIds.length > 0)
199
+ return { ids: row.requirementIds.filter((t) => REQUIREMENT_ID_RE.test(t)), source: '_phases.md' };
200
+ }
201
+ return { ids: [], source: null };
202
+ }
203
+ // ─── Collection ───────────────────────────────────────────────────────────────
204
+ function collectExtract(cwd, phase) {
205
+ const violations = [];
206
+ const present = Object.values(ART).some((rel) => node_fs_1.default.existsSync(node_path_1.default.join(cwd, rel)));
207
+ if (!present && !node_fs_1.default.existsSync(node_path_1.default.join(cwd, DISCOVERY_DIR)))
208
+ return null;
209
+ const { ids: idList, source } = resolveRequirementIds(cwd, phase, violations);
210
+ const ids = new Set(idList);
211
+ const tables = [];
212
+ const unfilterable = [];
213
+ const touched = new Set();
214
+ const push = (t) => {
215
+ if (t.rows.length === 0)
216
+ return;
217
+ tables.push(t);
218
+ touched.add(t.artifact);
219
+ };
220
+ // _requirements.md
221
+ const req = readIfPresent(node_path_1.default.join(cwd, ART.requirements));
222
+ if (req.kind === 'unreadable')
223
+ violations.push({ artifact: ART.requirements, reason: 'artifact_unreadable' });
224
+ if (req.kind === 'ok') {
225
+ const parsed = rollup.parseRequirementsArtifact(req.content);
226
+ if (!parsed.ok)
227
+ violations.push({ artifact: ART.requirements, reason: 'requirements_unparseable' });
228
+ const t = tableUnder(req.content, 'Requirements', REQUIREMENTS_HEADER, ART.requirements, violations);
229
+ if (t) {
230
+ push({
231
+ artifact: ART.requirements, title: 'Requirements', subtitle: null, header: [...REQUIREMENTS_HEADER], keyed: true, keyPrefix: '',
232
+ rows: t.rows.filter((r) => ids.has((r.cells[0] ?? '').trim())),
233
+ });
234
+ }
235
+ }
236
+ // _stories.md
237
+ const sto = readIfPresent(node_path_1.default.join(cwd, ART.stories));
238
+ if (sto.kind === 'unreadable')
239
+ violations.push({ artifact: ART.stories, reason: 'artifact_unreadable' });
240
+ if (sto.kind === 'ok') {
241
+ const t = tableUnder(sto.content, 'Stories', STORIES_HEADER, ART.stories, violations);
242
+ if (t) {
243
+ push({
244
+ artifact: ART.stories, title: 'User stories', subtitle: null, header: [...STORIES_HEADER], keyed: true, keyPrefix: '',
245
+ rows: t.rows.filter((r) => cites(r.cells[2], ids)),
246
+ });
247
+ }
248
+ }
249
+ // _processes.md — steps, then the to-be rows of the main processes touched
250
+ const entityNames = new Set();
251
+ const integrationIds = new Set();
252
+ const proc = readIfPresent(node_path_1.default.join(cwd, ART.processes));
253
+ if (proc.kind === 'unreadable')
254
+ violations.push({ artifact: ART.processes, reason: 'artifact_unreadable' });
255
+ if (proc.kind === 'ok') {
256
+ const mpsTouched = new Set();
257
+ const steps = sectionsOf(proc.content).find((s) => s.heading === 'Steps');
258
+ if (!steps) {
259
+ violations.push({ artifact: ART.processes, reason: 'missing_section', id: 'Steps' });
260
+ }
261
+ else {
262
+ const perProcess = table.splitSections(steps.body.map((l) => l.text), 3);
263
+ // splitSections renumbers from 1 relative to the body; map back to file lines.
264
+ const base = steps.body.length > 0 ? steps.body[0].n - 1 : 0;
265
+ for (const sec of perProcess) {
266
+ const m = MP_HEADING_RE.exec(sec.heading);
267
+ const tbl = table.parseTables(sec.body)[0];
268
+ if (!m || !tbl || !sameHeader(tbl.header, STEPS_HEADER)) {
269
+ violations.push({ artifact: ART.processes, reason: 'bad_table_shape', line: sec.line + base, id: sec.heading.slice(0, 40) });
270
+ continue;
271
+ }
272
+ const rows = tbl.rows
273
+ .filter((r) => cites(r.cells[7], ids))
274
+ .map((r) => ({ cells: r.cells, line: r.line + base }));
275
+ if (rows.length === 0)
276
+ continue;
277
+ mpsTouched.add(m[1]);
278
+ for (const r of rows) {
279
+ for (const obj of splitList(r.cells[4] ?? '')) {
280
+ const dm = DATA_OBJECT_RE.exec(obj);
281
+ if (dm)
282
+ entityNames.add(dm[1]);
283
+ }
284
+ for (const tok of splitList(r.cells[5] ?? ''))
285
+ if (INTEGRATION_ID_RE.test(tok))
286
+ integrationIds.add(tok);
287
+ }
288
+ push({ artifact: ART.processes, title: 'Process steps', subtitle: sec.heading, header: [...STEPS_HEADER], keyed: true, keyPrefix: `${m[1]} #`, rows });
289
+ }
290
+ }
291
+ const toBe = tableUnder(proc.content, 'To-be', TO_BE_HEADER, ART.processes, violations);
292
+ if (toBe && mpsTouched.size > 0) {
293
+ const rows = toBe.rows.filter((r) => mpsTouched.has((r.cells[0] ?? '').trim()));
294
+ // Main-process rows precede their steps in the extract.
295
+ const idx = tables.findIndex((t) => t.title === 'Process steps');
296
+ const mp = { artifact: ART.processes, title: 'Process steps', subtitle: 'Main processes', header: [...TO_BE_HEADER], keyed: false, keyPrefix: '', rows };
297
+ if (rows.length > 0)
298
+ tables.splice(idx === -1 ? tables.length : idx, 0, mp);
299
+ }
300
+ }
301
+ // _entities.md — the entities the selected steps touch
302
+ const ent = readIfPresent(node_path_1.default.join(cwd, ART.entities));
303
+ if (ent.kind === 'unreadable')
304
+ violations.push({ artifact: ART.entities, reason: 'artifact_unreadable' });
305
+ if (ent.kind === 'ok') {
306
+ const entities = tableUnder(ent.content, 'Entities', ENTITIES_HEADER, ART.entities, violations);
307
+ if (entities) {
308
+ push({
309
+ artifact: ART.entities, title: 'Entities', subtitle: null, header: [...ENTITIES_HEADER], keyed: true, keyPrefix: '',
310
+ rows: entities.rows.filter((r) => entityNames.has((r.cells[0] ?? '').trim())),
311
+ });
312
+ const attrs = sectionsOf(ent.content).find((s) => s.heading === 'Attributes');
313
+ if (attrs) {
314
+ const base = attrs.body.length > 0 ? attrs.body[0].n - 1 : 0;
315
+ for (const sec of table.splitSections(attrs.body.map((l) => l.text), 3)) {
316
+ if (!entityNames.has(sec.heading.trim()))
317
+ continue;
318
+ const tbl = table.parseTables(sec.body)[0];
319
+ if (!tbl || !sameHeader(tbl.header, ATTRIBUTES_HEADER)) {
320
+ violations.push({ artifact: ART.entities, reason: 'bad_table_shape', line: sec.line + base, id: sec.heading.slice(0, 40) });
321
+ continue;
322
+ }
323
+ push({
324
+ artifact: ART.entities, title: 'Entities', subtitle: `${sec.heading.trim()} (attributes)`, header: [...ATTRIBUTES_HEADER], keyed: false, keyPrefix: '',
325
+ rows: tbl.rows.map((r) => ({ cells: r.cells, line: r.line + base })),
326
+ });
327
+ }
328
+ }
329
+ const rel = sectionsOf(ent.content).find((s) => s.heading === 'Relationships');
330
+ if (rel) {
331
+ const tbl = table.parseTables(rel.body)[0];
332
+ if (tbl && sameHeader(tbl.header, RELATIONSHIPS_HEADER)) {
333
+ push({
334
+ artifact: ART.entities, title: 'Entities', subtitle: 'Relationships', header: [...RELATIONSHIPS_HEADER], keyed: false, keyPrefix: '',
335
+ rows: tbl.rows.filter((r) => entityNames.has((r.cells[0] ?? '').trim()) || entityNames.has((r.cells[2] ?? '').trim())),
336
+ });
337
+ }
338
+ }
339
+ const life = sectionsOf(ent.content).find((s) => s.heading === 'Lifecycles');
340
+ if (life) {
341
+ const tbl = table.parseTables(life.body)[0];
342
+ if (tbl && sameHeader(tbl.header, LIFECYCLES_HEADER)) {
343
+ push({
344
+ artifact: ART.entities, title: 'Entities', subtitle: 'Lifecycles', header: [...LIFECYCLES_HEADER], keyed: false, keyPrefix: '',
345
+ rows: tbl.rows.filter((r) => entityNames.has((r.cells[0] ?? '').trim())),
346
+ });
347
+ }
348
+ }
349
+ }
350
+ }
351
+ // _rules.md
352
+ const rul = readIfPresent(node_path_1.default.join(cwd, ART.rules));
353
+ if (rul.kind === 'unreadable')
354
+ violations.push({ artifact: ART.rules, reason: 'artifact_unreadable' });
355
+ if (rul.kind === 'ok') {
356
+ const t = tableUnder(rul.content, 'Rules', RULES_HEADER, ART.rules, violations);
357
+ if (t) {
358
+ push({
359
+ artifact: ART.rules, title: 'Business rules', subtitle: null, header: [...RULES_HEADER], keyed: true, keyPrefix: '',
360
+ rows: t.rows.filter((r) => cites(r.cells[4], ids)),
361
+ });
362
+ }
363
+ }
364
+ // _screens.md
365
+ const screenIds = new Set();
366
+ const scr = readIfPresent(node_path_1.default.join(cwd, ART.screens));
367
+ if (scr.kind === 'unreadable')
368
+ violations.push({ artifact: ART.screens, reason: 'artifact_unreadable' });
369
+ if (scr.kind === 'ok') {
370
+ const t = tableUnder(scr.content, 'Screens', SCREENS_HEADER, ART.screens, violations);
371
+ if (t) {
372
+ const rows = t.rows.filter((r) => cites(r.cells[4], ids));
373
+ for (const r of rows)
374
+ screenIds.add((r.cells[0] ?? '').trim());
375
+ push({ artifact: ART.screens, title: 'Screens', subtitle: null, header: [...SCREENS_HEADER], keyed: true, keyPrefix: '', rows });
376
+ }
377
+ const bucket = tableUnder(scr.content, NO_SCREEN_SECTION, NO_SCREEN_HEADER, ART.screens, violations);
378
+ if (bucket) {
379
+ push({
380
+ artifact: ART.screens, title: 'Screens', subtitle: NO_SCREEN_SECTION, header: [...NO_SCREEN_HEADER], keyed: true, keyPrefix: 'no-screen ',
381
+ rows: bucket.rows.filter((r) => ids.has((r.cells[0] ?? '').trim())),
382
+ });
383
+ }
384
+ }
385
+ // _roles.md — permissions by requirement; the roles they name, and those roles' screens
386
+ const rol = readIfPresent(node_path_1.default.join(cwd, ART.roles));
387
+ if (rol.kind === 'unreadable')
388
+ violations.push({ artifact: ART.roles, reason: 'artifact_unreadable' });
389
+ if (rol.kind === 'ok') {
390
+ const roles = tableUnder(rol.content, 'Roles', ROLES_HEADER, ART.roles, violations);
391
+ const permissions = tableUnder(rol.content, 'Permissions', PERMISSIONS_HEADER, ART.roles, violations);
392
+ const roleScreens = tableUnder(rol.content, 'Screens', ROLE_SCREENS_HEADER, ART.roles, violations);
393
+ if (roles && permissions && roleScreens) {
394
+ const permRows = permissions.rows.filter((r) => cites(r.cells[4], ids));
395
+ const roleNames = new Set(permRows.map((r) => (r.cells[0] ?? '').trim()));
396
+ push({ artifact: ART.roles, title: 'Roles', subtitle: 'Roles', header: [...ROLES_HEADER], keyed: true, keyPrefix: '', rows: roles.rows.filter((r) => roleNames.has((r.cells[0] ?? '').trim())) });
397
+ push({ artifact: ART.roles, title: 'Roles', subtitle: 'Permissions', header: [...PERMISSIONS_HEADER], keyed: true, keyPrefix: '', rows: permRows });
398
+ push({
399
+ artifact: ART.roles, title: 'Roles', subtitle: 'Screens', header: [...ROLE_SCREENS_HEADER], keyed: false, keyPrefix: '',
400
+ rows: roleScreens.rows.filter((r) => roleNames.has((r.cells[0] ?? '').trim()) || screenIds.has((r.cells[1] ?? '').trim())),
401
+ });
402
+ }
403
+ }
404
+ // _integrations.md — interfaces by requirement or step reference; their contract and error rows
405
+ const integ = readIfPresent(node_path_1.default.join(cwd, ART.integrations));
406
+ if (integ.kind === 'unreadable')
407
+ violations.push({ artifact: ART.integrations, reason: 'artifact_unreadable' });
408
+ if (integ.kind === 'ok') {
409
+ const interfaces = tableUnder(integ.content, 'Interfaces', INTERFACES_HEADER, ART.integrations, violations);
410
+ const contract = tableUnder(integ.content, 'Contract', CONTRACT_HEADER, ART.integrations, violations);
411
+ const errors = tableUnder(integ.content, 'Errors', ERRORS_HEADER, ART.integrations, violations);
412
+ if (interfaces && contract && errors) {
413
+ const rows = interfaces.rows.filter((r) => cites(r.cells[8], ids) || integrationIds.has((r.cells[0] ?? '').trim()));
414
+ const selected = new Set(rows.map((r) => (r.cells[0] ?? '').trim()));
415
+ push({ artifact: ART.integrations, title: 'Integrations', subtitle: 'Interfaces', header: [...INTERFACES_HEADER], keyed: true, keyPrefix: '', rows });
416
+ push({ artifact: ART.integrations, title: 'Integrations', subtitle: 'Contract', header: [...CONTRACT_HEADER], keyed: false, keyPrefix: '', rows: contract.rows.filter((r) => selected.has((r.cells[0] ?? '').trim())) });
417
+ push({ artifact: ART.integrations, title: 'Integrations', subtitle: 'Errors', header: [...ERRORS_HEADER], keyed: false, keyPrefix: '', rows: errors.rows.filter((r) => selected.has((r.cells[0] ?? '').trim())) });
418
+ }
419
+ }
420
+ // Generic registers: filtered by a Requirements column.
421
+ for (const g of GENERIC) {
422
+ const read = readIfPresent(node_path_1.default.join(cwd, g.file));
423
+ if (read.kind === 'unreadable')
424
+ violations.push({ artifact: g.file, reason: 'artifact_unreadable' });
425
+ if (read.kind !== 'ok')
426
+ continue;
427
+ for (const sec of sectionsOf(read.content)) {
428
+ const tbl = table.parseTables(sec.body)[0];
429
+ if (!tbl)
430
+ continue;
431
+ const col = tbl.header.findIndex((h) => REQUIREMENT_COLUMN_NAMES.includes(h.trim().toLowerCase()));
432
+ if (col === -1) {
433
+ unfilterable.push(`${g.file} § ${sec.heading}`);
434
+ continue;
435
+ }
436
+ const rows = tbl.rows.filter((r) => cites(r.cells[col], ids));
437
+ push({ artifact: g.file, title: g.title, subtitle: sec.heading, header: tbl.header, keyed: true, keyPrefix: '', rows });
438
+ }
439
+ }
440
+ return { phase, source, requirementIds: idList, tables, unfilterable, artifacts: [...touched], violations };
441
+ }
442
+ // ─── Rendering ────────────────────────────────────────────────────────────────
443
+ function keysOf(extract) {
444
+ const keys = [];
445
+ const seen = new Set();
446
+ for (const t of extract.tables) {
447
+ if (!t.keyed)
448
+ continue;
449
+ for (const r of t.rows) {
450
+ const row = `${t.keyPrefix}${(r.cells[0] ?? '').trim()}`;
451
+ const k = `${t.artifact} ${row}`;
452
+ if (seen.has(k))
453
+ continue;
454
+ seen.add(k);
455
+ keys.push({ artifact: node_path_1.default.basename(t.artifact), row });
456
+ }
457
+ }
458
+ return keys;
459
+ }
460
+ function renderExtract(extract) {
461
+ const out = [MARKER_OPEN, `## Discovery extract — Phase ${extract.phase}`, ''];
462
+ const src = extract.source === 'ROADMAP.md' ? ROADMAP_FILE : extract.source === '_phases.md' ? ART.phases : null;
463
+ out.push(extract.requirementIds.length === 0
464
+ ? `Requirement IDs: none resolved (no \`**Requirements**:\` line for this phase in ${ROADMAP_FILE} and no row in ${ART.phases}).`
465
+ : `Requirement IDs: ${extract.requirementIds.join(', ')} (source: ${src})`);
466
+ out.push(`Rendered by \`pp-tools discovery for-phase ${extract.phase}\`. Do not edit here: amend the artifact row (\`→ amended by D-P<nn>-<n>\`) and re-run.`, '');
467
+ if (extract.tables.length === 0) {
468
+ out.push('No artifact row cites these IDs.', '');
469
+ }
470
+ let currentTitle = null;
471
+ for (const t of extract.tables) {
472
+ if (t.title !== currentTitle) {
473
+ out.push(`### ${t.title} — \`${t.artifact}\``, '');
474
+ currentTitle = t.title;
475
+ }
476
+ if (t.subtitle)
477
+ out.push(`#### ${t.subtitle}`, '');
478
+ out.push(...renderTable(t.header, t.rows), '');
479
+ }
480
+ for (const u of extract.unfilterable)
481
+ out.push(`Not filterable by requirement: \`${u}\` has no requirement column — read it whole.`);
482
+ if (extract.unfilterable.length > 0)
483
+ out.push('');
484
+ out.push(`Artifacts read: ${extract.artifacts.length === 0 ? 'none' : extract.artifacts.join(', ')}`);
485
+ out.push(MARKER_CLOSE);
486
+ return out.join('\n') + '\n';
487
+ }
488
+ function renderConformanceTemplate(extract) {
489
+ const keys = keysOf(extract);
490
+ const rows = keys.map((k) => ({ cells: [`\`${k.artifact}\``, k.row, DEFAULT_STATUS, ''] }));
491
+ const out = [CONFORMANCE_HEADING, '', CONFORMANCE_OPEN, ...renderTable(CONFORMANCE_HEADER, rows), CONFORMANCE_CLOSE];
492
+ if (keys.length === 0)
493
+ out.splice(3, 0, '_No discovery row cites this phase._');
494
+ return out.join('\n') + '\n';
495
+ }
496
+ // ─── Conformance verification ─────────────────────────────────────────────────
497
+ function verifyConformance(text, extract) {
498
+ const violations = [];
499
+ const lines = text.split(/\r?\n/);
500
+ const opens = lines.map((l, i) => (l.trim() === CONFORMANCE_OPEN ? i : -1)).filter((i) => i !== -1);
501
+ const closes = lines.map((l, i) => (l.trim() === CONFORMANCE_CLOSE ? i : -1)).filter((i) => i !== -1);
502
+ if (opens.length !== 1 || closes.length !== 1 || closes[0] < opens[0]) {
503
+ return { ok: false, rows: 0, honoured: 0, amended: 0, missed: 0, violations: [{ artifact: 'VERIFICATION.md', reason: 'conformance_markers' }] };
504
+ }
505
+ const body = lines.slice(opens[0] + 1, closes[0]).map((text, i) => ({ n: opens[0] + 2 + i, text }));
506
+ const tbl = table.parseTables(body)[0];
507
+ if (!tbl || !sameHeader(tbl.header, CONFORMANCE_HEADER)) {
508
+ return { ok: false, rows: 0, honoured: 0, amended: 0, missed: 0, violations: [{ artifact: 'VERIFICATION.md', reason: 'bad_table_shape', line: opens[0] + 1 }] };
509
+ }
510
+ const expected = new Set(keysOf(extract).map((k) => `${k.artifact} ${k.row}`));
511
+ const seen = new Set();
512
+ let honoured = 0;
513
+ let amended = 0;
514
+ let missed = 0;
515
+ for (const r of tbl.rows) {
516
+ const artifact = (r.cells[0] ?? '').trim().replace(/^`|`$/g, '');
517
+ const key = `${artifact} ${(r.cells[1] ?? '').trim()}`;
518
+ const status = (r.cells[2] ?? '').trim();
519
+ if (!expected.has(key))
520
+ violations.push({ artifact: 'VERIFICATION.md', reason: 'unknown_row', line: r.line, id: key.slice(0, 60) });
521
+ else if (seen.has(key))
522
+ violations.push({ artifact: 'VERIFICATION.md', reason: 'duplicate_row', line: r.line, id: key.slice(0, 60) });
523
+ seen.add(key);
524
+ if (!STATUS_RE.test(status)) {
525
+ violations.push({ artifact: 'VERIFICATION.md', reason: 'bad_status', line: r.line, id: key.slice(0, 60) });
526
+ }
527
+ else if (status === 'honoured')
528
+ honoured += 1;
529
+ else if (status === 'missed')
530
+ missed += 1;
531
+ else
532
+ amended += 1;
533
+ }
534
+ for (const k of expected)
535
+ if (!seen.has(k))
536
+ violations.push({ artifact: 'VERIFICATION.md', reason: 'missing_row', id: k.slice(0, 60) });
537
+ return { ok: violations.length === 0, rows: tbl.rows.length, honoured, amended, missed, violations };
538
+ }
539
+ function formatViolation(v) {
540
+ const where = v.line !== undefined ? `:${v.line}` : '';
541
+ const what = v.id !== undefined ? ` (${v.id})` : '';
542
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
543
+ }
544
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
545
+ /**
546
+ * `pp-tools discovery for-phase <N> [--markdown | --conformance-template | --verify <file>]`
547
+ * (dotted `query discovery.for-phase`). No discovery package → exit 0,
548
+ * `present: false`, empty markdown.
549
+ */
550
+ function cmdDiscoveryForPhase(cwd, args, raw) {
551
+ const rest = args.slice(2).filter((a) => a !== '--raw');
552
+ const phase = rest[0];
553
+ const flags = rest.slice(1);
554
+ const markdown = flags.includes('--markdown');
555
+ const template = flags.includes('--conformance-template');
556
+ const vIdx = flags.indexOf('--verify');
557
+ const verifyFile = vIdx === -1 ? undefined : flags[vIdx + 1];
558
+ const known = flags.filter((f, i) => f !== '--markdown' && f !== '--conformance-template' && f !== '--verify' && (vIdx === -1 || i !== vIdx + 1));
559
+ if (!phase || !PHASE_ARG_RE.test(phase) || known.length > 0 || [markdown, template, vIdx !== -1].filter(Boolean).length > 1 || (vIdx !== -1 && !verifyFile)) {
560
+ error(USAGE, ERROR_REASON.USAGE);
561
+ return; // unreachable
562
+ }
563
+ const extract = collectExtract(cwd, phase);
564
+ if (extract === null) {
565
+ const payload = { ok: true, present: false, phase, requirementIds: [], artifacts: [], markdown: '' };
566
+ output(payload, markdown || template, '');
567
+ return;
568
+ }
569
+ if (extract.violations.length > 0) {
570
+ output({ ok: false, present: true, phase, violations: extract.violations.length }, raw);
571
+ error(`discovery for-phase: ${extract.violations.length} violation(s):\n` + extract.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_FOR_PHASE_FAILED);
572
+ return; // unreachable
573
+ }
574
+ if (verifyFile !== undefined) {
575
+ let text = '';
576
+ try {
577
+ text = node_fs_1.default.readFileSync(node_path_1.default.resolve(cwd, verifyFile), 'utf-8');
578
+ }
579
+ catch {
580
+ output({ ok: false, present: true, phase, file: verifyFile }, raw);
581
+ error(`discovery for-phase: ${verifyFile} could not be read`, ERROR_REASON.DISCOVERY_FOR_PHASE_FAILED);
582
+ return; // unreachable
583
+ }
584
+ const result = verifyConformance(text, extract);
585
+ const payload = { ok: result.ok, present: true, phase, file: verifyFile, rows: result.rows, honoured: result.honoured, amended: result.amended, missed: result.missed };
586
+ if (!result.ok) {
587
+ output({ ...payload, violations: result.violations.length }, raw);
588
+ error(`discovery for-phase: conformance block has ${result.violations.length} fault(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_FOR_PHASE_FAILED);
589
+ return; // unreachable
590
+ }
591
+ output(payload, raw);
592
+ return;
593
+ }
594
+ const md = renderExtract(extract);
595
+ const keys = keysOf(extract);
596
+ const payload = {
597
+ ok: true,
598
+ present: true,
599
+ phase,
600
+ source: extract.source,
601
+ requirementIds: extract.requirementIds,
602
+ artifacts: extract.artifacts,
603
+ rows: keys,
604
+ unfilterable: extract.unfilterable,
605
+ markdown: md,
606
+ };
607
+ if (template) {
608
+ output(payload, true, renderConformanceTemplate(extract));
609
+ return;
610
+ }
611
+ output(payload, markdown || raw, md);
612
+ }
613
+ module.exports = {
614
+ MARKER_OPEN,
615
+ MARKER_CLOSE,
616
+ CONFORMANCE_OPEN,
617
+ CONFORMANCE_CLOSE,
618
+ CONFORMANCE_HEADING,
619
+ CONFORMANCE_HEADER,
620
+ STATUS_RE,
621
+ FOR_PHASE_FAILED: ERROR_REASON.DISCOVERY_FOR_PHASE_FAILED,
622
+ parseRequirementsLine,
623
+ collectExtract,
624
+ renderExtract,
625
+ renderConformanceTemplate,
626
+ verifyConformance,
627
+ cmdDiscoveryForPhase,
628
+ };