@pareto4/pp-core 1.19.0 → 1.21.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 (126) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-nyquist-auditor.md +19 -14
  3. package/agents/pp-phase-researcher.md +10 -0
  4. package/agents/pp-plan-checker.md +59 -36
  5. package/agents/pp-planner.md +49 -53
  6. package/agents/pp-vault-reviewer.md +2 -0
  7. package/agents/pp-verifier.md +15 -60
  8. package/commands/pp-autonomous.md +2 -1
  9. package/commands/pp-discovery.md +3 -2
  10. package/commands/pp-plan-phase.md +2 -1
  11. package/commands/pp-plan-review-convergence.md +2 -1
  12. package/gemini-extension.json +1 -1
  13. package/package.json +6 -3
  14. package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
  15. package/pareto-skills/pp-project-setup/SKILL.md +18 -343
  16. package/pp-core/bin/lib/capability-activation.cjs +77 -0
  17. package/pp-core/bin/lib/capability-registry.cjs +15 -15
  18. package/pp-core/bin/lib/capability-state.cjs +8 -18
  19. package/pp-core/bin/lib/ceremony.cjs +1342 -0
  20. package/pp-core/bin/lib/check-command-router.cjs +62 -1
  21. package/pp-core/bin/lib/commands.cjs +29 -3
  22. package/pp-core/bin/lib/config.cjs +5 -0
  23. package/pp-core/bin/lib/discovery-access.cjs +276 -0
  24. package/pp-core/bin/lib/discovery-charter.cjs +664 -0
  25. package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
  26. package/pp-core/bin/lib/discovery-entities.cjs +323 -0
  27. package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
  28. package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
  29. package/pp-core/bin/lib/discovery-migration.cjs +246 -0
  30. package/pp-core/bin/lib/discovery-phases.cjs +233 -0
  31. package/pp-core/bin/lib/discovery-processes.cjs +355 -0
  32. package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
  33. package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
  34. package/pp-core/bin/lib/discovery-rules.cjs +216 -0
  35. package/pp-core/bin/lib/discovery-screens.cjs +1 -1
  36. package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
  37. package/pp-core/bin/lib/discovery-stories.cjs +273 -0
  38. package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
  39. package/pp-core/bin/lib/discovery-table.cjs +55 -0
  40. package/pp-core/bin/lib/init.cjs +50 -1
  41. package/pp-core/bin/lib/io.cjs +24 -0
  42. package/pp-core/bin/lib/loop-resolver.cjs +26 -12
  43. package/pp-core/bin/lib/phase.cjs +39 -3
  44. package/pp-core/bin/lib/plan-sizing.cjs +346 -0
  45. package/pp-core/bin/lib/test-substance.cjs +3190 -0
  46. package/pp-core/bin/lib/timeline.cjs +605 -0
  47. package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
  48. package/pp-core/bin/pp-tools.cjs +214 -5
  49. package/pp-core/bin/shared/ceremony-gates.json +17 -0
  50. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  51. package/pp-core/references/ceremony.md +81 -0
  52. package/pp-core/references/discovery-access.md +112 -0
  53. package/pp-core/references/discovery-build-consumption.md +256 -0
  54. package/pp-core/references/discovery-charter.md +127 -0
  55. package/pp-core/references/discovery-data-model.md +145 -0
  56. package/pp-core/references/discovery-id-conventions.md +50 -14
  57. package/pp-core/references/discovery-integrations.md +151 -0
  58. package/pp-core/references/discovery-processes.md +190 -0
  59. package/pp-core/references/discovery-rebaseline.md +2 -2
  60. package/pp-core/references/discovery-research-track.md +5 -6
  61. package/pp-core/references/discovery-session-output.md +70 -0
  62. package/pp-core/references/discovery-standards.md +107 -0
  63. package/pp-core/references/discovery-traceability.md +2 -2
  64. package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
  65. package/pp-core/references/phase-timeline.md +107 -0
  66. package/pp-core/references/planner-antipatterns.md +32 -0
  67. package/pp-core/references/planner-discovery-extract.md +35 -0
  68. package/pp-core/references/planner-guidance.md +17 -14
  69. package/pp-core/references/planner-revision.md +1 -1
  70. package/pp-core/references/planning-config.md +3 -2
  71. package/pp-core/references/test-substance.md +324 -0
  72. package/pp-core/references/verifier-discovery-conformance.md +36 -0
  73. package/pp-core/references/verifier-stub-patterns.md +51 -0
  74. package/pp-core/templates/VALIDATION.md +1 -0
  75. package/pp-core/templates/phase-prompt.md +19 -7
  76. package/pp-core/templates/project-charter.md +161 -100
  77. package/pp-core/templates/project.md +3 -3
  78. package/pp-core/templates/requirements.md +18 -0
  79. package/pp-core/templates/roadmap.md +1 -1
  80. package/pp-core/templates/verification-report.md +17 -1
  81. package/pp-core/workflows/audit-milestone.md +4 -1
  82. package/pp-core/workflows/autonomous.md +11 -8
  83. package/pp-core/workflows/code-review.md +15 -6
  84. package/pp-core/workflows/discovery-sprint/charter.md +114 -0
  85. package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
  86. package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
  87. package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
  88. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
  89. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
  90. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
  91. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
  92. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
  93. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
  94. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
  95. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
  96. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
  97. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
  98. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
  99. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
  100. package/pp-core/workflows/discovery-sprint.md +12 -15
  101. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
  102. package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
  103. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
  104. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
  105. package/pp-core/workflows/discovery-to-build.md +70 -34
  106. package/pp-core/workflows/discuss-phase/discovery.md +83 -0
  107. package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
  108. package/pp-core/workflows/discuss-phase.md +10 -7
  109. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
  110. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
  111. package/pp-core/workflows/execute-phase.md +14 -3
  112. package/pp-core/workflows/new-project.md +3 -3
  113. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
  114. package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
  115. package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
  116. package/pp-core/workflows/plan-phase.md +62 -124
  117. package/pp-core/workflows/plan-review-convergence.md +6 -3
  118. package/pp-core/workflows/ship.md +2 -2
  119. package/pp-core/workflows/stats.md +19 -4
  120. package/pp-core/workflows/validate-phase.md +19 -4
  121. package/pp-core/workflows/vault-review.md +5 -0
  122. package/pp-core/workflows/verify-phase.md +1 -1
  123. package/pp-core/workflows/verify-work.md +1 -1
  124. package/scripts/gen-capability-registry.cjs +43 -45
  125. package/scripts/run-tests.cjs +54 -30
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery phase-plan check (Discovery Sprint improvement plan, Batch 4).
4
+ *
5
+ * `.planning/discovery/_phases.md` is the Phase Plan Session 11 decides —
6
+ * one row per build phase with its goal, EPICs, requirement IDs, success
7
+ * criteria and dependencies — and the literal source of `ROADMAP.md` at the
8
+ * handoff (review F-5: before Batch 4 the roadmap was derived by prose
9
+ * instruction from an EPIC build order that carried no requirement IDs). This
10
+ * module checks the structure:
11
+ *
12
+ * - the `## Phases` table exists with the template's columns;
13
+ * - `Phase` is a positive integer, unique, contiguous from 1;
14
+ * - `Goal` and `Success criteria` are non-empty;
15
+ * - `EPICs` is `—` or a comma list of `E-nn`;
16
+ * - `Requirement IDs` is a comma list (or `—` for a phase that realises no
17
+ * requirement), each ID existing in `_requirements.md` when it is on file
18
+ * and parseable, none `Dropped`;
19
+ * - `Depends on` is `—` or a comma list of earlier phase numbers;
20
+ * - every MVP-disposition requirement appears in exactly one phase.
21
+ *
22
+ * Posture (D-43/D-53): reports at Sessions 11 and 13; hard-blocks at
23
+ * `/pp-discovery-to-build`. Absent artifact → exit 0.
24
+ */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ const node_fs_1 = __importDefault(require("node:fs"));
29
+ const node_path_1 = __importDefault(require("node:path"));
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ const io = require("./io.cjs");
32
+ const { output, error, ERROR_REASON } = io;
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const table = require("./discovery-table.cjs");
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
36
+ const rollup = require("./discovery-rollup.cjs");
37
+ // ─── Constants ────────────────────────────────────────────────────────────────
38
+ const DISCOVERY_DIR = '.planning/discovery';
39
+ const PHASES_FILE = `${DISCOVERY_DIR}/_phases.md`;
40
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
41
+ const PHASES_SECTION = 'Phases';
42
+ const PHASES_HEADER = Object.freeze(['Phase', 'Goal', 'EPICs', 'Requirement IDs', 'Success criteria', 'Depends on']);
43
+ const PHASE_RE = /^[1-9]\d{0,2}$/;
44
+ const EPIC_RE = /^E-\d{2}$/;
45
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
46
+ const PENDING = '—';
47
+ /** Separator between success criteria inside one cell: space, middle dot U+00B7, space (the Provenance grammar's). */
48
+ const CRITERIA_SEPARATOR = ' · ';
49
+ const USAGE = 'Usage: pp-tools discovery phases-check';
50
+ function readIfPresent(abs) {
51
+ if (!node_fs_1.default.existsSync(abs))
52
+ return { kind: 'absent' };
53
+ try {
54
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
55
+ }
56
+ catch {
57
+ return { kind: 'unreadable' };
58
+ }
59
+ }
60
+ function sameHeader(actual, expected) {
61
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
62
+ }
63
+ function splitList(cell) {
64
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
65
+ }
66
+ /**
67
+ * The `## Phases` rows of a `_phases.md` text, or `null` with the structural
68
+ * fault when the section or table is off the template. Shared with
69
+ * `for-phase`, which reads the plan as the pre-handoff source of a phase's
70
+ * requirement IDs.
71
+ */
72
+ function parsePhasePlan(content) {
73
+ const violations = [];
74
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === PHASES_SECTION);
75
+ if (!section)
76
+ return { rows: [], violations: [{ artifact: PHASES_FILE, reason: 'missing_section', id: PHASES_SECTION }] };
77
+ const tables = table.parseTables(section.body);
78
+ if (tables.length === 0)
79
+ return { rows: [], violations: [{ artifact: PHASES_FILE, reason: 'missing_section', line: section.line, id: PHASES_SECTION }] };
80
+ if (!sameHeader(tables[0].header, PHASES_HEADER)) {
81
+ return { rows: [], violations: [{ artifact: PHASES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: PHASES_SECTION }] };
82
+ }
83
+ const rows = [];
84
+ for (const row of tables[0].rows) {
85
+ const [phase, goal, epics, requirementIds, successCriteria, dependsOn] = row.cells.map((c) => c.trim());
86
+ if (!PHASE_RE.test(phase ?? '')) {
87
+ violations.push({ artifact: PHASES_FILE, reason: 'bad_phase_number', line: row.line });
88
+ continue;
89
+ }
90
+ const epicList = (epics ?? '') === PENDING ? [] : splitList(epics ?? '');
91
+ const depList = (dependsOn ?? '') === PENDING ? [] : splitList(dependsOn ?? '');
92
+ rows.push({
93
+ phase: Number(phase),
94
+ goal: goal ?? '',
95
+ epics: epicList,
96
+ requirementIds: (requirementIds ?? '') === PENDING ? [] : splitList(requirementIds ?? ''),
97
+ requirementsPending: (requirementIds ?? '') === PENDING,
98
+ successCriteria: (successCriteria ?? '').split(CRITERIA_SEPARATOR).map((s) => s.trim()).filter(Boolean),
99
+ dependsOn: depList.map((d) => (PHASE_RE.test(d) ? Number(d) : NaN)),
100
+ line: row.line,
101
+ });
102
+ }
103
+ return { rows, violations };
104
+ }
105
+ // ─── Check ────────────────────────────────────────────────────────────────────
106
+ function checkPhases(content, requirements) {
107
+ const parsed = parsePhasePlan(content);
108
+ const violations = [...parsed.violations];
109
+ if (parsed.rows.length === 0 && violations.length > 0 && violations[0].reason !== 'bad_phase_number') {
110
+ return { ok: false, phases: 0, violations };
111
+ }
112
+ const known = requirements instanceof Array ? new Map(requirements.map((r) => [r.id, r])) : null;
113
+ const seen = new Set();
114
+ const planned = new Map();
115
+ for (const row of parsed.rows) {
116
+ if (seen.has(row.phase))
117
+ violations.push({ artifact: PHASES_FILE, reason: 'duplicate_phase', line: row.line, id: String(row.phase) });
118
+ seen.add(row.phase);
119
+ if (row.goal === '')
120
+ violations.push({ artifact: PHASES_FILE, reason: 'empty_cell', line: row.line, id: 'Goal' });
121
+ if (row.successCriteria.length === 0)
122
+ violations.push({ artifact: PHASES_FILE, reason: 'empty_cell', line: row.line, id: 'Success criteria' });
123
+ for (const e of row.epics)
124
+ if (!EPIC_RE.test(e))
125
+ violations.push({ artifact: PHASES_FILE, reason: 'bad_epic', line: row.line, id: e.slice(0, 40) });
126
+ if (row.requirementIds.length === 0 && !row.requirementsPending)
127
+ violations.push({ artifact: PHASES_FILE, reason: 'no_requirement', line: row.line });
128
+ for (const r of row.requirementIds) {
129
+ if (!REQUIREMENT_ID_RE.test(r)) {
130
+ violations.push({ artifact: PHASES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
131
+ continue;
132
+ }
133
+ if (known !== null) {
134
+ const fact = known.get(r);
135
+ if (!fact) {
136
+ violations.push({ artifact: PHASES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
137
+ continue;
138
+ }
139
+ if (fact.disposition === rollup.DISPOSITIONS.DROPPED)
140
+ violations.push({ artifact: PHASES_FILE, reason: 'dropped_requirement_planned', line: row.line, id: r });
141
+ }
142
+ const list = planned.get(r) ?? [];
143
+ list.push(row.phase);
144
+ planned.set(r, list);
145
+ }
146
+ for (const d of row.dependsOn) {
147
+ if (Number.isNaN(d) || d >= row.phase)
148
+ violations.push({ artifact: PHASES_FILE, reason: 'bad_dependency', line: row.line, id: String(row.phase) });
149
+ }
150
+ }
151
+ const max = table.boundedSequenceMax([...seen]);
152
+ if (max !== null) {
153
+ for (let n = 1; n <= max; n += 1) {
154
+ if (!seen.has(n))
155
+ violations.push({ artifact: PHASES_FILE, reason: 'sequence_gap', id: String(n) });
156
+ }
157
+ }
158
+ for (const row of parsed.rows) {
159
+ for (const d of row.dependsOn) {
160
+ if (!Number.isNaN(d) && d < row.phase && !seen.has(d))
161
+ violations.push({ artifact: PHASES_FILE, reason: 'bad_dependency', line: row.line, id: String(d) });
162
+ }
163
+ }
164
+ for (const [id, phases] of planned) {
165
+ if (phases.length > 1)
166
+ violations.push({ artifact: PHASES_FILE, reason: 'requirement_in_multiple_phases', id: `${id} (${phases.join(', ')})` });
167
+ }
168
+ if (requirements instanceof Array) {
169
+ for (const r of requirements) {
170
+ if (r.disposition === rollup.DISPOSITIONS.MVP && !planned.has(r.id))
171
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'mvp_requirement_unplanned', id: r.id });
172
+ }
173
+ }
174
+ else if (requirements === 'unparseable') {
175
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
176
+ }
177
+ return { ok: violations.length === 0, phases: seen.size, violations };
178
+ }
179
+ function checkProjectPhases(cwd) {
180
+ const read = readIfPresent(node_path_1.default.join(cwd, PHASES_FILE));
181
+ if (read.kind === 'absent')
182
+ return null;
183
+ if (read.kind === 'unreadable')
184
+ return { ok: false, file: PHASES_FILE, phases: 0, violations: [{ artifact: PHASES_FILE, reason: 'artifact_unreadable' }] };
185
+ const extra = [];
186
+ let requirements = null;
187
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
188
+ if (req.kind === 'unreadable')
189
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
190
+ if (req.kind === 'ok') {
191
+ const parsed = rollup.parseRequirementsArtifact(req.content);
192
+ requirements = parsed.ok ? parsed.rows.map((r) => ({ id: r.id, disposition: r.disposition })) : 'unparseable';
193
+ }
194
+ const result = checkPhases(read.content, requirements);
195
+ result.violations.push(...extra);
196
+ result.ok = result.violations.length === 0;
197
+ return { file: PHASES_FILE, ...result };
198
+ }
199
+ function formatViolation(v) {
200
+ const where = v.line !== undefined ? `:${v.line}` : '';
201
+ const what = v.id !== undefined ? ` (${v.id})` : '';
202
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
203
+ }
204
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
205
+ /** `pp-tools discovery phases-check` (dotted `query discovery.phases-check`). Absent artifact → exit 0, `present: false`. */
206
+ function cmdDiscoveryPhasesCheck(cwd, args, raw) {
207
+ if (args.length > 2) {
208
+ error(USAGE, ERROR_REASON.USAGE);
209
+ return; // unreachable
210
+ }
211
+ const result = checkProjectPhases(cwd);
212
+ if (result === null) {
213
+ output({ ok: true, present: false, file: PHASES_FILE }, raw);
214
+ return;
215
+ }
216
+ const payload = { ok: result.ok, present: true, file: result.file, phases: result.phases };
217
+ if (!result.ok) {
218
+ output({ ...payload, violations: result.violations.length }, raw);
219
+ error(`discovery phases-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_PHASES_CHECK_FAILED);
220
+ return; // unreachable
221
+ }
222
+ output(payload, raw);
223
+ }
224
+ module.exports = {
225
+ PHASES_FILE,
226
+ PHASES_HEADER,
227
+ CRITERIA_SEPARATOR,
228
+ PHASES_CHECK_FAILED: ERROR_REASON.DISCOVERY_PHASES_CHECK_FAILED,
229
+ parsePhasePlan,
230
+ checkPhases,
231
+ checkProjectPhases,
232
+ cmdDiscoveryPhasesCheck,
233
+ };
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery process-model check (Discovery Sprint improvement plan, Batch 2).
4
+ *
5
+ * `.planning/discovery/_processes.md` holds the as-is processes (`PRC-`,
6
+ * Session 2), the to-be main processes (`MP-`, Session 3) with the as-is
7
+ * processes each one replaces, one swimlane flowchart per main process (a
8
+ * `subgraph` per actor) and one step matrix per main process whose rows are
9
+ * what Session 3 Step 5D mints requirements from — now with an `Exception`
10
+ * column, so every error path has a home. This module checks the structure:
11
+ *
12
+ * - the four sections exist and the tables have the template's columns;
13
+ * - `PRC-` and `MP-` IDs are well-formed, unique and contiguous;
14
+ * - every `Replaces` cell is `new` or names as-is processes that exist;
15
+ * - every main process has a flow (a `flowchart` with at least one
16
+ * `subgraph`) and a step table, and every flow or step heading names a
17
+ * main process that exists;
18
+ * - every actor is a Session 2 persona or `System`, when `session-02.md`
19
+ * is on file; every data object is `Name [C/R/U/D]` or `—`;
20
+ * - every cited requirement exists in `_requirements.md`, when it is on
21
+ * file and parseable;
22
+ * - with `--require-cited`: every step cites at least one requirement —
23
+ * the state Step 5D leaves behind, required at the handoff.
24
+ *
25
+ * Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at
26
+ * `/pp-discovery-to-build` with `--require-cited`. Absent artifact → exit 0.
27
+ *
28
+ * Content-free contract: output names artifacts, line numbers, validated IDs,
29
+ * section names, column names and reason codes — never cell prose.
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
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const table = require("./discovery-table.cjs");
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const rollup = require("./discovery-rollup.cjs");
43
+ // ─── Constants ────────────────────────────────────────────────────────────────
44
+ const DISCOVERY_DIR = '.planning/discovery';
45
+ const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
46
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
47
+ const SESSION_02_FILE = `${DISCOVERY_DIR}/session-02.md`;
48
+ const SECTIONS = Object.freeze({ asIs: 'As-is', toBe: 'To-be', flows: 'Flows', steps: 'Steps' });
49
+ const AS_IS_HEADER = Object.freeze(['ID', 'Process', 'Owner', 'Trigger', 'End state', 'Frequency', 'Avg duration', 'Criticality']);
50
+ const TO_BE_HEADER = Object.freeze(['ID', 'Main process', 'Replaces', 'Trigger', 'End state', 'Primary actor', 'EPICs']);
51
+ const STEPS_HEADER = Object.freeze(['#', 'Step', 'Actor', 'UI', 'Data objects', 'Integration', 'Exception', 'Requirements']);
52
+ const PRC_RE = /^PRC-(\d{2})$/;
53
+ const MP_RE = /^MP-(\d{2})$/;
54
+ /** `### MP-01 — <name>` under Flows and Steps. */
55
+ const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
56
+ const PENDING = '—';
57
+ const NEW = 'new';
58
+ const SYSTEM_ACTOR = 'System';
59
+ const DATA_OBJECT_RE = /^([A-Z][A-Za-z0-9]*) \[([CRUD](?:\/[CRUD])*)\]$/;
60
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
61
+ const PERSONAS_LABEL = 'User Personas';
62
+ const USAGE = 'Usage: pp-tools discovery process-check [--require-cited]';
63
+ function readIfPresent(abs) {
64
+ if (!node_fs_1.default.existsSync(abs))
65
+ return { kind: 'absent' };
66
+ try {
67
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
68
+ }
69
+ catch {
70
+ return { kind: 'unreadable' };
71
+ }
72
+ }
73
+ function sameHeader(actual, expected) {
74
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
75
+ }
76
+ function sectionTable(sections, heading, expected, violations) {
77
+ const section = sections.find((s) => s.heading === heading);
78
+ if (!section) {
79
+ violations.push({ artifact: PROCESSES_FILE, reason: 'missing_section', id: heading });
80
+ return null;
81
+ }
82
+ const tables = table.parseTables(section.body);
83
+ if (tables.length === 0) {
84
+ violations.push({ artifact: PROCESSES_FILE, reason: 'missing_section', line: section.line, id: heading });
85
+ return null;
86
+ }
87
+ if (!sameHeader(tables[0].header, expected)) {
88
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: heading });
89
+ return null;
90
+ }
91
+ return tables[0];
92
+ }
93
+ /** Validate an ID column: shape, uniqueness, contiguity from 01. Returns the accepted IDs. */
94
+ function validateIds(rows, re, violations) {
95
+ const ids = new Map();
96
+ const seqs = [];
97
+ for (const row of rows) {
98
+ const id = (row.cells[0] ?? '').trim();
99
+ const m = re.exec(id);
100
+ if (!m) {
101
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_id', line: row.line });
102
+ continue;
103
+ }
104
+ if (ids.has(id)) {
105
+ violations.push({ artifact: PROCESSES_FILE, reason: 'duplicate_id', line: row.line, id });
106
+ continue;
107
+ }
108
+ ids.set(id, row.line);
109
+ seqs.push(Number(m[1]));
110
+ }
111
+ const max = table.boundedSequenceMax(seqs);
112
+ if (max !== null) {
113
+ const prefix = re.source.startsWith('^PRC') ? 'PRC' : 'MP';
114
+ const have = new Set(seqs);
115
+ for (let n = 1; n <= max; n += 1) {
116
+ if (!have.has(n))
117
+ violations.push({ artifact: PROCESSES_FILE, reason: 'sequence_gap', id: `${prefix}-${String(n).padStart(2, '0')}` });
118
+ }
119
+ }
120
+ return ids;
121
+ }
122
+ // ─── Check ────────────────────────────────────────────────────────────────────
123
+ function checkProcesses(content, sources, requireCited = false) {
124
+ const violations = [];
125
+ const lines = content.split(/\r?\n/);
126
+ const sections = table.splitSections(lines, 2);
127
+ // 1 ─ As-is
128
+ let asIs = new Map();
129
+ const asIsTable = sectionTable(sections, SECTIONS.asIs, AS_IS_HEADER, violations);
130
+ if (asIsTable) {
131
+ asIs = validateIds(asIsTable.rows, PRC_RE, violations);
132
+ for (const row of asIsTable.rows) {
133
+ if ((row.cells[1] ?? '').trim() === '')
134
+ violations.push({ artifact: PROCESSES_FILE, reason: 'empty_cell', line: row.line, id: 'Process' });
135
+ }
136
+ }
137
+ // 2 ─ To-be
138
+ let toBe = new Map();
139
+ const toBeTable = sectionTable(sections, SECTIONS.toBe, TO_BE_HEADER, violations);
140
+ if (toBeTable) {
141
+ toBe = validateIds(toBeTable.rows, MP_RE, violations);
142
+ for (const row of toBeTable.rows) {
143
+ const [, name, replaces, , , actor] = row.cells.map((c) => c.trim());
144
+ if ((name ?? '') === '')
145
+ violations.push({ artifact: PROCESSES_FILE, reason: 'empty_cell', line: row.line, id: 'Main process' });
146
+ if ((replaces ?? '') === '') {
147
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_replaces', line: row.line });
148
+ }
149
+ else if (replaces !== NEW) {
150
+ for (const token of replaces.split(',').map((t) => t.trim())) {
151
+ if (!PRC_RE.test(token))
152
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_replaces', line: row.line });
153
+ else if (!asIs.has(token))
154
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_process', line: row.line, id: token });
155
+ }
156
+ }
157
+ checkActor(actor ?? '', row.line, sources, violations);
158
+ }
159
+ }
160
+ // 3 ─ Flows: one swimlane per main process
161
+ let flows = 0;
162
+ const flowsSection = sections.find((s) => s.heading === SECTIONS.flows);
163
+ const withFlow = new Set();
164
+ if (!flowsSection) {
165
+ violations.push({ artifact: PROCESSES_FILE, reason: 'missing_section', id: SECTIONS.flows });
166
+ }
167
+ else {
168
+ for (const s of table.splitSections(flowsSection.body.map((l) => l.text), 3)) {
169
+ const line = flowsSection.body[s.line - 1]?.n ?? flowsSection.line;
170
+ const m = MP_HEADING_RE.exec(s.heading);
171
+ if (!m || !toBe.has(m[1])) {
172
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_process', line, id: (m ? m[1] : s.heading).slice(0, 40) });
173
+ continue;
174
+ }
175
+ flows += 1;
176
+ withFlow.add(m[1]);
177
+ const text = s.body.map((l) => l.text).join('\n');
178
+ const hasFlowchart = /```mermaid\s*\n\s*flowchart\b/.test(text);
179
+ const hasSubgraph = /^\s*subgraph\b/m.test(text);
180
+ if (!hasFlowchart || !hasSubgraph)
181
+ violations.push({ artifact: PROCESSES_FILE, reason: 'flow_not_swimlane', line, id: m[1] });
182
+ }
183
+ }
184
+ // 4 ─ Steps: one matrix per main process
185
+ let steps = 0;
186
+ let uncited = 0;
187
+ const withSteps = new Set();
188
+ const stepsSection = sections.find((s) => s.heading === SECTIONS.steps);
189
+ if (!stepsSection) {
190
+ violations.push({ artifact: PROCESSES_FILE, reason: 'missing_section', id: SECTIONS.steps });
191
+ }
192
+ else {
193
+ for (const s of table.splitSections(stepsSection.body.map((l) => l.text), 3)) {
194
+ const line = stepsSection.body[s.line - 1]?.n ?? stepsSection.line;
195
+ const m = MP_HEADING_RE.exec(s.heading);
196
+ if (!m || !toBe.has(m[1])) {
197
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_process', line, id: (m ? m[1] : s.heading).slice(0, 40) });
198
+ continue;
199
+ }
200
+ const body = s.body.map((l, i) => ({ n: stepsSection.body[s.line + i]?.n ?? line, text: l.text }));
201
+ const tables = table.parseTables(body);
202
+ if (tables.length === 0 || !sameHeader(tables[0].header, STEPS_HEADER)) {
203
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_table_shape', line, id: `${SECTIONS.steps} · ${m[1]}` });
204
+ continue;
205
+ }
206
+ withSteps.add(m[1]);
207
+ for (const row of tables[0].rows) {
208
+ steps += 1;
209
+ const [num, step, actor, , objects, , , requirements] = row.cells.map((c) => c.trim());
210
+ if ((num ?? '') === '' || (step ?? '') === '')
211
+ violations.push({ artifact: PROCESSES_FILE, reason: 'empty_cell', line: row.line, id: 'Step' });
212
+ checkActor(actor ?? '', row.line, sources, violations);
213
+ if ((objects ?? '') !== '' && objects !== PENDING) {
214
+ for (const token of objects.split(',').map((t) => t.trim()).filter(Boolean)) {
215
+ if (!DATA_OBJECT_RE.test(token))
216
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_data_object', line: row.line });
217
+ }
218
+ }
219
+ if ((requirements ?? '') === '' || requirements === PENDING) {
220
+ uncited += 1;
221
+ if (requireCited)
222
+ violations.push({ artifact: PROCESSES_FILE, reason: 'uncited_step', line: row.line });
223
+ }
224
+ else {
225
+ for (const token of requirements.split(',').map((t) => t.trim()).filter(Boolean)) {
226
+ if (!REQUIREMENT_ID_RE.test(token))
227
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_requirement_id', line: row.line, id: token.slice(0, 40) });
228
+ else if (sources.requirementIds instanceof Set && !sources.requirementIds.has(token)) {
229
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_requirement_id', line: row.line, id: token });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+ for (const [id, line] of toBe) {
237
+ if (flowsSection && !withFlow.has(id))
238
+ violations.push({ artifact: PROCESSES_FILE, reason: 'flow_missing', line, id });
239
+ if (stepsSection && !withSteps.has(id))
240
+ violations.push({ artifact: PROCESSES_FILE, reason: 'steps_missing', line, id });
241
+ }
242
+ if (sources.requirementIds === 'unparseable')
243
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
244
+ return { ok: violations.length === 0, as_is: asIs.size, to_be: toBe.size, flows, steps, uncited, violations };
245
+ }
246
+ /** An actor cell: one persona or `System`, or a comma list of them when a step genuinely has two actors. */
247
+ function checkActor(actor, line, sources, violations) {
248
+ const tokens = actor.split(',').map((t) => t.trim()).filter(Boolean);
249
+ if (tokens.length === 0) {
250
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_actor', line });
251
+ return;
252
+ }
253
+ if (sources.personas === null)
254
+ return;
255
+ for (const t of tokens) {
256
+ if (t !== SYSTEM_ACTOR && !sources.personas.has(t))
257
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_actor', line });
258
+ }
259
+ }
260
+ /** Persona names: the first column of Session 2's User Personas table. */
261
+ function personasFrom(content) {
262
+ const lines = content.split(/\r?\n/);
263
+ const norm = (s) => s.replace(/^#+\s*/, '').replace(/\*\*/g, '').trim();
264
+ const at = lines.findIndex((l) => norm(l) === PERSONAS_LABEL);
265
+ if (at === -1)
266
+ return null;
267
+ const body = [];
268
+ for (let i = at + 1; i < lines.length; i += 1) {
269
+ if (/^#{1,3}\s/.test(lines[i]) || /^\*\*[^*]+\*\*$/.test(lines[i].trim()))
270
+ break;
271
+ body.push({ n: i + 1, text: lines[i] });
272
+ }
273
+ const tables = table.parseTables(body);
274
+ if (tables.length === 0)
275
+ return null;
276
+ return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter((c) => c !== ''));
277
+ }
278
+ function requirementIdsFrom(content) {
279
+ const parsed = rollup.parseRequirementsArtifact(content);
280
+ if (!parsed.ok)
281
+ return 'unparseable';
282
+ return new Set(parsed.rows.map((r) => r.id));
283
+ }
284
+ function checkProjectProcesses(cwd, requireCited = false) {
285
+ const read = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
286
+ if (read.kind === 'absent')
287
+ return null;
288
+ const empty = { as_is: 0, to_be: 0, flows: 0, steps: 0, uncited: 0 };
289
+ if (read.kind === 'unreadable')
290
+ return { ok: false, file: PROCESSES_FILE, ...empty, violations: [{ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' }] };
291
+ const extra = [];
292
+ const s2 = readIfPresent(node_path_1.default.join(cwd, SESSION_02_FILE));
293
+ if (s2.kind === 'unreadable')
294
+ extra.push({ artifact: SESSION_02_FILE, reason: 'artifact_unreadable' });
295
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
296
+ if (req.kind === 'unreadable')
297
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
298
+ const sources = {
299
+ personas: s2.kind === 'ok' ? personasFrom(s2.content) : null,
300
+ requirementIds: req.kind === 'ok' ? requirementIdsFrom(req.content) : null,
301
+ };
302
+ const result = checkProcesses(read.content, sources, requireCited);
303
+ result.violations.push(...extra);
304
+ result.ok = result.violations.length === 0;
305
+ return { file: PROCESSES_FILE, ...result };
306
+ }
307
+ function formatViolation(v) {
308
+ const where = v.line !== undefined ? `:${v.line}` : '';
309
+ const what = v.id !== undefined ? ` (${v.id})` : '';
310
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
311
+ }
312
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
313
+ /**
314
+ * CLI adapter for `pp-tools discovery process-check [--require-cited]`
315
+ * (dotted `query discovery.process-check`). Absent artifact → exit 0 with
316
+ * `present: false`. Structural faults exit via
317
+ * `DISCOVERY_PROCESS_CHECK_FAILED`; when every fault is an `uncited_step`
318
+ * raised by `--require-cited`, the identity is
319
+ * `DISCOVERY_PROCESS_STEPS_UNCITED`.
320
+ */
321
+ function cmdDiscoveryProcessCheck(cwd, args, raw) {
322
+ const rest = args.slice(2);
323
+ const requireCited = rest.includes('--require-cited');
324
+ if (rest.some((a) => a !== '--require-cited')) {
325
+ error(USAGE, ERROR_REASON.USAGE);
326
+ return; // unreachable
327
+ }
328
+ const result = checkProjectProcesses(cwd, requireCited);
329
+ if (result === null) {
330
+ output({ ok: true, present: false, file: PROCESSES_FILE }, raw);
331
+ return;
332
+ }
333
+ const payload = { ok: result.ok, present: true, file: result.file, as_is: result.as_is, to_be: result.to_be, flows: result.flows, steps: result.steps, uncited: result.uncited };
334
+ if (!result.ok) {
335
+ output({ ...payload, violations: result.violations.length }, raw);
336
+ const onlyUncited = result.violations.every((v) => v.reason === 'uncited_step');
337
+ error(`discovery process-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), onlyUncited ? ERROR_REASON.DISCOVERY_PROCESS_STEPS_UNCITED : ERROR_REASON.DISCOVERY_PROCESS_CHECK_FAILED);
338
+ return; // unreachable
339
+ }
340
+ output(payload, raw);
341
+ }
342
+ module.exports = {
343
+ PROCESSES_FILE,
344
+ AS_IS_HEADER,
345
+ TO_BE_HEADER,
346
+ STEPS_HEADER,
347
+ MP_HEADING_RE,
348
+ DATA_OBJECT_RE,
349
+ PROCESS_CHECK_FAILED: ERROR_REASON.DISCOVERY_PROCESS_CHECK_FAILED,
350
+ PROCESS_STEPS_UNCITED: ERROR_REASON.DISCOVERY_PROCESS_STEPS_UNCITED,
351
+ checkProcesses,
352
+ personasFrom,
353
+ checkProjectProcesses,
354
+ cmdDiscoveryProcessCheck,
355
+ };
@@ -6,8 +6,9 @@
6
6
  * Read-only structural validator over the three per-engagement research
7
7
  * registers — `.planning/discovery/_research.md` (the backlog),
8
8
  * `_risks.md` (the data-gathering register) and `_sparring.md` (the position
9
- * tracker) — plus every `⚠️ CONDITIONAL —` marker in the session files and
10
- * `_context.md` that cites them.
9
+ * tracker) — plus every `⚠️ CONDITIONAL —` marker in the session files that
10
+ * cites them (and in a legacy `_context.md`, when an engagement that predates
11
+ * its retirement still carries one).
11
12
  *
12
13
  * Fail-loud (D-42), one bullet per fault class:
13
14
  * - bad_id / duplicate_id — a register row whose ID is not `^(RS|DG|SP)-\d{2}$`,
@@ -156,10 +157,10 @@ const DG_ID_RE = /^DG-\d{2}$/;
156
157
  const SP_ID_RE = /^SP-\d{2}$/;
157
158
  const SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
158
159
  /** D-35 evidence form 1: a decision ID. Form 2 is an existing repo-relative path. */
159
- const EVIDENCE_DECISION_RE = /^D-\d{2}-\d+$/;
160
+ const EVIDENCE_DECISION_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
160
161
  const CLOSED_EVIDENCE_RE = /^Closed \((.+)\)$/;
161
162
  /** D-37 Source vocabulary. */
162
- const SOURCE_RE = /^(DG-\d{2}|SP-\d{2}|S\d+|D-\d{2}-\d+)$/;
163
+ const SOURCE_RE = /^(DG-\d{2}|SP-\d{2}|S\d+|D-(?:\d{2}|P\d{2})-\d+)$/;
163
164
  /** D-40 Feeds: comma-separated session refs. */
164
165
  const FEEDS_RE = /^S\d+$/;
165
166
  /** A requirement ID as `_requirements.md` mints them (rollup's ROW_ID_RE shape). */
@@ -13,10 +13,11 @@
13
13
  * must exist in the same table. Grammar single-source: same reference file.
14
14
  *
15
15
  * Phase 3 (D-44), extended by Phase 4 (D-50) and issue #12: a Prefix Registry
16
- * row may not claim one of the seven reserved prefixes (`D-` `A-` `SP-` `DG-`
17
- * `RS-` `SCR-` `PRC-`) — they all match the requirement-ID regex, so
18
- * registering one collides with the decision / action / sparring /
19
- * data-gathering / research / screen / process ID families. Reserved set
16
+ * row may not claim one of the twelve reserved prefixes (`D-` `A-` `SP-` `DG-`
17
+ * `RS-` `SCR-` `PRC-` `MP-` `BR-` `IF-` `MG-` `DEV-`) — they all match the
18
+ * requirement-ID regex, so registering one collides with the decision / action /
19
+ * sparring / data-gathering / research / screen / process / interface /
20
+ * migration / deviation ID families. Reserved set
20
21
  * single-source: same reference file.
21
22
  *
22
23
  * Fail-loud contract (D-08): any unparseable row, malformed/duplicate ID,
@@ -49,9 +50,9 @@ const MOSCOW = Object.freeze({ MUST: 'Must', SHOULD: 'Should', COULD: 'Could', W
49
50
  // requirements table.
50
51
  // Issue #12 added `PRC-`: Session 2 numbers its Process Inventory `PRC-<nn>`,
51
52
  // which a later session cites beside requirement IDs.
52
- const RESERVED_PREFIXES = new Set(['D-', 'A-', 'SP-', 'DG-', 'RS-', 'SCR-', 'PRC-']);
53
+ const RESERVED_PREFIXES = new Set(['D-', 'A-', 'SP-', 'DG-', 'RS-', 'SCR-', 'PRC-', 'MP-', 'BR-', 'IF-', 'MG-', 'DEV-']);
53
54
  const ROW_ID_RE = /^([A-Z][A-Z0-9]*-)(\d+)$/;
54
- const DROPPED_RE = /^Dropped \((D-\d{2}-\d+)\)$/;
55
+ const DROPPED_RE = /^Dropped \((D-(?:\d{2}|P\d{2})-\d+)\)$/;
55
56
  const SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
56
57
  const ROLLUP_OPEN = '<!-- rollup:auto -->';
57
58
  const ROLLUP_CLOSE = '<!-- /rollup:auto -->';
@@ -64,7 +65,7 @@ const STRIKE_RE = /^~~.+~~$/;
64
65
  const PROVENANCE_SEP = ' · ';
65
66
  const ACCESSION_CITE_RE = /^\[(\d{2})( §[^\]]{1,80})?\]$/;
66
67
  const SESSION_REF_RE = /^S\d+$/;
67
- const PROVENANCE_DECISION_RE = /^D-\d{2}-\d+$/;
68
+ const PROVENANCE_DECISION_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
68
69
  const RELATION_RE = /^(refines|absorbed into) ([A-Z][A-Z0-9]*-\d+)$/;
69
70
  const MOSCOW_SET = new Set(Object.values(MOSCOW));
70
71
  const REGISTRY_HEADING = '## Prefix Registry';