@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,273 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery user-stories check (Discovery Sprint improvement plan, Batch 4).
4
+ *
5
+ * `.planning/discovery/_stories.md` is the register of user stories Session 3
6
+ * Step 5E mints from the step matrices: `US-nn`, the EPIC it belongs to, the
7
+ * requirements it realises, the story itself, its acceptance criteria and the
8
+ * step it was derived from. Before Batch 4 the stories lived only in Session
9
+ * 3's Notes Template, where no build agent read them (review F-4); the
10
+ * verifier now takes its UAT from this register through `for-phase`. This
11
+ * module checks the structure:
12
+ *
13
+ * - the `## Stories` table exists with the template's columns;
14
+ * - `US-` IDs are well-formed, unique, contiguous;
15
+ * - `EPIC` is one `E-nn`;
16
+ * - every story cites at least one requirement, each existing in
17
+ * `_requirements.md` when it is on file and parseable;
18
+ * - `Story` and `Acceptance criteria` are non-empty;
19
+ * - `Acceptance criteria` is a criterion, not a pointer (`As in F-01`);
20
+ * - `Source step` is `—` or a comma list of `MP-nn #<cell>`, the main process
21
+ * and the step row existing in `_processes.md` when it is on file;
22
+ * - every MVP-disposition requirement that a persona-actor step cites has at
23
+ * least one story (the `N-`/`T-` prefixes never need one; a requirement
24
+ * only `System` steps realise, or no step at all, has no user to tell it).
25
+ *
26
+ * Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at
27
+ * `/pp-discovery-to-build`. Absent artifact → exit 0.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ const node_fs_1 = __importDefault(require("node:fs"));
33
+ const node_path_1 = __importDefault(require("node:path"));
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ const io = require("./io.cjs");
36
+ const { output, error, ERROR_REASON } = io;
37
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
38
+ const table = require("./discovery-table.cjs");
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const rollup = require("./discovery-rollup.cjs");
41
+ // ─── Constants ────────────────────────────────────────────────────────────────
42
+ const DISCOVERY_DIR = '.planning/discovery';
43
+ const STORIES_FILE = `${DISCOVERY_DIR}/_stories.md`;
44
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
45
+ const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
46
+ const STORIES_SECTION = 'Stories';
47
+ const STORIES_HEADER = Object.freeze(['ID', 'EPIC', 'Requirements', 'Story', 'Acceptance criteria', 'Source step']);
48
+ const US_RE = /^US-(\d{2})$/;
49
+ const EPIC_RE = /^E-\d{2}$/;
50
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
51
+ /** `MP-nn #<cell>` — the main process and the step matrix's `#` cell, copied verbatim (`01.3`, `5`). */
52
+ const SOURCE_STEP_RE = /^(MP-\d{2}) #(\S+)$/;
53
+ const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
54
+ const SYSTEM_ACTOR = 'System';
55
+ /** An acceptance-criteria cell that only points elsewhere is no criterion the verifier can run. */
56
+ const POINTER_AC_RE = /^(as in|see|zie|idem)\b/i;
57
+ const PENDING = '—';
58
+ /** Requirement prefixes that need no user story: non-functional and technical rows have no actor. */
59
+ const STORYLESS_PREFIXES = Object.freeze(['N-', 'T-']);
60
+ const USAGE = 'Usage: pp-tools discovery stories-check';
61
+ function readIfPresent(abs) {
62
+ if (!node_fs_1.default.existsSync(abs))
63
+ return { kind: 'absent' };
64
+ try {
65
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
66
+ }
67
+ catch {
68
+ return { kind: 'unreadable' };
69
+ }
70
+ }
71
+ function sameHeader(actual, expected) {
72
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
73
+ }
74
+ function splitList(cell) {
75
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
76
+ }
77
+ /** First-column IDs of the first table under a `## <heading>` of another artifact. */
78
+ function firstColumnOf(content, heading) {
79
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
80
+ if (!section)
81
+ return new Set();
82
+ const tables = table.parseTables(section.body);
83
+ if (tables.length === 0)
84
+ return new Set();
85
+ return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
86
+ }
87
+ /**
88
+ * Every step row of `_processes.md` § Steps: its key (`MP-nn #<cell>`), its
89
+ * actors and the requirements it cites. `null` when the section is absent —
90
+ * the step rule then cannot run and only the prefix exemption applies.
91
+ */
92
+ function stepsOf(content) {
93
+ const steps = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === 'Steps');
94
+ if (!steps)
95
+ return null;
96
+ const out = [];
97
+ for (const sec of table.splitSections(steps.body.map((l) => l.text), 3)) {
98
+ const m = MP_HEADING_RE.exec(sec.heading);
99
+ const tbl = table.parseTables(sec.body)[0];
100
+ if (!m || !tbl)
101
+ continue;
102
+ for (const r of tbl.rows) {
103
+ out.push({ key: `${m[1]} #${(r.cells[0] ?? '').trim()}`, actors: splitList(r.cells[2] ?? ''), requirements: splitList(r.cells[7] ?? '') });
104
+ }
105
+ }
106
+ return out;
107
+ }
108
+ /**
109
+ * Does this MVP requirement need a user story? Step 5E derives stories from the
110
+ * step rows where the actor is a persona, so the check mirrors that: a
111
+ * requirement needs one when at least one step citing it has an actor other
112
+ * than `System`. Without a step matrix the prefix exemption is all there is.
113
+ */
114
+ function needsStory(r, steps) {
115
+ if (STORYLESS_PREFIXES.includes(r.prefix))
116
+ return false;
117
+ if (steps === null)
118
+ return true;
119
+ return steps.some((s) => s.requirements.includes(r.id) && s.actors.some((a) => a !== SYSTEM_ACTOR));
120
+ }
121
+ // ─── Check ────────────────────────────────────────────────────────────────────
122
+ function checkStories(content, sources) {
123
+ const violations = [];
124
+ const sections = table.splitSections(content.split(/\r?\n/), 2);
125
+ const section = sections.find((s) => s.heading === STORIES_SECTION);
126
+ if (!section) {
127
+ return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', id: STORIES_SECTION }] };
128
+ }
129
+ const tables = table.parseTables(section.body);
130
+ if (tables.length === 0) {
131
+ return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', line: section.line, id: STORIES_SECTION }] };
132
+ }
133
+ if (!sameHeader(tables[0].header, STORIES_HEADER)) {
134
+ return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: STORIES_SECTION }] };
135
+ }
136
+ const knownIds = sources.requirements instanceof Array ? new Set(sources.requirements.map((r) => r.id)) : null;
137
+ const ids = new Set();
138
+ const seqs = [];
139
+ const storied = new Set();
140
+ for (const row of tables[0].rows) {
141
+ const [id, epic, requirements, story, acceptance, sourceStep] = row.cells.map((c) => c.trim());
142
+ const m = US_RE.exec(id ?? '');
143
+ if (!m) {
144
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_id', line: row.line });
145
+ }
146
+ else if (ids.has(id)) {
147
+ violations.push({ artifact: STORIES_FILE, reason: 'duplicate_id', line: row.line, id });
148
+ }
149
+ else {
150
+ ids.add(id);
151
+ seqs.push(Number(m[1]));
152
+ }
153
+ if (!EPIC_RE.test(epic ?? ''))
154
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_epic', line: row.line });
155
+ if ((story ?? '') === '')
156
+ violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Story' });
157
+ if ((acceptance ?? '') === '')
158
+ violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Acceptance criteria' });
159
+ else if (POINTER_AC_RE.test(acceptance))
160
+ violations.push({ artifact: STORIES_FILE, reason: 'acceptance_is_pointer', line: row.line, id });
161
+ const reqs = splitList(requirements ?? '');
162
+ if (reqs.length === 0)
163
+ violations.push({ artifact: STORIES_FILE, reason: 'no_requirement', line: row.line });
164
+ for (const r of reqs) {
165
+ if (!REQUIREMENT_ID_RE.test(r))
166
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
167
+ else if (knownIds !== null && !knownIds.has(r))
168
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
169
+ else
170
+ storied.add(r);
171
+ }
172
+ const src = sourceStep ?? '';
173
+ if (src !== PENDING) {
174
+ const refs = splitList(src);
175
+ if (refs.length === 0)
176
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
177
+ for (const ref of refs) {
178
+ const sm = SOURCE_STEP_RE.exec(ref);
179
+ if (!sm)
180
+ violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
181
+ else if (sources.processes !== null && !sources.processes.has(sm[1]))
182
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_id', line: row.line, id: sm[1] });
183
+ else if (sources.steps !== null && !sources.steps.some((st) => st.key === ref))
184
+ violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_step', line: row.line, id: ref.slice(0, 40) });
185
+ }
186
+ }
187
+ }
188
+ const max = table.boundedSequenceMax(seqs);
189
+ if (max !== null) {
190
+ const have = new Set(seqs);
191
+ for (let n = 1; n <= max; n += 1) {
192
+ if (!have.has(n))
193
+ violations.push({ artifact: STORIES_FILE, reason: 'sequence_gap', id: `US-${String(n).padStart(2, '0')}` });
194
+ }
195
+ }
196
+ if (sources.requirements instanceof Array) {
197
+ for (const r of sources.requirements) {
198
+ if (r.disposition !== rollup.DISPOSITIONS.MVP)
199
+ continue;
200
+ if (!needsStory(r, sources.steps))
201
+ continue;
202
+ if (!storied.has(r.id))
203
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'mvp_requirement_without_story', id: r.id });
204
+ }
205
+ }
206
+ else if (sources.requirements === 'unparseable') {
207
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
208
+ }
209
+ return { ok: violations.length === 0, stories: ids.size, violations };
210
+ }
211
+ function checkProjectStories(cwd) {
212
+ const read = readIfPresent(node_path_1.default.join(cwd, STORIES_FILE));
213
+ if (read.kind === 'absent')
214
+ return null;
215
+ if (read.kind === 'unreadable')
216
+ return { ok: false, file: STORIES_FILE, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'artifact_unreadable' }] };
217
+ const extra = [];
218
+ const sources = { requirements: null, processes: null, steps: null };
219
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
220
+ if (req.kind === 'unreadable')
221
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
222
+ if (req.kind === 'ok') {
223
+ const parsed = rollup.parseRequirementsArtifact(req.content);
224
+ sources.requirements = parsed.ok ? parsed.rows.map((r) => ({ id: r.id, prefix: r.prefix, disposition: r.disposition })) : 'unparseable';
225
+ }
226
+ const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
227
+ if (proc.kind === 'unreadable')
228
+ extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
229
+ if (proc.kind === 'ok') {
230
+ sources.processes = firstColumnOf(proc.content, 'To-be');
231
+ sources.steps = stepsOf(proc.content);
232
+ }
233
+ const result = checkStories(read.content, sources);
234
+ result.violations.push(...extra);
235
+ result.ok = result.violations.length === 0;
236
+ return { file: STORIES_FILE, ...result };
237
+ }
238
+ function formatViolation(v) {
239
+ const where = v.line !== undefined ? `:${v.line}` : '';
240
+ const what = v.id !== undefined ? ` (${v.id})` : '';
241
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
242
+ }
243
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
244
+ /** `pp-tools discovery stories-check` (dotted `query discovery.stories-check`). Absent artifact → exit 0, `present: false`. */
245
+ function cmdDiscoveryStoriesCheck(cwd, args, raw) {
246
+ if (args.length > 2) {
247
+ error(USAGE, ERROR_REASON.USAGE);
248
+ return; // unreachable
249
+ }
250
+ const result = checkProjectStories(cwd);
251
+ if (result === null) {
252
+ output({ ok: true, present: false, file: STORIES_FILE }, raw);
253
+ return;
254
+ }
255
+ const payload = { ok: result.ok, present: true, file: result.file, stories: result.stories };
256
+ if (!result.ok) {
257
+ output({ ...payload, violations: result.violations.length }, raw);
258
+ error(`discovery stories-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED);
259
+ return; // unreachable
260
+ }
261
+ output(payload, raw);
262
+ }
263
+ module.exports = {
264
+ STORIES_FILE,
265
+ STORIES_HEADER,
266
+ STORYLESS_PREFIXES,
267
+ SYSTEM_ACTOR,
268
+ needsStory,
269
+ STORIES_CHECK_FAILED: ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED,
270
+ checkStories,
271
+ checkProjectStories,
272
+ cmdDiscoveryStoriesCheck,
273
+ };
@@ -40,7 +40,7 @@ const VOID_TAGS = new Set([
40
40
  const RAW_TEXT_TAGS = new Set(['script', 'style', 'noscript']);
41
41
  const ID_RE = /^[A-Za-z][A-Za-z0-9_:.-]{0,79}$/;
42
42
  const REQ_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
43
- const DECISION_RE = /^D-\d{2}-\d+$/;
43
+ const DECISION_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
44
44
  const ELEMENT_RE = /^(\*|[A-Za-z0-9_.-]+\.html?)#([A-Za-z][A-Za-z0-9_:.-]{0,79})$/;
45
45
  const ROUND_RE = /^## Sweep (\d{1,2}) — \d{4}-\d{2}-\d{2} — .+$/;
46
46
  const TABLE_HEADER = ['Element', 'Outcome', 'Reference'];
@@ -90,9 +90,64 @@ function boundedSequenceMax(seqs) {
90
90
  const max = seqs.reduce((a, b) => (b > a ? b : a), 0);
91
91
  return max > MAX_SEQUENCE_SPAN ? null : max;
92
92
  }
93
+ /**
94
+ * Every pipe table in a run of numbered lines (Discovery Sprint improvement
95
+ * plan, Batch 2). The header is the row before the first separator; data rows
96
+ * follow until the first line that is not a pipe row; a second separator
97
+ * starts the next table. Cells go through `splitTableCells`, so an escaped pipe
98
+ * inside a free-text cell never shifts a column.
99
+ */
100
+ function parseTables(body) {
101
+ const tables = [];
102
+ for (let i = 1; i < body.length; i += 1) {
103
+ if (!SEPARATOR_ROW_RE.test(body[i].text) || !body[i - 1].text.trim().startsWith('|'))
104
+ continue;
105
+ const table = { line: body[i - 1].n, header: splitTableCells(body[i - 1].text), rows: [] };
106
+ let j = i + 1;
107
+ for (; j < body.length; j += 1) {
108
+ const text = body[j].text.trim();
109
+ if (!text.startsWith('|') || SEPARATOR_ROW_RE.test(text))
110
+ break;
111
+ table.rows.push({ line: body[j].n, cells: splitTableCells(text) });
112
+ }
113
+ tables.push(table);
114
+ i = j - 1;
115
+ }
116
+ return tables;
117
+ }
118
+ /**
119
+ * Split a file into the sections opened by headings of exactly `level` hashes
120
+ * (`## ` for 2). A section's body runs to the next heading of the same or a
121
+ * higher level; lower-level headings stay inside the body, so a caller can
122
+ * split a body again at the next level. Lines before the first heading belong
123
+ * to no section.
124
+ */
125
+ function splitSections(lines, level) {
126
+ const open = new RegExp(`^#{${level}}\\s+(.*)$`);
127
+ const close = new RegExp(`^#{1,${level}}\\s`);
128
+ const out = [];
129
+ let current = null;
130
+ for (let i = 0; i < lines.length; i += 1) {
131
+ const m = open.exec(lines[i]);
132
+ if (m) {
133
+ current = { heading: m[1].trim(), line: i + 1, body: [] };
134
+ out.push(current);
135
+ continue;
136
+ }
137
+ if (close.test(lines[i])) {
138
+ current = null;
139
+ continue;
140
+ }
141
+ if (current)
142
+ current.body.push({ n: i + 1, text: lines[i] });
143
+ }
144
+ return out;
145
+ }
93
146
  module.exports = {
94
147
  splitTableCells,
95
148
  SEPARATOR_ROW_RE,
96
149
  MAX_SEQUENCE_SPAN,
97
150
  boundedSequenceMax,
151
+ parseTables,
152
+ splitSections,
98
153
  };
@@ -203,6 +203,28 @@ const ERROR_REASON = Object.freeze({
203
203
  DISCOVERY_REBASELINE_CHECK_FAILED: 'discovery_rebaseline_check_failed',
204
204
  // discovery vault context record (PBI 75336)
205
205
  DISCOVERY_VAULT_CHECK_FAILED: 'discovery_vault_check_failed',
206
+ // discovery project charter (Discovery Sprint improvement plan, Batch 1)
207
+ DISCOVERY_CHARTER_CHECK_FAILED: 'discovery_charter_check_failed',
208
+ DISCOVERY_CHARTER_UNSIGNED: 'discovery_charter_unsigned',
209
+ // discovery data model, processes, business rules (improvement plan, Batch 2)
210
+ DISCOVERY_ENTITY_CHECK_FAILED: 'discovery_entity_check_failed',
211
+ DISCOVERY_ENTITY_CHECK_INCOMPLETE: 'discovery_entity_check_incomplete',
212
+ DISCOVERY_PROCESS_CHECK_FAILED: 'discovery_process_check_failed',
213
+ DISCOVERY_PROCESS_STEPS_UNCITED: 'discovery_process_steps_uncited',
214
+ DISCOVERY_RULES_CHECK_FAILED: 'discovery_rules_check_failed',
215
+ DISCOVERY_STORIES_CHECK_FAILED: 'discovery_stories_check_failed',
216
+ DISCOVERY_PHASES_CHECK_FAILED: 'discovery_phases_check_failed',
217
+ DISCOVERY_FOR_PHASE_FAILED: 'discovery_for_phase_failed',
218
+ // discovery roles, integrations, migration, deviations (improvement plan, Batch 3)
219
+ DISCOVERY_ROLES_CHECK_FAILED: 'discovery_roles_check_failed',
220
+ DISCOVERY_INTEGRATIONS_CHECK_FAILED: 'discovery_integrations_check_failed',
221
+ DISCOVERY_INTEGRATIONS_UNCITED: 'discovery_integrations_uncited',
222
+ DISCOVERY_MIGRATION_CHECK_FAILED: 'discovery_migration_check_failed',
223
+ DISCOVERY_MIGRATION_UNCITED: 'discovery_migration_uncited',
224
+ // discovery session output gate (improvement plan, Batch 5)
225
+ DISCOVERY_SESSION_CHECK_FAILED: 'discovery_session_check_failed',
226
+ DISCOVERY_DEVIATIONS_CHECK_FAILED: 'discovery_deviations_check_failed',
227
+ DISCOVERY_DEVIATIONS_UNAPPROVED: 'discovery_deviations_unapproved',
206
228
  // generic
207
229
  USAGE: 'usage',
208
230
  UNKNOWN: 'unknown',
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.REVIEW_FILES_CAP = exports.REVIEW_VERDICTS_HEADING = exports.REVIEW_ARTIFACT_MARKER = exports.REVIEW_ARTIFACT_SUFFIX = exports.REVIEW_VERDICTS = void 0;
6
+ exports.DEVIATIONS_FILE = exports.REVIEW_FILES_CAP = exports.REVIEW_VERDICTS_HEADING = exports.REVIEW_ARTIFACT_MARKER = exports.REVIEW_ARTIFACT_SUFFIX = exports.REVIEW_VERDICTS = void 0;
7
+ exports.readDeviations = readDeviations;
7
8
  exports.deriveStandardsReview = deriveStandardsReview;
8
9
  exports.formatReviewCounts = formatReviewCounts;
9
10
  exports.validateReviewArtifact = validateReviewArtifact;
@@ -59,6 +60,8 @@ exports.REVIEW_VERDICTS_HEADING = '## Standard Verdicts';
59
60
  exports.REVIEW_FILES_CAP = 200;
60
61
  /** Bound on a git query a review makes. */
61
62
  const REVIEW_GIT_TIMEOUT_MS = 30_000;
63
+ /** Where the Discovery Sprint records agreed departures from the vault. */
64
+ exports.DEVIATIONS_FILE = '.planning/discovery/_deviations.md';
62
65
  // ─── Derivation ──────────────────────────────────────────────────────────────
63
66
  const PLAN_RE = /(^|-)PLAN\.md$/;
64
67
  const SUMMARY_RE = /(^|-)SUMMARY\.md$/;
@@ -73,6 +76,47 @@ function readBounded(abs) {
73
76
  return null;
74
77
  }
75
78
  }
79
+ /**
80
+ * The deviations on file, read by header name so Batch 3's full grammar can
81
+ * add or reorder columns without touching this reader. `gap` is set when the
82
+ * file exists but yields no readable table with both join columns.
83
+ */
84
+ function readDeviations(root) {
85
+ const abs = node_path_1.default.join(root, exports.DEVIATIONS_FILE);
86
+ if (!node_fs_1.default.existsSync(abs))
87
+ return { deviations: [], gap: null };
88
+ const text = readBounded(abs);
89
+ if (text === null)
90
+ return { deviations: [], gap: `${exports.DEVIATIONS_FILE} could not be read (unreadable or over the size cap), so deviations on file are unknown` };
91
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
92
+ const table = require('./discovery-table.cjs');
93
+ const deviations = [];
94
+ let joinable = false;
95
+ for (const section of table.splitSections(text.split(/\r?\n/), 2)) {
96
+ for (const tbl of table.parseTables(section.body)) {
97
+ const names = tbl.header.map((h) => h.trim().toLowerCase());
98
+ const col = (name) => names.indexOf(name);
99
+ const standard = col('standard');
100
+ const status = col('status');
101
+ if (standard === -1 || status === -1)
102
+ continue;
103
+ joinable = true;
104
+ const cell = (row, i) => (i === -1 ? '' : (row.cells[i] ?? '').trim());
105
+ for (const row of tbl.rows) {
106
+ deviations.push({
107
+ id: cell(row, col('id')),
108
+ standard: cell(row, standard),
109
+ status: cell(row, status),
110
+ decision: cell(row, col('decision')),
111
+ approver: cell(row, col('approver')),
112
+ });
113
+ }
114
+ }
115
+ }
116
+ if (!joinable)
117
+ return { deviations: [], gap: `${exports.DEVIATIONS_FILE} carries no table with Standard and Status columns, so deviations on file are unknown` };
118
+ return { deviations, gap: null };
119
+ }
76
120
  function consultedText(entry, hasSummary) {
77
121
  if (!hasSummary)
78
122
  return 'no summary';
@@ -206,6 +250,9 @@ function deriveStandardsReview(root, cwd, positional, opts = {}) {
206
250
  });
207
251
  }
208
252
  }
253
+ const onFile = readDeviations(root);
254
+ if (onFile.gap !== null)
255
+ gaps.push(onFile.gap);
209
256
  if (plans.length === 0 || documents.size === 0) {
210
257
  return {
211
258
  status: 'no-records',
@@ -216,9 +263,10 @@ function deriveStandardsReview(root, cwd, positional, opts = {}) {
216
263
  plans,
217
264
  documents: [],
218
265
  gaps,
266
+ deviations: onFile.deviations,
219
267
  };
220
268
  }
221
- return { status: 'ok', phaseDir: phase.rel, plans, documents: [...documents.values()], gaps };
269
+ return { status: 'ok', phaseDir: phase.rel, plans, documents: [...documents.values()], gaps, deviations: onFile.deviations };
222
270
  }
223
271
  /** The counts line, in verdict order: `p PASS · f FAIL · n N-A · i INCONCLUSIVE (of N documents)`. */
224
272
  function formatReviewCounts(counts) {
@@ -2564,13 +2564,155 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
2564
2564
  } catch (vaultCheckErr) {
2565
2565
  error(`discovery vault-check: ${vaultCheckErr && vaultCheckErr.message ? vaultCheckErr.message : String(vaultCheckErr)}`, ERROR_REASON.UNKNOWN);
2566
2566
  }
2567
+ } else if (subcommand === 'charter-check') {
2568
+ // discovery charter-check [--require-signed]
2569
+ //
2570
+ // Structural check of docs/discovery/PROJECT-CHARTER.md: frontmatter
2571
+ // enums, the twenty `<!-- charter:<key> -->` sections in order, every
2572
+ // cited ID resolving to a session table or _requirements.md row, the
2573
+ // Go / No-Go table equal to Session 13's, must-resolve open items
2574
+ // closed or listed as conditions, sign-off rows agreeing with status.
2575
+ // --require-signed also fails (discovery_charter_unsigned) unless the
2576
+ // status is Signed or Signed with conditions — the handoff gate's arm.
2577
+ const charter = require('./lib/discovery-charter.cjs');
2578
+ try {
2579
+ charter.cmdDiscoveryCharterCheck(cwd, args, raw);
2580
+ } catch (charterCheckErr) {
2581
+ error(`discovery charter-check: ${charterCheckErr && charterCheckErr.message ? charterCheckErr.message : String(charterCheckErr)}`, ERROR_REASON.UNKNOWN);
2582
+ }
2583
+ } else if (subcommand === 'entity-check') {
2584
+ // discovery entity-check [--require-complete] — _entities.md structure
2585
+ // (improvement plan, Batch 2): named entities, closed lifecycle and
2586
+ // classification cells, attributes per entity, Mermaid cardinalities,
2587
+ // step-matrix data objects resolving. --require-complete also fails on
2588
+ // pending `—` cells (discovery_entity_check_incomplete).
2589
+ const entities = require('./lib/discovery-entities.cjs');
2590
+ try {
2591
+ entities.cmdDiscoveryEntityCheck(cwd, args, raw);
2592
+ } catch (entityCheckErr) {
2593
+ error(`discovery entity-check: ${entityCheckErr && entityCheckErr.message ? entityCheckErr.message : String(entityCheckErr)}`, ERROR_REASON.UNKNOWN);
2594
+ }
2595
+ } else if (subcommand === 'process-check') {
2596
+ // discovery process-check [--require-cited] — _processes.md structure:
2597
+ // PRC-/MP- sequences, Replaces resolving, one swimlane and one step
2598
+ // matrix per main process, actors, data objects, requirement IDs.
2599
+ // --require-cited also fails on a step citing no requirement.
2600
+ const processes = require('./lib/discovery-processes.cjs');
2601
+ try {
2602
+ processes.cmdDiscoveryProcessCheck(cwd, args, raw);
2603
+ } catch (processCheckErr) {
2604
+ error(`discovery process-check: ${processCheckErr && processCheckErr.message ? processCheckErr.message : String(processCheckErr)}`, ERROR_REASON.UNKNOWN);
2605
+ }
2606
+ } else if (subcommand === 'rules-check') {
2607
+ // discovery rules-check — _rules.md structure: BR- sequence, layer
2608
+ // vocabulary, every rule citing an existing requirement and an existing
2609
+ // entity or process.
2610
+ const rules = require('./lib/discovery-rules.cjs');
2611
+ try {
2612
+ rules.cmdDiscoveryRulesCheck(cwd, args, raw);
2613
+ } catch (rulesCheckErr) {
2614
+ error(`discovery rules-check: ${rulesCheckErr && rulesCheckErr.message ? rulesCheckErr.message : String(rulesCheckErr)}`, ERROR_REASON.UNKNOWN);
2615
+ }
2616
+ } else if (subcommand === 'stories-check') {
2617
+ // discovery stories-check — _stories.md structure (improvement plan,
2618
+ // Batch 4): US- sequence, EPIC shape, every story citing an existing
2619
+ // requirement, source step resolving, every MVP domain requirement
2620
+ // carrying at least one story.
2621
+ const stories = require('./lib/discovery-stories.cjs');
2622
+ try {
2623
+ stories.cmdDiscoveryStoriesCheck(cwd, args, raw);
2624
+ } catch (storiesCheckErr) {
2625
+ error(`discovery stories-check: ${storiesCheckErr && storiesCheckErr.message ? storiesCheckErr.message : String(storiesCheckErr)}`, ERROR_REASON.UNKNOWN);
2626
+ }
2627
+ } else if (subcommand === 'phases-check') {
2628
+ // discovery phases-check — _phases.md structure (improvement plan,
2629
+ // Batch 4): contiguous phases, requirement IDs resolving and not
2630
+ // dropped, dependencies on earlier phases, every MVP requirement in
2631
+ // exactly one phase.
2632
+ const phasesCheck = require('./lib/discovery-phases.cjs');
2633
+ try {
2634
+ phasesCheck.cmdDiscoveryPhasesCheck(cwd, args, raw);
2635
+ } catch (phasesCheckErr) {
2636
+ error(`discovery phases-check: ${phasesCheckErr && phasesCheckErr.message ? phasesCheckErr.message : String(phasesCheckErr)}`, ERROR_REASON.UNKNOWN);
2637
+ }
2638
+ } else if (subcommand === 'for-phase') {
2639
+ // discovery for-phase <N> [--markdown | --conformance-template |
2640
+ // --verify <file>] — the rows of every canonical artifact that cite
2641
+ // the phase's requirement IDs as one extract between
2642
+ // <!-- discovery:auto --> markers (improvement plan, Batch 4, R1);
2643
+ // the verifier's conformance block and its check (R3). Fails loud on
2644
+ // artifact structure only; no discovery package → exit 0, empty.
2645
+ const forPhase = require('./lib/discovery-for-phase.cjs');
2646
+ try {
2647
+ forPhase.cmdDiscoveryForPhase(cwd, args, raw);
2648
+ } catch (forPhaseErr) {
2649
+ error(`discovery for-phase: ${forPhaseErr && forPhaseErr.message ? forPhaseErr.message : String(forPhaseErr)}`, ERROR_REASON.UNKNOWN);
2650
+ }
2651
+ } else if (subcommand === 'roles-check') {
2652
+ // discovery roles-check — _roles.md structure (improvement plan,
2653
+ // Batch 3): every role a persona, a step-matrix actor or System;
2654
+ // permissions naming a known role, an existing entity, lowercase
2655
+ // operations and at least one requirement; screen rows resolving.
2656
+ const roles = require('./lib/discovery-access.cjs');
2657
+ try {
2658
+ roles.cmdDiscoveryRolesCheck(cwd, args, raw);
2659
+ } catch (rolesCheckErr) {
2660
+ error(`discovery roles-check: ${rolesCheckErr && rolesCheckErr.message ? rolesCheckErr.message : String(rolesCheckErr)}`, ERROR_REASON.UNKNOWN);
2661
+ }
2662
+ } else if (subcommand === 'integrations-check') {
2663
+ // discovery integrations-check [--require-cited] — _integrations.md
2664
+ // structure: IF- sequence, direction vocabulary, contract and error
2665
+ // rows naming a known interface, step-matrix IF- tokens resolving.
2666
+ // --require-cited also fails on an uncited interface and on a step
2667
+ // naming a system without an IF- (discovery_integrations_uncited).
2668
+ const integrations = require('./lib/discovery-integrations.cjs');
2669
+ try {
2670
+ integrations.cmdDiscoveryIntegrationsCheck(cwd, args, raw);
2671
+ } catch (integrationsCheckErr) {
2672
+ error(`discovery integrations-check: ${integrationsCheckErr && integrationsCheckErr.message ? integrationsCheckErr.message : String(integrationsCheckErr)}`, ERROR_REASON.UNKNOWN);
2673
+ }
2674
+ } else if (subcommand === 'migration-check') {
2675
+ // discovery migration-check [--require-cited] — _migration.md
2676
+ // structure: MG- sequence, source system in Session 2's landscape,
2677
+ // entities resolving, requirement IDs resolving. --require-cited
2678
+ // also fails on an uncited row (discovery_migration_uncited).
2679
+ const migration = require('./lib/discovery-migration.cjs');
2680
+ try {
2681
+ migration.cmdDiscoveryMigrationCheck(cwd, args, raw);
2682
+ } catch (migrationCheckErr) {
2683
+ error(`discovery migration-check: ${migrationCheckErr && migrationCheckErr.message ? migrationCheckErr.message : String(migrationCheckErr)}`, ERROR_REASON.UNKNOWN);
2684
+ }
2685
+ } else if (subcommand === 'session-check') {
2686
+ // discovery session-check <N> [<N>…] | --all — every Notes Template
2687
+ // section of the session content file present exactly once in
2688
+ // session-NN.md, each table with a data row or the literal
2689
+ // `None identified in Discovery.`; --all covers the Completed sessions.
2690
+ const sessionCheck = require('./lib/discovery-session-check.cjs');
2691
+ try {
2692
+ sessionCheck.cmdDiscoverySessionCheck(cwd, args, raw);
2693
+ } catch (sessionCheckErr) {
2694
+ error(`discovery session-check: ${sessionCheckErr && sessionCheckErr.message ? sessionCheckErr.message : String(sessionCheckErr)}`, ERROR_REASON.UNKNOWN);
2695
+ }
2696
+ } else if (subcommand === 'deviations-check') {
2697
+ // discovery deviations-check [--require-approved] — _deviations.md
2698
+ // structure: DEV- sequence, a [vault: path] standard present in the
2699
+ // cache, reversible and status vocabularies, approver in Session 1's
2700
+ // Approval Matrix, decision ID shape. --require-approved also fails
2701
+ // on a hard-to-reverse deviation still Proposed
2702
+ // (discovery_deviations_unapproved).
2703
+ const deviations = require('./lib/discovery-deviations.cjs');
2704
+ try {
2705
+ deviations.cmdDiscoveryDeviationsCheck(cwd, args, raw);
2706
+ } catch (deviationsCheckErr) {
2707
+ error(`discovery deviations-check: ${deviationsCheckErr && deviationsCheckErr.message ? deviationsCheckErr.message : String(deviationsCheckErr)}`, ERROR_REASON.UNKNOWN);
2708
+ }
2567
2709
  } else {
2568
2710
  // WIRING, NOT HELP TEXT: tests/discovery-artifacts.test.cjs derives each
2569
2711
  // subcommand spelling by regexing `<namespace>_<subcommand>_failed` out of
2570
2712
  // the engine's exported exit identity, splits this string on commas and
2571
2713
  // asserts membership. A spelling missing here is a command the engine
2572
2714
  // claims to be and the router does not dispatch.
2573
- error('Unknown discovery subcommand. Available: rollup, input-check, research-check, screen-check, element-sweep, rebaseline-check, vault-check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2715
+ error('Unknown discovery subcommand. Available: rollup, input-check, research-check, screen-check, element-sweep, rebaseline-check, vault-check, charter-check, entity-check, process-check, rules-check, stories-check, phases-check, for-phase, roles-check, integrations-check, migration-check, deviations-check, session-check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
2574
2716
  }
2575
2717
  break;
2576
2718
  }