@pareto4/pp-core 1.21.1 → 1.23.1

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 (136) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/install.js +205 -28
  3. package/commands/pp-help.md +2 -1
  4. package/gemini-extension.json +1 -1
  5. package/hooks/dist/pp-check-update-worker.js +1 -1
  6. package/hooks/dist/pp-context-monitor.js +3 -3
  7. package/hooks/dist/pp-statusline.js +88 -14
  8. package/hooks/dist/pp-update-banner.js +44 -4
  9. package/hooks/dist/pp-workflow-guard.js +3 -3
  10. package/hooks/pp-check-update-worker.js +1 -1
  11. package/hooks/pp-context-monitor.js +3 -3
  12. package/hooks/pp-statusline.js +88 -14
  13. package/hooks/pp-update-banner.js +44 -4
  14. package/hooks/pp-workflow-guard.js +3 -3
  15. package/package.json +2 -2
  16. package/pp-core/bin/lib/audience-command.cjs +271 -0
  17. package/pp-core/bin/lib/audience.cjs +293 -0
  18. package/pp-core/bin/lib/commands.cjs +26 -17
  19. package/pp-core/bin/lib/discovery-charter.cjs +13 -1
  20. package/pp-core/bin/lib/discovery-flow-page.cjs +191 -0
  21. package/pp-core/bin/lib/discovery-for-phase.cjs +33 -5
  22. package/pp-core/bin/lib/discovery-input-check.cjs +11 -11
  23. package/pp-core/bin/lib/discovery-processes.cjs +97 -4
  24. package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
  25. package/pp-core/bin/lib/discovery-status.cjs +419 -0
  26. package/pp-core/bin/lib/discovery-stories.cjs +36 -5
  27. package/pp-core/bin/lib/discovery-tests.cjs +398 -0
  28. package/pp-core/bin/lib/explain-command.cjs +48 -0
  29. package/pp-core/bin/lib/gate-card.cjs +97 -0
  30. package/pp-core/bin/lib/hook-messages.cjs +132 -0
  31. package/pp-core/bin/lib/init-contract.cjs +116 -76
  32. package/pp-core/bin/lib/init-interactive.cjs +110 -49
  33. package/pp-core/bin/lib/init.cjs +44 -3
  34. package/pp-core/bin/lib/io.cjs +38 -0
  35. package/pp-core/bin/lib/message-labels.cjs +75 -0
  36. package/pp-core/bin/lib/messages/core.cjs +20 -0
  37. package/pp-core/bin/lib/messages/discovery.cjs +267 -0
  38. package/pp-core/bin/lib/messages/gate.cjs +257 -0
  39. package/pp-core/bin/lib/messages/hook.cjs +114 -0
  40. package/pp-core/bin/lib/messages/init.cjs +513 -0
  41. package/pp-core/bin/lib/messages/progress.cjs +110 -0
  42. package/pp-core/bin/lib/messages.cjs +208 -0
  43. package/pp-core/bin/lib/vault-command-router.cjs +16 -3
  44. package/pp-core/bin/lib/violation-render.cjs +78 -0
  45. package/pp-core/bin/pp-tools.cjs +92 -23
  46. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  47. package/pp-core/references/audience-moments.md +119 -0
  48. package/pp-core/references/audience.md +114 -0
  49. package/pp-core/references/discovery-build-consumption.md +5 -2
  50. package/pp-core/references/discovery-charter.md +2 -2
  51. package/pp-core/references/discovery-id-conventions.md +1 -1
  52. package/pp-core/references/discovery-processes.md +31 -7
  53. package/pp-core/references/discovery-session-output.md +1 -1
  54. package/pp-core/references/discovery-tests.md +186 -0
  55. package/pp-core/templates/project-charter.md +3 -4
  56. package/pp-core/workflows/add-phase.md +2 -0
  57. package/pp-core/workflows/add-tests.md +2 -0
  58. package/pp-core/workflows/add-todo.md +2 -0
  59. package/pp-core/workflows/ai-integration-phase.md +2 -0
  60. package/pp-core/workflows/audit-milestone.md +2 -0
  61. package/pp-core/workflows/autonomous.md +2 -0
  62. package/pp-core/workflows/check-todos.md +2 -0
  63. package/pp-core/workflows/code-review-fix.md +2 -0
  64. package/pp-core/workflows/code-review.md +2 -0
  65. package/pp-core/workflows/complete-milestone.md +10 -1
  66. package/pp-core/workflows/discovery-sprint/charter.md +3 -2
  67. package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
  68. package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
  69. package/pp-core/workflows/discovery-sprint/session-13-gates.md +2 -0
  70. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
  71. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +135 -76
  72. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +94 -64
  73. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +3 -2
  74. package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
  75. package/pp-core/workflows/discovery-sprint.md +16 -2
  76. package/pp-core/workflows/discovery-to-build/steps/handoff-gates.md +16 -9
  77. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +1 -1
  78. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +9 -1
  79. package/pp-core/workflows/discovery-to-build.md +10 -4
  80. package/pp-core/workflows/discuss-phase/discovery.md +2 -2
  81. package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
  82. package/pp-core/workflows/discuss-phase.md +1 -1
  83. package/pp-core/workflows/edit-phase.md +2 -0
  84. package/pp-core/workflows/eval-review.md +2 -0
  85. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
  86. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
  87. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
  88. package/pp-core/workflows/execute-phase.md +1 -1
  89. package/pp-core/workflows/execute-plan.md +2 -0
  90. package/pp-core/workflows/extract-learnings.md +2 -0
  91. package/pp-core/workflows/help/modes/business.md +56 -0
  92. package/pp-core/workflows/help.md +12 -2
  93. package/pp-core/workflows/import.md +10 -1
  94. package/pp-core/workflows/ingest-docs.md +2 -0
  95. package/pp-core/workflows/init.md +17 -1
  96. package/pp-core/workflows/insert-phase.md +2 -0
  97. package/pp-core/workflows/list-workspaces.md +2 -0
  98. package/pp-core/workflows/manager.md +2 -0
  99. package/pp-core/workflows/map-codebase.md +2 -0
  100. package/pp-core/workflows/milestone-summary.md +2 -0
  101. package/pp-core/workflows/new-milestone.md +10 -1
  102. package/pp-core/workflows/new-project.md +12 -2
  103. package/pp-core/workflows/new-workspace.md +2 -0
  104. package/pp-core/workflows/next.md +8 -0
  105. package/pp-core/workflows/plan-milestone-gaps.md +10 -1
  106. package/pp-core/workflows/plan-phase.md +2 -1
  107. package/pp-core/workflows/plan-review-convergence.md +1 -1
  108. package/pp-core/workflows/progress.md +10 -0
  109. package/pp-core/workflows/quick.md +11 -1
  110. package/pp-core/workflows/remove-phase.md +2 -0
  111. package/pp-core/workflows/remove-workspace.md +2 -0
  112. package/pp-core/workflows/resume-project.md +25 -0
  113. package/pp-core/workflows/review.md +2 -0
  114. package/pp-core/workflows/scan.md +2 -0
  115. package/pp-core/workflows/secure-phase.md +2 -0
  116. package/pp-core/workflows/settings.md +41 -0
  117. package/pp-core/workflows/ship.md +2 -0
  118. package/pp-core/workflows/spec-phase.md +1 -1
  119. package/pp-core/workflows/ui-phase.md +2 -0
  120. package/pp-core/workflows/ui-review.md +2 -0
  121. package/pp-core/workflows/ultraplan-phase.md +2 -0
  122. package/pp-core/workflows/validate-phase.md +2 -0
  123. package/pp-core/workflows/vault-review.md +2 -0
  124. package/pp-core/workflows/verify-phase.md +2 -0
  125. package/pp-core/workflows/verify-work.md +2 -0
  126. package/scripts/affected-tests-lib.cjs +26 -12
  127. package/scripts/ci-release-only.cjs +158 -0
  128. package/scripts/live-runs/driver.cjs +326 -0
  129. package/scripts/live-runs/run.cjs +906 -0
  130. package/scripts/live-runs/scenarios.cjs +326 -0
  131. package/scripts/live-runs/scratch-project.cjs +132 -0
  132. package/scripts/live-runs/scrub.cjs +291 -0
  133. package/scripts/live-runs/turn-guard.cjs +107 -0
  134. package/scripts/live-runs/walk-setup.cjs +198 -0
  135. package/scripts/run-tests.cjs +151 -132
  136. package/scripts/test-env.cjs +64 -0
@@ -5,7 +5,7 @@
5
5
  * The Project Charter is the one document every stakeholder signs at the end
6
6
  * of a Discovery Sprint. Session 13 drafts it from the session record into
7
7
  * `docs/discovery/PROJECT-CHARTER.md` (client-visible, like `docs/architecture/`),
8
- * in the sprint language, from the fixed-shape template in
8
+ * in the charter language, from the fixed-shape template in
9
9
  * `pp-core/templates/project-charter.md`. This module checks the structure of
10
10
  * that document — never its judgement:
11
11
  *
@@ -148,6 +148,17 @@ function parseFrontmatter(lines) {
148
148
  }
149
149
  return null;
150
150
  }
151
+ /**
152
+ * The charter's frontmatter `status` exactly as `charter-check` reads it: one of
153
+ * `STATUS_VALUES`, else null (no frontmatter, no field, or a value the check
154
+ * reports as `bad_status`). `discovery status` predicts the gate with this, so
155
+ * the two never disagree on what the status is (42-REVIEW WR-01).
156
+ */
157
+ function charterStatusOf(content) {
158
+ const fm = parseFrontmatter(content.split(/\r?\n/));
159
+ const value = fm?.fields.get('status') ?? '';
160
+ return STATUS_VALUES.includes(value) ? value : null;
161
+ }
151
162
  /**
152
163
  * Every pipe table in a run of lines: the header is the row before the first
153
164
  * separator, data rows follow until the first non-pipe line. A second table
@@ -658,6 +669,7 @@ module.exports = {
658
669
  CHARTER_CHECK_FAILED: ERROR_REASON.DISCOVERY_CHARTER_CHECK_FAILED,
659
670
  CHARTER_UNSIGNED: ERROR_REASON.DISCOVERY_CHARTER_UNSIGNED,
660
671
  parseTables,
672
+ charterStatusOf,
661
673
  checkCharter,
662
674
  checkProjectCharter,
663
675
  cmdDiscoveryCharterCheck,
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery flow page (Session 3 Step 5C — iterate a swimlane with the room).
4
+ *
5
+ * Session 3 refines one main process at a time: the agent drafts the
6
+ * swimlane into `_processes.md` § Flows, the room reacts, the agent edits the
7
+ * draft in place, and only an accepted diagram becomes a step matrix. A
8
+ * terminal cannot draw Mermaid, so this command renders one main process —
9
+ * its swimlane, its § Sub-processes rows and its step matrix, read from the
10
+ * register and nothing else — as a self-contained HTML page the agent
11
+ * publishes as a private artifact (which renders `<pre class="mermaid">`
12
+ * natively) and republishes after every round. The register stays the only
13
+ * source: the page is regenerated, never edited.
14
+ *
15
+ * The page carries no script and no external resource; every cell is
16
+ * HTML-escaped, the Mermaid source included (Mermaid reads the element's
17
+ * text). Structure fails loud (`discovery_flow_page_failed`): no
18
+ * `_processes.md`, an `MP-nn` that is not in § To-be, or a main process
19
+ * without a flow under § Flows.
20
+ */
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ const node_fs_1 = __importDefault(require("node:fs"));
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const io = require("./io.cjs");
28
+ const { output, error, ERROR_REASON } = io;
29
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
30
+ const table = require("./discovery-table.cjs");
31
+ // ─── Constants ────────────────────────────────────────────────────────────────
32
+ const DISCOVERY_DIR = '.planning/discovery';
33
+ const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
34
+ const PAGES_DIR = `${DISCOVERY_DIR}/_flows`;
35
+ const MP_RE = /^MP-\d{2}$/;
36
+ const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+(.+)$/;
37
+ const SUB_PROCESS_RE = /^(MP-\d{2})\.\d+$/;
38
+ const MERMAID_RE = /```mermaid\s*\n([\s\S]*?)\n\s*```/;
39
+ const TO_BE_LABELS = Object.freeze(['ID', 'Main process', 'Replaces', 'Trigger', 'End state', 'Primary actor', 'EPICs']);
40
+ const USAGE = 'Usage: pp-tools discovery flow-page <MP-nn> [--out <file.html>]';
41
+ // ─── Read ─────────────────────────────────────────────────────────────────────
42
+ function readProcessView(content, mp) {
43
+ const sections = table.splitSections(content.split(/\r?\n/), 2);
44
+ const toBe = sections.find((s) => s.heading === 'To-be');
45
+ const toBeRow = toBe ? table.parseTables(toBe.body)[0]?.rows.find((r) => (r.cells[0] ?? '').trim() === mp) : undefined;
46
+ if (!toBeRow)
47
+ return { ok: false, reason: 'unknown_process' };
48
+ const sub = (heading) => {
49
+ const s = sections.find((x) => x.heading === heading);
50
+ return s ? table.splitSections(s.body.map((l) => l.text), 3) : [];
51
+ };
52
+ const flow = sub('Flows').find((s) => MP_HEADING_RE.exec(s.heading)?.[1] === mp);
53
+ const mermaid = flow ? MERMAID_RE.exec(flow.body.map((l) => l.text).join('\n'))?.[1] : undefined;
54
+ if (!mermaid)
55
+ return { ok: false, reason: 'flow_missing' };
56
+ const stepsSec = sub('Steps').find((s) => MP_HEADING_RE.exec(s.heading)?.[1] === mp);
57
+ const stepsTable = stepsSec ? table.parseTables(stepsSec.body)[0] : undefined;
58
+ const subSec = sections.find((s) => s.heading === 'Sub-processes');
59
+ const subRows = subSec ? (table.parseTables(subSec.body)[0]?.rows ?? []) : [];
60
+ return {
61
+ ok: true,
62
+ view: {
63
+ id: mp,
64
+ name: (toBeRow.cells[1] ?? '').trim(),
65
+ toBe: toBeRow.cells.map((c) => c.trim()),
66
+ mermaid,
67
+ subProcesses: subRows.filter((r) => SUB_PROCESS_RE.exec((r.cells[0] ?? '').trim())?.[1] === mp),
68
+ stepsHeader: stepsTable ? stepsTable.header.map((c) => c.trim()) : [],
69
+ steps: stepsTable ? stepsTable.rows : [],
70
+ },
71
+ };
72
+ }
73
+ // ─── Render ───────────────────────────────────────────────────────────────────
74
+ function esc(s) {
75
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
76
+ }
77
+ function htmlTable(header, rows) {
78
+ const head = `<tr>${header.map((h) => `<th>${esc(h)}</th>`).join('')}</tr>`;
79
+ const body = rows.map((r) => `<tr>${header.map((_, i) => `<td>${esc((r.cells[i] ?? '').trim())}</td>`).join('')}</tr>`).join('\n');
80
+ return `<div class="scroll"><table>\n<thead>${head}</thead>\n<tbody>\n${body}\n</tbody>\n</table></div>`;
81
+ }
82
+ function renderPage(v) {
83
+ const facts = TO_BE_LABELS.slice(2)
84
+ .map((label, i) => `<div><dt>${esc(label)}</dt><dd>${esc(v.toBe[i + 2] ?? '')}</dd></div>`)
85
+ .join('');
86
+ const subs = v.subProcesses.length > 0
87
+ ? htmlTable(['ID', 'Sub-process', 'Steps'], v.subProcesses)
88
+ : '<p class="empty">No sub-processes yet.</p>';
89
+ const steps = v.steps.length > 0
90
+ ? htmlTable(v.stepsHeader, v.steps)
91
+ : '<p class="empty">No step matrix yet — it follows once the room accepts the diagram.</p>';
92
+ return `<title>${esc(`${v.id} ${v.name}`)}</title>
93
+ <meta name="claude:viewport-fit-sides" content="cover">
94
+ <style>
95
+ :root { --bg: #f7f8fa; --surface: #ffffff; --fg: #14181f; --muted: #5b6472; --line: #dde1e8; --accent: #2251ff; }
96
+ @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0f1218; --surface: #171b23; --fg: #e8ebf0; --muted: #9aa3b2; --line: #2a303b; --accent: #7d97ff; color-scheme: dark; } }
97
+ :root[data-theme="dark"] { --bg: #0f1218; --surface: #171b23; --fg: #e8ebf0; --muted: #9aa3b2; --line: #2a303b; --accent: #7d97ff; color-scheme: dark; }
98
+ body { background: var(--bg); color: var(--fg); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); padding-block: 24px 48px; }
99
+ main { max-width: 1200px; margin: 0 auto; display: grid; gap: 24px; }
100
+ header p { margin: 0; color: var(--muted); font-size: 13px; letter-spacing: .04em; text-transform: uppercase; }
101
+ h1 { margin: 4px 0 0; font-size: 26px; text-wrap: balance; }
102
+ h2 { margin: 0 0 8px; font-size: 16px; }
103
+ dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px 24px; margin: 12px 0 0; }
104
+ dt { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }
105
+ dd { margin: 0; }
106
+ section { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 16px; min-width: 0; }
107
+ .scroll { overflow-x: auto; }
108
+ pre.mermaid { margin: 0; overflow-x: auto; background: transparent; }
109
+ table { border-collapse: collapse; width: 100%; font-size: 14px; }
110
+ th, td { text-align: left; vertical-align: top; padding: 6px 10px; border-bottom: 1px solid var(--line); }
111
+ th { color: var(--muted); font-weight: 600; white-space: nowrap; }
112
+ td:first-child { font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--accent); }
113
+ .empty { color: var(--muted); margin: 0; }
114
+ footer { color: var(--muted); font-size: 12px; }
115
+ </style>
116
+ <main>
117
+ <header>
118
+ <p>Discovery · Session 3 · process refinement</p>
119
+ <h1>${esc(v.id)} — ${esc(v.name)}</h1>
120
+ <dl>${facts}</dl>
121
+ </header>
122
+ <section aria-label="Swimlane">
123
+ <h2>Swimlane</h2>
124
+ <pre class="mermaid">
125
+ ${esc(v.mermaid)}
126
+ </pre>
127
+ </section>
128
+ <section aria-label="Sub-processes">
129
+ <h2>Sub-processes</h2>
130
+ ${subs}
131
+ </section>
132
+ <section aria-label="Steps">
133
+ <h2>Steps</h2>
134
+ ${steps}
135
+ </section>
136
+ <footer>Generated from <code>${esc(PROCESSES_FILE)}</code> by <code>pp-tools discovery flow-page ${esc(v.id)}</code>. Change the register, not this page.</footer>
137
+ </main>
138
+ `;
139
+ }
140
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
141
+ /**
142
+ * `pp-tools discovery flow-page <MP-nn> [--out <file.html>]` (dotted
143
+ * `query discovery.flow-page`). Writes the page (default
144
+ * `.planning/discovery/_flows/<MP-nn>.html`) and prints `{ ok, process, file,
145
+ * steps, sub_processes }`.
146
+ */
147
+ function cmdDiscoveryFlowPage(cwd, args, raw) {
148
+ const rest = args.slice(2);
149
+ const mp = rest[0];
150
+ const outAt = rest.indexOf('--out');
151
+ const extra = rest.filter((a, i) => i !== 0 && a !== '--out' && i !== outAt + 1);
152
+ if (!mp || !MP_RE.test(mp) || extra.length > 0 || (outAt !== -1 && !rest[outAt + 1])) {
153
+ error(USAGE, ERROR_REASON.USAGE);
154
+ return; // unreachable
155
+ }
156
+ const out = outAt !== -1 ? rest[outAt + 1] : `${PAGES_DIR}/${mp}.html`;
157
+ if (!out.endsWith('.html')) {
158
+ error(USAGE, ERROR_REASON.USAGE);
159
+ return; // unreachable
160
+ }
161
+ const abs = node_path_1.default.join(cwd, PROCESSES_FILE);
162
+ const fail = (reason) => error(`discovery flow-page: ${PROCESSES_FILE}: ${reason} (${mp})`, ERROR_REASON.DISCOVERY_FLOW_PAGE_FAILED);
163
+ if (!node_fs_1.default.existsSync(abs)) {
164
+ fail('processes_missing');
165
+ return; // unreachable
166
+ }
167
+ let content;
168
+ try {
169
+ content = node_fs_1.default.readFileSync(abs, 'utf-8');
170
+ }
171
+ catch {
172
+ fail('artifact_unreadable');
173
+ return; // unreachable
174
+ }
175
+ const result = readProcessView(content, mp);
176
+ if (!result.ok) {
177
+ fail(result.reason);
178
+ return; // unreachable
179
+ }
180
+ const outAbs = node_path_1.default.resolve(cwd, out);
181
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(outAbs), { recursive: true });
182
+ node_fs_1.default.writeFileSync(outAbs, renderPage(result.view));
183
+ output({ ok: true, process: mp, file: out, steps: result.view.steps.length, sub_processes: result.view.subProcesses.length }, raw);
184
+ }
185
+ module.exports = {
186
+ PAGES_DIR,
187
+ FLOW_PAGE_FAILED: ERROR_REASON.DISCOVERY_FLOW_PAGE_FAILED,
188
+ readProcessView,
189
+ renderPage,
190
+ cmdDiscoveryFlowPage,
191
+ };
@@ -29,6 +29,9 @@
29
29
  * - `_integrations.md` — the § Interfaces rows citing one or named by a
30
30
  * selected step's `Integration` cell (`IF-nn`), with their § Contract and
31
31
  * § Errors rows (`references/discovery-integrations.md`);
32
+ * - `_tests.md` — the § Process risk rows with `End-to-end` `yes` of the
33
+ * main processes touched, the § Focus areas rows citing one, and the
34
+ * § Acceptance rows of the selected stories (`references/discovery-tests.md`);
32
35
  * - `_migration.md`, `_deviations.md` — generic: every table carrying a
33
36
  * `Requirements` column contributes the rows citing one; a table without
34
37
  * that column is reported as unfilterable, never silently dropped.
@@ -75,6 +78,7 @@ const ART = Object.freeze({
75
78
  integrations: `${DISCOVERY_DIR}/_integrations.md`,
76
79
  migration: `${DISCOVERY_DIR}/_migration.md`,
77
80
  deviations: `${DISCOVERY_DIR}/_deviations.md`,
81
+ tests: `${DISCOVERY_DIR}/_tests.md`,
78
82
  phases: `${DISCOVERY_DIR}/_phases.md`,
79
83
  });
80
84
  /** The generic registers: filtered by a `Requirements` column wherever one exists. */
@@ -108,6 +112,9 @@ const ROLE_SCREENS_HEADER = Object.freeze(['Role', 'Screen']);
108
112
  const INTERFACES_HEADER = Object.freeze(['ID', 'System', 'Direction', 'Pattern', 'Protocol', 'Format', 'Idempotency', 'Owner', 'Requirements']);
109
113
  const CONTRACT_HEADER = Object.freeze(['Interface', 'Field', 'Type', 'Required', 'Mapping', 'Validation']);
110
114
  const ERRORS_HEADER = Object.freeze(['Interface', 'Failure', 'Handling', 'Retry', 'Dead letter', 'Alert']);
115
+ const RISK_HEADER = Object.freeze(['Process', 'Impact', 'Likelihood', 'Risk', 'End-to-end', 'Roles', 'Test data', 'Rationale']);
116
+ const FOCUS_HEADER = Object.freeze(['Subject', 'Complexity', 'Extra testing', 'Requirements']);
117
+ const ACCEPTANCE_HEADER = Object.freeze(['Story', 'Testable', 'Role', 'Test data']);
111
118
  const PHASE_ARG_RE = /^\d+(\.\d+)?$/;
112
119
  const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
113
120
  const REQUIREMENTS_LINE_RE = /^\*\*Requirements\*\*:?\s*(.*)$/im;
@@ -234,26 +241,27 @@ function collectExtract(cwd, phase) {
234
241
  }
235
242
  }
236
243
  // _stories.md
244
+ const storyIds = new Set();
237
245
  const sto = readIfPresent(node_path_1.default.join(cwd, ART.stories));
238
246
  if (sto.kind === 'unreadable')
239
247
  violations.push({ artifact: ART.stories, reason: 'artifact_unreadable' });
240
248
  if (sto.kind === 'ok') {
241
249
  const t = tableUnder(sto.content, 'Stories', STORIES_HEADER, ART.stories, violations);
242
250
  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
- });
251
+ const rows = t.rows.filter((r) => cites(r.cells[2], ids));
252
+ for (const r of rows)
253
+ storyIds.add((r.cells[0] ?? '').trim());
254
+ push({ artifact: ART.stories, title: 'User stories', subtitle: null, header: [...STORIES_HEADER], keyed: true, keyPrefix: '', rows });
247
255
  }
248
256
  }
249
257
  // _processes.md — steps, then the to-be rows of the main processes touched
250
258
  const entityNames = new Set();
251
259
  const integrationIds = new Set();
260
+ const mpsTouched = new Set();
252
261
  const proc = readIfPresent(node_path_1.default.join(cwd, ART.processes));
253
262
  if (proc.kind === 'unreadable')
254
263
  violations.push({ artifact: ART.processes, reason: 'artifact_unreadable' });
255
264
  if (proc.kind === 'ok') {
256
- const mpsTouched = new Set();
257
265
  const steps = sectionsOf(proc.content).find((s) => s.heading === 'Steps');
258
266
  if (!steps) {
259
267
  violations.push({ artifact: ART.processes, reason: 'missing_section', id: 'Steps' });
@@ -417,6 +425,26 @@ function collectExtract(cwd, phase) {
417
425
  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
426
  }
419
427
  }
428
+ // _tests.md — end-to-end processes touched, focus areas by requirement, acceptance of the selected stories
429
+ const tst = readIfPresent(node_path_1.default.join(cwd, ART.tests));
430
+ if (tst.kind === 'unreadable')
431
+ violations.push({ artifact: ART.tests, reason: 'artifact_unreadable' });
432
+ if (tst.kind === 'ok') {
433
+ const risk = tableUnder(tst.content, 'Process risk', RISK_HEADER, ART.tests, violations);
434
+ const focus = tableUnder(tst.content, 'Focus areas', FOCUS_HEADER, ART.tests, violations);
435
+ const acceptance = tableUnder(tst.content, 'Acceptance', ACCEPTANCE_HEADER, ART.tests, violations);
436
+ if (risk && focus && acceptance) {
437
+ push({
438
+ artifact: ART.tests, title: 'Tests', subtitle: 'End-to-end', header: [...RISK_HEADER], keyed: true, keyPrefix: 'e2e ',
439
+ rows: risk.rows.filter((r) => mpsTouched.has((r.cells[0] ?? '').trim()) && (r.cells[4] ?? '').trim() === 'yes'),
440
+ });
441
+ push({ artifact: ART.tests, title: 'Tests', subtitle: 'Focus areas', header: [...FOCUS_HEADER], keyed: true, keyPrefix: 'focus ', rows: focus.rows.filter((r) => cites(r.cells[3], ids)) });
442
+ push({
443
+ artifact: ART.tests, title: 'Tests', subtitle: 'Acceptance', header: [...ACCEPTANCE_HEADER], keyed: false, keyPrefix: '',
444
+ rows: acceptance.rows.filter((r) => storyIds.has((r.cells[0] ?? '').trim())),
445
+ });
446
+ }
447
+ }
420
448
  // Generic registers: filtered by a Requirements column.
421
449
  for (const g of GENERIC) {
422
450
  const read = readIfPresent(node_path_1.default.join(cwd, g.file));
@@ -30,9 +30,10 @@ const node_path_1 = __importDefault(require("node:path"));
30
30
  const node_child_process_1 = require("node:child_process");
31
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
32
32
  const io = require("./io.cjs");
33
- const { output, error, ERROR_REASON } = io;
33
+ const { output, error, errorCode, ERROR_REASON } = io;
34
34
  // eslint-disable-next-line @typescript-eslint/no-require-imports
35
35
  const fm = require("./frontmatter.cjs");
36
+ const violation_render_cjs_1 = require("./violation-render.cjs");
36
37
  // ─── Constants ────────────────────────────────────────────────────────────────
37
38
  const SESSION_STATUS = Object.freeze({
38
39
  NOT_STARTED: 'Not Started',
@@ -169,14 +170,6 @@ function checkInputPackage(cwd) {
169
170
  return { ok: true, ...summary };
170
171
  }
171
172
  // ─── CLI adapter ──────────────────────────────────────────────────────────────
172
- /** One content-free line per violation: path + structured reason + counts (T-02-05). */
173
- function formatViolation(v) {
174
- if (v.reason === 'confidential_tracked') {
175
- return ` ${v.file}: confidential_tracked`;
176
- }
177
- const score = v.reason === 'score_counts_unconfirmed' ? `score: ${v.score}, ` : '';
178
- return ` ${v.session}: ${v.reason} (${score}markers: ${v.markers})`;
179
- }
180
173
  /**
181
174
  * CLI adapter for `pp-tools discovery input-check` (dotted form
182
175
  * `pp-tools query discovery.input-check`).
@@ -197,8 +190,15 @@ function cmdDiscoveryInputCheck(cwd, args, raw) {
197
190
  }
198
191
  const result = checkInputPackage(cwd);
199
192
  if (!result.ok) {
200
- const lines = result.violations.map(formatViolation).join('\n');
201
- error(`discovery input-check: ${result.violations.length} violation(s):\n${lines}`, ERROR_REASON.DISCOVERY_INPUT_CHECK_FAILED);
193
+ // Content-free contract (T-02-05): each line names a path, a structured reason and counts, never file contents.
194
+ // The code is the ERROR_REASON value itself (D-03); the lines come from the shared renderer (D-11).
195
+ const violations = result.violations;
196
+ errorCode(ERROR_REASON.DISCOVERY_INPUT_CHECK_FAILED, (audience) => ({
197
+ count: violations.length,
198
+ lines: violations
199
+ .map((v) => (0, violation_render_cjs_1.renderViolation)('discovery.input', v, { audience, extra: { marker: TO_CONFIRM_MARKER, status_word: SESSION_STATUS.COMPLETED } }))
200
+ .join('\n'),
201
+ }), ERROR_REASON.DISCOVERY_INPUT_CHECK_FAILED);
202
202
  return; // unreachable — error() exits
203
203
  }
204
204
  output({
@@ -19,6 +19,12 @@
19
19
  * is on file; every data object is `Name [C/R/U/D]` or `—`;
20
20
  * - every cited requirement exists in `_requirements.md`, when it is on
21
21
  * file and parseable;
22
+ * - when the optional `## Sub-processes` section is on file (engagements
23
+ * since the Session 3 rework): every main process has sub-processes
24
+ * `MP-nn.k`, numbered from 1; every step row belongs to exactly one
25
+ * sub-process of its main process and every sub-process has steps; and
26
+ * every step's `#` appears in its main process's flow, so the diagram
27
+ * and the matrix describe the same steps;
22
28
  * - with `--require-cited`: every step cites at least one requirement —
23
29
  * the state Step 5D leaves behind, required at the handoff.
24
30
  *
@@ -45,12 +51,15 @@ const DISCOVERY_DIR = '.planning/discovery';
45
51
  const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
46
52
  const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
47
53
  const SESSION_02_FILE = `${DISCOVERY_DIR}/session-02.md`;
48
- const SECTIONS = Object.freeze({ asIs: 'As-is', toBe: 'To-be', flows: 'Flows', steps: 'Steps' });
54
+ const SECTIONS = Object.freeze({ asIs: 'As-is', toBe: 'To-be', subProcesses: 'Sub-processes', flows: 'Flows', steps: 'Steps' });
49
55
  const AS_IS_HEADER = Object.freeze(['ID', 'Process', 'Owner', 'Trigger', 'End state', 'Frequency', 'Avg duration', 'Criticality']);
50
56
  const TO_BE_HEADER = Object.freeze(['ID', 'Main process', 'Replaces', 'Trigger', 'End state', 'Primary actor', 'EPICs']);
51
57
  const STEPS_HEADER = Object.freeze(['#', 'Step', 'Actor', 'UI', 'Data objects', 'Integration', 'Exception', 'Requirements']);
58
+ const SUB_PROCESSES_HEADER = Object.freeze(['ID', 'Sub-process', 'Steps']);
52
59
  const PRC_RE = /^PRC-(\d{2})$/;
53
60
  const MP_RE = /^MP-(\d{2})$/;
61
+ /** A sub-process: its main process and its 1-based number within it (`MP-01.2`). */
62
+ const SUB_PROCESS_RE = /^(MP-\d{2})\.([1-9]\d*)$/;
54
63
  /** `### MP-01 — <name>` under Flows and Steps. */
55
64
  const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
56
65
  const PENDING = '—';
@@ -161,6 +170,7 @@ function checkProcesses(content, sources, requireCited = false) {
161
170
  let flows = 0;
162
171
  const flowsSection = sections.find((s) => s.heading === SECTIONS.flows);
163
172
  const withFlow = new Set();
173
+ const flowText = new Map();
164
174
  if (!flowsSection) {
165
175
  violations.push({ artifact: PROCESSES_FILE, reason: 'missing_section', id: SECTIONS.flows });
166
176
  }
@@ -175,6 +185,7 @@ function checkProcesses(content, sources, requireCited = false) {
175
185
  flows += 1;
176
186
  withFlow.add(m[1]);
177
187
  const text = s.body.map((l) => l.text).join('\n');
188
+ flowText.set(m[1], (flowText.get(m[1]) ?? '') + text);
178
189
  const hasFlowchart = /```mermaid\s*\n\s*flowchart\b/.test(text);
179
190
  const hasSubgraph = /^\s*subgraph\b/m.test(text);
180
191
  if (!hasFlowchart || !hasSubgraph)
@@ -185,6 +196,8 @@ function checkProcesses(content, sources, requireCited = false) {
185
196
  let steps = 0;
186
197
  let uncited = 0;
187
198
  const withSteps = new Set();
199
+ /** Per main process: each step's `#` cell and its line. */
200
+ const stepNums = new Map();
188
201
  const stepsSection = sections.find((s) => s.heading === SECTIONS.steps);
189
202
  if (!stepsSection) {
190
203
  violations.push({ artifact: PROCESSES_FILE, reason: 'missing_section', id: SECTIONS.steps });
@@ -204,9 +217,13 @@ function checkProcesses(content, sources, requireCited = false) {
204
217
  continue;
205
218
  }
206
219
  withSteps.add(m[1]);
220
+ const nums = stepNums.get(m[1]) ?? new Map();
221
+ stepNums.set(m[1], nums);
207
222
  for (const row of tables[0].rows) {
208
223
  steps += 1;
209
224
  const [num, step, actor, , objects, , , requirements] = row.cells.map((c) => c.trim());
225
+ if ((num ?? '') !== '' && !nums.has(num))
226
+ nums.set(num, row.line);
210
227
  if ((num ?? '') === '' || (step ?? '') === '')
211
228
  violations.push({ artifact: PROCESSES_FILE, reason: 'empty_cell', line: row.line, id: 'Step' });
212
229
  checkActor(actor ?? '', row.line, sources, violations);
@@ -239,9 +256,85 @@ function checkProcesses(content, sources, requireCited = false) {
239
256
  if (stepsSection && !withSteps.has(id))
240
257
  violations.push({ artifact: PROCESSES_FILE, reason: 'steps_missing', line, id });
241
258
  }
259
+ // 5 ─ Sub-processes (optional section): every step in exactly one, every flow showing every step
260
+ let subProcesses = 0;
261
+ if (sections.some((s) => s.heading === SECTIONS.subProcesses)) {
262
+ const subTable = sectionTable(sections, SECTIONS.subProcesses, SUB_PROCESSES_HEADER, violations);
263
+ if (subTable)
264
+ subProcesses = checkSubProcesses(subTable.rows, toBe, stepNums, flowText, violations);
265
+ }
242
266
  if (sources.requirementIds === 'unparseable')
243
267
  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 };
268
+ return { ok: violations.length === 0, as_is: asIs.size, to_be: toBe.size, flows, steps, sub_processes: subProcesses, uncited, violations };
269
+ }
270
+ /** A step's `#` cell as a whole token in a flow: `01.3` must not match inside `101.3` or `01.30`. */
271
+ function flowShowsStep(text, num) {
272
+ const escaped = num.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
273
+ return new RegExp(`(^|[^\\w.])${escaped}(?![\\w]|\\.\\d)`).test(text);
274
+ }
275
+ function checkSubProcesses(rows, toBe, stepNums, flowText, violations) {
276
+ const ids = new Set();
277
+ const perProcess = new Map();
278
+ /** `MP-nn` → step `#` → the sub-process that claimed it. */
279
+ const claimed = new Map();
280
+ for (const row of rows) {
281
+ const [id, name, stepsCell] = row.cells.map((c) => c.trim());
282
+ const m = SUB_PROCESS_RE.exec(id ?? '');
283
+ if (!m) {
284
+ violations.push({ artifact: PROCESSES_FILE, reason: 'bad_subprocess', line: row.line });
285
+ continue;
286
+ }
287
+ if (ids.has(id)) {
288
+ violations.push({ artifact: PROCESSES_FILE, reason: 'duplicate_id', line: row.line, id });
289
+ continue;
290
+ }
291
+ ids.add(id);
292
+ const mp = m[1];
293
+ if (!toBe.has(mp)) {
294
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_process', line: row.line, id: mp });
295
+ continue;
296
+ }
297
+ perProcess.set(mp, [...(perProcess.get(mp) ?? []), Number(m[2])]);
298
+ if ((name ?? '') === '')
299
+ violations.push({ artifact: PROCESSES_FILE, reason: 'empty_cell', line: row.line, id: 'Sub-process' });
300
+ const tokens = (stepsCell ?? '') === PENDING ? [] : (stepsCell ?? '').split(',').map((t) => t.trim()).filter(Boolean);
301
+ if (tokens.length === 0)
302
+ violations.push({ artifact: PROCESSES_FILE, reason: 'subprocess_without_steps', line: row.line, id });
303
+ const known = stepNums.get(mp);
304
+ const owner = claimed.get(mp) ?? new Map();
305
+ claimed.set(mp, owner);
306
+ for (const t of tokens) {
307
+ if (known && !known.has(t))
308
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_step', line: row.line, id: `${mp} #${t}`.slice(0, 40) });
309
+ else if (owner.has(t))
310
+ violations.push({ artifact: PROCESSES_FILE, reason: 'step_in_two_subprocesses', line: row.line, id: `${mp} #${t}` });
311
+ else
312
+ owner.set(t, id);
313
+ }
314
+ }
315
+ for (const [mp, nums] of perProcess) {
316
+ const max = table.boundedSequenceMax(nums);
317
+ if (max === null)
318
+ continue;
319
+ const have = new Set(nums);
320
+ for (let n = 1; n <= max; n += 1) {
321
+ if (!have.has(n))
322
+ violations.push({ artifact: PROCESSES_FILE, reason: 'sequence_gap', id: `${mp}.${n}` });
323
+ }
324
+ }
325
+ for (const [mp, line] of toBe) {
326
+ if (!perProcess.has(mp))
327
+ violations.push({ artifact: PROCESSES_FILE, reason: 'subprocess_missing', line, id: mp });
328
+ const owner = claimed.get(mp);
329
+ const text = flowText.get(mp);
330
+ for (const [num, stepLine] of stepNums.get(mp) ?? []) {
331
+ if (owner && !owner.has(num))
332
+ violations.push({ artifact: PROCESSES_FILE, reason: 'step_without_subprocess', line: stepLine, id: `${mp} #${num}` });
333
+ if (text !== undefined && !flowShowsStep(text, num))
334
+ violations.push({ artifact: PROCESSES_FILE, reason: 'step_not_in_flow', line: stepLine, id: `${mp} #${num}` });
335
+ }
336
+ }
337
+ return ids.size;
245
338
  }
246
339
  /** An actor cell: one persona or `System`, or a comma list of them when a step genuinely has two actors. */
247
340
  function checkActor(actor, line, sources, violations) {
@@ -285,7 +378,7 @@ function checkProjectProcesses(cwd, requireCited = false) {
285
378
  const read = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
286
379
  if (read.kind === 'absent')
287
380
  return null;
288
- const empty = { as_is: 0, to_be: 0, flows: 0, steps: 0, uncited: 0 };
381
+ const empty = { as_is: 0, to_be: 0, flows: 0, steps: 0, sub_processes: 0, uncited: 0 };
289
382
  if (read.kind === 'unreadable')
290
383
  return { ok: false, file: PROCESSES_FILE, ...empty, violations: [{ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' }] };
291
384
  const extra = [];
@@ -330,7 +423,7 @@ function cmdDiscoveryProcessCheck(cwd, args, raw) {
330
423
  output({ ok: true, present: false, file: PROCESSES_FILE }, raw);
331
424
  return;
332
425
  }
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 };
426
+ 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, sub_processes: result.sub_processes, uncited: result.uncited };
334
427
  if (!result.ok) {
335
428
  output({ ...payload, violations: result.violations.length }, raw);
336
429
  const onlyUncited = result.violations.every((v) => v.reason === 'uncited_step');
@@ -32,9 +32,10 @@ const node_fs_1 = __importDefault(require("node:fs"));
32
32
  const node_path_1 = __importDefault(require("node:path"));
33
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
34
34
  const io = require("./io.cjs");
35
- const { output, error, ERROR_REASON } = io;
35
+ const { output, error, errorCode, ERROR_REASON } = io;
36
36
  // eslint-disable-next-line @typescript-eslint/no-require-imports
37
37
  const table = require("./discovery-table.cjs");
38
+ const violation_render_cjs_1 = require("./violation-render.cjs");
38
39
  // ─── Constants ────────────────────────────────────────────────────────────────
39
40
  const DISCOVERY_DIR = '.planning/discovery';
40
41
  const SESSION_COUNT = 13;
@@ -213,11 +214,6 @@ function checkProjectSessions(cwd, numbers, sessionsDir = SESSIONS_DIR) {
213
214
  const sessions = numbers.map((n) => checkProjectSession(cwd, n, sessionsDir));
214
215
  return { ok: sessions.every((s) => s.violations.length === 0), sessions };
215
216
  }
216
- function formatViolation(v) {
217
- const where = v.line !== undefined ? `:${v.line}` : '';
218
- const what = v.id !== undefined ? ` (${v.id})` : '';
219
- return ` - ${v.artifact}${where}: ${v.reason}${what}`;
220
- }
221
217
  // ─── CLI adapter ──────────────────────────────────────────────────────────────
222
218
  /**
223
219
  * `pp-tools discovery session-check <N> [<N>…] | --all` (dotted
@@ -245,7 +241,11 @@ function cmdDiscoverySessionCheck(cwd, args, raw) {
245
241
  if (!result.ok) {
246
242
  const violations = result.sessions.flatMap((s) => s.violations);
247
243
  output({ ...payload, violations: violations.length }, raw);
248
- error(`discovery session-check: ${violations.length} violation(s):\n` + violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED);
244
+ // The code is the ERROR_REASON value itself (D-03); the lines come from the shared renderer (D-11).
245
+ errorCode(ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED, (audience) => ({
246
+ count: violations.length,
247
+ lines: violations.map((v) => (0, violation_render_cjs_1.renderViolation)('discovery.session', v, { audience, extra: { none: NONE_LITERAL } })).join('\n'),
248
+ }), ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED);
249
249
  return; // unreachable
250
250
  }
251
251
  output(payload, raw);