@pareto4/pp-core 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/pp-nyquist-auditor.md +19 -14
- package/agents/pp-phase-researcher.md +10 -0
- package/agents/pp-plan-checker.md +59 -36
- package/agents/pp-planner.md +49 -53
- package/agents/pp-vault-reviewer.md +2 -0
- package/agents/pp-verifier.md +15 -60
- package/commands/pp-autonomous.md +2 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-plan-review-convergence.md +2 -1
- package/gemini-extension.json +1 -1
- package/package.json +6 -3
- package/pareto-skills/_shared/pp-claude-md-protocol.md +1 -1
- package/pareto-skills/pp-project-setup/SKILL.md +18 -343
- package/pp-core/bin/lib/capability-activation.cjs +77 -0
- package/pp-core/bin/lib/capability-registry.cjs +15 -15
- package/pp-core/bin/lib/capability-state.cjs +8 -18
- package/pp-core/bin/lib/ceremony.cjs +1342 -0
- package/pp-core/bin/lib/check-command-router.cjs +62 -1
- package/pp-core/bin/lib/commands.cjs +29 -3
- package/pp-core/bin/lib/config.cjs +5 -0
- package/pp-core/bin/lib/discovery-access.cjs +276 -0
- package/pp-core/bin/lib/discovery-charter.cjs +664 -0
- package/pp-core/bin/lib/discovery-deviations.cjs +264 -0
- package/pp-core/bin/lib/discovery-entities.cjs +323 -0
- package/pp-core/bin/lib/discovery-for-phase.cjs +628 -0
- package/pp-core/bin/lib/discovery-integrations.cjs +286 -0
- package/pp-core/bin/lib/discovery-migration.cjs +246 -0
- package/pp-core/bin/lib/discovery-phases.cjs +233 -0
- package/pp-core/bin/lib/discovery-processes.cjs +355 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +5 -4
- package/pp-core/bin/lib/discovery-rollup.cjs +8 -7
- package/pp-core/bin/lib/discovery-rules.cjs +216 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1 -1
- package/pp-core/bin/lib/discovery-session-check.cjs +264 -0
- package/pp-core/bin/lib/discovery-stories.cjs +273 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +1 -1
- package/pp-core/bin/lib/discovery-table.cjs +55 -0
- package/pp-core/bin/lib/init.cjs +50 -1
- package/pp-core/bin/lib/io.cjs +24 -0
- package/pp-core/bin/lib/loop-resolver.cjs +26 -12
- package/pp-core/bin/lib/phase.cjs +39 -3
- package/pp-core/bin/lib/plan-sizing.cjs +346 -0
- package/pp-core/bin/lib/test-substance.cjs +3190 -0
- package/pp-core/bin/lib/timeline.cjs +605 -0
- package/pp-core/bin/lib/vault-standards-review.cjs +50 -2
- package/pp-core/bin/pp-tools.cjs +214 -5
- package/pp-core/bin/shared/ceremony-gates.json +17 -0
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/ceremony.md +81 -0
- package/pp-core/references/discovery-access.md +112 -0
- package/pp-core/references/discovery-build-consumption.md +256 -0
- package/pp-core/references/discovery-charter.md +127 -0
- package/pp-core/references/discovery-data-model.md +145 -0
- package/pp-core/references/discovery-id-conventions.md +50 -14
- package/pp-core/references/discovery-integrations.md +151 -0
- package/pp-core/references/discovery-processes.md +190 -0
- package/pp-core/references/discovery-rebaseline.md +2 -2
- package/pp-core/references/discovery-research-track.md +5 -6
- package/pp-core/references/discovery-session-output.md +70 -0
- package/pp-core/references/discovery-standards.md +107 -0
- package/pp-core/references/discovery-traceability.md +2 -2
- package/pp-core/references/few-shot-examples/plan-checker.md +8 -8
- package/pp-core/references/phase-timeline.md +107 -0
- package/pp-core/references/planner-antipatterns.md +32 -0
- package/pp-core/references/planner-discovery-extract.md +35 -0
- package/pp-core/references/planner-guidance.md +17 -14
- package/pp-core/references/planner-revision.md +1 -1
- package/pp-core/references/planning-config.md +3 -2
- package/pp-core/references/test-substance.md +324 -0
- package/pp-core/references/verifier-discovery-conformance.md +36 -0
- package/pp-core/references/verifier-stub-patterns.md +51 -0
- package/pp-core/templates/VALIDATION.md +1 -0
- package/pp-core/templates/phase-prompt.md +19 -7
- package/pp-core/templates/project-charter.md +161 -100
- package/pp-core/templates/project.md +3 -3
- package/pp-core/templates/requirements.md +18 -0
- package/pp-core/templates/roadmap.md +1 -1
- package/pp-core/templates/verification-report.md +17 -1
- package/pp-core/workflows/audit-milestone.md +4 -1
- package/pp-core/workflows/autonomous.md +11 -8
- package/pp-core/workflows/code-review.md +15 -6
- package/pp-core/workflows/discovery-sprint/charter.md +114 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +1 -1
- package/pp-core/workflows/discovery-sprint/prototype.md +4 -4
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +10 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +5 -2
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +27 -5
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +56 -31
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +1 -1
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +42 -19
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +27 -12
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +41 -23
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +50 -23
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +26 -11
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +28 -9
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +21 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +24 -9
- package/pp-core/workflows/discovery-sprint.md +12 -15
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +80 -29
- package/pp-core/workflows/discovery-to-build/steps/{screen-inventory-gate.md → handoff-gates.md} +111 -3
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +12 -9
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +27 -26
- package/pp-core/workflows/discovery-to-build.md +70 -34
- package/pp-core/workflows/discuss-phase/discovery.md +83 -0
- package/pp-core/workflows/discuss-phase/templates/context.md +4 -0
- package/pp-core/workflows/discuss-phase.md +10 -7
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +7 -7
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +3 -2
- package/pp-core/workflows/execute-phase.md +14 -3
- package/pp-core/workflows/new-project.md +3 -3
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +7 -3
- package/pp-core/workflows/plan-phase/steps/discovery-extract.md +41 -0
- package/pp-core/workflows/plan-phase/steps/prd-express.md +104 -0
- package/pp-core/workflows/plan-phase.md +62 -124
- package/pp-core/workflows/plan-review-convergence.md +6 -3
- package/pp-core/workflows/ship.md +2 -2
- package/pp-core/workflows/stats.md +19 -4
- package/pp-core/workflows/validate-phase.md +19 -4
- package/pp-core/workflows/vault-review.md +5 -0
- package/pp-core/workflows/verify-phase.md +1 -1
- package/pp-core/workflows/verify-work.md +1 -1
- package/scripts/gen-capability-registry.cjs +43 -45
- package/scripts/run-tests.cjs +54 -30
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +0 -272
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discovery user-stories check (Discovery Sprint improvement plan, Batch 4).
|
|
4
|
+
*
|
|
5
|
+
* `.planning/discovery/_stories.md` is the register of user stories Session 3
|
|
6
|
+
* Step 5E mints from the step matrices: `US-nn`, the EPIC it belongs to, the
|
|
7
|
+
* requirements it realises, the story itself, its acceptance criteria and the
|
|
8
|
+
* step it was derived from. Before Batch 4 the stories lived only in Session
|
|
9
|
+
* 3's Notes Template, where no build agent read them (review F-4); the
|
|
10
|
+
* verifier now takes its UAT from this register through `for-phase`. This
|
|
11
|
+
* module checks the structure:
|
|
12
|
+
*
|
|
13
|
+
* - the `## Stories` table exists with the template's columns;
|
|
14
|
+
* - `US-` IDs are well-formed, unique, contiguous;
|
|
15
|
+
* - `EPIC` is one `E-nn`;
|
|
16
|
+
* - every story cites at least one requirement, each existing in
|
|
17
|
+
* `_requirements.md` when it is on file and parseable;
|
|
18
|
+
* - `Story` and `Acceptance criteria` are non-empty;
|
|
19
|
+
* - `Acceptance criteria` is a criterion, not a pointer (`As in F-01`);
|
|
20
|
+
* - `Source step` is `—` or a comma list of `MP-nn #<cell>`, the main process
|
|
21
|
+
* and the step row existing in `_processes.md` when it is on file;
|
|
22
|
+
* - every MVP-disposition requirement that a persona-actor step cites has at
|
|
23
|
+
* least one story (the `N-`/`T-` prefixes never need one; a requirement
|
|
24
|
+
* only `System` steps realise, or no step at all, has no user to tell it).
|
|
25
|
+
*
|
|
26
|
+
* Posture (D-43/D-53): reports at Sessions 8 and 13; hard-blocks at
|
|
27
|
+
* `/pp-discovery-to-build`. Absent artifact → exit 0.
|
|
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 STORIES_FILE = `${DISCOVERY_DIR}/_stories.md`;
|
|
44
|
+
const REQUIREMENTS_FILE = `${DISCOVERY_DIR}/_requirements.md`;
|
|
45
|
+
const PROCESSES_FILE = `${DISCOVERY_DIR}/_processes.md`;
|
|
46
|
+
const STORIES_SECTION = 'Stories';
|
|
47
|
+
const STORIES_HEADER = Object.freeze(['ID', 'EPIC', 'Requirements', 'Story', 'Acceptance criteria', 'Source step']);
|
|
48
|
+
const US_RE = /^US-(\d{2})$/;
|
|
49
|
+
const EPIC_RE = /^E-\d{2}$/;
|
|
50
|
+
const REQUIREMENT_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
51
|
+
/** `MP-nn #<cell>` — the main process and the step matrix's `#` cell, copied verbatim (`01.3`, `5`). */
|
|
52
|
+
const SOURCE_STEP_RE = /^(MP-\d{2}) #(\S+)$/;
|
|
53
|
+
const MP_HEADING_RE = /^(MP-\d{2})\s+—\s+\S/;
|
|
54
|
+
const SYSTEM_ACTOR = 'System';
|
|
55
|
+
/** An acceptance-criteria cell that only points elsewhere is no criterion the verifier can run. */
|
|
56
|
+
const POINTER_AC_RE = /^(as in|see|zie|idem)\b/i;
|
|
57
|
+
const PENDING = '—';
|
|
58
|
+
/** Requirement prefixes that need no user story: non-functional and technical rows have no actor. */
|
|
59
|
+
const STORYLESS_PREFIXES = Object.freeze(['N-', 'T-']);
|
|
60
|
+
const USAGE = 'Usage: pp-tools discovery stories-check';
|
|
61
|
+
function readIfPresent(abs) {
|
|
62
|
+
if (!node_fs_1.default.existsSync(abs))
|
|
63
|
+
return { kind: 'absent' };
|
|
64
|
+
try {
|
|
65
|
+
return { kind: 'ok', content: node_fs_1.default.readFileSync(abs, 'utf-8') };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { kind: 'unreadable' };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function sameHeader(actual, expected) {
|
|
72
|
+
return actual.length === expected.length && actual.every((c, i) => c.trim().toLowerCase() === expected[i].toLowerCase());
|
|
73
|
+
}
|
|
74
|
+
function splitList(cell) {
|
|
75
|
+
return cell.split(',').map((t) => t.trim()).filter(Boolean);
|
|
76
|
+
}
|
|
77
|
+
/** First-column IDs of the first table under a `## <heading>` of another artifact. */
|
|
78
|
+
function firstColumnOf(content, heading) {
|
|
79
|
+
const section = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === heading);
|
|
80
|
+
if (!section)
|
|
81
|
+
return new Set();
|
|
82
|
+
const tables = table.parseTables(section.body);
|
|
83
|
+
if (tables.length === 0)
|
|
84
|
+
return new Set();
|
|
85
|
+
return new Set(tables[0].rows.map((r) => (r.cells[0] ?? '').trim()).filter(Boolean));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Every step row of `_processes.md` § Steps: its key (`MP-nn #<cell>`), its
|
|
89
|
+
* actors and the requirements it cites. `null` when the section is absent —
|
|
90
|
+
* the step rule then cannot run and only the prefix exemption applies.
|
|
91
|
+
*/
|
|
92
|
+
function stepsOf(content) {
|
|
93
|
+
const steps = table.splitSections(content.split(/\r?\n/), 2).find((s) => s.heading === 'Steps');
|
|
94
|
+
if (!steps)
|
|
95
|
+
return null;
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const sec of table.splitSections(steps.body.map((l) => l.text), 3)) {
|
|
98
|
+
const m = MP_HEADING_RE.exec(sec.heading);
|
|
99
|
+
const tbl = table.parseTables(sec.body)[0];
|
|
100
|
+
if (!m || !tbl)
|
|
101
|
+
continue;
|
|
102
|
+
for (const r of tbl.rows) {
|
|
103
|
+
out.push({ key: `${m[1]} #${(r.cells[0] ?? '').trim()}`, actors: splitList(r.cells[2] ?? ''), requirements: splitList(r.cells[7] ?? '') });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Does this MVP requirement need a user story? Step 5E derives stories from the
|
|
110
|
+
* step rows where the actor is a persona, so the check mirrors that: a
|
|
111
|
+
* requirement needs one when at least one step citing it has an actor other
|
|
112
|
+
* than `System`. Without a step matrix the prefix exemption is all there is.
|
|
113
|
+
*/
|
|
114
|
+
function needsStory(r, steps) {
|
|
115
|
+
if (STORYLESS_PREFIXES.includes(r.prefix))
|
|
116
|
+
return false;
|
|
117
|
+
if (steps === null)
|
|
118
|
+
return true;
|
|
119
|
+
return steps.some((s) => s.requirements.includes(r.id) && s.actors.some((a) => a !== SYSTEM_ACTOR));
|
|
120
|
+
}
|
|
121
|
+
// ─── Check ────────────────────────────────────────────────────────────────────
|
|
122
|
+
function checkStories(content, sources) {
|
|
123
|
+
const violations = [];
|
|
124
|
+
const sections = table.splitSections(content.split(/\r?\n/), 2);
|
|
125
|
+
const section = sections.find((s) => s.heading === STORIES_SECTION);
|
|
126
|
+
if (!section) {
|
|
127
|
+
return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', id: STORIES_SECTION }] };
|
|
128
|
+
}
|
|
129
|
+
const tables = table.parseTables(section.body);
|
|
130
|
+
if (tables.length === 0) {
|
|
131
|
+
return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'missing_section', line: section.line, id: STORIES_SECTION }] };
|
|
132
|
+
}
|
|
133
|
+
if (!sameHeader(tables[0].header, STORIES_HEADER)) {
|
|
134
|
+
return { ok: false, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'bad_table_shape', line: tables[0].line, id: STORIES_SECTION }] };
|
|
135
|
+
}
|
|
136
|
+
const knownIds = sources.requirements instanceof Array ? new Set(sources.requirements.map((r) => r.id)) : null;
|
|
137
|
+
const ids = new Set();
|
|
138
|
+
const seqs = [];
|
|
139
|
+
const storied = new Set();
|
|
140
|
+
for (const row of tables[0].rows) {
|
|
141
|
+
const [id, epic, requirements, story, acceptance, sourceStep] = row.cells.map((c) => c.trim());
|
|
142
|
+
const m = US_RE.exec(id ?? '');
|
|
143
|
+
if (!m) {
|
|
144
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_id', line: row.line });
|
|
145
|
+
}
|
|
146
|
+
else if (ids.has(id)) {
|
|
147
|
+
violations.push({ artifact: STORIES_FILE, reason: 'duplicate_id', line: row.line, id });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
ids.add(id);
|
|
151
|
+
seqs.push(Number(m[1]));
|
|
152
|
+
}
|
|
153
|
+
if (!EPIC_RE.test(epic ?? ''))
|
|
154
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_epic', line: row.line });
|
|
155
|
+
if ((story ?? '') === '')
|
|
156
|
+
violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Story' });
|
|
157
|
+
if ((acceptance ?? '') === '')
|
|
158
|
+
violations.push({ artifact: STORIES_FILE, reason: 'empty_cell', line: row.line, id: 'Acceptance criteria' });
|
|
159
|
+
else if (POINTER_AC_RE.test(acceptance))
|
|
160
|
+
violations.push({ artifact: STORIES_FILE, reason: 'acceptance_is_pointer', line: row.line, id });
|
|
161
|
+
const reqs = splitList(requirements ?? '');
|
|
162
|
+
if (reqs.length === 0)
|
|
163
|
+
violations.push({ artifact: STORIES_FILE, reason: 'no_requirement', line: row.line });
|
|
164
|
+
for (const r of reqs) {
|
|
165
|
+
if (!REQUIREMENT_ID_RE.test(r))
|
|
166
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r.slice(0, 40) });
|
|
167
|
+
else if (knownIds !== null && !knownIds.has(r))
|
|
168
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_requirement_id', line: row.line, id: r });
|
|
169
|
+
else
|
|
170
|
+
storied.add(r);
|
|
171
|
+
}
|
|
172
|
+
const src = sourceStep ?? '';
|
|
173
|
+
if (src !== PENDING) {
|
|
174
|
+
const refs = splitList(src);
|
|
175
|
+
if (refs.length === 0)
|
|
176
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
|
|
177
|
+
for (const ref of refs) {
|
|
178
|
+
const sm = SOURCE_STEP_RE.exec(ref);
|
|
179
|
+
if (!sm)
|
|
180
|
+
violations.push({ artifact: STORIES_FILE, reason: 'bad_source_step', line: row.line });
|
|
181
|
+
else if (sources.processes !== null && !sources.processes.has(sm[1]))
|
|
182
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_id', line: row.line, id: sm[1] });
|
|
183
|
+
else if (sources.steps !== null && !sources.steps.some((st) => st.key === ref))
|
|
184
|
+
violations.push({ artifact: STORIES_FILE, reason: 'unknown_process_step', line: row.line, id: ref.slice(0, 40) });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const max = table.boundedSequenceMax(seqs);
|
|
189
|
+
if (max !== null) {
|
|
190
|
+
const have = new Set(seqs);
|
|
191
|
+
for (let n = 1; n <= max; n += 1) {
|
|
192
|
+
if (!have.has(n))
|
|
193
|
+
violations.push({ artifact: STORIES_FILE, reason: 'sequence_gap', id: `US-${String(n).padStart(2, '0')}` });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (sources.requirements instanceof Array) {
|
|
197
|
+
for (const r of sources.requirements) {
|
|
198
|
+
if (r.disposition !== rollup.DISPOSITIONS.MVP)
|
|
199
|
+
continue;
|
|
200
|
+
if (!needsStory(r, sources.steps))
|
|
201
|
+
continue;
|
|
202
|
+
if (!storied.has(r.id))
|
|
203
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'mvp_requirement_without_story', id: r.id });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else if (sources.requirements === 'unparseable') {
|
|
207
|
+
violations.push({ artifact: REQUIREMENTS_FILE, reason: 'requirements_unparseable' });
|
|
208
|
+
}
|
|
209
|
+
return { ok: violations.length === 0, stories: ids.size, violations };
|
|
210
|
+
}
|
|
211
|
+
function checkProjectStories(cwd) {
|
|
212
|
+
const read = readIfPresent(node_path_1.default.join(cwd, STORIES_FILE));
|
|
213
|
+
if (read.kind === 'absent')
|
|
214
|
+
return null;
|
|
215
|
+
if (read.kind === 'unreadable')
|
|
216
|
+
return { ok: false, file: STORIES_FILE, stories: 0, violations: [{ artifact: STORIES_FILE, reason: 'artifact_unreadable' }] };
|
|
217
|
+
const extra = [];
|
|
218
|
+
const sources = { requirements: null, processes: null, steps: null };
|
|
219
|
+
const req = readIfPresent(node_path_1.default.join(cwd, REQUIREMENTS_FILE));
|
|
220
|
+
if (req.kind === 'unreadable')
|
|
221
|
+
extra.push({ artifact: REQUIREMENTS_FILE, reason: 'artifact_unreadable' });
|
|
222
|
+
if (req.kind === 'ok') {
|
|
223
|
+
const parsed = rollup.parseRequirementsArtifact(req.content);
|
|
224
|
+
sources.requirements = parsed.ok ? parsed.rows.map((r) => ({ id: r.id, prefix: r.prefix, disposition: r.disposition })) : 'unparseable';
|
|
225
|
+
}
|
|
226
|
+
const proc = readIfPresent(node_path_1.default.join(cwd, PROCESSES_FILE));
|
|
227
|
+
if (proc.kind === 'unreadable')
|
|
228
|
+
extra.push({ artifact: PROCESSES_FILE, reason: 'artifact_unreadable' });
|
|
229
|
+
if (proc.kind === 'ok') {
|
|
230
|
+
sources.processes = firstColumnOf(proc.content, 'To-be');
|
|
231
|
+
sources.steps = stepsOf(proc.content);
|
|
232
|
+
}
|
|
233
|
+
const result = checkStories(read.content, sources);
|
|
234
|
+
result.violations.push(...extra);
|
|
235
|
+
result.ok = result.violations.length === 0;
|
|
236
|
+
return { file: STORIES_FILE, ...result };
|
|
237
|
+
}
|
|
238
|
+
function formatViolation(v) {
|
|
239
|
+
const where = v.line !== undefined ? `:${v.line}` : '';
|
|
240
|
+
const what = v.id !== undefined ? ` (${v.id})` : '';
|
|
241
|
+
return ` - ${v.artifact}${where}: ${v.reason}${what}`;
|
|
242
|
+
}
|
|
243
|
+
// ─── CLI adapter ──────────────────────────────────────────────────────────────
|
|
244
|
+
/** `pp-tools discovery stories-check` (dotted `query discovery.stories-check`). Absent artifact → exit 0, `present: false`. */
|
|
245
|
+
function cmdDiscoveryStoriesCheck(cwd, args, raw) {
|
|
246
|
+
if (args.length > 2) {
|
|
247
|
+
error(USAGE, ERROR_REASON.USAGE);
|
|
248
|
+
return; // unreachable
|
|
249
|
+
}
|
|
250
|
+
const result = checkProjectStories(cwd);
|
|
251
|
+
if (result === null) {
|
|
252
|
+
output({ ok: true, present: false, file: STORIES_FILE }, raw);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const payload = { ok: result.ok, present: true, file: result.file, stories: result.stories };
|
|
256
|
+
if (!result.ok) {
|
|
257
|
+
output({ ...payload, violations: result.violations.length }, raw);
|
|
258
|
+
error(`discovery stories-check: ${result.violations.length} violation(s):\n` + result.violations.map(formatViolation).join('\n'), ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED);
|
|
259
|
+
return; // unreachable
|
|
260
|
+
}
|
|
261
|
+
output(payload, raw);
|
|
262
|
+
}
|
|
263
|
+
module.exports = {
|
|
264
|
+
STORIES_FILE,
|
|
265
|
+
STORIES_HEADER,
|
|
266
|
+
STORYLESS_PREFIXES,
|
|
267
|
+
SYSTEM_ACTOR,
|
|
268
|
+
needsStory,
|
|
269
|
+
STORIES_CHECK_FAILED: ERROR_REASON.DISCOVERY_STORIES_CHECK_FAILED,
|
|
270
|
+
checkStories,
|
|
271
|
+
checkProjectStories,
|
|
272
|
+
cmdDiscoveryStoriesCheck,
|
|
273
|
+
};
|
|
@@ -40,7 +40,7 @@ const VOID_TAGS = new Set([
|
|
|
40
40
|
const RAW_TEXT_TAGS = new Set(['script', 'style', 'noscript']);
|
|
41
41
|
const ID_RE = /^[A-Za-z][A-Za-z0-9_:.-]{0,79}$/;
|
|
42
42
|
const REQ_ID_RE = /^[A-Z][A-Z0-9]*-\d+$/;
|
|
43
|
-
const DECISION_RE = /^D
|
|
43
|
+
const DECISION_RE = /^D-(?:\d{2}|P\d{2})-\d+$/;
|
|
44
44
|
const ELEMENT_RE = /^(\*|[A-Za-z0-9_.-]+\.html?)#([A-Za-z][A-Za-z0-9_:.-]{0,79})$/;
|
|
45
45
|
const ROUND_RE = /^## Sweep (\d{1,2}) — \d{4}-\d{2}-\d{2} — .+$/;
|
|
46
46
|
const TABLE_HEADER = ['Element', 'Outcome', 'Reference'];
|
|
@@ -90,9 +90,64 @@ function boundedSequenceMax(seqs) {
|
|
|
90
90
|
const max = seqs.reduce((a, b) => (b > a ? b : a), 0);
|
|
91
91
|
return max > MAX_SEQUENCE_SPAN ? null : max;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Every pipe table in a run of numbered lines (Discovery Sprint improvement
|
|
95
|
+
* plan, Batch 2). The header is the row before the first separator; data rows
|
|
96
|
+
* follow until the first line that is not a pipe row; a second separator
|
|
97
|
+
* starts the next table. Cells go through `splitTableCells`, so an escaped pipe
|
|
98
|
+
* inside a free-text cell never shifts a column.
|
|
99
|
+
*/
|
|
100
|
+
function parseTables(body) {
|
|
101
|
+
const tables = [];
|
|
102
|
+
for (let i = 1; i < body.length; i += 1) {
|
|
103
|
+
if (!SEPARATOR_ROW_RE.test(body[i].text) || !body[i - 1].text.trim().startsWith('|'))
|
|
104
|
+
continue;
|
|
105
|
+
const table = { line: body[i - 1].n, header: splitTableCells(body[i - 1].text), rows: [] };
|
|
106
|
+
let j = i + 1;
|
|
107
|
+
for (; j < body.length; j += 1) {
|
|
108
|
+
const text = body[j].text.trim();
|
|
109
|
+
if (!text.startsWith('|') || SEPARATOR_ROW_RE.test(text))
|
|
110
|
+
break;
|
|
111
|
+
table.rows.push({ line: body[j].n, cells: splitTableCells(text) });
|
|
112
|
+
}
|
|
113
|
+
tables.push(table);
|
|
114
|
+
i = j - 1;
|
|
115
|
+
}
|
|
116
|
+
return tables;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Split a file into the sections opened by headings of exactly `level` hashes
|
|
120
|
+
* (`## ` for 2). A section's body runs to the next heading of the same or a
|
|
121
|
+
* higher level; lower-level headings stay inside the body, so a caller can
|
|
122
|
+
* split a body again at the next level. Lines before the first heading belong
|
|
123
|
+
* to no section.
|
|
124
|
+
*/
|
|
125
|
+
function splitSections(lines, level) {
|
|
126
|
+
const open = new RegExp(`^#{${level}}\\s+(.*)$`);
|
|
127
|
+
const close = new RegExp(`^#{1,${level}}\\s`);
|
|
128
|
+
const out = [];
|
|
129
|
+
let current = null;
|
|
130
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
131
|
+
const m = open.exec(lines[i]);
|
|
132
|
+
if (m) {
|
|
133
|
+
current = { heading: m[1].trim(), line: i + 1, body: [] };
|
|
134
|
+
out.push(current);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (close.test(lines[i])) {
|
|
138
|
+
current = null;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (current)
|
|
142
|
+
current.body.push({ n: i + 1, text: lines[i] });
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
93
146
|
module.exports = {
|
|
94
147
|
splitTableCells,
|
|
95
148
|
SEPARATOR_ROW_RE,
|
|
96
149
|
MAX_SEQUENCE_SPAN,
|
|
97
150
|
boundedSequenceMax,
|
|
151
|
+
parseTables,
|
|
152
|
+
splitSections,
|
|
98
153
|
};
|
package/pp-core/bin/lib/init.cjs
CHANGED
|
@@ -32,6 +32,7 @@ const worktreeSafety = require("./worktree-safety.cjs");
|
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- planning-workspace.cjs is an export= CommonJS module
|
|
33
33
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
34
34
|
const secrets_cjs_1 = require("./secrets.cjs");
|
|
35
|
+
const plan_sizing_cjs_1 = require("./plan-sizing.cjs");
|
|
35
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
36
37
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
37
38
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
@@ -222,6 +223,49 @@ function resolvePhaseForInit(cwd, phase) {
|
|
|
222
223
|
}
|
|
223
224
|
return { phaseInfo, roadmapPhase };
|
|
224
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Phase 37 (D-15): the recorded verdict as init keys, plus the research flag folded
|
|
228
|
+
* through it. Only plan-phase ever computes a verdict; this reads the recorded one,
|
|
229
|
+
* else full. Any throw returns the full keys and the configured research value, so a
|
|
230
|
+
* broken read never lowers ceremony (T-37-08). The fold can only turn research off when
|
|
231
|
+
* config has it on, the verdict says there is no codebase to match and nothing forced
|
|
232
|
+
* the researcher; it never turns a config-off gate on (T-37-10).
|
|
233
|
+
*/
|
|
234
|
+
function ceremonyInitKeys(cwd, phase, researchConfigured) {
|
|
235
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
236
|
+
const loadCeremony = () => require('./ceremony.cjs');
|
|
237
|
+
try {
|
|
238
|
+
const { effectiveVerdict, resolveGateConfig, deriveCeremonyKeys } = loadCeremony();
|
|
239
|
+
const { verdict } = effectiveVerdict(cwd, phase);
|
|
240
|
+
const gateConfig = resolveGateConfig(cwd);
|
|
241
|
+
const keys = deriveCeremonyKeys(verdict, gateConfig.code_review_depth);
|
|
242
|
+
const keepResearch = verdict.mode === 'full' ||
|
|
243
|
+
verdict.answers.codebase_to_match.value === true ||
|
|
244
|
+
verdict.gates['research']?.decision === 'run';
|
|
245
|
+
return {
|
|
246
|
+
keys,
|
|
247
|
+
research_enabled: researchConfigured === undefined ? researchConfigured : Boolean(researchConfigured) && keepResearch,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
let depth = 'standard';
|
|
252
|
+
try {
|
|
253
|
+
depth = loadCeremony().resolveGateConfig(cwd).code_review_depth;
|
|
254
|
+
}
|
|
255
|
+
catch { /* keep standard */ }
|
|
256
|
+
return {
|
|
257
|
+
keys: {
|
|
258
|
+
ceremony_tier: 'heavy',
|
|
259
|
+
ceremony_mode: 'full',
|
|
260
|
+
plan_check_rounds: 3,
|
|
261
|
+
plan_check_mode: 'standard',
|
|
262
|
+
verifier_depth: 'full',
|
|
263
|
+
code_review_depth: depth === 'quick' || depth === 'deep' ? depth : 'standard',
|
|
264
|
+
},
|
|
265
|
+
research_enabled: researchConfigured,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
225
269
|
function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
226
270
|
if (!phase) {
|
|
227
271
|
error('phase required for init execute-phase');
|
|
@@ -235,6 +279,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
235
279
|
: null;
|
|
236
280
|
const phase_req_ids = reqExtracted && reqExtracted !== 'TBD' ? reqExtracted : null;
|
|
237
281
|
const wf = (config.workflow ?? {});
|
|
282
|
+
const ceremony = ceremonyInitKeys(cwd, phase, undefined);
|
|
238
283
|
const result = {
|
|
239
284
|
executor_model: resolveModelInternal(cwd, 'pp-executor'),
|
|
240
285
|
verifier_model: resolveModelInternal(cwd, 'pp-verifier'),
|
|
@@ -247,6 +292,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
247
292
|
phase_branch_template: config.phase_branch_template,
|
|
248
293
|
milestone_branch_template: config.milestone_branch_template,
|
|
249
294
|
verifier_enabled: config.verifier,
|
|
295
|
+
...ceremony.keys,
|
|
250
296
|
phase_found: !!phaseInfo,
|
|
251
297
|
phase_dir: phaseInfo?.['directory'] || null,
|
|
252
298
|
phase_number: phaseInfo?.['phase_number'] || null,
|
|
@@ -331,13 +377,16 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
331
377
|
assertValidGranularityOverride(granularityOverride, error);
|
|
332
378
|
const granularity = resolveGranularityInternal(cwd, 'planning', granularityOverride || undefined);
|
|
333
379
|
const wf = (config.workflow ?? {});
|
|
380
|
+
const ceremony = ceremonyInitKeys(cwd, phase, wf['research']);
|
|
334
381
|
const result = {
|
|
335
382
|
researcher_model: resolveModelInternal(cwd, 'pp-phase-researcher'),
|
|
336
383
|
planner_model: resolveModelInternal(cwd, 'pp-planner'),
|
|
337
384
|
checker_model: resolveModelInternal(cwd, 'pp-plan-checker'),
|
|
338
385
|
tdd_mode: options['tdd'] || Boolean(wf['tdd_mode']) || false,
|
|
339
386
|
granularity,
|
|
340
|
-
|
|
387
|
+
...(0, plan_sizing_cjs_1.planBudget)(config.context_window),
|
|
388
|
+
...ceremony.keys,
|
|
389
|
+
research_enabled: ceremony.research_enabled,
|
|
341
390
|
plan_checker_enabled: config.plan_checker,
|
|
342
391
|
nyquist_validation_enabled: wf['nyquist_validation'],
|
|
343
392
|
commit_docs: config.commit_docs,
|
package/pp-core/bin/lib/io.cjs
CHANGED
|
@@ -203,6 +203,30 @@ const ERROR_REASON = Object.freeze({
|
|
|
203
203
|
DISCOVERY_REBASELINE_CHECK_FAILED: 'discovery_rebaseline_check_failed',
|
|
204
204
|
// discovery vault context record (PBI 75336)
|
|
205
205
|
DISCOVERY_VAULT_CHECK_FAILED: 'discovery_vault_check_failed',
|
|
206
|
+
// discovery project charter (Discovery Sprint improvement plan, Batch 1)
|
|
207
|
+
DISCOVERY_CHARTER_CHECK_FAILED: 'discovery_charter_check_failed',
|
|
208
|
+
DISCOVERY_CHARTER_UNSIGNED: 'discovery_charter_unsigned',
|
|
209
|
+
// discovery data model, processes, business rules (improvement plan, Batch 2)
|
|
210
|
+
DISCOVERY_ENTITY_CHECK_FAILED: 'discovery_entity_check_failed',
|
|
211
|
+
DISCOVERY_ENTITY_CHECK_INCOMPLETE: 'discovery_entity_check_incomplete',
|
|
212
|
+
DISCOVERY_PROCESS_CHECK_FAILED: 'discovery_process_check_failed',
|
|
213
|
+
DISCOVERY_PROCESS_STEPS_UNCITED: 'discovery_process_steps_uncited',
|
|
214
|
+
DISCOVERY_RULES_CHECK_FAILED: 'discovery_rules_check_failed',
|
|
215
|
+
DISCOVERY_STORIES_CHECK_FAILED: 'discovery_stories_check_failed',
|
|
216
|
+
DISCOVERY_PHASES_CHECK_FAILED: 'discovery_phases_check_failed',
|
|
217
|
+
DISCOVERY_FOR_PHASE_FAILED: 'discovery_for_phase_failed',
|
|
218
|
+
// discovery roles, integrations, migration, deviations (improvement plan, Batch 3)
|
|
219
|
+
DISCOVERY_ROLES_CHECK_FAILED: 'discovery_roles_check_failed',
|
|
220
|
+
DISCOVERY_INTEGRATIONS_CHECK_FAILED: 'discovery_integrations_check_failed',
|
|
221
|
+
DISCOVERY_INTEGRATIONS_UNCITED: 'discovery_integrations_uncited',
|
|
222
|
+
DISCOVERY_MIGRATION_CHECK_FAILED: 'discovery_migration_check_failed',
|
|
223
|
+
DISCOVERY_MIGRATION_UNCITED: 'discovery_migration_uncited',
|
|
224
|
+
// discovery session output gate (improvement plan, Batch 5)
|
|
225
|
+
DISCOVERY_SESSION_CHECK_FAILED: 'discovery_session_check_failed',
|
|
226
|
+
DISCOVERY_DEVIATIONS_CHECK_FAILED: 'discovery_deviations_check_failed',
|
|
227
|
+
DISCOVERY_DEVIATIONS_UNAPPROVED: 'discovery_deviations_unapproved',
|
|
228
|
+
// timeline (Phase 35: end with no matching open run)
|
|
229
|
+
TIMELINE_NO_OPEN_RUN: 'timeline_no_open_run',
|
|
206
230
|
// generic
|
|
207
231
|
USAGE: 'usage',
|
|
208
232
|
UNKNOWN: 'unknown',
|
|
@@ -40,7 +40,7 @@ const { resolveCapabilityRuntimeState, SEAL_FIELD, sealRenderVerdict, resolveInj
|
|
|
40
40
|
// ─── Capability-activation engine (single owner for config-key precedence) ────
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
42
|
const capabilityActivationModule = require("./capability-activation.cjs");
|
|
43
|
-
const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey } = capabilityActivationModule;
|
|
43
|
+
const { _getNestedConfigValue, _readRawConfigKey, _resolveActivationValue, resolveConfigKey, evaluateWhen } = capabilityActivationModule;
|
|
44
44
|
// ─── Canonical points (derived from LOOP_HOST_CONTRACT — authoritative 12) ───
|
|
45
45
|
// FIX 2: Derive the authoritative canonical set from LOOP_HOST_CONTRACT so it
|
|
46
46
|
// cannot drift from the host contract. CANONICAL_POINTS_FALLBACK is kept as an
|
|
@@ -99,11 +99,14 @@ function _getCanonicalPoints(_registry) {
|
|
|
99
99
|
* Ordering: steps first, then contributions, then gates. Within each array,
|
|
100
100
|
* the materialized registry order is preserved.
|
|
101
101
|
*
|
|
102
|
-
* Activation: a hook with no `when` is always active. With `when` (dotted key
|
|
103
|
-
*
|
|
102
|
+
* Activation: a hook with no `when` is always active. With `when` (a dotted key,
|
|
103
|
+
* or dotted keys joined by `&&`), resolved against `config` by evaluateWhen in
|
|
104
|
+
* capability-activation; active iff every conjunct holds. A `ceremony.<gate>`
|
|
105
|
+
* conjunct reads the phase verdict through the `ceremony` thunk and is true
|
|
106
|
+
* without one. Inactive hooks are filtered out.
|
|
104
107
|
*/
|
|
105
108
|
function resolveLoopHooks(input) {
|
|
106
|
-
const { point, registry, config, cwd, capabilityStatesById, recordedArtifacts, platformBindingsPresent, } = input;
|
|
109
|
+
const { point, registry, config, cwd, capabilityStatesById, recordedArtifacts, platformBindingsPresent, ceremony, } = input;
|
|
107
110
|
// Validate point
|
|
108
111
|
const canonicalPoints = _getCanonicalPoints(registry);
|
|
109
112
|
if (!canonicalPoints.includes(point)) {
|
|
@@ -124,14 +127,10 @@ function resolveLoopHooks(input) {
|
|
|
124
127
|
const activeHooks = [];
|
|
125
128
|
// Helper: check activation using single-key precedence resolver (FIX 1 + FIX 3)
|
|
126
129
|
function isActive(hook) {
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// FIX 3: `when` present but not a non-empty string → malformed registry data → INACTIVE
|
|
132
|
-
if (typeof when !== 'string' || when.length === 0)
|
|
133
|
-
return false;
|
|
134
|
-
return _resolveActivationValue(when, config, cwd, registry);
|
|
130
|
+
// One evaluator owns the `when` rules (no `when` -> active; malformed -> inactive;
|
|
131
|
+
// `&&` conjuncts; `ceremony.<gate>` through the injected thunk), shared with
|
|
132
|
+
// capability-state so the two cannot drift (Phase 37 D-14).
|
|
133
|
+
return evaluateWhen(hook['when'], { config, cwd, registry, ceremony });
|
|
135
134
|
}
|
|
136
135
|
function isCapabilityActive(capId) {
|
|
137
136
|
if (!capabilityStatesById)
|
|
@@ -487,6 +486,11 @@ function renderLoopHooks(resolved) {
|
|
|
487
486
|
* seal gate render (Phase 16 D-19). `--active-cap <cap>` also requires the
|
|
488
487
|
* capability's own state to be active (D-27).
|
|
489
488
|
*
|
|
489
|
+
* options.phase + options.resolveCeremonyGate: an injected
|
|
490
|
+
* `(phase) => (gate) => boolean`; with a non-empty `--phase` a `ceremony.<gate>`
|
|
491
|
+
* conjunct of a hook's `when` answers from that phase's recorded verdict. Without
|
|
492
|
+
* both, or under `--active-cap`, every such conjunct is true (Phase 37 D-14).
|
|
493
|
+
*
|
|
490
494
|
* options.resolvePlatformBindingsPresent: an injected `() => boolean` (pp-tools
|
|
491
495
|
* builds it on the vault leaf's hasPlatformBindings); only literal `false`
|
|
492
496
|
* drops a `requiresPlatformBindings` capability's hooks (QD-13). `--into` and
|
|
@@ -544,6 +548,15 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
544
548
|
// (pp-tools passes one built on the vault leaf's hasPlatformBindings).
|
|
545
549
|
const resolvePlatformBindingsPresent = options['resolvePlatformBindingsPresent'];
|
|
546
550
|
const platformBindingsPresent = () => resolveInjectedPlatformBindingsPresent(resolvePlatformBindingsPresent);
|
|
551
|
+
// Phase 37 D-14: the ceremony verdict arrives the same way. Only a non-empty
|
|
552
|
+
// `--phase` plus an injected resolver builds the thunk (once, so the verdict
|
|
553
|
+
// is read once per run); anything else passes none and every `ceremony.*`
|
|
554
|
+
// conjunct stays true. `--active-cap` reads the unfiltered resolution.
|
|
555
|
+
const ceremonyPhase = typeof options['phase'] === 'string' ? options['phase'] : '';
|
|
556
|
+
const resolveCeremonyGate = options['resolveCeremonyGate'];
|
|
557
|
+
const ceremonyThunk = activeCapId === undefined && ceremonyPhase.length > 0 && typeof resolveCeremonyGate === 'function'
|
|
558
|
+
? resolveCeremonyGate(ceremonyPhase)
|
|
559
|
+
: undefined;
|
|
547
560
|
let resolved;
|
|
548
561
|
try {
|
|
549
562
|
// --active-cap reads the UNFILTERED resolution (16-16 D-81, see above):
|
|
@@ -551,6 +564,7 @@ function cmdLoopRenderHooks(cwd, point, raw, options = {}) {
|
|
|
551
564
|
resolved = resolveLoopHooks({
|
|
552
565
|
point, registry, config, cwd, capabilityStatesById, recordedArtifacts,
|
|
553
566
|
...(activeCapId === undefined ? { platformBindingsPresent } : {}),
|
|
567
|
+
...(ceremonyThunk !== undefined ? { ceremony: ceremonyThunk } : {}),
|
|
554
568
|
});
|
|
555
569
|
}
|
|
556
570
|
catch (err) {
|
|
@@ -347,6 +347,43 @@ function extractObjective(content) {
|
|
|
347
347
|
const m = content.match(/<objective>\s*\n?\s*(.+)/);
|
|
348
348
|
return m ? m[1].trim() : null;
|
|
349
349
|
}
|
|
350
|
+
// Tasks in a PLAN.md body: `<task>` elements (case-insensitive; the `<tasks>` wrapper
|
|
351
|
+
// does not match), else `## Task N` headings for markdown-format plans. A task counts
|
|
352
|
+
// only where it really is one: the opening tag (or heading) starts a line and the tag
|
|
353
|
+
// has a closing `</task>`, so a `<task>` mentioned in prose or inline code is not a
|
|
354
|
+
// task. Shared with `check plan-sizing`, where the count feeds `over_ceiling`.
|
|
355
|
+
//
|
|
356
|
+
// One pass (36-W-1b): the earlier single regex `^[ \t]*<task(?=[\s>])[^>]*>[\s\S]*?</task>`
|
|
357
|
+
// rescanned to the end of the file for every unclosed open line, which is quadratic. Here
|
|
358
|
+
// the open tag is found, its `>` is the first one after it (what `[^>]*>` always ended
|
|
359
|
+
// at), and the close is searched from there (what the lazy `[\s\S]*?` ended at). When
|
|
360
|
+
// either search fails the loop stops: any later line-start open tag begins after this
|
|
361
|
+
// one, so its first `>` and its close search start no earlier and fail too. After a
|
|
362
|
+
// match the open search resumes where the old global match resumed, at the end of the
|
|
363
|
+
// close tag. The content is not lowercased: `toLowerCase` maps U+212A to `k` and
|
|
364
|
+
// changes the length of U+0130, which the regex `i` flag (non-unicode) does not do.
|
|
365
|
+
function countPlanTasks(content) {
|
|
366
|
+
const open = /^[ \t]*<task(?=[\s>])/gim;
|
|
367
|
+
const close = /<\/task>/gi;
|
|
368
|
+
let xmlTasks = 0;
|
|
369
|
+
for (;;) {
|
|
370
|
+
const found = open.exec(content);
|
|
371
|
+
if (found === null)
|
|
372
|
+
break;
|
|
373
|
+
const gt = content.indexOf('>', open.lastIndex);
|
|
374
|
+
if (gt < 0)
|
|
375
|
+
break;
|
|
376
|
+
close.lastIndex = gt + 1;
|
|
377
|
+
const closed = close.exec(content);
|
|
378
|
+
if (closed === null)
|
|
379
|
+
break;
|
|
380
|
+
xmlTasks++;
|
|
381
|
+
open.lastIndex = close.lastIndex;
|
|
382
|
+
}
|
|
383
|
+
if (xmlTasks > 0)
|
|
384
|
+
return xmlTasks;
|
|
385
|
+
return (content.match(/^[ \t]*#{2,}\s*Task\s*\d+/gim) || []).length;
|
|
386
|
+
}
|
|
350
387
|
// O(V + E). Assigns each in-phase plan its longest-path topological level over the
|
|
351
388
|
// in-phase dependsOn DAG (Kahn's algorithm). Returns { level: Map<id,number>, visited: number }.
|
|
352
389
|
// visited < rawPlans.length signals a dependency cycle.
|
|
@@ -444,9 +481,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
|
|
444
481
|
const planPath = node_path_1.default.join(phaseDir, planFile);
|
|
445
482
|
const content = node_fs_1.default.readFileSync(planPath, 'utf-8');
|
|
446
483
|
const fm = extractFrontmatter(content);
|
|
447
|
-
const
|
|
448
|
-
const mdTasks = content.match(/##\s*Task\s*\d+/gi) || [];
|
|
449
|
-
const taskCount = xmlTasks.length || mdTasks.length;
|
|
484
|
+
const taskCount = countPlanTasks(content);
|
|
450
485
|
const parsedWave = parseInt(fm['wave'], 10);
|
|
451
486
|
const declaredWave = Number.isNaN(parsedWave) ? null : parsedWave;
|
|
452
487
|
let dependsOn = [];
|
|
@@ -1477,4 +1512,5 @@ module.exports = {
|
|
|
1477
1512
|
cmdPhaseComplete,
|
|
1478
1513
|
cmdPhaseUatPassed,
|
|
1479
1514
|
computeDependencyLevels,
|
|
1515
|
+
countPlanTasks,
|
|
1480
1516
|
};
|