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