@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,398 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery tests check (Session 9 — testing and UAT).
|
|
4
|
+
*
|
|
5
|
+
* `.planning/discovery/_tests.md` is the register of the test manager's calls
|
|
6
|
+
* the agents cannot make: which main processes get an end-to-end test (a risk
|
|
7
|
+
* rating per `MP-` process), which parts of the solution get extra test
|
|
8
|
+
* attention because they are complex (components, interfaces, rules,
|
|
9
|
+
* migration rows, entities), and the testability review of every `US-` story.
|
|
10
|
+
* Session 9 writes it; the build reads it by requirement ID through
|
|
11
|
+
* `discovery for-phase`. This module checks the structure:
|
|
12
|
+
*
|
|
13
|
+
* - `## Process risk`, `## Focus areas` and `## Acceptance` exist with the
|
|
14
|
+
* template's columns;
|
|
15
|
+
* - every main process of `_processes.md` § To-be has exactly one risk row,
|
|
16
|
+
* the risk follows from impact and likelihood by the fixed matrix, and a
|
|
17
|
+
* high-risk process without an end-to-end test cites a `D-09-<n>`;
|
|
18
|
+
* - every focus-area subject resolves against its source when on file;
|
|
19
|
+
* - every story of `_stories.md` has exactly one acceptance row;
|
|
20
|
+
* - roles resolve against `_roles.md` § Roles when on file, requirements
|
|
21
|
+
* against `_requirements.md` when on file and parseable;
|
|
22
|
+
* - `--require-complete`: an `open` story or a focus area whose
|
|
23
|
+
* `Requirements` is still `—` fails on its own exit identity, so the
|
|
24
|
+
* handoff can hard-block on it while Sessions 9 and 13 only report.
|
|
25
|
+
*
|
|
26
|
+
* Posture (D-43/D-53): reports at Sessions 9 and 13; hard-blocks at the
|
|
27
|
+
* handoff. Absent artifact → exit 0 — an engagement that predates it.
|
|
28
|
+
*/
|
|
29
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
30
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
|
+
};
|
|
32
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
35
|
+
const io = require("./io.cjs");
|
|
36
|
+
const { output, error, ERROR_REASON } = io;
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
+
const table = require("./discovery-table.cjs");
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const rollup = require("./discovery-rollup.cjs");
|
|
41
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
42
|
+
const DISCOVERY_DIR = '.planning/discovery';
|
|
43
|
+
const TESTS_FILE = `${DISCOVERY_DIR}/_tests.md`;
|
|
44
|
+
const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
|
|
45
|
+
const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
|
|
46
|
+
const STORIES_FILE = `${DISCOVERY_DIR}/_stories.md`;
|
|
47
|
+
const ROLES_FILE = `${DISCOVERY_DIR}/_roles.md`;
|
|
48
|
+
const INTEGRATIONS_FILE = `${DISCOVERY_DIR}/_integrations.md`;
|
|
49
|
+
const RULES_FILE = `${DISCOVERY_DIR}/_rules.md`;
|
|
50
|
+
const MIGRATION_FILE = `${DISCOVERY_DIR}/_migration.md`;
|
|
51
|
+
const ENTITIES_FILE = `${DISCOVERY_DIR}/_entities.md`;
|
|
52
|
+
const SESSION_05_FILE = `${DISCOVERY_DIR}/session-05.md`;
|
|
53
|
+
const RISK_SECTION = 'Process risk';
|
|
54
|
+
const FOCUS_SECTION = 'Focus areas';
|
|
55
|
+
const ACCEPTANCE_SECTION = 'Acceptance';
|
|
56
|
+
const RISK_HEADER = Object.freeze(['Process', 'Impact', 'Likelihood', 'Risk', 'End-to-end', 'Roles', 'Test data', 'Rationale']);
|
|
57
|
+
const FOCUS_HEADER = Object.freeze(['Subject', 'Complexity', 'Extra testing', 'Requirements']);
|
|
58
|
+
const ACCEPTANCE_HEADER = Object.freeze(['Story', 'Testable', 'Role', 'Test data']);
|
|
59
|
+
const MP_RE = /^MP-\d{2}$/;
|
|
60
|
+
const US_RE = /^US-\d{2}$/;
|
|
61
|
+
const ENTITY_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;
|
|
62
|
+
const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
63
|
+
const SESSION_9_DECISION_RE = /\bD-09-\d+\b/;
|
|
64
|
+
const TESTABLE_RE = /^(yes|amended \(D-09-\d+\)|open \(A-09-\d+\))$/;
|
|
65
|
+
const RATINGS = Object.freeze(['low', 'medium', 'high']);
|
|
66
|
+
/** RISK_MATRIX[impact][likelihood] — `references/discovery-tests.md` § Risk rating. */
|
|
67
|
+
const RISK_MATRIX = Object.freeze({
|
|
68
|
+
high: Object.freeze({ low: 'medium', medium: 'high', high: 'high' }),
|
|
69
|
+
medium: Object.freeze({ low: 'low', medium: 'medium', high: 'high' }),
|
|
70
|
+
low: Object.freeze({ low: 'low', medium: 'low', high: 'medium' }),
|
|
71
|
+
});
|
|
72
|
+
/** Subject ID prefixes and the source each resolves against. */
|
|
73
|
+
const SUBJECT_KINDS = Object.freeze([
|
|
74
|
+
{ re: /^C-\d{2}$/, source: 'components' },
|
|
75
|
+
{ re: /^IF-\d{2}$/, source: 'interfaces' },
|
|
76
|
+
{ re: /^BR-\d{2}$/, source: 'rules' },
|
|
77
|
+
{ re: /^MG-\d{2}$/, source: 'migrations' },
|
|
78
|
+
]);
|
|
79
|
+
const PENDING = '—';
|
|
80
|
+
const COMPONENT_INVENTORY_LABEL = 'Component Inventory';
|
|
81
|
+
const USAGE = 'Usage: pp-tools discovery tests-check [--require-complete]';
|
|
82
|
+
function readIfPresent(abs) {
|
|
83
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
84
|
+
return { kind: 'absent' };
|
|
85
|
+
try {
|
|
86
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { kind: 'unreadable' };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function sameHeader(actual, expected) {
|
|
93
|
+
return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
|
|
94
|
+
}
|
|
95
|
+
function splitList(cell) {
|
|
96
|
+
return cell.split(',').map((t) => t.trim()).filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
function firstColumnOf(content, heading) {
|
|
99
|
+
const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
|
|
100
|
+
if (!section)
|
|
101
|
+
return new Set();
|
|
102
|
+
const tables = table.parseTables(section.body);
|
|
103
|
+
if (tables.length === 0)
|
|
104
|
+
return new Set();
|
|
105
|
+
return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
|
|
106
|
+
}
|
|
107
|
+
/** Component IDs: the first column of Session 5's Component Inventory; null when absent or without IDs. */
|
|
108
|
+
function componentsFrom(content) {
|
|
109
|
+
const lines = content.split(/\r?\n/);
|
|
110
|
+
const norm = (s) => s.replace(/^#+\s*/, '').replace(/\*\*/g, '').trim();
|
|
111
|
+
const at = lines.findIndex((l) => norm(l) === COMPONENT_INVENTORY_LABEL);
|
|
112
|
+
if (at === -1)
|
|
113
|
+
return null;
|
|
114
|
+
const body = [];
|
|
115
|
+
for (let i = at + 1; i < lines.length; i += 1) {
|
|
116
|
+
if (/^#{1,3}\s/.test(lines[i]) || /^\*\*[^*]+\*\*$/.test(lines[i].trim()))
|
|
117
|
+
break;
|
|
118
|
+
body.push({ n: i + 1, text: lines[i] });
|
|
119
|
+
}
|
|
120
|
+
const tables = table.parseTables(body);
|
|
121
|
+
if (tables.length === 0)
|
|
122
|
+
return null;
|
|
123
|
+
// A row counts only when it is more than the template's `| C-01 | | |` label.
|
|
124
|
+
const out = new Set(tables[0].rows
|
|
125
|
+
.filter((r) => r.cells.slice(1).some((c) => c.trim() !== ''))
|
|
126
|
+
.map((r) => (r.cells[0] ?? '').trim())
|
|
127
|
+
.filter((c) => c !== ''));
|
|
128
|
+
return out.size === 0 ? null : out;
|
|
129
|
+
}
|
|
130
|
+
/** The table under a `##` heading, or a violation when it is missing or off the template. */
|
|
131
|
+
function sectionTable(sections, heading, header, violations) {
|
|
132
|
+
const section = sections.find((s) => s.heading === heading);
|
|
133
|
+
if (!section) {
|
|
134
|
+
violations.push({ artifact: TESTS_FILE, reason: 'missing_section', id: heading });
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const tables = table.parseTables(section.body);
|
|
138
|
+
if (tables.length === 0) {
|
|
139
|
+
violations.push({ artifact: TESTS_FILE, reason: 'missing_section', line: section.line, id: heading });
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
if (!sameHeader(tables[0].header, header)) {
|
|
143
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_table_shape', line: tables[0].line, id: heading });
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return tables[0];
|
|
147
|
+
}
|
|
148
|
+
function checkRequirements(cell, sources, line, violations) {
|
|
149
|
+
for (const r of splitList(cell)) {
|
|
150
|
+
if (!REQUIREMENT_ID_RE.test(r))
|
|
151
|
+
violations.push({ artifact: TESTS_FILE, reason: 'unknown_requirement_id', line, id: r.slice(0, 40) });
|
|
152
|
+
else if (sources.requirementIds instanceof Set && !sources.requirementIds.has(r))
|
|
153
|
+
violations.push({ artifact: TESTS_FILE, reason: 'unknown_requirement_id', line, id: r });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function checkRole(role, sources, line, violations) {
|
|
157
|
+
if (sources.roles !== null && !sources.roles.has(role))
|
|
158
|
+
violations.push({ artifact: TESTS_FILE, reason: 'unknown_role', line, id: role.slice(0, 40) });
|
|
159
|
+
}
|
|
160
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
161
|
+
function checkTests(content, sources, requireComplete = false) {
|
|
162
|
+
const violations = [];
|
|
163
|
+
const sections = table.splitSections(content.split(/\r?\n/), 2);
|
|
164
|
+
const risk = sectionTable(sections, RISK_SECTION, RISK_HEADER, violations);
|
|
165
|
+
const focus = sectionTable(sections, FOCUS_SECTION, FOCUS_HEADER, violations);
|
|
166
|
+
const acceptance = sectionTable(sections, ACCEPTANCE_SECTION, ACCEPTANCE_HEADER, violations);
|
|
167
|
+
// Process risk — one row per main process.
|
|
168
|
+
const assessed = new Set();
|
|
169
|
+
let endToEnd = 0;
|
|
170
|
+
for (const row of risk?.rows ?? []) {
|
|
171
|
+
const [mp, impact, likelihood, rating, e2e, roles, testData, rationale] = row.cells.map((c) => c.trim());
|
|
172
|
+
const line = row.line;
|
|
173
|
+
if (!MP_RE.test(mp ?? ''))
|
|
174
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_process', line });
|
|
175
|
+
else if (assessed.has(mp))
|
|
176
|
+
violations.push({ artifact: TESTS_FILE, reason: 'duplicate_process', line, id: mp });
|
|
177
|
+
else {
|
|
178
|
+
assessed.add(mp);
|
|
179
|
+
if (sources.processes !== null && !sources.processes.has(mp))
|
|
180
|
+
violations.push({ artifact: TESTS_FILE, reason: 'unknown_process', line, id: mp });
|
|
181
|
+
}
|
|
182
|
+
const ratings = [['Impact', impact], ['Likelihood', likelihood], ['Risk', rating]];
|
|
183
|
+
let ratingsOk = true;
|
|
184
|
+
for (const [name, cell] of ratings) {
|
|
185
|
+
if (!RATINGS.includes(cell ?? '')) {
|
|
186
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_rating', line, id: name });
|
|
187
|
+
ratingsOk = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (ratingsOk && RISK_MATRIX[impact][likelihood] !== rating)
|
|
191
|
+
violations.push({ artifact: TESTS_FILE, reason: 'risk_mismatch', line, id: MP_RE.test(mp ?? '') ? mp : undefined });
|
|
192
|
+
if (e2e !== 'yes' && e2e !== 'no')
|
|
193
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_end_to_end', line });
|
|
194
|
+
if (e2e === 'yes') {
|
|
195
|
+
endToEnd += 1;
|
|
196
|
+
const names = (roles ?? '') === PENDING ? [] : splitList(roles ?? '');
|
|
197
|
+
if (names.length === 0)
|
|
198
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Roles' });
|
|
199
|
+
for (const n of names)
|
|
200
|
+
checkRole(n, sources, line, violations);
|
|
201
|
+
if ((testData ?? '') === '' || testData === PENDING)
|
|
202
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Test data' });
|
|
203
|
+
}
|
|
204
|
+
if ((rationale ?? '') === '')
|
|
205
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Rationale' });
|
|
206
|
+
if (rating === 'high' && e2e === 'no' && !SESSION_9_DECISION_RE.test(rationale ?? '')) {
|
|
207
|
+
violations.push({ artifact: TESTS_FILE, reason: 'high_risk_not_end_to_end', line, id: MP_RE.test(mp ?? '') ? mp : undefined });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (risk && sources.processes !== null) {
|
|
211
|
+
for (const mp of [...sources.processes].sort()) {
|
|
212
|
+
if (!assessed.has(mp))
|
|
213
|
+
violations.push({ artifact: TESTS_FILE, reason: 'process_not_assessed', id: mp });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Focus areas — one row per subject.
|
|
217
|
+
const subjects = new Set();
|
|
218
|
+
let uncited = 0;
|
|
219
|
+
for (const row of focus?.rows ?? []) {
|
|
220
|
+
const [subject, complexity, extra, requirements] = row.cells.map((c) => c.trim());
|
|
221
|
+
const line = row.line;
|
|
222
|
+
const kind = SUBJECT_KINDS.find((k) => k.re.test(subject ?? ''));
|
|
223
|
+
if (!kind && !ENTITY_NAME_RE.test(subject ?? ''))
|
|
224
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_subject', line });
|
|
225
|
+
else if (subjects.has(subject))
|
|
226
|
+
violations.push({ artifact: TESTS_FILE, reason: 'duplicate_subject', line, id: subject });
|
|
227
|
+
else {
|
|
228
|
+
subjects.add(subject);
|
|
229
|
+
const known = kind ? sources[kind.source] : sources.entities;
|
|
230
|
+
if (known !== null && !known.has(subject))
|
|
231
|
+
violations.push({ artifact: TESTS_FILE, reason: 'unknown_subject', line, id: subject });
|
|
232
|
+
}
|
|
233
|
+
if ((complexity ?? '') === '')
|
|
234
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Complexity' });
|
|
235
|
+
if ((extra ?? '') === '')
|
|
236
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Extra testing' });
|
|
237
|
+
const reqCell = requirements ?? '';
|
|
238
|
+
if (reqCell === '' || reqCell === PENDING) {
|
|
239
|
+
uncited += 1;
|
|
240
|
+
if (requireComplete)
|
|
241
|
+
violations.push({ artifact: TESTS_FILE, reason: 'uncited_focus', line, id: subjects.has(subject) ? subject : undefined });
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
checkRequirements(reqCell, sources, line, violations);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Acceptance — one row per story.
|
|
248
|
+
const reviewed = new Set();
|
|
249
|
+
let open = 0;
|
|
250
|
+
for (const row of acceptance?.rows ?? []) {
|
|
251
|
+
const [story, testable, role, testData] = row.cells.map((c) => c.trim());
|
|
252
|
+
const line = row.line;
|
|
253
|
+
if (!US_RE.test(story ?? ''))
|
|
254
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_story', line });
|
|
255
|
+
else if (reviewed.has(story))
|
|
256
|
+
violations.push({ artifact: TESTS_FILE, reason: 'duplicate_story', line, id: story });
|
|
257
|
+
else {
|
|
258
|
+
reviewed.add(story);
|
|
259
|
+
if (sources.stories !== null && !sources.stories.has(story))
|
|
260
|
+
violations.push({ artifact: TESTS_FILE, reason: 'unknown_story', line, id: story });
|
|
261
|
+
}
|
|
262
|
+
if (!TESTABLE_RE.test(testable ?? ''))
|
|
263
|
+
violations.push({ artifact: TESTS_FILE, reason: 'bad_testable', line });
|
|
264
|
+
else if (testable.startsWith('open')) {
|
|
265
|
+
open += 1;
|
|
266
|
+
if (requireComplete)
|
|
267
|
+
violations.push({ artifact: TESTS_FILE, reason: 'story_not_testable', line, id: US_RE.test(story ?? '') ? story : undefined });
|
|
268
|
+
}
|
|
269
|
+
if ((role ?? '') === '' || role === PENDING)
|
|
270
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Role' });
|
|
271
|
+
else
|
|
272
|
+
checkRole(role, sources, line, violations);
|
|
273
|
+
if ((testData ?? '') === '')
|
|
274
|
+
violations.push({ artifact: TESTS_FILE, reason: 'empty_cell', line, id: 'Test data' });
|
|
275
|
+
}
|
|
276
|
+
if (acceptance && sources.stories !== null) {
|
|
277
|
+
for (const us of [...sources.stories].sort()) {
|
|
278
|
+
if (!reviewed.has(us))
|
|
279
|
+
violations.push({ artifact: TESTS_FILE, reason: 'story_not_reviewed', id: us });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (sources.requirementIds === 'unparseable')
|
|
283
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
|
|
284
|
+
return {
|
|
285
|
+
ok: violations.length === 0,
|
|
286
|
+
processes: assessed.size,
|
|
287
|
+
end_to_end: endToEnd,
|
|
288
|
+
focus_areas: subjects.size,
|
|
289
|
+
stories: reviewed.size,
|
|
290
|
+
open_stories: open,
|
|
291
|
+
uncited,
|
|
292
|
+
violations,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function checkProjectTests(cwd, requireComplete = false) {
|
|
296
|
+
const read = readIfPresent(node_path_1.default.join(cwd, TESTS_FILE));
|
|
297
|
+
if (read.kind === 'absent')
|
|
298
|
+
return null;
|
|
299
|
+
const empty = { processes: 0, end_to_end: 0, focus_areas: 0, stories: 0, open_stories: 0, uncited: 0 };
|
|
300
|
+
if (read.kind === 'unreadable')
|
|
301
|
+
return { ok: false, file: TESTS_FILE, ...empty, violations: [{ artifact: TESTS_FILE, reason: 'artifact_unreadable' }] };
|
|
302
|
+
const extra = [];
|
|
303
|
+
const sources = {
|
|
304
|
+
requirementIds: null, processes: null, stories: null, roles: null,
|
|
305
|
+
components: null, interfaces: null, rules: null, migrations: null, entities: null,
|
|
306
|
+
};
|
|
307
|
+
const load = (file) => {
|
|
308
|
+
const r = readIfPresent(node_path_1.default.join(cwd, file));
|
|
309
|
+
if (r.kind === 'unreadable')
|
|
310
|
+
extra.push({ artifact: file, reason: 'artifact_unreadable' });
|
|
311
|
+
return r.kind === 'ok' ? r.content : null;
|
|
312
|
+
};
|
|
313
|
+
const req = load(REQUIREMENTS_FILE);
|
|
314
|
+
if (req !== null) {
|
|
315
|
+
const parsed = rollup.parseRequirementsArtifact(req);
|
|
316
|
+
sources.requirementIds = parsed.ok ? new Set(parsed.rows.map((r) => r.id)) : 'unparseable';
|
|
317
|
+
}
|
|
318
|
+
const proc = load(PROCESSES_FILE);
|
|
319
|
+
if (proc !== null)
|
|
320
|
+
sources.processes = firstColumnOf(proc, 'To-be');
|
|
321
|
+
const sto = load(STORIES_FILE);
|
|
322
|
+
if (sto !== null)
|
|
323
|
+
sources.stories = firstColumnOf(sto, 'Stories');
|
|
324
|
+
const rol = load(ROLES_FILE);
|
|
325
|
+
if (rol !== null)
|
|
326
|
+
sources.roles = firstColumnOf(rol, 'Roles');
|
|
327
|
+
const integ = load(INTEGRATIONS_FILE);
|
|
328
|
+
if (integ !== null)
|
|
329
|
+
sources.interfaces = firstColumnOf(integ, 'Interfaces');
|
|
330
|
+
const rules = load(RULES_FILE);
|
|
331
|
+
if (rules !== null)
|
|
332
|
+
sources.rules = firstColumnOf(rules, 'Rules');
|
|
333
|
+
const mig = load(MIGRATION_FILE);
|
|
334
|
+
if (mig !== null)
|
|
335
|
+
sources.migrations = firstColumnOf(mig, 'Migration');
|
|
336
|
+
const ent = load(ENTITIES_FILE);
|
|
337
|
+
if (ent !== null)
|
|
338
|
+
sources.entities = firstColumnOf(ent, 'Entities');
|
|
339
|
+
const s5 = load(SESSION_05_FILE);
|
|
340
|
+
if (s5 !== null)
|
|
341
|
+
sources.components = componentsFrom(s5);
|
|
342
|
+
const result = checkTests(read.content, sources, requireComplete);
|
|
343
|
+
result.violations.push(...extra);
|
|
344
|
+
result.ok = result.violations.length === 0;
|
|
345
|
+
return { file: TESTS_FILE, ...result };
|
|
346
|
+
}
|
|
347
|
+
function formatViolation(v) {
|
|
348
|
+
const where = v.line !== undefined ? `:${v.line}` : '';
|
|
349
|
+
const what = v.id !== undefined ? ` (${v.id})` : '';
|
|
350
|
+
return ` - ${v.artifact}${where}: ${v.reason}${what}`;
|
|
351
|
+
}
|
|
352
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
353
|
+
/**
|
|
354
|
+
* `pp-tools discovery tests-check [--require-complete]` (dotted
|
|
355
|
+
* `query discovery.tests-check`). Absent artifact → exit 0,
|
|
356
|
+
* `present: false`. Structural faults exit via
|
|
357
|
+
* `DISCOVERY_TESTS_CHECK_FAILED`; when every fault was raised by
|
|
358
|
+
* `--require-complete` (`story_not_testable`, `uncited_focus`), the identity
|
|
359
|
+
* is `DISCOVERY_TESTS_INCOMPLETE`.
|
|
360
|
+
*/
|
|
361
|
+
function cmdDiscoveryTestsCheck(cwd, args, raw) {
|
|
362
|
+
const rest = args.slice(2);
|
|
363
|
+
const requireComplete = rest.includes('--require-complete');
|
|
364
|
+
if (rest.some((a) => a !== '--require-complete')) {
|
|
365
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
366
|
+
return; // unreachable
|
|
367
|
+
}
|
|
368
|
+
const result = checkProjectTests(cwd, requireComplete);
|
|
369
|
+
if (result === null) {
|
|
370
|
+
output({ ok: true, present: false, file: TESTS_FILE }, raw);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const payload = {
|
|
374
|
+
ok: result.ok, present: true, file: result.file,
|
|
375
|
+
processes: result.processes, end_to_end: result.end_to_end, focus_areas: result.focus_areas,
|
|
376
|
+
stories: result.stories, open_stories: result.open_stories, uncited: result.uncited,
|
|
377
|
+
};
|
|
378
|
+
if (!result.ok) {
|
|
379
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
380
|
+
const onlyIncomplete = result.violations.every((v) => v.reason === 'story_not_testable' || v.reason === 'uncited_focus');
|
|
381
|
+
error(`discovery tests-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), onlyIncomplete ? ERROR_REASON.DISCOVERY_TESTS_INCOMPLETE : ERROR_REASON.DISCOVERY_TESTS_CHECK_FAILED);
|
|
382
|
+
return; // unreachable
|
|
383
|
+
}
|
|
384
|
+
output(payload, raw);
|
|
385
|
+
}
|
|
386
|
+
module.exports = {
|
|
387
|
+
TESTS_FILE,
|
|
388
|
+
RISK_HEADER,
|
|
389
|
+
FOCUS_HEADER,
|
|
390
|
+
ACCEPTANCE_HEADER,
|
|
391
|
+
RISK_MATRIX,
|
|
392
|
+
TESTS_CHECK_FAILED: ERROR_REASON.DISCOVERY_TESTS_CHECK_FAILED,
|
|
393
|
+
TESTS_INCOMPLETE: ERROR_REASON.DISCOVERY_TESTS_INCOMPLETE,
|
|
394
|
+
componentsFrom,
|
|
395
|
+
checkTests,
|
|
396
|
+
checkProjectTests,
|
|
397
|
+
cmdDiscoveryTestsCheck,
|
|
398
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `pp-tools explain <code>`: the longer explanation of a message's `Ref:` code
|
|
4
|
+
* (Phase 41: D-19, MSG-05).
|
|
5
|
+
*
|
|
6
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
7
|
+
* compiles to the gitignored `pp-core/bin/lib/explain-command.cjs`; do NOT
|
|
8
|
+
* hand-edit the compiled output.
|
|
9
|
+
*
|
|
10
|
+
* A thin adapter over the message catalog. The code is only ever looked up in
|
|
11
|
+
* the catalog's Map (`explainText`), never used to index an object, so
|
|
12
|
+
* `__proto__`, `constructor` and `toString` are unknown codes like any other
|
|
13
|
+
* (T-41-13). The typed code is echoed only after `sanitizeParam` has stripped
|
|
14
|
+
* control characters (T-41-11). There is no listing mode: a bare call prints
|
|
15
|
+
* only the usage (T-41-25).
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.USAGE = void 0;
|
|
19
|
+
exports.cmdExplain = cmdExplain;
|
|
20
|
+
const messages_cjs_1 = require("./messages.cjs");
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const io = require("./io.cjs");
|
|
23
|
+
const { output, ERROR_REASON } = io;
|
|
24
|
+
/** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
|
|
25
|
+
function error(message, reason) {
|
|
26
|
+
return io.error(message, reason);
|
|
27
|
+
}
|
|
28
|
+
const HELP_FLAGS = ['-h', '--help', '-?', '--h', '--usage'];
|
|
29
|
+
exports.USAGE = 'Usage: pp-tools explain <code> [--raw] — <code> is the text after "Ref:" at the end of a message';
|
|
30
|
+
function cmdExplain(_cwd, args, raw) {
|
|
31
|
+
if (args.length === 1 && HELP_FLAGS.includes(args[0])) {
|
|
32
|
+
output({ usage: exports.USAGE }, true, exports.USAGE + '\n');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (args.length !== 1 || args[0].startsWith('--'))
|
|
36
|
+
error(exports.USAGE, ERROR_REASON.USAGE);
|
|
37
|
+
const code = args[0];
|
|
38
|
+
const audience = (0, messages_cjs_1.audienceOf)(process.env);
|
|
39
|
+
const body = (0, messages_cjs_1.explainText)(code, { audience });
|
|
40
|
+
if (body === null) {
|
|
41
|
+
io.errorCode('explain_unknown_code', { code: (0, messages_cjs_1.sanitizeParam)(code) }, ERROR_REASON.EXPLAIN_UNKNOWN_CODE);
|
|
42
|
+
}
|
|
43
|
+
if (raw) {
|
|
44
|
+
output({ code, audience, text: body }, false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
output({ code, audience, text: body }, true, body + '\n');
|
|
48
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `pp-tools gate-card <query>`: the stopped-check card in the person's wording
|
|
4
|
+
* (Phase 43: D-11, TPL-02).
|
|
5
|
+
*
|
|
6
|
+
* ADR-457 build-at-publish: this TypeScript file is the SOURCE OF TRUTH. It
|
|
7
|
+
* compiles to the gitignored `pp-core/bin/lib/gate-card.cjs`; do NOT hand-edit
|
|
8
|
+
* the compiled output.
|
|
9
|
+
*
|
|
10
|
+
* Like `audience fields`, it prints nothing for a developer, so a workflow fence
|
|
11
|
+
* that calls it prints no extra line for a developer (D-03). Every word comes
|
|
12
|
+
* from the `gate` catalog slice through `msg()` / `text()` (D-10); this verb never
|
|
13
|
+
* composes wording. Codes are looked up only through the catalog Map (`has`), and
|
|
14
|
+
* the query only through `GATE_QUERIES.includes`, so `__proto__`, `constructor`
|
|
15
|
+
* and an unknown name print nothing (T-43-10). The query argument is never echoed
|
|
16
|
+
* on any path, including usage errors (T-43-11). The no-state output is one
|
|
17
|
+
* `JSON.stringify` line, so card text cannot forge a line of its own (T-43-12).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.USAGE = void 0;
|
|
21
|
+
exports.cardsFor = cardsFor;
|
|
22
|
+
exports.cmdGateCard = cmdGateCard;
|
|
23
|
+
const messages_cjs_1 = require("./messages.cjs");
|
|
24
|
+
const gate_cjs_1 = require("./messages/gate.cjs");
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
|
+
const io = require("./io.cjs");
|
|
27
|
+
const { output, ERROR_REASON } = io;
|
|
28
|
+
/** Explicitly typed so TypeScript narrows after a failing guard (the destructured `io.error` does not). */
|
|
29
|
+
function error(message, reason) {
|
|
30
|
+
return io.error(message, reason);
|
|
31
|
+
}
|
|
32
|
+
exports.USAGE = "Usage: pp-tools gate-card <query> [--state blocked|advisory|error] [--details] [--raw] — the stopped-check card in the person's wording; prints nothing for a developer";
|
|
33
|
+
/**
|
|
34
|
+
* The cards of one query, keyed by state; null for a developer or an unknown query.
|
|
35
|
+
* Only the states the query defines appear.
|
|
36
|
+
*/
|
|
37
|
+
function cardsFor(query, audience) {
|
|
38
|
+
if (audience === 'developer')
|
|
39
|
+
return null;
|
|
40
|
+
if (!gate_cjs_1.GATE_QUERIES.includes(query))
|
|
41
|
+
return null;
|
|
42
|
+
const cards = {};
|
|
43
|
+
for (const state of gate_cjs_1.GATE_STATES) {
|
|
44
|
+
const code = `gate.${query}.${state}`;
|
|
45
|
+
if ((0, messages_cjs_1.has)(code))
|
|
46
|
+
cards[state] = { code, text: (0, messages_cjs_1.text)(code, {}, { audience }) };
|
|
47
|
+
}
|
|
48
|
+
return cards;
|
|
49
|
+
}
|
|
50
|
+
function cmdGateCard(_cwd, args, raw) {
|
|
51
|
+
let query = null;
|
|
52
|
+
let state = null;
|
|
53
|
+
let details = false;
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
const a = args[i];
|
|
56
|
+
if (a === '--raw')
|
|
57
|
+
continue;
|
|
58
|
+
if (a === '--details') {
|
|
59
|
+
details = true;
|
|
60
|
+
}
|
|
61
|
+
else if (a === '--state') {
|
|
62
|
+
const next = args[i + 1];
|
|
63
|
+
if (next === undefined || !gate_cjs_1.GATE_STATES.includes(next))
|
|
64
|
+
error(exports.USAGE, ERROR_REASON.USAGE);
|
|
65
|
+
state = next;
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
else if (a.startsWith('-')) {
|
|
69
|
+
error(exports.USAGE, ERROR_REASON.USAGE);
|
|
70
|
+
}
|
|
71
|
+
else if (query === null) {
|
|
72
|
+
query = a;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
error(exports.USAGE, ERROR_REASON.USAGE);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (query === null)
|
|
79
|
+
error(exports.USAGE, ERROR_REASON.USAGE);
|
|
80
|
+
const audience = (0, messages_cjs_1.audienceOf)(process.env);
|
|
81
|
+
const cards = cardsFor(query, audience);
|
|
82
|
+
if (cards === null)
|
|
83
|
+
return;
|
|
84
|
+
if (state === null) {
|
|
85
|
+
output(cards, true, JSON.stringify(cards) + '\n');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const card = Object.prototype.hasOwnProperty.call(cards, state) ? cards[state] : undefined;
|
|
89
|
+
if (card === undefined)
|
|
90
|
+
return;
|
|
91
|
+
const body = (0, messages_cjs_1.msg)(card.code, {}, { audience });
|
|
92
|
+
if (raw) {
|
|
93
|
+
output({ code: card.code, text: body }, false);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
output({ code: card.code, text: body }, true, body + (details ? '\nDetails:' : '') + '\n');
|
|
97
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The hooks' light catalog entry (Phase 43, MSG-06; D-18, D-20).
|
|
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/hook-messages.cjs`; do NOT
|
|
7
|
+
* hand-edit the compiled output.
|
|
8
|
+
*
|
|
9
|
+
* The statusline runs on every prompt, so it cannot load the whole catalog
|
|
10
|
+
* (`messages.cjs` pulls in every slice, ~73 KB). This module imports only
|
|
11
|
+
* `./audience.cjs`, `./message-labels.cjs` and `./messages/hook.cjs`, plus
|
|
12
|
+
* `import type { CatalogEntry } from './messages.cjs'`, which compiles away.
|
|
13
|
+
* It carries its own copy of the engine's control-character regex, slot regex,
|
|
14
|
+
* ECHO_CAP, sanitise, fill and pick (about 25 lines), kept equal to
|
|
15
|
+
* `src/messages.cts` by the parity test in tests/hook-messages.test.cjs
|
|
16
|
+
* (PITFALLS #19).
|
|
17
|
+
*
|
|
18
|
+
* Its functions never throw: an unknown code, a bad param or an unreadable
|
|
19
|
+
* person file returns the fallback text. The hooks wrap their calls in
|
|
20
|
+
* try/catch as well and fall back to today's text when this module cannot load
|
|
21
|
+
* (D-20).
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.audienceOf = audienceOf;
|
|
25
|
+
exports.developerText = developerText;
|
|
26
|
+
exports.text = text;
|
|
27
|
+
exports.msg = msg;
|
|
28
|
+
exports.label = label;
|
|
29
|
+
exports.statusWord = statusWord;
|
|
30
|
+
const audience_cjs_1 = require("./audience.cjs");
|
|
31
|
+
const message_labels_cjs_1 = require("./message-labels.cjs");
|
|
32
|
+
const hook_cjs_1 = require("./messages/hook.cjs");
|
|
33
|
+
// Copies of the engine's constants; see the header.
|
|
34
|
+
const CONTROL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\p{Cf}]/gu;
|
|
35
|
+
const SLOT_RE = /\{([a-z][a-z0-9_]*)\}/g;
|
|
36
|
+
const ECHO_CAP = 120;
|
|
37
|
+
/** The shape ship.md writes into the STATE.md status: `Phase N shipped — PR #N`. Anchored, linear. */
|
|
38
|
+
const SHIPPED_STATUS_RE = /^Phase (\S+) shipped — PR #(\d+)$/;
|
|
39
|
+
// Lookup only: a Map, so `__proto__` and `constructor` are unknown codes.
|
|
40
|
+
const ENTRIES = new Map(Object.entries(hook_cjs_1.hook));
|
|
41
|
+
function sanitize(value) {
|
|
42
|
+
let raw;
|
|
43
|
+
if (typeof value === 'number')
|
|
44
|
+
raw = String(value);
|
|
45
|
+
else if (typeof value === 'string')
|
|
46
|
+
raw = value;
|
|
47
|
+
else
|
|
48
|
+
return '';
|
|
49
|
+
const collapsed = raw.replace(CONTROL_CHARS_RE, '').replace(/\s+/g, ' ').trim();
|
|
50
|
+
return collapsed.length <= ECHO_CAP ? collapsed : `${collapsed.slice(0, ECHO_CAP)}…`;
|
|
51
|
+
}
|
|
52
|
+
function fill(template, params, clean) {
|
|
53
|
+
return template.replace(SLOT_RE, (match, name) => {
|
|
54
|
+
if (!Object.hasOwn(params, name))
|
|
55
|
+
return clean ? match : '';
|
|
56
|
+
const value = params[name];
|
|
57
|
+
if (!clean)
|
|
58
|
+
return typeof value === 'string' || typeof value === 'number' ? String(value) : '';
|
|
59
|
+
return sanitize(value);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function pick(entry, audience) {
|
|
63
|
+
if (audience === 'business')
|
|
64
|
+
return entry.business ?? entry.engineer ?? entry.developer;
|
|
65
|
+
if (audience === 'engineer')
|
|
66
|
+
return entry.engineer ?? entry.developer;
|
|
67
|
+
return entry.developer;
|
|
68
|
+
}
|
|
69
|
+
/** The audience for this process; developer on any failure. */
|
|
70
|
+
function audienceOf(env = process.env) {
|
|
71
|
+
try {
|
|
72
|
+
return (0, audience_cjs_1.resolveAudience)(env, (0, audience_cjs_1.readPerson)(env)).audience;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return 'developer';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** The developer text, params untouched. An unknown code returns the code. */
|
|
79
|
+
function developerText(code, params = {}) {
|
|
80
|
+
const entry = ENTRIES.get(code);
|
|
81
|
+
if (entry === undefined)
|
|
82
|
+
return code;
|
|
83
|
+
return fill(entry.developer, params ?? {}, false);
|
|
84
|
+
}
|
|
85
|
+
/** The text for an audience with no Ref line. An unknown code returns the code. */
|
|
86
|
+
function text(code, params = {}, audience = 'developer') {
|
|
87
|
+
const entry = ENTRIES.get(code);
|
|
88
|
+
if (entry === undefined)
|
|
89
|
+
return code;
|
|
90
|
+
if (audience === 'developer')
|
|
91
|
+
return developerText(code, params);
|
|
92
|
+
return fill(pick(entry, audience), params ?? {}, true);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The message for an audience: business and engineer messages that are not
|
|
96
|
+
* parts end with the Ref line. An unknown code returns `fallback` when it is a
|
|
97
|
+
* string, else the code.
|
|
98
|
+
*/
|
|
99
|
+
function msg(code, params = {}, audience = 'developer', fallback) {
|
|
100
|
+
const entry = ENTRIES.get(code);
|
|
101
|
+
if (entry === undefined)
|
|
102
|
+
return typeof fallback === 'string' ? fallback : code;
|
|
103
|
+
if (audience === 'developer')
|
|
104
|
+
return developerText(code, params);
|
|
105
|
+
const body = text(code, params, audience);
|
|
106
|
+
return entry.part === true ? body : `${body}\nRef: ${code}`;
|
|
107
|
+
}
|
|
108
|
+
/** The display word for a canonical value; the value itself for a developer, an unknown domain or an unknown value. */
|
|
109
|
+
function label(domain, canonical, audience = 'developer') {
|
|
110
|
+
try {
|
|
111
|
+
return (0, message_labels_cjs_1.label)(domain, canonical, { audience });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return canonical;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The display text for a STATE.md `status`. A lifecycle word is relabelled
|
|
119
|
+
* through `state.status`; the ship shape renders through
|
|
120
|
+
* `hook.statusline.status_shipped`; anything else shows as written. A non-string
|
|
121
|
+
* returns ''.
|
|
122
|
+
*/
|
|
123
|
+
function statusWord(status, audience = 'developer') {
|
|
124
|
+
if (typeof status !== 'string')
|
|
125
|
+
return '';
|
|
126
|
+
if (audience === 'developer')
|
|
127
|
+
return status;
|
|
128
|
+
const shipped = SHIPPED_STATUS_RE.exec(status);
|
|
129
|
+
if (shipped !== null)
|
|
130
|
+
return text('hook.statusline.status_shipped', { number: shipped[1], pr: shipped[2] }, audience);
|
|
131
|
+
return label('state.status', status, audience);
|
|
132
|
+
}
|