@pareto4/pp-core 1.21.1 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/gemini-extension.json +1 -1
- package/package.json +2 -2
- 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-processes.cjs +97 -4
- 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/io.cjs +5 -0
- package/pp-core/bin/pp-tools.cjs +25 -1
- package/pp-core/references/discovery-build-consumption.md +5 -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/workflows/discovery-sprint/session-13-gates.md +2 -0
- 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 +2 -1
- package/pp-core/workflows/discovery-sprint.md +2 -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 +2 -2
- package/pp-core/workflows/discuss-phase/discovery.md +2 -2
- package/scripts/run-tests.cjs +9 -6
|
@@ -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
|
+
};
|
package/pp-core/bin/lib/io.cjs
CHANGED
|
@@ -225,6 +225,11 @@ const ERROR_REASON = Object.freeze({
|
|
|
225
225
|
DISCOVERY_SESSION_CHECK_FAILED: 'discovery_session_check_failed',
|
|
226
226
|
DISCOVERY_DEVIATIONS_CHECK_FAILED: 'discovery_deviations_check_failed',
|
|
227
227
|
DISCOVERY_DEVIATIONS_UNAPPROVED: 'discovery_deviations_unapproved',
|
|
228
|
+
// discovery tests register (Session 9 — testing and UAT)
|
|
229
|
+
DISCOVERY_TESTS_CHECK_FAILED: 'discovery_tests_check_failed',
|
|
230
|
+
DISCOVERY_TESTS_INCOMPLETE: 'discovery_tests_incomplete',
|
|
231
|
+
// discovery flow page (Session 3 Step 5C — iterate a swimlane)
|
|
232
|
+
DISCOVERY_FLOW_PAGE_FAILED: 'discovery_flow_page_failed',
|
|
228
233
|
// timeline (Phase 35: end with no matching open run)
|
|
229
234
|
TIMELINE_NO_OPEN_RUN: 'timeline_no_open_run',
|
|
230
235
|
// generic
|
package/pp-core/bin/pp-tools.cjs
CHANGED
|
@@ -2773,13 +2773,37 @@ async function runCommand(command, args, cwd, raw, defaultValue, originalCommand
|
|
|
2773
2773
|
} catch (deviationsCheckErr) {
|
|
2774
2774
|
error(`discovery deviations-check: ${deviationsCheckErr && deviationsCheckErr.message ? deviationsCheckErr.message : String(deviationsCheckErr)}`, ERROR_REASON.UNKNOWN);
|
|
2775
2775
|
}
|
|
2776
|
+
} else if (subcommand === 'tests-check') {
|
|
2777
|
+
// discovery tests-check [--require-complete] — _tests.md structure:
|
|
2778
|
+
// one risk row per main process with the matrix rating, end-to-end
|
|
2779
|
+
// for every high risk unless a D-09 decision says why, focus-area
|
|
2780
|
+
// subjects resolving, one acceptance row per story.
|
|
2781
|
+
// --require-complete also fails on an open story or an uncited focus
|
|
2782
|
+
// area (discovery_tests_incomplete).
|
|
2783
|
+
const testsRegister = require('./lib/discovery-tests.cjs');
|
|
2784
|
+
try {
|
|
2785
|
+
testsRegister.cmdDiscoveryTestsCheck(cwd, args, raw);
|
|
2786
|
+
} catch (testsCheckErr) {
|
|
2787
|
+
error(`discovery tests-check: ${testsCheckErr && testsCheckErr.message ? testsCheckErr.message : String(testsCheckErr)}`, ERROR_REASON.UNKNOWN);
|
|
2788
|
+
}
|
|
2789
|
+
} else if (subcommand === 'flow-page') {
|
|
2790
|
+
// discovery flow-page <MP-nn> [--out <file.html>] — render one main
|
|
2791
|
+
// process (swimlane, sub-processes, step matrix) from _processes.md
|
|
2792
|
+
// as a page the agent publishes and republishes while the room
|
|
2793
|
+
// iterates the diagram (Session 3 Step 5C).
|
|
2794
|
+
const flowPage = require('./lib/discovery-flow-page.cjs');
|
|
2795
|
+
try {
|
|
2796
|
+
flowPage.cmdDiscoveryFlowPage(cwd, args, raw);
|
|
2797
|
+
} catch (flowPageErr) {
|
|
2798
|
+
error(`discovery flow-page: ${flowPageErr && flowPageErr.message ? flowPageErr.message : String(flowPageErr)}`, ERROR_REASON.UNKNOWN);
|
|
2799
|
+
}
|
|
2776
2800
|
} else {
|
|
2777
2801
|
// WIRING, NOT HELP TEXT: tests/discovery-artifacts.test.cjs derives each
|
|
2778
2802
|
// subcommand spelling by regexing `<namespace>_<subcommand>_failed` out of
|
|
2779
2803
|
// the engine's exported exit identity, splits this string on commas and
|
|
2780
2804
|
// asserts membership. A spelling missing here is a command the engine
|
|
2781
2805
|
// claims to be and the router does not dispatch.
|
|
2782
|
-
error('Unknown discovery subcommand. Available: rollup, input-check, research-check, screen-check, element-sweep, rebaseline-check, vault-check, charter-check, entity-check, process-check, rules-check, stories-check, phases-check, for-phase, roles-check, integrations-check, migration-check, deviations-check, session-check', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2806
|
+
error('Unknown discovery subcommand. Available: rollup, input-check, research-check, screen-check, element-sweep, rebaseline-check, vault-check, charter-check, entity-check, process-check, rules-check, stories-check, phases-check, for-phase, roles-check, integrations-check, migration-check, deviations-check, session-check, tests-check, flow-page', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
2783
2807
|
}
|
|
2784
2808
|
break;
|
|
2785
2809
|
}
|
|
@@ -43,6 +43,7 @@ blocked by it. The verifier takes the acceptance criteria of the extract's `_req
|
|
|
43
43
|
| `_screens.md` § Screens, § No screen (still MVP) | screens whose `Requirements` cites one; no-screen rows keyed by one | ui-phase (exists); planner |
|
|
44
44
|
| `_roles.md` § Permissions, § Roles, § Screens | the permission rows whose `Requirements` cites one; the § Roles row of every role those permissions name; the § Screens rows of those roles and of the phase's screens (`references/discovery-access.md`) | planner auth phase; forbidden-path tests |
|
|
45
45
|
| `_integrations.md` § Interfaces, § Contract, § Errors | the interface rows whose `Requirements` cites one or that a selected step names in its `Integration` cell (`IF-nn`); their contract and error rows (`references/discovery-integrations.md`) | planner integration phases |
|
|
46
|
+
| `_tests.md` § Process risk, § Focus areas, § Acceptance | the risk rows with `End-to-end` `yes` of the main processes touched; the focus-area rows whose `Requirements` cites one; the acceptance rows of the selected stories (`references/discovery-tests.md`) | planner end-to-end and extra test tasks; verifier UAT (role and test data per story) |
|
|
46
47
|
| `_migration.md`, `_deviations.md` | generic: every table carrying a `Requirements` (or `Requirement IDs`) column contributes its citing rows. A table with no requirement column is listed as *not filterable* in the extract, never silently dropped — `_deviations.md § Deviations` always is | planner migration phase; `pp-vault-review` reads `_deviations.md` whole (§ Deviations read contract) |
|
|
47
48
|
| `_phases.md` § Phases | the phase's requirement IDs when ROADMAP.md has none yet | ROADMAP.md (handoff 5f, verbatim); `for-phase` |
|
|
48
49
|
|
|
@@ -69,7 +70,7 @@ table must carry a `Requirements` column; the column name is the join, not the p
|
|
|
69
70
|
| `Requirements` | comma list of `_requirements.md` IDs (`[A-Z][A-Z0-9]*-\d+`), at least one |
|
|
70
71
|
| `Story` | free text — `As a <persona>, I want <goal> so that <benefit>`; pipes escaped `\|` |
|
|
71
72
|
| `Acceptance criteria` | free text, non-empty, and a criterion — never a pointer such as `As in F-01`; the verifier reads it verbatim as the UAT |
|
|
72
|
-
| `Source step` | comma list of `MP-<nn> #<cell>` — the main process and the `#` cell of the step-matrix row the story was derived from, copied verbatim (`MP-05 #05.1`, `MP-01 #3`) —
|
|
73
|
+
| `Source step` | comma list of `MP-<nn> #<cell>` — the main process and the `#` cell of the step-matrix row the story was derived from, copied verbatim (`MP-05 #05.1`, `MP-01 #3`); never `—` — a story comes from a step |
|
|
73
74
|
|
|
74
75
|
### Parser contract — `pp_run query discovery.stories-check`
|
|
75
76
|
|
|
@@ -82,7 +83,9 @@ table must carry a `Requirements` column; the column name is the join, not the p
|
|
|
82
83
|
| `empty_cell` | `Story` or `Acceptance criteria` is empty (the cell is named) |
|
|
83
84
|
| `acceptance_is_pointer` | `Acceptance criteria` starts with `As in`, `See`, `Zie` or `Idem` — a pointer, not a criterion the verifier can run |
|
|
84
85
|
| `no_requirement` / `unknown_requirement_id` | no requirement cited, or one of the wrong shape or absent from `_requirements.md` (resolution skipped when that file is absent) |
|
|
85
|
-
| `bad_source_step` / `unknown_process_id` / `unknown_process_step` | a `Source step` token is
|
|
86
|
+
| `bad_source_step` / `unknown_process_id` / `unknown_process_step` | a `Source step` token is not `MP-nn #<cell>`; the main process is not in `_processes.md` § To-be; the step row is not in its matrix (both resolutions skipped when `_processes.md` is absent) |
|
|
87
|
+
| `no_source_step` | `Source step` is `—` or empty — every story is tied to the step it came from |
|
|
88
|
+
| `step_without_story` | a step row whose actor is a persona and that cites at least one `MVP` requirement (not `N-`/`T-`) is no story's source step — the stories do not cover the process. Without a parseable `_requirements.md` every persona step needs one |
|
|
86
89
|
| `mvp_requirement_without_story` | an `MVP` requirement that at least one persona-actor step row cites has no story. Mirrors Step 5E: stories come from the step rows whose actor is not `System`, so a requirement realised only by `System` steps, or cited by no step at all (engagement process rows), needs none; `N-`/`T-` rows never do. Without `_processes.md` only the prefix exemption applies |
|
|
87
90
|
| `requirements_unparseable` / `artifact_unreadable` | `_requirements.md` present but failing `rollup --check`; an artifact present but unreadable |
|
|
88
91
|
|
|
@@ -102,7 +102,7 @@ Non-functional requirements are not in this list: they are `N-` rows of
|
|
|
102
102
|
|
|
103
103
|
The canonical artifacts — `_requirements.md`, `_screens.md`, `_feedback.md`, `_sweep.md`,
|
|
104
104
|
`_processes.md`, `_entities.md`, `_rules.md`, `_glossary.md`, `_stories.md`, `_phases.md`,
|
|
105
|
-
`_roles.md`, `_integrations.md`, `_migration.md`, `_deviations.md`, the research registers and
|
|
105
|
+
`_roles.md`, `_integrations.md`, `_migration.md`, `_deviations.md`, `_tests.md`, the research registers and
|
|
106
106
|
the Project Charter — are the single source of truth for the life of the engagement, through the
|
|
107
107
|
build and after it. The session files are the facilitation record behind them.
|
|
108
108
|
|
|
@@ -11,7 +11,7 @@ lazily; it is never `@`-imported.
|
|
|
11
11
|
| Session | `_processes.md` | `_rules.md` | `_glossary.md` |
|
|
12
12
|
|---|---|---|---|
|
|
13
13
|
| 2 | created from the template; `## As-is` rows (`PRC-`) from topic 1 | — | created from the template; terms the client used for the same thing |
|
|
14
|
-
| 3 | Step
|
|
14
|
+
| 3 | Step 5B `## To-be` rows (`MP-`, each with `Replaces`) — the full process inventory; per main process, Step 5C its swimlane under `## Flows` with every step node labelled with its `#`, its `## Sub-processes` rows (`MP-nn.k`) and its step matrix under `## Steps`, `Exception` column filled; 5D back-fills `Requirements` | Step 5D′ mints `BR-` rows from the step matrices | topic 1 glossary sweep: one row per term, synonyms under `Do not use` |
|
|
15
15
|
| 8 | `process-check` and `rules-check` in the Closing Step (reporting) | | |
|
|
16
16
|
| 13 | both in the Step 0 gates (reporting) | | |
|
|
17
17
|
| handoff | `process-check --require-cited` and `rules-check` hard-block | | |
|
|
@@ -48,13 +48,17 @@ Illustration only — not part of the template, names invented:
|
|
|
48
48
|
|---|---|---|---|---|---|---|---|
|
|
49
49
|
| 01.1 | Create task | Team member | Task form | Task [C] | — | Title missing → form error, nothing saved | K-06 |
|
|
50
50
|
|
|
51
|
+
| ID | Sub-process | Steps |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| MP-01.1 | Register the request | 01.1, 01.2 |
|
|
54
|
+
|
|
51
55
|
Bootstrap template — Session 2 copies it verbatim to `.planning/discovery/_processes.md`, replacing
|
|
52
56
|
`[PROJECT_NAME]`:
|
|
53
57
|
|
|
54
58
|
```markdown
|
|
55
59
|
# Processes — [PROJECT_NAME]
|
|
56
60
|
|
|
57
|
-
**Conventions:** pp-core reference `references/discovery-processes.md`. `PRC-` rows are the as-is processes (Session 2), `MP-` rows the to-be main processes (Session 3) with the as-is processes each replaces or `new`. One swimlane and one step matrix per main process
|
|
61
|
+
**Conventions:** pp-core reference `references/discovery-processes.md`. `PRC-` rows are the as-is processes (Session 2), `MP-` rows the to-be main processes (Session 3) with the as-is processes each replaces or `new`. Every main process has sub-processes `MP-nn.k`, each listing its steps by `#`; every step belongs to exactly one. One swimlane and one step matrix per main process; the swimlane labels every step node with its `#`. Nothing is renumbered or deleted; a change after sign-off edits the row in place and appends `→ amended by D-<ss>-<n>`.
|
|
58
62
|
|
|
59
63
|
## As-is
|
|
60
64
|
|
|
@@ -66,12 +70,23 @@ Bootstrap template — Session 2 copies it verbatim to `.planning/discovery/_pro
|
|
|
66
70
|
| ID | Main process | Replaces | Trigger | End state | Primary actor | EPICs |
|
|
67
71
|
|----|--------------|----------|---------|-----------|---------------|-------|
|
|
68
72
|
|
|
73
|
+
## Sub-processes
|
|
74
|
+
|
|
75
|
+
| ID | Sub-process | Steps |
|
|
76
|
+
|----|-------------|-------|
|
|
77
|
+
|
|
69
78
|
## Flows
|
|
70
79
|
|
|
71
80
|
## Steps
|
|
72
81
|
```
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
An engagement whose `_processes.md` predates the Session 3 rework has no `## Sub-processes`
|
|
84
|
+
section; `process-check` then skips the sub-process and flow-coverage rules below. Adding the
|
|
85
|
+
section (empty) opts the engagement in.
|
|
86
|
+
|
|
87
|
+
Under `## Flows`, Step 5C adds one `### MP-nn — <name>` heading per main process holding a swimlane.
|
|
88
|
+
Every step node carries its matrix `#` at the start of its label, so the diagram and the matrix
|
|
89
|
+
describe the same steps; decisions and end states need no number:
|
|
75
90
|
|
|
76
91
|
````markdown
|
|
77
92
|
### MP-01 — Handle a task
|
|
@@ -79,12 +94,14 @@ Under `## Flows`, Step 5B adds one `### MP-nn — <name>` heading per main proce
|
|
|
79
94
|
```mermaid
|
|
80
95
|
flowchart LR
|
|
81
96
|
subgraph Team member
|
|
82
|
-
S1[Create task] --> S2[Assign]
|
|
97
|
+
S1["01.1 Create task"] --> S2["01.2 Assign"]
|
|
98
|
+
S2 --> D1{Urgent?}
|
|
83
99
|
end
|
|
84
100
|
subgraph System
|
|
85
|
-
S3[Notify assignee]
|
|
101
|
+
S3["01.3 Notify assignee"]
|
|
86
102
|
end
|
|
87
|
-
|
|
103
|
+
D1 -->|yes| S3
|
|
104
|
+
D1 -->|no| E1([Task queued])
|
|
88
105
|
```
|
|
89
106
|
````
|
|
90
107
|
|
|
@@ -117,6 +134,13 @@ column name and the reason code — never cell prose. Absent `_processes.md` →
|
|
|
117
134
|
| `### MP-nn — <name>` under Flows and Steps | names a To-be row | `unknown_process` |
|
|
118
135
|
| a main process without a `### MP-nn` under Flows / Steps | one swimlane and one matrix each | `flow_missing`, `steps_missing` naming the MP |
|
|
119
136
|
| a flow body | a ```` ```mermaid ```` block opening with `flowchart` and holding at least one `subgraph` | `flow_not_swimlane` |
|
|
137
|
+
| `## Sub-processes` (optional — absent on engagements that predate it) | one table with the template's columns | `bad_table_shape (Sub-processes)` |
|
|
138
|
+
| Sub-processes `ID` | `MP-nn.k` — a To-be main process and `k` from 1, unique, contiguous per main process | `bad_subprocess`, `duplicate_id`, `unknown_process`, `sequence_gap` naming `MP-nn.k` |
|
|
139
|
+
| Sub-processes `Sub-process` | non-empty | `empty_cell (Sub-process)` |
|
|
140
|
+
| Sub-processes `Steps` | a comma list of `#` cells of that main process's step matrix | `subprocess_without_steps`, `unknown_step`, `step_in_two_subprocesses` naming `MP-nn #<cell>` |
|
|
141
|
+
| every main process, when the section is on file | at least one sub-process | `subprocess_missing` |
|
|
142
|
+
| every step row, when the section is on file | listed by exactly one sub-process | `step_without_subprocess` naming `MP-nn #<cell>` |
|
|
143
|
+
| every step row, when the section is on file | its `#` appears as a whole token in its main process's flow | `step_not_in_flow` naming `MP-nn #<cell>` |
|
|
120
144
|
| step `#`, `Step` | non-empty | `empty_cell (Step)` |
|
|
121
145
|
| `UI`, `Integration`, `Exception` | free text (`—` allowed) | **not mechanically checked** — the Exception column must exist, its content is the facilitator's |
|
|
122
146
|
| `Data objects` | `—`, or a comma list of `Name [C/R/U/D]` tokens | `bad_data_object`; whether each name is an entity is `entity-check`'s `unknown_entity` |
|
|
@@ -126,7 +150,7 @@ column name and the reason code — never cell prose. Absent `_processes.md` →
|
|
|
126
150
|
|
|
127
151
|
**Exit identity.** `discovery_process_check_failed` for structural faults;
|
|
128
152
|
`discovery_process_steps_uncited` when every fault is an `uncited_step`. Payload: `ok`, `present`,
|
|
129
|
-
`file`, `as_is`, `to_be`, `flows`, `steps`, `uncited`.
|
|
153
|
+
`file`, `as_is`, `to_be`, `flows`, `steps`, `sub_processes`, `uncited`.
|
|
130
154
|
|
|
131
155
|
## `_rules.md`
|
|
132
156
|
|
|
@@ -61,7 +61,7 @@ before setting the score; it exits 0 before a session may read 100.
|
|
|
61
61
|
|
|
62
62
|
The session files and the registers they point at (`_requirements.md`, `_entities.md`,
|
|
63
63
|
`_processes.md`, `_rules.md`, `_glossary.md`, `_screens.md`, `_roles.md`,
|
|
64
|
-
`_integrations.md`, `_migration.md`, `_deviations.md`) are the only record of what a session
|
|
64
|
+
`_integrations.md`, `_migration.md`, `_deviations.md`, `_tests.md`) are the only record of what a session
|
|
65
65
|
produced. Nothing is summarised into a second file at session close: later sessions read the
|
|
66
66
|
earlier session's Notes Template sections (the **Context-Awareness Rules** table names
|
|
67
67
|
which), the Session 13 cross-reference reads the session files, and `/pp-discovery-to-build`
|