@pareto4/pp-core 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/agents/pp-nyquist-auditor.md +19 -14
  3. package/agents/pp-phase-researcher.md +10 -0
  4. package/agents/pp-plan-checker.md +59 -36
  5. package/agents/pp-planner.md +49 -53
  6. package/agents/pp-vault-reviewer.md +2 -0
  7. package/agents/pp-verifier.md +15 -60
  8. package/commands/pp-autonomous.md +2 -1
  9. package/commands/pp-discovery.md +3 -2
  10. package/commands/pp-plan-phase.md +2 -1
  11. package/commands/pp-plan-review-convergence.md +2 -1
  12. package/gemini-extension.json +1 -1
  13. package/package.json +6 -3
  14. package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
  15. package/pareto-skills/pp-project-setup/SKILL.md +18 -343
  16. package/pp-core/bin/lib/capability-activation.cjs +77 -0
  17. package/pp-core/bin/lib/capability-registry.cjs +15 -15
  18. package/pp-core/bin/lib/capability-state.cjs +8 -18
  19. package/pp-core/bin/lib/ceremony.cjs +1342 -0
  20. package/pp-core/bin/lib/check-command-router.cjs +62 -1
  21. package/pp-core/bin/lib/commands.cjs +29 -3
  22. package/pp-core/bin/lib/config.cjs +5 -0
  23. package/pp-core/bin/lib/discovery-access.cjs +276 -0
  24. package/pp-core/bin/lib/discovery-charter.cjs +664 -0
  25. package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
  26. package/pp-core/bin/lib/discovery-entities.cjs +323 -0
  27. package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
  28. package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
  29. package/pp-core/bin/lib/discovery-migration.cjs +246 -0
  30. package/pp-core/bin/lib/discovery-phases.cjs +233 -0
  31. package/pp-core/bin/lib/discovery-processes.cjs +355 -0
  32. package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
  33. package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
  34. package/pp-core/bin/lib/discovery-rules.cjs +216 -0
  35. package/pp-core/bin/lib/discovery-screens.cjs +1 -1
  36. package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
  37. package/pp-core/bin/lib/discovery-stories.cjs +273 -0
  38. package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
  39. package/pp-core/bin/lib/discovery-table.cjs +55 -0
  40. package/pp-core/bin/lib/init.cjs +50 -1
  41. package/pp-core/bin/lib/io.cjs +24 -0
  42. package/pp-core/bin/lib/loop-resolver.cjs +26 -12
  43. package/pp-core/bin/lib/phase.cjs +39 -3
  44. package/pp-core/bin/lib/plan-sizing.cjs +346 -0
  45. package/pp-core/bin/lib/test-substance.cjs +3190 -0
  46. package/pp-core/bin/lib/timeline.cjs +605 -0
  47. package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
  48. package/pp-core/bin/pp-tools.cjs +214 -5
  49. package/pp-core/bin/shared/ceremony-gates.json +17 -0
  50. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  51. package/pp-core/references/ceremony.md +81 -0
  52. package/pp-core/references/discovery-access.md +112 -0
  53. package/pp-core/references/discovery-build-consumption.md +256 -0
  54. package/pp-core/references/discovery-charter.md +127 -0
  55. package/pp-core/references/discovery-data-model.md +145 -0
  56. package/pp-core/references/discovery-id-conventions.md +50 -14
  57. package/pp-core/references/discovery-integrations.md +151 -0
  58. package/pp-core/references/discovery-processes.md +190 -0
  59. package/pp-core/references/discovery-rebaseline.md +2 -2
  60. package/pp-core/references/discovery-research-track.md +5 -6
  61. package/pp-core/references/discovery-session-output.md +70 -0
  62. package/pp-core/references/discovery-standards.md +107 -0
  63. package/pp-core/references/discovery-traceability.md +2 -2
  64. package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
  65. package/pp-core/references/phase-timeline.md +107 -0
  66. package/pp-core/references/planner-antipatterns.md +32 -0
  67. package/pp-core/references/planner-discovery-extract.md +35 -0
  68. package/pp-core/references/planner-guidance.md +17 -14
  69. package/pp-core/references/planner-revision.md +1 -1
  70. package/pp-core/references/planning-config.md +3 -2
  71. package/pp-core/references/test-substance.md +324 -0
  72. package/pp-core/references/verifier-discovery-conformance.md +36 -0
  73. package/pp-core/references/verifier-stub-patterns.md +51 -0
  74. package/pp-core/templates/VALIDATION.md +1 -0
  75. package/pp-core/templates/phase-prompt.md +19 -7
  76. package/pp-core/templates/project-charter.md +161 -100
  77. package/pp-core/templates/project.md +3 -3
  78. package/pp-core/templates/requirements.md +18 -0
  79. package/pp-core/templates/roadmap.md +1 -1
  80. package/pp-core/templates/verification-report.md +17 -1
  81. package/pp-core/workflows/audit-milestone.md +4 -1
  82. package/pp-core/workflows/autonomous.md +11 -8
  83. package/pp-core/workflows/code-review.md +15 -6
  84. package/pp-core/workflows/discovery-sprint/charter.md +114 -0
  85. package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
  86. package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
  87. package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
  88. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
  89. package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
  90. package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
  91. package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
  92. package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
  93. package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
  94. package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
  95. package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
  96. package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
  97. package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
  98. package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
  99. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
  100. package/pp-core/workflows/discovery-sprint.md +12 -15
  101. package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
  102. package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
  103. package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
  104. package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
  105. package/pp-core/workflows/discovery-to-build.md +70 -34
  106. package/pp-core/workflows/discuss-phase/discovery.md +83 -0
  107. package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
  108. package/pp-core/workflows/discuss-phase.md +10 -7
  109. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
  110. package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
  111. package/pp-core/workflows/execute-phase.md +14 -3
  112. package/pp-core/workflows/new-project.md +3 -3
  113. package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
  114. package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
  115. package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
  116. package/pp-core/workflows/plan-phase.md +62 -124
  117. package/pp-core/workflows/plan-review-convergence.md +6 -3
  118. package/pp-core/workflows/ship.md +2 -2
  119. package/pp-core/workflows/stats.md +19 -4
  120. package/pp-core/workflows/validate-phase.md +19 -4
  121. package/pp-core/workflows/vault-review.md +5 -0
  122. package/pp-core/workflows/verify-phase.md +1 -1
  123. package/pp-core/workflows/verify-work.md +1 -1
  124. package/scripts/gen-capability-registry.cjs +43 -45
  125. package/scripts/run-tests.cjs +54 -30
  126. package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery integrations check (Discovery Sprint improvement plan, Batch 3).
4
+ *
5
+ * `.planning/discovery/_integrations.md` is the register of interfaces with
6
+ * other systems: `IF-nn` rows seeded by Session 5 (system, direction,
7
+ * pattern, owner, requirements) and completed by Session 8 (protocol, format,
8
+ * idempotency), a `## Contract` table of fields per interface, and an
9
+ * `## Errors` table of failure handling per interface. The step matrices of
10
+ * `_processes.md` name an interface by its `IF-nn` in their `Integration`
11
+ * cell, which is how the build's planner finds the interfaces a phase
12
+ * touches. This module checks the structure:
13
+ *
14
+ * - the three sections exist with the template's columns;
15
+ * - `IF-` IDs are well-formed, unique, contiguous;
16
+ * - `Direction` and a contract field's `Required` come from closed
17
+ * vocabularies; the cells the sessions must fill are non-empty;
18
+ * - every contract and error row names an interface of `## Interfaces`;
19
+ * - every `IF-nn` a step matrix cites exists (`unknown_interface` located
20
+ * in `_processes.md`);
21
+ * - every cited requirement exists in `_requirements.md` when it is on file
22
+ * and parseable;
23
+ * - `--require-cited`: an interface whose `Requirements` is still `—`, or a
24
+ * step whose `Integration` cell names a system without an `IF-nn`, fails
25
+ * on its own exit identity, so the handoff can hard-block on it while
26
+ * Sessions 8 and 13 only report.
27
+ *
28
+ * Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at the
29
+ * handoff. Absent artifact → exit 0.
30
+ */
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ const node_fs_1 = __importDefault(require("node:fs"));
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const io = require("./io.cjs");
38
+ const { output, error, ERROR_REASON } = io;
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const table = require("./discovery-table.cjs");
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const rollup = require("./discovery-rollup.cjs");
43
+ // ─── Constants ────────────────────────────────────────────────────────────────
44
+ const DISCOVERY_DIR = '.planning/discovery';
45
+ const INTEGRATIONS_FILE = `${DISCOVERY_DIR}/_integrations.md`;
46
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
47
+ const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
48
+ const INTERFACES_SECTION = 'Interfaces';
49
+ const CONTRACT_SECTION = 'Contract';
50
+ const ERRORS_SECTION = 'Errors';
51
+ const INTERFACES_HEADER = Object.freeze(['ID', 'System', 'Direction', 'Pattern', 'Protocol', 'Format', 'Idempotency', 'Owner', 'Requirements']);
52
+ const CONTRACT_HEADER = Object.freeze(['Interface', 'Field', 'Type', 'Required', 'Mapping', 'Validation']);
53
+ const ERRORS_HEADER = Object.freeze(['Interface', 'Failure', 'Handling', 'Retry', 'Dead letter', 'Alert']);
54
+ const IF_RE = /^IF-(\d{2})$/;
55
+ const IF_TOKEN_RE = /\bIF-\d{2}\b/g;
56
+ const DIRECTIONS = Object.freeze(['inbound', 'outbound', 'bidirectional']);
57
+ const REQUIRED = Object.freeze(['yes', 'no']);
58
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
59
+ const PENDING = '—';
60
+ const STEPS_INTEGRATION_COLUMN = 'Integration';
61
+ const USAGE = 'Usage: pp-tools discovery integrations-check [--require-cited]';
62
+ function readIfPresent(abs) {
63
+ if (!node_fs_1.default.existsSync(abs))
64
+ return { kind: 'absent' };
65
+ try {
66
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
67
+ }
68
+ catch {
69
+ return { kind: 'unreadable' };
70
+ }
71
+ }
72
+ function sameHeader(actual, expected) {
73
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
74
+ }
75
+ function splitList(cell) {
76
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
77
+ }
78
+ /** The `Integration` cells of every step table in `_processes.md` § Steps, by header name. */
79
+ function stepIntegrationCells(content) {
80
+ const out = [];
81
+ const steps = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === 'Steps');
82
+ if (!steps)
83
+ return out;
84
+ for (const t of table.parseTables(steps.body)) {
85
+ const col = t.header.findIndex((h) => h.trim().toLowerCase() === STEPS_INTEGRATION_COLUMN.toLowerCase());
86
+ if (col === -1)
87
+ continue;
88
+ for (const r of t.rows)
89
+ out.push({ line: r.line, cell: (r.cells[col] ?? '').trim() });
90
+ }
91
+ return out;
92
+ }
93
+ function sectionTable(content, heading, expected, violations) {
94
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
95
+ if (!section) {
96
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'missing_section', id: heading });
97
+ return null;
98
+ }
99
+ const tables = table.parseTables(section.body);
100
+ if (tables.length === 0) {
101
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'missing_section', line: section.line, id: heading });
102
+ return null;
103
+ }
104
+ if (!sameHeader(tables[0].header, expected)) {
105
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'bad_table_shape', line: tables[0].line, id: heading });
106
+ return null;
107
+ }
108
+ return tables[0];
109
+ }
110
+ // ─── Check ────────────────────────────────────────────────────────────────────
111
+ function checkIntegrations(content, sources, requireCited = false) {
112
+ const violations = [];
113
+ const interfaces = sectionTable(content, INTERFACES_SECTION, INTERFACES_HEADER, violations);
114
+ const contract = sectionTable(content, CONTRACT_SECTION, CONTRACT_HEADER, violations);
115
+ const errors = sectionTable(content, ERRORS_SECTION, ERRORS_HEADER, violations);
116
+ const ids = new Set();
117
+ const seqs = [];
118
+ let uncited = 0;
119
+ if (interfaces) {
120
+ for (const row of interfaces.rows) {
121
+ const [id, system, direction, pattern, protocol, format, idempotency, owner, requirements] = row.cells.map((c) => c.trim());
122
+ const line = row.line;
123
+ const m = IF_RE.exec(id ?? '');
124
+ if (!m)
125
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'bad_id', line });
126
+ else if (ids.has(id))
127
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'duplicate_id', line, id });
128
+ else {
129
+ ids.add(id);
130
+ seqs.push(Number(m[1]));
131
+ }
132
+ const required = [['System', system], ['Pattern', pattern], ['Protocol', protocol], ['Format', format], ['Idempotency', idempotency], ['Owner', owner]];
133
+ for (const [name, cell] of required)
134
+ if ((cell ?? '') === '')
135
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'empty_cell', line, id: name });
136
+ if (!DIRECTIONS.includes(direction ?? ''))
137
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'bad_direction', line });
138
+ const reqCell = requirements ?? '';
139
+ if (reqCell === '' || reqCell === PENDING) {
140
+ uncited += 1;
141
+ if (requireCited)
142
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'uncited_interface', line, id: m ? id : undefined });
143
+ }
144
+ else {
145
+ for (const r of splitList(reqCell)) {
146
+ if (!REQUIREMENT_ID_RE.test(r))
147
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'unknown_requirement_id', line, id: r.slice(0, 40) });
148
+ else if (sources.requirementIds instanceof Set && !sources.requirementIds.has(r))
149
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'unknown_requirement_id', line, id: r });
150
+ }
151
+ }
152
+ }
153
+ const max = table.boundedSequenceMax(seqs);
154
+ if (max !== null) {
155
+ const have = new Set(seqs);
156
+ for (let n = 1; n <= max; n += 1) {
157
+ if (!have.has(n))
158
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'sequence_gap', id: `IF-${String(n).padStart(2, '0')}` });
159
+ }
160
+ }
161
+ }
162
+ const interfaceRef = (ref, line) => {
163
+ if (!IF_RE.test(ref))
164
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'unknown_interface', line, id: ref.slice(0, 40) });
165
+ else if (interfaces && !ids.has(ref))
166
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'unknown_interface', line, id: ref });
167
+ };
168
+ if (contract) {
169
+ for (const row of contract.rows) {
170
+ const [ref, field, type, required] = row.cells.map((c) => c.trim());
171
+ interfaceRef(ref ?? '', row.line);
172
+ if ((field ?? '') === '')
173
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'empty_cell', line: row.line, id: 'Field' });
174
+ if ((type ?? '') === '')
175
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'empty_cell', line: row.line, id: 'Type' });
176
+ if (!REQUIRED.includes(required ?? ''))
177
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'bad_required', line: row.line });
178
+ }
179
+ }
180
+ if (errors) {
181
+ for (const row of errors.rows) {
182
+ const [ref, failure, handling] = row.cells.map((c) => c.trim());
183
+ interfaceRef(ref ?? '', row.line);
184
+ if ((failure ?? '') === '')
185
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'empty_cell', line: row.line, id: 'Failure' });
186
+ if ((handling ?? '') === '')
187
+ violations.push({ artifact: INTEGRATIONS_FILE, reason: 'empty_cell', line: row.line, id: 'Handling' });
188
+ }
189
+ }
190
+ if (sources.stepIntegrations !== null && interfaces) {
191
+ for (const { line, cell } of sources.stepIntegrations) {
192
+ if (cell === '' || cell === PENDING)
193
+ continue;
194
+ const tokens = cell.match(IF_TOKEN_RE) ?? [];
195
+ for (const t of tokens)
196
+ if (!ids.has(t))
197
+ violations.push({ artifact: PROCESSES_FILE, reason: 'unknown_interface', line, id: t });
198
+ if (tokens.length === 0 && requireCited)
199
+ violations.push({ artifact: PROCESSES_FILE, reason: 'step_integration_unlinked', line });
200
+ }
201
+ }
202
+ if (sources.requirementIds === 'unparseable')
203
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
204
+ return {
205
+ ok: violations.length === 0,
206
+ interfaces: ids.size,
207
+ contract_rows: contract ? contract.rows.length : 0,
208
+ error_rows: errors ? errors.rows.length : 0,
209
+ uncited,
210
+ violations,
211
+ };
212
+ }
213
+ function checkProjectIntegrations(cwd, requireCited = false) {
214
+ const read = readIfPresent(node_path_1.default.join(cwd, INTEGRATIONS_FILE));
215
+ if (read.kind === 'absent')
216
+ return null;
217
+ if (read.kind === 'unreadable') {
218
+ return { ok: false, file: INTEGRATIONS_FILE, interfaces: 0, contract_rows: 0, error_rows: 0, uncited: 0, violations: [{ artifact: INTEGRATIONS_FILE, reason: 'artifact_unreadable' }] };
219
+ }
220
+ const extra = [];
221
+ const sources = { requirementIds: null, stepIntegrations: null };
222
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
223
+ if (req.kind === 'unreadable')
224
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
225
+ if (req.kind === 'ok') {
226
+ const parsed = rollup.parseRequirementsArtifact(req.content);
227
+ sources.requirementIds = parsed.ok ? new Set(parsed.rows.map((r) => r.id)) : 'unparseable';
228
+ }
229
+ const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
230
+ if (proc.kind === 'unreadable')
231
+ extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
232
+ if (proc.kind === 'ok')
233
+ sources.stepIntegrations = stepIntegrationCells(proc.content);
234
+ const result = checkIntegrations(read.content, sources, requireCited);
235
+ result.violations.push(...extra);
236
+ result.ok = result.violations.length === 0;
237
+ return { file: INTEGRATIONS_FILE, ...result };
238
+ }
239
+ function formatViolation(v) {
240
+ const where = v.line !== undefined ? `:${v.line}` : '';
241
+ const what = v.id !== undefined ? ` (${v.id})` : '';
242
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
243
+ }
244
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
245
+ const STRICT_ONLY = new Set(['uncited_interface', 'step_integration_unlinked']);
246
+ /**
247
+ * `pp-tools discovery integrations-check [--require-cited]` (dotted
248
+ * `query discovery.integrations-check`). Absent artifact → exit 0,
249
+ * `present: false`. Structural faults exit via
250
+ * `DISCOVERY_INTEGRATIONS_CHECK_FAILED`; when every fault was raised by
251
+ * `--require-cited`, the identity is `DISCOVERY_INTEGRATIONS_UNCITED`.
252
+ */
253
+ function cmdDiscoveryIntegrationsCheck(cwd, args, raw) {
254
+ const rest = args.slice(2);
255
+ const requireCited = rest.includes('--require-cited');
256
+ if (rest.some((a) => a !== '--require-cited')) {
257
+ error(USAGE, ERROR_REASON.USAGE);
258
+ return; // unreachable
259
+ }
260
+ const result = checkProjectIntegrations(cwd, requireCited);
261
+ if (result === null) {
262
+ output({ ok: true, present: false, file: INTEGRATIONS_FILE }, raw);
263
+ return;
264
+ }
265
+ const payload = { ok: result.ok, present: true, file: result.file, interfaces: result.interfaces, contract_rows: result.contract_rows, error_rows: result.error_rows, uncited: result.uncited };
266
+ if (!result.ok) {
267
+ output({ ...payload, violations: result.violations.length }, raw);
268
+ const onlyStrict = result.violations.every((v) => STRICT_ONLY.has(v.reason));
269
+ error(`discovery integrations-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), onlyStrict ? ERROR_REASON.DISCOVERY_INTEGRATIONS_UNCITED : ERROR_REASON.DISCOVERY_INTEGRATIONS_CHECK_FAILED);
270
+ return; // unreachable
271
+ }
272
+ output(payload, raw);
273
+ }
274
+ module.exports = {
275
+ INTEGRATIONS_FILE,
276
+ INTERFACES_HEADER,
277
+ CONTRACT_HEADER,
278
+ ERRORS_HEADER,
279
+ DIRECTIONS,
280
+ INTEGRATIONS_CHECK_FAILED: ERROR_REASON.DISCOVERY_INTEGRATIONS_CHECK_FAILED,
281
+ INTEGRATIONS_UNCITED: ERROR_REASON.DISCOVERY_INTEGRATIONS_UNCITED,
282
+ stepIntegrationCells,
283
+ checkIntegrations,
284
+ checkProjectIntegrations,
285
+ cmdDiscoveryIntegrationsCheck,
286
+ };
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * Discovery migration check (Discovery Sprint improvement plan, Batch 3).
4
+ *
5
+ * `.planning/discovery/_migration.md` is the register of data the new
6
+ * solution takes over from existing systems: `MG-nn` rows naming the source
7
+ * system (a system of Session 2's Systems Landscape), the entities moved
8
+ * (rows of `_entities.md`), the volume, the mapping, the cut-over strategy,
9
+ * the reconciliation that proves the move, and the requirements the row
10
+ * realises. Session 2 topic 7 seeds it (what has to move, how much); Session
11
+ * 8 topic 6 completes it (mapping, cut-over, reconciliation) and back-fills
12
+ * the requirements. The build's migration phase reads it by requirement ID.
13
+ * This module checks the structure:
14
+ *
15
+ * - the `## Migration` table exists with the template's columns;
16
+ * - `MG-` IDs are well-formed, unique, contiguous;
17
+ * - the source system is one Session 2's Systems Landscape lists, when that
18
+ * table is on file with rows;
19
+ * - every entity is a PascalCase name present in `_entities.md` when it is
20
+ * on file;
21
+ * - the cells the sessions must fill are non-empty (`—` is a pending cell);
22
+ * - every cited requirement exists in `_requirements.md` when it is on file
23
+ * and parseable;
24
+ * - `--require-cited`: a row whose `Requirements` is still `—` fails on its
25
+ * own exit identity, so the handoff can hard-block on it while Sessions 8
26
+ * and 13 only report.
27
+ *
28
+ * Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at the
29
+ * handoff. Absent artifact → exit 0 — most engagements migrate nothing.
30
+ */
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ const node_fs_1 = __importDefault(require("node:fs"));
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
37
+ const io = require("./io.cjs");
38
+ const { output, error, ERROR_REASON } = io;
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ const table = require("./discovery-table.cjs");
41
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
42
+ const rollup = require("./discovery-rollup.cjs");
43
+ // ─── Constants ────────────────────────────────────────────────────────────────
44
+ const DISCOVERY_DIR = '.planning/discovery';
45
+ const MIGRATION_FILE = `${DISCOVERY_DIR}/_migration.md`;
46
+ const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
47
+ const ENTITIES_FILE = `${DISCOVERY_DIR}/_entities.md`;
48
+ const SESSION_02_FILE = `${DISCOVERY_DIR}/session-02.md`;
49
+ const MIGRATION_SECTION = 'Migration';
50
+ const MIGRATION_HEADER = Object.freeze(['ID', 'Source system', 'Entity', 'Volume', 'Mapping', 'Cut-over', 'Reconciliation', 'Requirements']);
51
+ const MG_RE = /^MG-(\d{2})$/;
52
+ const ENTITY_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;
53
+ const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
54
+ const PENDING = '—';
55
+ const SYSTEMS_LANDSCAPE_LABEL = 'Systems Landscape';
56
+ const USAGE = 'Usage: pp-tools discovery migration-check [--require-cited]';
57
+ function readIfPresent(abs) {
58
+ if (!node_fs_1.default.existsSync(abs))
59
+ return { kind: 'absent' };
60
+ try {
61
+ return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
62
+ }
63
+ catch {
64
+ return { kind: 'unreadable' };
65
+ }
66
+ }
67
+ function sameHeader(actual, expected) {
68
+ return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
69
+ }
70
+ function splitList(cell) {
71
+ return cell.split(',').map((t) => t.trim()).filter(Boolean);
72
+ }
73
+ function firstColumnOf(content, heading) {
74
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
75
+ if (!section)
76
+ return new Set();
77
+ const tables = table.parseTables(section.body);
78
+ if (tables.length === 0)
79
+ return new Set();
80
+ return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
81
+ }
82
+ /** System names: the first column of Session 2's Systems Landscape table; null when absent or empty. */
83
+ function systemsFrom(content) {
84
+ const lines = content.split(/\r?\n/);
85
+ const norm = (s) => s.replace(/^#+\s*/, '').replace(/\*\*/g, '').trim();
86
+ const at = lines.findIndex((l) => norm(l) === SYSTEMS_LANDSCAPE_LABEL);
87
+ if (at === -1)
88
+ return null;
89
+ const body = [];
90
+ for (let i = at + 1; i < lines.length; i += 1) {
91
+ if (/^#{1,3}\s/.test(lines[i]) || /^\*\*[^*]+\*\*$/.test(lines[i].trim()))
92
+ break;
93
+ body.push({ n: i + 1, text: lines[i] });
94
+ }
95
+ const tables = table.parseTables(body);
96
+ if (tables.length === 0)
97
+ return null;
98
+ const out = new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter((c) => c !== ''));
99
+ return out.size === 0 ? null : out;
100
+ }
101
+ // ─── Check ────────────────────────────────────────────────────────────────────
102
+ function checkMigration(content, sources, requireCited = false) {
103
+ const violations = [];
104
+ const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === MIGRATION_SECTION);
105
+ if (!section)
106
+ return { ok: false, rows: 0, uncited: 0, violations: [{ artifact: MIGRATION_FILE, reason: 'missing_section', id: MIGRATION_SECTION }] };
107
+ const tables = table.parseTables(section.body);
108
+ if (tables.length === 0)
109
+ return { ok: false, rows: 0, uncited: 0, violations: [{ artifact: MIGRATION_FILE, reason: 'missing_section', line: section.line, id: MIGRATION_SECTION }] };
110
+ if (!sameHeader(tables[0].header, MIGRATION_HEADER)) {
111
+ return { ok: false, rows: 0, uncited: 0, violations: [{ artifact: MIGRATION_FILE, reason: 'bad_table_shape', line: tables[0].line, id: MIGRATION_SECTION }] };
112
+ }
113
+ const ids = new Set();
114
+ const seqs = [];
115
+ let uncited = 0;
116
+ for (const row of tables[0].rows) {
117
+ const [id, system, entity, volume, mapping, cutOver, reconciliation, requirements] = row.cells.map((c) => c.trim());
118
+ const line = row.line;
119
+ const m = MG_RE.exec(id ?? '');
120
+ if (!m)
121
+ violations.push({ artifact: MIGRATION_FILE, reason: 'bad_id', line });
122
+ else if (ids.has(id))
123
+ violations.push({ artifact: MIGRATION_FILE, reason: 'duplicate_id', line, id });
124
+ else {
125
+ ids.add(id);
126
+ seqs.push(Number(m[1]));
127
+ }
128
+ if ((system ?? '') === '')
129
+ violations.push({ artifact: MIGRATION_FILE, reason: 'empty_cell', line, id: 'Source system' });
130
+ else if (sources.systems !== null && !sources.systems.has(system))
131
+ violations.push({ artifact: MIGRATION_FILE, reason: 'unknown_system', line, id: system.slice(0, 40) });
132
+ const names = splitList(entity ?? '');
133
+ if (names.length === 0)
134
+ violations.push({ artifact: MIGRATION_FILE, reason: 'bad_entity', line });
135
+ for (const n of names) {
136
+ if (!ENTITY_NAME_RE.test(n))
137
+ violations.push({ artifact: MIGRATION_FILE, reason: 'bad_entity', line });
138
+ else if (sources.entities !== null && !sources.entities.has(n))
139
+ violations.push({ artifact: MIGRATION_FILE, reason: 'unknown_entity', line, id: n });
140
+ }
141
+ const filled = [['Volume', volume], ['Mapping', mapping], ['Cut-over', cutOver], ['Reconciliation', reconciliation]];
142
+ for (const [name, cell] of filled)
143
+ if ((cell ?? '') === '')
144
+ violations.push({ artifact: MIGRATION_FILE, reason: 'empty_cell', line, id: name });
145
+ const reqCell = requirements ?? '';
146
+ if (reqCell === '' || reqCell === PENDING) {
147
+ uncited += 1;
148
+ if (requireCited)
149
+ violations.push({ artifact: MIGRATION_FILE, reason: 'uncited_migration', line, id: m ? id : undefined });
150
+ }
151
+ else {
152
+ for (const r of splitList(reqCell)) {
153
+ if (!REQUIREMENT_ID_RE.test(r))
154
+ violations.push({ artifact: MIGRATION_FILE, reason: 'unknown_requirement_id', line, id: r.slice(0, 40) });
155
+ else if (sources.requirementIds instanceof Set && !sources.requirementIds.has(r))
156
+ violations.push({ artifact: MIGRATION_FILE, reason: 'unknown_requirement_id', line, id: r });
157
+ }
158
+ }
159
+ }
160
+ const max = table.boundedSequenceMax(seqs);
161
+ if (max !== null) {
162
+ const have = new Set(seqs);
163
+ for (let n = 1; n <= max; n += 1) {
164
+ if (!have.has(n))
165
+ violations.push({ artifact: MIGRATION_FILE, reason: 'sequence_gap', id: `MG-${String(n).padStart(2, '0')}` });
166
+ }
167
+ }
168
+ if (sources.requirementIds === 'unparseable')
169
+ violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
170
+ return { ok: violations.length === 0, rows: ids.size, uncited, violations };
171
+ }
172
+ function checkProjectMigration(cwd, requireCited = false) {
173
+ const read = readIfPresent(node_path_1.default.join(cwd, MIGRATION_FILE));
174
+ if (read.kind === 'absent')
175
+ return null;
176
+ if (read.kind === 'unreadable')
177
+ return { ok: false, file: MIGRATION_FILE, rows: 0, uncited: 0, violations: [{ artifact: MIGRATION_FILE, reason: 'artifact_unreadable' }] };
178
+ const extra = [];
179
+ const sources = { requirementIds: null, entities: null, systems: null };
180
+ const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
181
+ if (req.kind === 'unreadable')
182
+ extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
183
+ if (req.kind === 'ok') {
184
+ const parsed = rollup.parseRequirementsArtifact(req.content);
185
+ sources.requirementIds = parsed.ok ? new Set(parsed.rows.map((r) => r.id)) : 'unparseable';
186
+ }
187
+ const ent = readIfPresent(node_path_1.default.join(cwd, ENTITIES_FILE));
188
+ if (ent.kind === 'unreadable')
189
+ extra.push({ artifact: ENTITIES_FILE, reason: 'artifact_unreadable' });
190
+ if (ent.kind === 'ok')
191
+ sources.entities = firstColumnOf(ent.content, 'Entities');
192
+ const s2 = readIfPresent(node_path_1.default.join(cwd, SESSION_02_FILE));
193
+ if (s2.kind === 'unreadable')
194
+ extra.push({ artifact: SESSION_02_FILE, reason: 'artifact_unreadable' });
195
+ if (s2.kind === 'ok')
196
+ sources.systems = systemsFrom(s2.content);
197
+ const result = checkMigration(read.content, sources, requireCited);
198
+ result.violations.push(...extra);
199
+ result.ok = result.violations.length === 0;
200
+ return { file: MIGRATION_FILE, ...result };
201
+ }
202
+ function formatViolation(v) {
203
+ const where = v.line !== undefined ? `:${v.line}` : '';
204
+ const what = v.id !== undefined ? ` (${v.id})` : '';
205
+ return ` - ${v.artifact}${where}: ${v.reason}${what}`;
206
+ }
207
+ // ─── CLI adapter ──────────────────────────────────────────────────────────────
208
+ /**
209
+ * `pp-tools discovery migration-check [--require-cited]` (dotted
210
+ * `query discovery.migration-check`). Absent artifact → exit 0,
211
+ * `present: false`. Structural faults exit via
212
+ * `DISCOVERY_MIGRATION_CHECK_FAILED`; when every fault is an
213
+ * `uncited_migration` raised by `--require-cited`, the identity is
214
+ * `DISCOVERY_MIGRATION_UNCITED`.
215
+ */
216
+ function cmdDiscoveryMigrationCheck(cwd, args, raw) {
217
+ const rest = args.slice(2);
218
+ const requireCited = rest.includes('--require-cited');
219
+ if (rest.some((a) => a !== '--require-cited')) {
220
+ error(USAGE, ERROR_REASON.USAGE);
221
+ return; // unreachable
222
+ }
223
+ const result = checkProjectMigration(cwd, requireCited);
224
+ if (result === null) {
225
+ output({ ok: true, present: false, file: MIGRATION_FILE }, raw);
226
+ return;
227
+ }
228
+ const payload = { ok: result.ok, present: true, file: result.file, rows: result.rows, uncited: result.uncited };
229
+ if (!result.ok) {
230
+ output({ ...payload, violations: result.violations.length }, raw);
231
+ const onlyUncited = result.violations.every((v) => v.reason === 'uncited_migration');
232
+ error(`discovery migration-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), onlyUncited ? ERROR_REASON.DISCOVERY_MIGRATION_UNCITED : ERROR_REASON.DISCOVERY_MIGRATION_CHECK_FAILED);
233
+ return; // unreachable
234
+ }
235
+ output(payload, raw);
236
+ }
237
+ module.exports = {
238
+ MIGRATION_FILE,
239
+ MIGRATION_HEADER,
240
+ MIGRATION_CHECK_FAILED: ERROR_REASON.DISCOVERY_MIGRATION_CHECK_FAILED,
241
+ MIGRATION_UNCITED: ERROR_REASON.DISCOVERY_MIGRATION_UNCITED,
242
+ systemsFrom,
243
+ checkMigration,
244
+ checkProjectMigration,
245
+ cmdDiscoveryMigrationCheck,
246
+ };