@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.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/pp-nyquist-auditor.md +19 -14
- package/agents/pp-phase-researcher.md +10 -0
- package/agents/pp-plan-checker.md +59 -36
- package/agents/pp-planner.md +49 -53
- package/agents/pp-vault-reviewer.md +2 -0
- package/agents/pp-verifier.md +15 -60
- package/commands/pp-autonomous.md +2 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-plan-review-convergence.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +6 -3
- package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
- package/pareto-skills/pp-project-setup/SKILL.md +18 -343
- package/pp-core/bin/lib/capability-activation.cjs +77 -0
- package/pp-core/bin/lib/capability-registry.cjs +15 -15
- package/pp-core/bin/lib/capability-state.cjs +8 -18
- package/pp-core/bin/lib/ceremony.cjs +1342 -0
- package/pp-core/bin/lib/check-command-router.cjs +62 -1
- package/pp-core/bin/lib/commands.cjs +29 -3
- package/pp-core/bin/lib/config.cjs +5 -0
- package/pp-core/bin/lib/discovery-access.cjs +276 -0
- package/pp-core/bin/lib/discovery-charter.cjs +664 -0
- package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
- package/pp-core/bin/lib/discovery-entities.cjs +323 -0
- package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
- package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
- package/pp-core/bin/lib/discovery-migration.cjs +246 -0
- package/pp-core/bin/lib/discovery-phases.cjs +233 -0
- package/pp-core/bin/lib/discovery-processes.cjs +355 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
- package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
- package/pp-core/bin/lib/discovery-rules.cjs +216 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1 -1
- package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
- package/pp-core/bin/lib/discovery-stories.cjs +273 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
- package/pp-core/bin/lib/discovery-table.cjs +55 -0
- package/pp-core/bin/lib/init.cjs +50 -1
- package/pp-core/bin/lib/io.cjs +24 -0
- package/pp-core/bin/lib/loop-resolver.cjs +26 -12
- package/pp-core/bin/lib/phase.cjs +39 -3
- package/pp-core/bin/lib/plan-sizing.cjs +346 -0
- package/pp-core/bin/lib/test-substance.cjs +3190 -0
- package/pp-core/bin/lib/timeline.cjs +605 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
- package/pp-core/bin/pp-tools.cjs +214 -5
- package/pp-core/bin/shared/ceremony-gates.json +17 -0
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/ceremony.md +81 -0
- package/pp-core/references/discovery-access.md +112 -0
- package/pp-core/references/discovery-build-consumption.md +256 -0
- package/pp-core/references/discovery-charter.md +127 -0
- package/pp-core/references/discovery-data-model.md +145 -0
- package/pp-core/references/discovery-id-conventions.md +50 -14
- package/pp-core/references/discovery-integrations.md +151 -0
- package/pp-core/references/discovery-processes.md +190 -0
- package/pp-core/references/discovery-rebaseline.md +2 -2
- package/pp-core/references/discovery-research-track.md +5 -6
- package/pp-core/references/discovery-session-output.md +70 -0
- package/pp-core/references/discovery-standards.md +107 -0
- package/pp-core/references/discovery-traceability.md +2 -2
- package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
- package/pp-core/references/phase-timeline.md +107 -0
- package/pp-core/references/planner-antipatterns.md +32 -0
- package/pp-core/references/planner-discovery-extract.md +35 -0
- package/pp-core/references/planner-guidance.md +17 -14
- package/pp-core/references/planner-revision.md +1 -1
- package/pp-core/references/planning-config.md +3 -2
- package/pp-core/references/test-substance.md +324 -0
- package/pp-core/references/verifier-discovery-conformance.md +36 -0
- package/pp-core/references/verifier-stub-patterns.md +51 -0
- package/pp-core/templates/VALIDATION.md +1 -0
- package/pp-core/templates/phase-prompt.md +19 -7
- package/pp-core/templates/project-charter.md +161 -100
- package/pp-core/templates/project.md +3 -3
- package/pp-core/templates/requirements.md +18 -0
- package/pp-core/templates/roadmap.md +1 -1
- package/pp-core/templates/verification-report.md +17 -1
- package/pp-core/workflows/audit-milestone.md +4 -1
- package/pp-core/workflows/autonomous.md +11 -8
- package/pp-core/workflows/code-review.md +15 -6
- package/pp-core/workflows/discovery-sprint/charter.md +114 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
- package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
- package/pp-core/workflows/discovery-sprint.md +12 -15
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
- package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
- package/pp-core/workflows/discovery-to-build.md +70 -34
- package/pp-core/workflows/discuss-phase/discovery.md +83 -0
- package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
- package/pp-core/workflows/discuss-phase.md +10 -7
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
- package/pp-core/workflows/execute-phase.md +14 -3
- package/pp-core/workflows/new-project.md +3 -3
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
- package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
- package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
- package/pp-core/workflows/plan-phase.md +62 -124
- package/pp-core/workflows/plan-review-convergence.md +6 -3
- package/pp-core/workflows/ship.md +2 -2
- package/pp-core/workflows/stats.md +19 -4
- package/pp-core/workflows/validate-phase.md +19 -4
- package/pp-core/workflows/vault-review.md +5 -0
- package/pp-core/workflows/verify-phase.md +1 -1
- package/pp-core/workflows/verify-work.md +1 -1
- package/scripts/gen-capability-registry.cjs +43 -45
- package/scripts/run-tests.cjs +54 -30
- 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
|
+
};
|