@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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `pp-tools discovery status` (Phase 42, DOOR-01; D-01..D-08).
|
|
4
|
+
*
|
|
5
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
6
|
+
* compiles to the gitignored `pp-core/bin/lib/discovery-status.cjs`; do NOT
|
|
7
|
+
* hand-edit the compiled output.
|
|
8
|
+
*
|
|
9
|
+
* Tells a project that is only in the Discovery Sprint from one that has a build,
|
|
10
|
+
* and shows the sprint's state in the person's own wording through the Phase 41
|
|
11
|
+
* catalog: a headline count, one row per session, where the sprint stopped and
|
|
12
|
+
* the next step with its command. The wording lives in `src/messages/discovery.cts`
|
|
13
|
+
* (`discovery.status*`); this module supplies every slot.
|
|
14
|
+
*
|
|
15
|
+
* Display only: the JSON keeps the canonical status words and the canonical
|
|
16
|
+
* commands, and a status word is relabelled for the person only at the last step,
|
|
17
|
+
* when a row or a line is written (D-13). `--raw` prints the same JSON for every
|
|
18
|
+
* audience (41 D-08); only the text view follows the audience.
|
|
19
|
+
*
|
|
20
|
+
* Everything echoed from the tracker or the charter (project name, session names,
|
|
21
|
+
* charter status) is cleaned for EVERY audience, developer included, because the
|
|
22
|
+
* developer path of the catalog does not sanitise (T-42-01).
|
|
23
|
+
*/
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.MAX_FILE_BYTES = exports.REGISTRY_SIZE = exports.MAX_ROWS = exports.TRACKER_FILE = void 0;
|
|
29
|
+
exports.renderStatus = renderStatus;
|
|
30
|
+
exports.computeStatus = computeStatus;
|
|
31
|
+
exports.cmdDiscoveryStatus = cmdDiscoveryStatus;
|
|
32
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
const messages_cjs_1 = require("./messages.cjs");
|
|
35
|
+
const message_labels_cjs_1 = require("./message-labels.cjs");
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
|
+
const io = require("./io.cjs");
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
39
|
+
const table = require("./discovery-table.cjs");
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
41
|
+
const inputCheck = require("./discovery-input-check.cjs");
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
const charterMod = require("./discovery-charter.cjs");
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
46
|
+
const { output, ERROR_REASON } = io;
|
|
47
|
+
const { SESSION_STATUS } = inputCheck;
|
|
48
|
+
const { CHARTER_FILE, SIGNED_VALUES } = charterMod;
|
|
49
|
+
/** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
|
|
50
|
+
function error(message, reason) {
|
|
51
|
+
return io.error(message, reason);
|
|
52
|
+
}
|
|
53
|
+
/** The progress sheet every discovery session start and prefill creates (D-01). */
|
|
54
|
+
exports.TRACKER_FILE = '.planning/discovery/_progress.md';
|
|
55
|
+
/** The tracker's table is examined up to the table-primitive span, never beyond (T-42-03). */
|
|
56
|
+
exports.MAX_ROWS = 99;
|
|
57
|
+
/**
|
|
58
|
+
* The Session Registry's size (discovery-sprint.md). Only sessions 1..13 count, a
|
|
59
|
+
* registry session with no row counts as Not Started, and the totals are always
|
|
60
|
+
* out of 13, as Flow 1 counts them (42-REVIEW WR-02).
|
|
61
|
+
*/
|
|
62
|
+
exports.REGISTRY_SIZE = 13;
|
|
63
|
+
/** A tracker or charter larger than this is never read (T-42-03). */
|
|
64
|
+
exports.MAX_FILE_BYTES = 1048576;
|
|
65
|
+
/** The columns a Session Progress table must have, by lower-cased header name (D-06). */
|
|
66
|
+
const REQUIRED_COLUMNS = ['#', 'session', 'status'];
|
|
67
|
+
const PLACEHOLDER_PROJECT = '[PROJECT_NAME]';
|
|
68
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
69
|
+
const SESSION_NUMBER_RE = /^\d{1,2}$/;
|
|
70
|
+
/** Every Unicode format character (bidi overrides and isolates, zero-width, BOM, tag characters). */
|
|
71
|
+
const FORMAT_CHARS_RE = /\p{Cf}/gu;
|
|
72
|
+
/**
|
|
73
|
+
* A value read from a file, made safe to print for every audience: format
|
|
74
|
+
* characters removed first, then control characters stripped, whitespace
|
|
75
|
+
* collapsed and the length capped (T-42-01).
|
|
76
|
+
*/
|
|
77
|
+
function cleanText(v) {
|
|
78
|
+
if (typeof v !== 'string' && typeof v !== 'number')
|
|
79
|
+
return '';
|
|
80
|
+
return (0, messages_cjs_1.sanitizeParam)(String(v).replace(FORMAT_CHARS_RE, ''));
|
|
81
|
+
}
|
|
82
|
+
/** A table cell: cleaned, and a `|` can never add a column (T-42-02). */
|
|
83
|
+
function cell(v) {
|
|
84
|
+
return cleanText(v).replace(/\|/g, '/');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The sprint view for one audience. Text only: every visible word comes from the
|
|
88
|
+
* catalog, every status word through `label()`, and business and engineer text
|
|
89
|
+
* ends with exactly one `Ref:` line (developer text has none).
|
|
90
|
+
*/
|
|
91
|
+
function renderStatus(status, audience) {
|
|
92
|
+
if (!status.in_discovery) {
|
|
93
|
+
return (0, messages_cjs_1.msg)('discovery.status.none', { file: exports.TRACKER_FILE, command: '/pp-discovery 1' }, { audience }) + '\n';
|
|
94
|
+
}
|
|
95
|
+
if (status.tracker === 'unreadable') {
|
|
96
|
+
return (0, messages_cjs_1.msg)('discovery.status.unreadable', { file: exports.TRACKER_FILE, command: '/pp-discovery' }, { audience }) + '\n';
|
|
97
|
+
}
|
|
98
|
+
const words = (code) => (0, messages_cjs_1.text)(code, {}, { audience });
|
|
99
|
+
const nameOf = (name) => cell(name) || words('discovery.status.session_unnamed');
|
|
100
|
+
const wording = (canonical) => (0, message_labels_cjs_1.label)('discovery.status', String(canonical), { audience });
|
|
101
|
+
const counts = status.counts ?? { total: 0, completed: 0, in_progress: 0, pre_filled: 0, not_started: 0 };
|
|
102
|
+
const sessions = status.sessions ?? [];
|
|
103
|
+
const out = [];
|
|
104
|
+
out.push((0, messages_cjs_1.text)('discovery.status', { project: cleanText(status.project) || words('discovery.status.project_unknown') }, { audience }));
|
|
105
|
+
out.push('');
|
|
106
|
+
out.push((0, messages_cjs_1.text)('discovery.status.counts', {
|
|
107
|
+
done: counts.completed,
|
|
108
|
+
total: counts.total,
|
|
109
|
+
in_progress: counts.in_progress,
|
|
110
|
+
pre_filled: counts.pre_filled,
|
|
111
|
+
not_started: counts.not_started,
|
|
112
|
+
}, { audience }));
|
|
113
|
+
out.push('');
|
|
114
|
+
out.push(words('discovery.status.header'));
|
|
115
|
+
for (const s of sessions) {
|
|
116
|
+
out.push(`| ${String(s.number)} | ${nameOf(s.name)} | ${wording(s.status)} |`);
|
|
117
|
+
}
|
|
118
|
+
out.push('');
|
|
119
|
+
const unknown = status.unknown_status ?? [];
|
|
120
|
+
if (unknown.length > 0) {
|
|
121
|
+
out.push((0, messages_cjs_1.text)('discovery.status.unknown_status', { sessions: unknown.join(', ') }, { audience }));
|
|
122
|
+
}
|
|
123
|
+
const stopped = status.stopped_at;
|
|
124
|
+
if (stopped !== undefined && stopped.basis === 'in_progress') {
|
|
125
|
+
out.push((0, messages_cjs_1.text)('discovery.status.stopped.in_progress', { session: stopped.session ?? '', name: nameOf(stopped.name), status: wording(stopped.status ?? '') }, { audience }));
|
|
126
|
+
}
|
|
127
|
+
else if (stopped !== undefined && stopped.basis === 'completed') {
|
|
128
|
+
out.push((0, messages_cjs_1.text)('discovery.status.stopped.completed', { session: stopped.session ?? '', name: nameOf(stopped.name) }, { audience }));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
out.push(words('discovery.status.stopped.none'));
|
|
132
|
+
}
|
|
133
|
+
const next = status.next;
|
|
134
|
+
if (next !== undefined) {
|
|
135
|
+
const named = (code) => {
|
|
136
|
+
const target = sessions.find((s) => s.number === next.session);
|
|
137
|
+
return (0, messages_cjs_1.text)(code, { session: next.session ?? '', name: nameOf(target === undefined ? '' : target.name), command: next.command }, { audience });
|
|
138
|
+
};
|
|
139
|
+
switch (next.kind) {
|
|
140
|
+
case 'resume':
|
|
141
|
+
out.push(named('discovery.status.next.resume'));
|
|
142
|
+
break;
|
|
143
|
+
case 'confirm':
|
|
144
|
+
out.push(named('discovery.status.next.confirm'));
|
|
145
|
+
break;
|
|
146
|
+
case 'start':
|
|
147
|
+
out.push(named('discovery.status.next.start'));
|
|
148
|
+
break;
|
|
149
|
+
case 'charter_draft':
|
|
150
|
+
out.push((0, messages_cjs_1.text)('discovery.status.next.charter_draft', { command: next.command }, { audience }));
|
|
151
|
+
break;
|
|
152
|
+
case 'charter_sign':
|
|
153
|
+
out.push((0, messages_cjs_1.text)('discovery.status.next.charter_sign', {
|
|
154
|
+
command: next.command,
|
|
155
|
+
charter_status: cleanText(status.charter?.status) || words('discovery.status.charter_no_status'),
|
|
156
|
+
}, { audience }));
|
|
157
|
+
break;
|
|
158
|
+
case 'to_build':
|
|
159
|
+
out.push((0, messages_cjs_1.text)('discovery.status.next.to_build', { command: next.command }, { audience }));
|
|
160
|
+
break;
|
|
161
|
+
case 'open_sprint':
|
|
162
|
+
// With a readable tracker: a session with an unrecognised status is the lowest open one (WR-03).
|
|
163
|
+
out.push((0, messages_cjs_1.text)('discovery.status.next.check_sheet', { command: next.command }, { audience }));
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (audience !== 'developer')
|
|
168
|
+
out.push((0, messages_cjs_1.refLine)('discovery.status'));
|
|
169
|
+
return out.join('\n') + '\n';
|
|
170
|
+
}
|
|
171
|
+
// ─── computeStatus ────────────────────────────────────────────────────────────
|
|
172
|
+
const CANONICAL_WORDS = [
|
|
173
|
+
SESSION_STATUS.NOT_STARTED,
|
|
174
|
+
SESSION_STATUS.PRE_FILLED,
|
|
175
|
+
SESSION_STATUS.IN_PROGRESS,
|
|
176
|
+
SESSION_STATUS.COMPLETED,
|
|
177
|
+
];
|
|
178
|
+
/**
|
|
179
|
+
* What is at `p`: a regular file within the size bound, something else, or nothing.
|
|
180
|
+
* The tracker is judged without following a link; the charter follows one, as
|
|
181
|
+
* `charter-check` does (42-REVIEW WR-01).
|
|
182
|
+
*/
|
|
183
|
+
function kindOf(p, followLink = false) {
|
|
184
|
+
try {
|
|
185
|
+
const st = followLink ? node_fs_1.default.statSync(p) : node_fs_1.default.lstatSync(p);
|
|
186
|
+
return st.isFile() && st.size <= exports.MAX_FILE_BYTES ? 'regular' : 'other';
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return 'absent';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function readText(p) {
|
|
193
|
+
try {
|
|
194
|
+
return node_fs_1.default.readFileSync(p, 'utf8');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** The sessions of a tracker's `## Session Progress` table, or null when the table cannot be read (D-06, D-08). */
|
|
201
|
+
function parseSessions(content) {
|
|
202
|
+
const lines = content.split(/\r?\n/);
|
|
203
|
+
const section = table.splitSections(lines, 2).find((s) => s.heading.trim().toLowerCase() === 'session progress');
|
|
204
|
+
if (section === undefined)
|
|
205
|
+
return null;
|
|
206
|
+
const parsed = table.parseTables(section.body)[0];
|
|
207
|
+
if (parsed === undefined)
|
|
208
|
+
return null;
|
|
209
|
+
const header = parsed.header.map((h) => h.trim().toLowerCase());
|
|
210
|
+
const [iNumber, iName, iStatus] = REQUIRED_COLUMNS.map((c) => header.indexOf(c));
|
|
211
|
+
if (iNumber < 0 || iName < 0 || iStatus < 0)
|
|
212
|
+
return null;
|
|
213
|
+
const iDone = header.indexOf('completed');
|
|
214
|
+
const byNumber = new Map();
|
|
215
|
+
const unknown = [];
|
|
216
|
+
for (const row of parsed.rows.slice(0, exports.MAX_ROWS)) {
|
|
217
|
+
const numberCell = (row.cells[iNumber] ?? '').trim();
|
|
218
|
+
if (!SESSION_NUMBER_RE.test(numberCell))
|
|
219
|
+
continue;
|
|
220
|
+
const number = Number(numberCell);
|
|
221
|
+
// Outside the registry: `/pp-discovery` rejects it, so it is never a session here (WR-02).
|
|
222
|
+
if (number < 1 || number > exports.REGISTRY_SIZE || byNumber.has(number))
|
|
223
|
+
continue;
|
|
224
|
+
const wordCell = (row.cells[iStatus] ?? '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
225
|
+
const canonical = CANONICAL_WORDS.find((w) => w.toLowerCase() === wordCell);
|
|
226
|
+
if (canonical === undefined)
|
|
227
|
+
unknown.push(number);
|
|
228
|
+
const doneCell = iDone >= 0 ? (row.cells[iDone] ?? '').trim() : '';
|
|
229
|
+
byNumber.set(number, {
|
|
230
|
+
number,
|
|
231
|
+
name: cleanText(row.cells[iName] ?? ''),
|
|
232
|
+
status: canonical ?? SESSION_STATUS.NOT_STARTED,
|
|
233
|
+
completed: ISO_DATE_RE.test(doneCell) ? doneCell : null,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (byNumber.size === 0)
|
|
237
|
+
return null;
|
|
238
|
+
// A registry session with no row has not been held: Not Started, so a truncated
|
|
239
|
+
// tracker is never "every session done" (WR-02).
|
|
240
|
+
for (let n = 1; n <= exports.REGISTRY_SIZE; n += 1) {
|
|
241
|
+
if (!byNumber.has(n))
|
|
242
|
+
byNumber.set(n, { number: n, name: '', status: SESSION_STATUS.NOT_STARTED, completed: null });
|
|
243
|
+
}
|
|
244
|
+
const sessions = [...byNumber.values()].sort((a, b) => a.number - b.number);
|
|
245
|
+
return { sessions, unknown: unknown.sort((a, b) => a - b) };
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* The Project Charter's state, read only when every session is Completed (D-05, D-06).
|
|
249
|
+
* The status is read by `charter-check`'s own reader, so "signed" here means what
|
|
250
|
+
* `/pp-discovery-to-build`'s gate means by it (42-REVIEW WR-01).
|
|
251
|
+
*/
|
|
252
|
+
function charterOf(cwd) {
|
|
253
|
+
const file = node_path_1.default.join(cwd, CHARTER_FILE);
|
|
254
|
+
const kind = kindOf(file, true);
|
|
255
|
+
if (kind === 'absent')
|
|
256
|
+
return { state: 'missing', status: null };
|
|
257
|
+
let value = null;
|
|
258
|
+
if (kind === 'regular') {
|
|
259
|
+
const content = readText(file);
|
|
260
|
+
if (content !== null)
|
|
261
|
+
value = charterMod.charterStatusOf(content);
|
|
262
|
+
}
|
|
263
|
+
const signed = value !== null && SIGNED_VALUES.includes(value);
|
|
264
|
+
return { state: signed ? 'signed' : 'unsigned', status: value === null ? null : cleanText(value) || null };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Flow 1's rule (discovery-sprint.md, Flow 1 step 3): in progress, then pre-filled, then not started, then the charter.
|
|
268
|
+
* A session whose status word is not recognised counts as not started but is never
|
|
269
|
+
* the session to start: it may be a finished workshop with a typo (`Done`). When it
|
|
270
|
+
* is the lowest open session the next step is the sprint itself, so the sheet is
|
|
271
|
+
* checked first (42-REVIEW WR-03).
|
|
272
|
+
*/
|
|
273
|
+
function nextOf(sessions, unknown, charter) {
|
|
274
|
+
const unknownSet = new Set(unknown);
|
|
275
|
+
const lowest = (word) => sessions.find((s) => s.status === word && !unknownSet.has(s.number));
|
|
276
|
+
const firstUnknown = sessions.find((s) => unknownSet.has(s.number));
|
|
277
|
+
const open = [
|
|
278
|
+
[SESSION_STATUS.IN_PROGRESS, 'resume'],
|
|
279
|
+
[SESSION_STATUS.PRE_FILLED, 'confirm'],
|
|
280
|
+
[SESSION_STATUS.NOT_STARTED, 'start'],
|
|
281
|
+
];
|
|
282
|
+
for (const [word, kind] of open) {
|
|
283
|
+
const hit = lowest(word);
|
|
284
|
+
if (kind === 'start' && firstUnknown !== undefined && (hit === undefined || firstUnknown.number < hit.number)) {
|
|
285
|
+
return { kind: 'open_sprint', session: null, command: '/pp-discovery' };
|
|
286
|
+
}
|
|
287
|
+
// The command is built from the integer session number, never from file text (T-42-05).
|
|
288
|
+
if (hit !== undefined)
|
|
289
|
+
return { kind, session: hit.number, command: `/pp-discovery ${String(hit.number)}` };
|
|
290
|
+
}
|
|
291
|
+
if (charter.state === 'missing')
|
|
292
|
+
return { kind: 'charter_draft', session: null, command: '/pp-discovery 13' };
|
|
293
|
+
if (charter.state === 'signed')
|
|
294
|
+
return { kind: 'to_build', session: null, command: '/pp-discovery-to-build' };
|
|
295
|
+
return { kind: 'charter_sign', session: null, command: '/pp-discovery charter' };
|
|
296
|
+
}
|
|
297
|
+
/** Where the sprint stopped (D-08): the lowest In Progress session, else the one completed last, else nowhere. */
|
|
298
|
+
function stoppedOf(sessions) {
|
|
299
|
+
const inProgress = sessions.find((s) => s.status === SESSION_STATUS.IN_PROGRESS);
|
|
300
|
+
if (inProgress !== undefined) {
|
|
301
|
+
return { basis: 'in_progress', session: inProgress.number, name: inProgress.name, status: SESSION_STATUS.IN_PROGRESS };
|
|
302
|
+
}
|
|
303
|
+
const done = sessions.filter((s) => s.status === SESSION_STATUS.COMPLETED);
|
|
304
|
+
if (done.length === 0)
|
|
305
|
+
return { basis: 'none', session: null, name: null, status: null };
|
|
306
|
+
let best = done[0];
|
|
307
|
+
for (const s of done) {
|
|
308
|
+
// Sessions are sorted by number, so `>=` makes a tie, or a blank date, go to the higher number.
|
|
309
|
+
if ((s.completed ?? '') >= (best.completed ?? ''))
|
|
310
|
+
best = s;
|
|
311
|
+
}
|
|
312
|
+
return { basis: 'completed', session: best.number, name: best.name, status: SESSION_STATUS.COMPLETED };
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The sprint's state for `cwd`. Read-only and never throws: every file step has
|
|
316
|
+
* its own guard, and a workspace setting that cannot be resolved answers "not
|
|
317
|
+
* discovery-only", so a broken workstream setup keeps today's routing (research A4).
|
|
318
|
+
*/
|
|
319
|
+
function computeStatus(cwd) {
|
|
320
|
+
let projectExists = false;
|
|
321
|
+
let roadmapExists = false;
|
|
322
|
+
let workspaceError = false;
|
|
323
|
+
try {
|
|
324
|
+
// The resolvers init.progress and init.resume use (D-02). STATE.md is never read.
|
|
325
|
+
projectExists = planningWorkspace.resolveProjectFile(cwd).exists;
|
|
326
|
+
roadmapExists = node_fs_1.default.existsSync(node_path_1.default.join(planningWorkspace.planningDir(cwd), 'ROADMAP.md'));
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
projectExists = false;
|
|
330
|
+
roadmapExists = false;
|
|
331
|
+
workspaceError = true;
|
|
332
|
+
}
|
|
333
|
+
const hasBuild = projectExists || roadmapExists;
|
|
334
|
+
let trackerPath = '';
|
|
335
|
+
let trackerKind = 'absent';
|
|
336
|
+
try {
|
|
337
|
+
trackerPath = node_path_1.default.join(planningWorkspace.planningRoot(cwd), 'discovery', '_progress.md');
|
|
338
|
+
trackerKind = kindOf(trackerPath);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
trackerKind = 'absent';
|
|
342
|
+
}
|
|
343
|
+
const facts = {
|
|
344
|
+
has_build: hasBuild,
|
|
345
|
+
project_exists: projectExists,
|
|
346
|
+
roadmap_exists: roadmapExists,
|
|
347
|
+
workspace_error: workspaceError,
|
|
348
|
+
};
|
|
349
|
+
if (trackerKind === 'absent') {
|
|
350
|
+
return { discovery_only: false, in_discovery: false, ...facts, tracker: 'absent' };
|
|
351
|
+
}
|
|
352
|
+
const discoveryOnly = !hasBuild && !workspaceError;
|
|
353
|
+
const unreadable = (project) => ({
|
|
354
|
+
discovery_only: discoveryOnly,
|
|
355
|
+
in_discovery: true,
|
|
356
|
+
...facts,
|
|
357
|
+
tracker: 'unreadable',
|
|
358
|
+
project,
|
|
359
|
+
next: { kind: 'open_sprint', session: null, command: '/pp-discovery' },
|
|
360
|
+
});
|
|
361
|
+
try {
|
|
362
|
+
const content = trackerKind === 'regular' ? readText(trackerPath) : null;
|
|
363
|
+
if (content === null)
|
|
364
|
+
return unreadable(null);
|
|
365
|
+
let project = null;
|
|
366
|
+
for (const line of content.split(/\r?\n/)) {
|
|
367
|
+
const m = /^\*\*Project:\*\*\s*(.+)$/.exec(line);
|
|
368
|
+
if (m !== null) {
|
|
369
|
+
const name = cleanText(m[1]);
|
|
370
|
+
project = name === '' || name === PLACEHOLDER_PROJECT ? null : name;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const parsed = parseSessions(content);
|
|
375
|
+
if (parsed === null)
|
|
376
|
+
return unreadable(project);
|
|
377
|
+
const { sessions, unknown } = parsed;
|
|
378
|
+
const count = (word) => sessions.filter((s) => s.status === word).length;
|
|
379
|
+
const counts = {
|
|
380
|
+
total: sessions.length,
|
|
381
|
+
completed: count(SESSION_STATUS.COMPLETED),
|
|
382
|
+
in_progress: count(SESSION_STATUS.IN_PROGRESS),
|
|
383
|
+
pre_filled: count(SESSION_STATUS.PRE_FILLED),
|
|
384
|
+
not_started: count(SESSION_STATUS.NOT_STARTED),
|
|
385
|
+
};
|
|
386
|
+
// The charter matters only once all 13 sessions are Completed; until then it is not read.
|
|
387
|
+
const charter = counts.completed === exports.REGISTRY_SIZE ? charterOf(cwd) : { state: 'not_checked', status: null };
|
|
388
|
+
return {
|
|
389
|
+
discovery_only: discoveryOnly,
|
|
390
|
+
in_discovery: true,
|
|
391
|
+
...facts,
|
|
392
|
+
tracker: 'ok',
|
|
393
|
+
project,
|
|
394
|
+
counts,
|
|
395
|
+
sessions,
|
|
396
|
+
unknown_status: unknown,
|
|
397
|
+
stopped_at: stoppedOf(sessions),
|
|
398
|
+
charter,
|
|
399
|
+
next: nextOf(sessions, unknown, charter),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return unreadable(null);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* `pp-tools discovery status [--raw]`. `--raw` prints the JSON, the same for every
|
|
408
|
+
* audience (41 D-08); without it the view is printed in the person's wording.
|
|
409
|
+
* Do not copy `cmdProgressRender`, whose `--raw` prints audience text (41-REVIEW WR-05).
|
|
410
|
+
*/
|
|
411
|
+
function cmdDiscoveryStatus(cwd, args, raw) {
|
|
412
|
+
if (args.slice(2).length > 0)
|
|
413
|
+
error('Usage: pp-tools discovery status [--raw]', ERROR_REASON.USAGE);
|
|
414
|
+
const status = computeStatus(cwd);
|
|
415
|
+
if (raw)
|
|
416
|
+
output(status, false);
|
|
417
|
+
else
|
|
418
|
+
output(status, true, renderStatus(status, (0, messages_cjs_1.audienceOf)(process.env)));
|
|
419
|
+
}
|
|
@@ -17,8 +17,12 @@
|
|
|
17
17
|
* `_requirements.md` when it is on file and parseable;
|
|
18
18
|
* - `Story` and `Acceptance criteria` are non-empty;
|
|
19
19
|
* - `Acceptance criteria` is a criterion, not a pointer (`As in F-01`);
|
|
20
|
-
* - `Source step` is
|
|
21
|
-
*
|
|
20
|
+
* - `Source step` is a comma list of `MP-nn #<cell>`, the main process and
|
|
21
|
+
* the step row existing in `_processes.md` when it is on file — a story
|
|
22
|
+
* tied to no step (`—`) is refused (Session 3 rework: stories come from
|
|
23
|
+
* steps);
|
|
24
|
+
* - every persona-actor step that realises an MVP requirement is the
|
|
25
|
+
* source step of at least one story, so the stories cover the processes;
|
|
22
26
|
* - every MVP-disposition requirement that a persona-actor step cites has at
|
|
23
27
|
* least one story (the `N-`/`T-` prefixes never need one; a requirement
|
|
24
28
|
* only `System` steps realise, or no step at all, has no user to tell it).
|
|
@@ -118,6 +122,23 @@ function needsStory(r, steps) {
|
|
|
118
122
|
return true;
|
|
119
123
|
return steps.some((s) => s.requirements.includes(r.id) && s.actors.some((a) => a !== SYSTEM_ACTOR));
|
|
120
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Does this step need a story naming it as its source? A step a persona
|
|
127
|
+
* performs does, when it realises an MVP requirement that is not `N-`/`T-`.
|
|
128
|
+
* With no parseable `_requirements.md` every persona step does; a step that
|
|
129
|
+
* cites nothing yet is `process-check --require-cited`'s finding, not this one.
|
|
130
|
+
*/
|
|
131
|
+
function stepNeedsStory(step, requirements) {
|
|
132
|
+
if (!step.actors.some((a) => a !== SYSTEM_ACTOR))
|
|
133
|
+
return false;
|
|
134
|
+
if (!(requirements instanceof Array))
|
|
135
|
+
return true;
|
|
136
|
+
const byId = new Map(requirements.map((r) => [r.id, r]));
|
|
137
|
+
return step.requirements.some((id) => {
|
|
138
|
+
const r = byId.get(id);
|
|
139
|
+
return r !== undefined && r.disposition === rollup.DISPOSITIONS.MVP && !STORYLESS_PREFIXES.includes(r.prefix);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
121
142
|
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
122
143
|
function checkStories(content, sources) {
|
|
123
144
|
const violations = [];
|
|
@@ -137,6 +158,7 @@ function checkStories(content, sources) {
|
|
|
137
158
|
const ids = new Set();
|
|
138
159
|
const seqs = [];
|
|
139
160
|
const storied = new Set();
|
|
161
|
+
const sourced = new Set();
|
|
140
162
|
for (const row of tables[0].rows) {
|
|
141
163
|
const [id, epic, requirements, story, acceptance, sourceStep] = row.cells.map((c) => c.trim());
|
|
142
164
|
const m = US_RE.exec(id ?? '');
|
|
@@ -170,11 +192,13 @@ function checkStories(content, sources) {
|
|
|
170
192
|
storied.add(r);
|
|
171
193
|
}
|
|
172
194
|
const src = sourceStep ?? '';
|
|
173
|
-
if (src
|
|
195
|
+
if (src === PENDING || src === '') {
|
|
196
|
+
violations.push({ artifact: STORIES_FILE, reason: 'no_source_step', line: row.line, id: m ? id : undefined });
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
174
199
|
const refs = splitList(src);
|
|
175
|
-
if (refs.length === 0)
|
|
176
|
-
violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
|
|
177
200
|
for (const ref of refs) {
|
|
201
|
+
sourced.add(ref);
|
|
178
202
|
const sm = SOURCE_STEP_RE.exec(ref);
|
|
179
203
|
if (!sm)
|
|
180
204
|
violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
|
|
@@ -193,6 +217,12 @@ function checkStories(content, sources) {
|
|
|
193
217
|
violations.push({ artifact: STORIES_FILE, reason: 'sequence_gap', id: `US-${String(n).padStart(2, '0')}` });
|
|
194
218
|
}
|
|
195
219
|
}
|
|
220
|
+
for (const step of sources.steps ?? []) {
|
|
221
|
+
if (!stepNeedsStory(step, sources.requirements))
|
|
222
|
+
continue;
|
|
223
|
+
if (!sourced.has(step.key))
|
|
224
|
+
violations.push({ artifact: PROCESSES_FILE, reason: 'step_without_story', id: step.key.slice(0, 40) });
|
|
225
|
+
}
|
|
196
226
|
if (sources.requirements instanceof Array) {
|
|
197
227
|
for (const r of sources.requirements) {
|
|
198
228
|
if (r.disposition !== rollup.DISPOSITIONS.MVP)
|
|
@@ -266,6 +296,7 @@ module.exports = {
|
|
|
266
296
|
STORYLESS_PREFIXES,
|
|
267
297
|
SYSTEM_ACTOR,
|
|
268
298
|
needsStory,
|
|
299
|
+
stepNeedsStory,
|
|
269
300
|
STORIES_CHECK_FAILED: ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED,
|
|
270
301
|
checkStories,
|
|
271
302
|
checkProjectStories,
|