@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,216 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery business-rules check (Discovery Sprint improvement plan, Batch 2).
4
+ *
5
+ * `.planning/discovery/_rules.md` is the register of business rules —
6
+ * validations, calculations, invariants — Session 3 Step 5D′ mints from the
7
+ * step matrices: `BR-nn`, the rule, what it applies to (an entity of
8
+ * `_entities.md` or a process of `_processes.md`), where it is checked
9
+ * (`UI`, `service`, `DB` — the vault's rule that a check has one owning
10
+ * layer), the requirements it realises, and its source. The build's planner
11
+ * and tests read it by requirement ID. This module checks the structure:
12
+ *
13
+ * - the `## Rules` table exists with the template's columns;
14
+ * - `BR-` IDs are well-formed, unique, contiguous;
15
+ * - `Checked at` is a comma list from the closed layer vocabulary;
16
+ * - every rule cites at least one requirement, each existing in
17
+ * `_requirements.md` when it is on file and parseable;
18
+ * - every `Applies to` token is an entity name that `_entities.md` has or a
19
+ * `PRC-`/`MP-` ID that `_processes.md` has, when those artifacts are on
20
+ * file (an absent artifact leaves the token shape-checked only).
21
+ *
22
+ * `_glossary.md` (`Term | Definition | Do not use | Source`) lives beside it
23
+ * and is deliberately not checked: a definition has no grammar.
24
+ *
25
+ * Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at
26
+ * `/pp-discovery-to-build`. Absent artifact → exit 0.
27
+ */
28
+ var __importDefault = (this && this.__importDefault) || function (mod) {
29
+ return (mod && mod.__esModule) ? mod : { "default": mod };
30
+ };
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const io = require("./io.cjs");
35
+ const { output, error, ERROR_REASON } = io;
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const table = require("./discovery-table.cjs");
38
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
39
+ const rollup = require("./discovery-rollup.cjs");
40
+ // ─── Constants ────────────────────────────────────────────────────────────────
41
+ const DISCOVERY_DIR = '.planning/discovery';
42
+ const RULES_FILE = `${DISCOVERY_DIR}/_rules.md`;
43
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
44
+ const ENTITIES_FILE = `${DISCOVERY_DIR}/_entities.md`;
45
+ const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
46
+ const RULES_SECTION = 'Rules';
47
+ const RULES_HEADER = Object.freeze(['ID', 'Rule', 'Applies to', 'Checked at', 'Requirements', 'Source']);
48
+ const BR_RE = /^BR-(\d{2})$/;
49
+ const LAYERS = Object.freeze(['UI', 'service', 'DB']);
50
+ const ENTITY_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;
51
+ const PROCESS_ID_RE = /^(PRC|MP)-\d{2}$/;
52
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
53
+ const USAGE = 'Usage: pp-tools discovery rules-check';
54
+ function readIfPresent(abs) {
55
+ if (!node_fs_1.default.existsSync(abs))
56
+ return { kind: 'absent' };
57
+ try {
58
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
59
+ }
60
+ catch {
61
+ return { kind: 'unreadable' };
62
+ }
63
+ }
64
+ function sameHeader(actual, expected) {
65
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
66
+ }
67
+ function splitList(cell) {
68
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
69
+ }
70
+ /** First-column IDs/names of the first table under a `## <heading>` of another artifact. */
71
+ function firstColumnOf(content, heading) {
72
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
73
+ if (!section)
74
+ return new Set();
75
+ const tables = table.parseTables(section.body);
76
+ if (tables.length === 0)
77
+ return new Set();
78
+ return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
79
+ }
80
+ // ─── Check ────────────────────────────────────────────────────────────────────
81
+ function checkRules(content, sources) {
82
+ const violations = [];
83
+ const sections = table.splitSections(content.split(/\r?\n/), 2);
84
+ const section = sections.find((s) => s.heading === RULES_SECTION);
85
+ if (!section) {
86
+ return { ok: false, rules: 0, violations: [{ artifact: RULES_FILE, reason: 'missing_section', id: RULES_SECTION }] };
87
+ }
88
+ const tables = table.parseTables(section.body);
89
+ if (tables.length === 0) {
90
+ return { ok: false, rules: 0, violations: [{ artifact: RULES_FILE, reason: 'missing_section', line: section.line, id: RULES_SECTION }] };
91
+ }
92
+ if (!sameHeader(tables[0].header, RULES_HEADER)) {
93
+ return { ok: false, rules: 0, violations: [{ artifact: RULES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: RULES_SECTION }] };
94
+ }
95
+ const ids = new Set();
96
+ const seqs = [];
97
+ for (const row of tables[0].rows) {
98
+ const [id, rule, appliesTo, checkedAt, requirements] = row.cells.map((c) => c.trim());
99
+ const m = BR_RE.exec(id ?? '');
100
+ if (!m) {
101
+ violations.push({ artifact: RULES_FILE, reason: 'bad_id', line: row.line });
102
+ }
103
+ else if (ids.has(id)) {
104
+ violations.push({ artifact: RULES_FILE, reason: 'duplicate_id', line: row.line, id });
105
+ }
106
+ else {
107
+ ids.add(id);
108
+ seqs.push(Number(m[1]));
109
+ }
110
+ if ((rule ?? '') === '')
111
+ violations.push({ artifact: RULES_FILE, reason: 'empty_cell', line: row.line, id: 'Rule' });
112
+ const layers = splitList(checkedAt ?? '');
113
+ if (layers.length === 0 || layers.some((l) => !LAYERS.includes(l)))
114
+ violations.push({ artifact: RULES_FILE, reason: 'bad_checked_at', line: row.line });
115
+ const subjects = splitList(appliesTo ?? '');
116
+ if (subjects.length === 0)
117
+ violations.push({ artifact: RULES_FILE, reason: 'bad_subject', line: row.line });
118
+ for (const s of subjects) {
119
+ if (ENTITY_NAME_RE.test(s)) {
120
+ if (sources.entities !== null && !sources.entities.has(s))
121
+ violations.push({ artifact: RULES_FILE, reason: 'unknown_subject', line: row.line, id: s });
122
+ }
123
+ else if (PROCESS_ID_RE.test(s)) {
124
+ if (sources.processes !== null && !sources.processes.has(s))
125
+ violations.push({ artifact: RULES_FILE, reason: 'unknown_subject', line: row.line, id: s });
126
+ }
127
+ else {
128
+ violations.push({ artifact: RULES_FILE, reason: 'bad_subject', line: row.line });
129
+ }
130
+ }
131
+ const reqs = splitList(requirements ?? '');
132
+ if (reqs.length === 0)
133
+ violations.push({ artifact: RULES_FILE, reason: 'no_requirement', line: row.line });
134
+ for (const r of reqs) {
135
+ if (!REQUIREMENT_ID_RE.test(r))
136
+ violations.push({ artifact: RULES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
137
+ else if (sources.requirementIds instanceof Set && !sources.requirementIds.has(r))
138
+ violations.push({ artifact: RULES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
139
+ }
140
+ }
141
+ const max = table.boundedSequenceMax(seqs);
142
+ if (max !== null) {
143
+ const have = new Set(seqs);
144
+ for (let n = 1; n <= max; n += 1) {
145
+ if (!have.has(n))
146
+ violations.push({ artifact: RULES_FILE, reason: 'sequence_gap', id: `BR-${String(n).padStart(2, '0')}` });
147
+ }
148
+ }
149
+ if (sources.requirementIds === 'unparseable')
150
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
151
+ return { ok: violations.length === 0, rules: ids.size, violations };
152
+ }
153
+ function checkProjectRules(cwd) {
154
+ const read = readIfPresent(node_path_1.default.join(cwd, RULES_FILE));
155
+ if (read.kind === 'absent')
156
+ return null;
157
+ if (read.kind === 'unreadable')
158
+ return { ok: false, file: RULES_FILE, rules: 0, violations: [{ artifact: RULES_FILE, reason: 'artifact_unreadable' }] };
159
+ const extra = [];
160
+ const sources = { requirementIds: null, entities: null, processes: null };
161
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
162
+ if (req.kind === 'unreadable')
163
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
164
+ if (req.kind === 'ok') {
165
+ const parsed = rollup.parseRequirementsArtifact(req.content);
166
+ sources.requirementIds = parsed.ok ? new Set(parsed.rows.map((r) => r.id)) : 'unparseable';
167
+ }
168
+ const ent = readIfPresent(node_path_1.default.join(cwd, ENTITIES_FILE));
169
+ if (ent.kind === 'unreadable')
170
+ extra.push({ artifact: ENTITIES_FILE, reason: 'artifact_unreadable' });
171
+ if (ent.kind === 'ok')
172
+ sources.entities = firstColumnOf(ent.content, 'Entities');
173
+ const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
174
+ if (proc.kind === 'unreadable')
175
+ extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
176
+ if (proc.kind === 'ok')
177
+ sources.processes = new Set([...firstColumnOf(proc.content, 'As-is'), ...firstColumnOf(proc.content, 'To-be')]);
178
+ const result = checkRules(read.content, sources);
179
+ result.violations.push(...extra);
180
+ result.ok = result.violations.length === 0;
181
+ return { file: RULES_FILE, ...result };
182
+ }
183
+ function formatViolation(v) {
184
+ const where = v.line !== undefined ? `:${v.line}` : '';
185
+ const what = v.id !== undefined ? ` (${v.id})` : '';
186
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
187
+ }
188
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
189
+ /** `pp-tools discovery rules-check` (dotted `query discovery.rules-check`). Absent artifact → exit 0, `present: false`. */
190
+ function cmdDiscoveryRulesCheck(cwd, args, raw) {
191
+ if (args.length > 2) {
192
+ error(USAGE, ERROR_REASON.USAGE);
193
+ return; // unreachable
194
+ }
195
+ const result = checkProjectRules(cwd);
196
+ if (result === null) {
197
+ output({ ok: true, present: false, file: RULES_FILE }, raw);
198
+ return;
199
+ }
200
+ const payload = { ok: result.ok, present: true, file: result.file, rules: result.rules };
201
+ if (!result.ok) {
202
+ output({ ...payload, violations: result.violations.length }, raw);
203
+ error(`discovery rules-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_RULES_CHECK_FAILED);
204
+ return; // unreachable
205
+ }
206
+ output(payload, raw);
207
+ }
208
+ module.exports = {
209
+ RULES_FILE,
210
+ RULES_HEADER,
211
+ LAYERS,
212
+ RULES_CHECK_FAILED: ERROR_REASON.DISCOVERY_RULES_CHECK_FAILED,
213
+ checkRules,
214
+ checkProjectRules,
215
+ cmdDiscoveryRulesCheck,
216
+ };
@@ -355,7 +355,7 @@ const VERDICTS = Object.freeze(['change', 'keep', 'drop', 'question']);
355
355
  const PROCESSING = Object.freeze({ OPEN: 'Open', NO_CHANGE: 'No change' });
356
356
  // D-57: the three citation shapes a Processing cell may take. Anchored,
357
357
  // quantifier-free, applied to a SINGLE already-split cell (T-04-13).
358
- const DECISION_CITE_RE = /^D-\d{2}-\d+$/;
358
+ const DECISION_CITE_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
359
359
  // D-57: a data-gathering item in `_risks.md`.
360
360
  const DG_CITE_RE = /^DG-\d{2}$/;
361
361
  // D-57: a research-backlog item in `_research.md`.
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery session output check (Discovery Sprint improvement plan, Batch 5).
4
+ *
5
+ * A session's Notes Template is the contract for what the session produces:
6
+ * every whole-line bold title under `**Notes Template:**` in
7
+ * `workflows/discovery-sprint/sessions/NN-*.md` names a section the session
8
+ * file `.planning/discovery/session-NN.md` must carry exactly once, and a
9
+ * section that is a table in the template must hold at least one data row
10
+ * (or say, on one line under its title, `None identified in Discovery.`).
11
+ * A prose section must hold at least one line of prose or that literal.
12
+ * The score formula in `discovery-sprint.md` Step 7 counts a section this
13
+ * check reports empty as not covered, so a session can no longer close at
14
+ * 100 with an empty Entity Register or an empty Roles table (review F-6).
15
+ *
16
+ * The template is read from the pp-core install itself (two directories up
17
+ * from the compiled engine), so the required-section list cannot drift from
18
+ * the session file the facilitator reads.
19
+ *
20
+ * - `session-check <N>` checks one session whatever its status;
21
+ * - `session-check --all` checks every `session-NN.md` on disk whose
22
+ * frontmatter says `status: "Completed"` — Session 8's Closing Step and
23
+ * the Session 13 gates run it over the sessions closed so far.
24
+ *
25
+ * Posture: reports at every session close, at Session 8 and at Session 13;
26
+ * hard-blocks at the handoff like the other artifact checks.
27
+ */
28
+ var __importDefault = (this && this.__importDefault) || function (mod) {
29
+ return (mod && mod.__esModule) ? mod : { "default": mod };
30
+ };
31
+ const node_fs_1 = __importDefault(require("node:fs"));
32
+ const node_path_1 = __importDefault(require("node:path"));
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const io = require("./io.cjs");
35
+ const { output, error, ERROR_REASON } = io;
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const table = require("./discovery-table.cjs");
38
+ // ─── Constants ────────────────────────────────────────────────────────────────
39
+ const DISCOVERY_DIR = '.planning/discovery';
40
+ const SESSION_COUNT = 13;
41
+ /** The literal a section carries when the session found nothing to record. */
42
+ const NONE_LITERAL = 'None identified in Discovery.';
43
+ /** A parked topic's marker (Flow 2 Step 5.3) — the section is accounted for, the score is not. */
44
+ const PARKED_LITERAL = 'PARKED — needs business/senior input';
45
+ const NOTES_TEMPLATE_MARKER = /^\*\*Notes Template:\*\*$/;
46
+ const BOLD_TITLE_RE = /^\*\*([^*]+)\*\*$/;
47
+ const HEADING_RE = /^#{1,6}\s/;
48
+ const HEADING_TITLE_RE = /^#{1,6}\s+(.+?)\s*$/;
49
+ const RULE_RE = /^---$/;
50
+ const TABLE_ROW_RE = /^\|/;
51
+ const HTML_COMMENT_RE = /^<!--.*-->$/;
52
+ const COMPLETED_RE = /^status:\s*"?Completed"?\s*$/m;
53
+ const SESSIONS_DIR = node_path_1.default.resolve(__dirname, '..', '..', 'workflows', 'discovery-sprint', 'sessions');
54
+ const USAGE = 'Usage: pp-tools discovery session-check <N> [<N>…] | --all';
55
+ function readIfPresent(abs) {
56
+ if (!node_fs_1.default.existsSync(abs))
57
+ return { kind: 'absent' };
58
+ try {
59
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
60
+ }
61
+ catch {
62
+ return { kind: 'unreadable' };
63
+ }
64
+ }
65
+ function pad(n) {
66
+ return String(n).padStart(2, '0');
67
+ }
68
+ function sessionFile(n) {
69
+ return `${DISCOVERY_DIR}/session-${pad(n)}.md`;
70
+ }
71
+ /** The session content file `NN-*.md` under the install's sessions directory, or null. */
72
+ function templatePath(n, sessionsDir) {
73
+ let names;
74
+ try {
75
+ names = node_fs_1.default.readdirSync(sessionsDir);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ const prefix = `${pad(n)}-`;
81
+ const found = names.find((f) => f.startsWith(prefix) && f.endsWith('.md'));
82
+ return found ? node_path_1.default.join(sessionsDir, found) : null;
83
+ }
84
+ /**
85
+ * Split a run of lines into titled sections. In the template (`stopAtHeading`)
86
+ * a title is a whole-line `**Title**` and the run ends at the first heading or
87
+ * rule. In a session file a facilitator writes the same titles either as
88
+ * `**Title**` or as a heading `## Title` (both are found in the field), so
89
+ * there every whole-line bold and every heading opens a section.
90
+ */
91
+ function titledSections(lines, from, stopAtHeading) {
92
+ const out = [];
93
+ let current = null;
94
+ for (let i = from; i < lines.length; i += 1) {
95
+ const text = lines[i].trim();
96
+ if (stopAtHeading && (HEADING_RE.test(text) || RULE_RE.test(text)))
97
+ break;
98
+ const bold = BOLD_TITLE_RE.exec(text);
99
+ const heading = HEADING_TITLE_RE.exec(text);
100
+ const title = bold ? bold[1].trim() : heading ? heading[1].trim() : null;
101
+ if (title !== null) {
102
+ current = { title, line: i + 1, body: [] };
103
+ out.push(current);
104
+ continue;
105
+ }
106
+ if (current)
107
+ current.body.push({ n: i + 1, text: lines[i] });
108
+ }
109
+ return out;
110
+ }
111
+ /** The required sections of a session content file, in template order; null without a Notes Template. */
112
+ function requiredSectionsOf(templateContent) {
113
+ const lines = templateContent.split(/\r?\n/);
114
+ const at = lines.findIndex((l) => NOTES_TEMPLATE_MARKER.test(l.trim()));
115
+ if (at === -1)
116
+ return null;
117
+ return titledSections(lines, at + 1, true).map((s) => {
118
+ const tables = table.parseTables(s.body);
119
+ return tables.length > 0
120
+ ? { title: s.title, kind: 'table', header: tables[0].header.map((c) => c.trim()) }
121
+ : { title: s.title, kind: 'prose', header: null };
122
+ });
123
+ }
124
+ /** A table body row holds data when at least two cells are filled (one, for a one-column table). */
125
+ function isDataRow(cells) {
126
+ const filled = cells.filter((c) => c.trim() !== '').length;
127
+ return cells.length <= 1 ? filled >= 1 : filled >= 2;
128
+ }
129
+ function hasLiteral(body) {
130
+ return body.some((l) => {
131
+ const t = l.text.trim();
132
+ if (t === NONE_LITERAL || t === PARKED_LITERAL || t.startsWith(`${PARKED_LITERAL}`))
133
+ return true;
134
+ return TABLE_ROW_RE.test(t) && table.splitTableCells(t).some((c) => c.trim() === NONE_LITERAL);
135
+ });
136
+ }
137
+ function hasProse(body) {
138
+ return body.some((l) => {
139
+ const t = l.text.trim();
140
+ return t !== '' && !TABLE_ROW_RE.test(t) && !HTML_COMMENT_RE.test(t);
141
+ });
142
+ }
143
+ // ─── Check ────────────────────────────────────────────────────────────────────
144
+ function checkSession(n, sessionContent, required) {
145
+ const file = sessionFile(n);
146
+ const violations = [];
147
+ const empty = [];
148
+ const sections = titledSections(sessionContent.split(/\r?\n/), 0, false);
149
+ let filled = 0;
150
+ for (const req of required) {
151
+ const found = sections.filter((s) => s.title === req.title);
152
+ if (found.length === 0) {
153
+ violations.push({ artifact: file, reason: 'missing_section', id: req.title });
154
+ continue;
155
+ }
156
+ if (found.length > 1) {
157
+ violations.push({ artifact: file, reason: 'duplicate_section', line: found[1].line, id: req.title });
158
+ continue;
159
+ }
160
+ const s = found[0];
161
+ if (hasLiteral(s.body)) {
162
+ filled += 1;
163
+ continue;
164
+ }
165
+ if (req.kind === 'table') {
166
+ const tables = table.parseTables(s.body);
167
+ if (tables.length === 0 || tables[0].header.length !== (req.header ?? []).length) {
168
+ violations.push({ artifact: file, reason: 'bad_table_shape', line: tables.length > 0 ? tables[0].line : s.line, id: req.title });
169
+ continue;
170
+ }
171
+ if (tables[0].rows.some((r) => isDataRow(r.cells)))
172
+ filled += 1;
173
+ else {
174
+ empty.push(req.title);
175
+ violations.push({ artifact: file, reason: 'empty_section', line: s.line, id: req.title });
176
+ }
177
+ }
178
+ else if (hasProse(s.body) || table.parseTables(s.body).some((t) => t.rows.some((r) => isDataRow(r.cells))))
179
+ filled += 1;
180
+ else {
181
+ empty.push(req.title);
182
+ violations.push({ artifact: file, reason: 'empty_section', line: s.line, id: req.title });
183
+ }
184
+ }
185
+ return { required: required.length, filled, empty, violations };
186
+ }
187
+ function checkProjectSession(cwd, n, sessionsDir = SESSIONS_DIR) {
188
+ const file = sessionFile(n);
189
+ const base = { session: n, file, required: 0, filled: 0, empty: [] };
190
+ const tpl = templatePath(n, sessionsDir);
191
+ const tplRead = tpl ? readIfPresent(tpl) : { kind: 'absent' };
192
+ const required = tplRead.kind === 'ok' ? requiredSectionsOf(tplRead.content) : null;
193
+ if (required === null)
194
+ return { ...base, violations: [{ artifact: `sessions/${pad(n)}-*.md`, reason: 'template_missing', id: `session ${n}` }] };
195
+ const read = readIfPresent(node_path_1.default.join(cwd, file));
196
+ if (read.kind === 'absent')
197
+ return { ...base, required: required.length, violations: [{ artifact: file, reason: 'session_missing' }] };
198
+ if (read.kind === 'unreadable')
199
+ return { ...base, required: required.length, violations: [{ artifact: file, reason: 'artifact_unreadable' }] };
200
+ return { session: n, file, ...checkSession(n, read.content, required) };
201
+ }
202
+ /** The sessions `--all` covers: every session file on disk whose frontmatter says Completed. */
203
+ function completedSessions(cwd) {
204
+ const out = [];
205
+ for (let n = 1; n <= SESSION_COUNT; n += 1) {
206
+ const read = readIfPresent(node_path_1.default.join(cwd, sessionFile(n)));
207
+ if (read.kind === 'ok' && COMPLETED_RE.test(read.content.slice(0, 2000)))
208
+ out.push(n);
209
+ }
210
+ return out;
211
+ }
212
+ function checkProjectSessions(cwd, numbers, sessionsDir = SESSIONS_DIR) {
213
+ const sessions = numbers.map((n) => checkProjectSession(cwd, n, sessionsDir));
214
+ return { ok: sessions.every((s) => s.violations.length === 0), sessions };
215
+ }
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
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
222
+ /**
223
+ * `pp-tools discovery session-check <N> [<N>…] | --all` (dotted
224
+ * `query discovery.session-check`). Faults exit via
225
+ * `DISCOVERY_SESSION_CHECK_FAILED`; `--all` over a project with no completed
226
+ * session exits 0 with `sessions: []`.
227
+ */
228
+ function cmdDiscoverySessionCheck(cwd, args, raw) {
229
+ const rest = args.slice(2);
230
+ let numbers;
231
+ if (rest.length === 1 && rest[0] === '--all')
232
+ numbers = completedSessions(cwd);
233
+ else {
234
+ if (rest.length === 0 || rest.some((a) => !/^(?:[1-9]|1[0-3])$/.test(a))) {
235
+ error(USAGE, ERROR_REASON.USAGE);
236
+ return; // unreachable
237
+ }
238
+ numbers = [...new Set(rest.map(Number))];
239
+ }
240
+ const result = checkProjectSessions(cwd, numbers);
241
+ const payload = {
242
+ ok: result.ok,
243
+ sessions: result.sessions.map((s) => ({ session: s.session, file: s.file, required: s.required, filled: s.filled, empty: s.empty })),
244
+ };
245
+ if (!result.ok) {
246
+ const violations = result.sessions.flatMap((s) => s.violations);
247
+ 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);
249
+ return; // unreachable
250
+ }
251
+ output(payload, raw);
252
+ }
253
+ module.exports = {
254
+ NONE_LITERAL,
255
+ PARKED_LITERAL,
256
+ SESSIONS_DIR,
257
+ SESSION_CHECK_FAILED: ERROR_REASON.DISCOVERY_SESSION_CHECK_FAILED,
258
+ requiredSectionsOf,
259
+ checkSession,
260
+ checkProjectSession,
261
+ checkProjectSessions,
262
+ completedSessions,
263
+ cmdDiscoverySessionCheck,
264
+ };