@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,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Plan Sizing: the per-plan budget and the plan-split check (Phase 36).
|
|
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/plan-sizing.cjs`; do NOT
|
|
7
|
+
* hand-edit the compiled output.
|
|
8
|
+
*
|
|
9
|
+
* Two consumers share these numbers so the planner and the plan checker cannot
|
|
10
|
+
* disagree about them:
|
|
11
|
+
* - `init plan-phase` spreads `planBudget(config.context_window)` into its JSON
|
|
12
|
+
* (D-03): the planner reads the window and the per-plan budget there.
|
|
13
|
+
* - `check plan-sizing <phase>` reads the phase's PLAN.md files and reports
|
|
14
|
+
* plans over the 6-task ceiling, plans over the budget, and serial splits
|
|
15
|
+
* that have neither a parallelism nor a budget reason (D-05, D-14). Executed
|
|
16
|
+
* plans are reported but never block.
|
|
17
|
+
*
|
|
18
|
+
* A larger window raises only the per-plan budget (50% of the window). The task
|
|
19
|
+
* band (20000-60000 tokens, the 10-30% of the 200k calibration) and the 6-task
|
|
20
|
+
* ceiling stay fixed (D-04). The check is read-only: it never writes.
|
|
21
|
+
*/
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.MIN_TOKENS_PER_FILE = exports.TASK_TOKENS_MAX = exports.TASK_TOKENS_MIN = exports.MAX_TASKS_PER_PLAN = exports.DEFAULT_CONTEXT_WINDOW = void 0;
|
|
27
|
+
exports.resolveContextWindow = resolveContextWindow;
|
|
28
|
+
exports.planBudget = planBudget;
|
|
29
|
+
exports.parseEstimate = parseEstimate;
|
|
30
|
+
exports.analyzePlanSizing = analyzePlanSizing;
|
|
31
|
+
exports.readPhasePlans = readPhasePlans;
|
|
32
|
+
exports.computePlanSizing = computePlanSizing;
|
|
33
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
34
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
|
+
const phaseMod = require("./phase.cjs");
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
38
|
+
const coreUtilsMod = require("./core-utils.cjs");
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
40
|
+
const frontmatterMod = require("./frontmatter.cjs");
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
const configLoaderMod = require("./config-loader.cjs");
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
44
|
+
const phaseScopeMod = require("./phase-scope.cjs");
|
|
45
|
+
const { computeDependencyLevels, countPlanTasks } = phaseMod;
|
|
46
|
+
const { extractCanonicalPlanId } = coreUtilsMod;
|
|
47
|
+
const { extractFrontmatter } = frontmatterMod;
|
|
48
|
+
const { loadConfig } = configLoaderMod;
|
|
49
|
+
const { resolvePhaseForRead } = phaseScopeMod;
|
|
50
|
+
exports.DEFAULT_CONTEXT_WINDOW = 200000;
|
|
51
|
+
exports.MAX_TASKS_PER_PLAN = 6;
|
|
52
|
+
/** 10% of the 200k calibration (D-04); stated in tokens so a 1M window does not inflate tasks. */
|
|
53
|
+
exports.TASK_TOKENS_MIN = 20000;
|
|
54
|
+
/** 30% of the 200k calibration (D-04). */
|
|
55
|
+
exports.TASK_TOKENS_MAX = 60000;
|
|
56
|
+
/** Cross-check floor for the estimate against the number of files a plan touches (D-10). */
|
|
57
|
+
exports.MIN_TOKENS_PER_FILE = 5000;
|
|
58
|
+
/** A positive integer window; anything else (a bad config value) falls back to the default. */
|
|
59
|
+
function resolveContextWindow(raw) {
|
|
60
|
+
const n = typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : raw;
|
|
61
|
+
return typeof n === 'number' && Number.isInteger(n) && n > 0 ? n : exports.DEFAULT_CONTEXT_WINDOW;
|
|
62
|
+
}
|
|
63
|
+
/** The flat budget object: one place for the numbers (D-03, D-04). */
|
|
64
|
+
function planBudget(raw) {
|
|
65
|
+
const context_window = resolveContextWindow(raw);
|
|
66
|
+
return {
|
|
67
|
+
context_window,
|
|
68
|
+
plan_budget_tokens: Math.floor(context_window / 2),
|
|
69
|
+
task_tokens_min: exports.TASK_TOKENS_MIN,
|
|
70
|
+
task_tokens_max: exports.TASK_TOKENS_MAX,
|
|
71
|
+
max_tasks_per_plan: exports.MAX_TASKS_PER_PLAN,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read `context_estimate`. The frontmatter parser keeps an inline comment
|
|
76
|
+
* (`90000 # tokens` parses to that whole string), so a trailing `# comment` is
|
|
77
|
+
* allowed. Accepted: digits (optionally grouped in threes by `,` or `_`) and an
|
|
78
|
+
* optional `k` multiplier (`60k` is 60000; a decimal only with `k`, `1.5k`).
|
|
79
|
+
* Anything else - a range, an exponent, a `~` prefix, a unit - is null (treated
|
|
80
|
+
* as missing) rather than its leading digits; this never throws. readPhasePlans
|
|
81
|
+
* keeps the rejected text so the report can name it (`unparseable_estimate`).
|
|
82
|
+
* The comment is cut at the first `#` and the text trimmed before the anchored
|
|
83
|
+
* match, so one whitespace quantifier is left (36-W-1a: adjacent whitespace
|
|
84
|
+
* quantifiers were quadratic on a long run of spaces).
|
|
85
|
+
*/
|
|
86
|
+
function parseEstimate(raw) {
|
|
87
|
+
if (typeof raw === 'number') {
|
|
88
|
+
return Number.isSafeInteger(raw) && raw >= 0 ? raw : null;
|
|
89
|
+
}
|
|
90
|
+
if (typeof raw !== 'string')
|
|
91
|
+
return null;
|
|
92
|
+
// The old comment group was `#` then `.*` to the end, and nothing else in the pattern
|
|
93
|
+
// matches `#`, so a comment always starts at the first `#`. `.` refuses a line
|
|
94
|
+
// terminator, so a comment that runs across one made the whole value invalid.
|
|
95
|
+
let text = raw;
|
|
96
|
+
const hash = raw.indexOf('#');
|
|
97
|
+
if (hash >= 0) {
|
|
98
|
+
if (/[\n\r\u2028\u2029]/.test(raw.slice(hash)))
|
|
99
|
+
return null;
|
|
100
|
+
text = raw.slice(0, hash);
|
|
101
|
+
}
|
|
102
|
+
const m = /^(\d+|\d{1,3}(?:[_,]\d{3})+)(?:\.(\d+))?\s*([kK])?$/.exec(text.trim());
|
|
103
|
+
if (!m)
|
|
104
|
+
return null;
|
|
105
|
+
const [, whole, fraction, kilo] = m;
|
|
106
|
+
if (fraction !== undefined && !kilo)
|
|
107
|
+
return null;
|
|
108
|
+
const base = Number(`${whole.replace(/[_,]/g, '')}.${fraction ?? '0'}`);
|
|
109
|
+
const n = kilo ? Math.round(base * 1000) : base;
|
|
110
|
+
return Number.isSafeInteger(n) ? n : null;
|
|
111
|
+
}
|
|
112
|
+
/** Analyse already-parsed plans against a budget. Pure: tests feed literals. */
|
|
113
|
+
function analyzePlanSizing(plans, budget) {
|
|
114
|
+
const { plan_budget_tokens: limit, max_tasks_per_plan: ceiling, context_window } = budget;
|
|
115
|
+
const rawPlans = plans.map((p) => ({
|
|
116
|
+
id: p.id,
|
|
117
|
+
declaredWave: null,
|
|
118
|
+
dependsOn: p.dependsOn,
|
|
119
|
+
autonomous: true,
|
|
120
|
+
objective: null,
|
|
121
|
+
filesModified: p.files,
|
|
122
|
+
taskCount: p.tasks,
|
|
123
|
+
hasSummary: p.hasSummary,
|
|
124
|
+
}));
|
|
125
|
+
const planMap = new Map(rawPlans.map((p) => [p.id.toLowerCase(), p]));
|
|
126
|
+
const canonicalToId = new Map(rawPlans.map((p) => [extractCanonicalPlanId(p.id).toLowerCase(), p.id]));
|
|
127
|
+
const { level, visited } = computeDependencyLevels(rawPlans, planMap, canonicalToId);
|
|
128
|
+
const cycle = visited < plans.length;
|
|
129
|
+
const waveOf = (id) => (level.get(id) ?? 0) + 1;
|
|
130
|
+
const entries = plans.map((p) => ({
|
|
131
|
+
id: p.id,
|
|
132
|
+
wave: cycle ? 0 : waveOf(p.id),
|
|
133
|
+
tasks: p.tasks,
|
|
134
|
+
files: p.files.length,
|
|
135
|
+
context_estimate: p.estimate,
|
|
136
|
+
percent_of_window: p.estimate === null ? null : Math.floor((p.estimate * 100) / context_window),
|
|
137
|
+
gap_closure: p.gapClosure,
|
|
138
|
+
has_summary: p.hasSummary,
|
|
139
|
+
}));
|
|
140
|
+
const over_ceiling = plans.filter((p) => p.tasks > ceiling).map((p) => p.id);
|
|
141
|
+
const over_budget = plans.filter((p) => p.estimate !== null && p.estimate > limit).map((p) => p.id);
|
|
142
|
+
// An executed plan (it has a SUMMARY) is reported but never blocks: the planner
|
|
143
|
+
// cannot split it, and a lowered context_window would otherwise block forever.
|
|
144
|
+
const over_blocking = plans.some((p) => !p.hasSummary && (over_ceiling.includes(p.id) || over_budget.includes(p.id)));
|
|
145
|
+
const unestimated = plans.filter((p) => p.estimate === null).map((p) => p.id);
|
|
146
|
+
// Present but rejected (`~35000`, `35000 tokens`): named with its value so the
|
|
147
|
+
// planner can tell it from a missing key; it still counts as unestimated.
|
|
148
|
+
const unparseable_estimate = plans.flatMap((p) => p.estimate === null && p.rawEstimate !== undefined ? [{ id: p.id, value: p.rawEstimate }] : []);
|
|
149
|
+
const estimate_files_mismatch = plans
|
|
150
|
+
.filter((p) => p.estimate !== null && p.estimate < p.files.length * exports.MIN_TOKENS_PER_FILE)
|
|
151
|
+
.map((p) => p.id);
|
|
152
|
+
const unjustified_splits = [];
|
|
153
|
+
const unestimated_serial = [];
|
|
154
|
+
let min_plans = plans.length;
|
|
155
|
+
if (!cycle) {
|
|
156
|
+
const byWave = new Map();
|
|
157
|
+
for (const p of plans) {
|
|
158
|
+
const w = waveOf(p.id);
|
|
159
|
+
const list = byWave.get(w);
|
|
160
|
+
if (list)
|
|
161
|
+
list.push(p);
|
|
162
|
+
else
|
|
163
|
+
byWave.set(w, [p]);
|
|
164
|
+
}
|
|
165
|
+
const resolveDeps = (p) => {
|
|
166
|
+
const out = new Set();
|
|
167
|
+
for (const dep of p.dependsOn) {
|
|
168
|
+
const lower = dep.toLowerCase();
|
|
169
|
+
const hit = planMap.get(lower)?.id ?? canonicalToId.get(lower);
|
|
170
|
+
if (hit)
|
|
171
|
+
out.add(hit);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
};
|
|
175
|
+
// Serial link (a, b): a alone in wave w, b alone in wave w+1, b depends on a,
|
|
176
|
+
// neither a gap-closure plan, a `type: tdd` plan nor already executed.
|
|
177
|
+
const links = [];
|
|
178
|
+
for (const [w, list] of [...byWave.entries()].sort((x, y) => x[0] - y[0])) {
|
|
179
|
+
const next = byWave.get(w + 1);
|
|
180
|
+
if (list.length !== 1 || !next || next.length !== 1)
|
|
181
|
+
continue;
|
|
182
|
+
const a = list[0];
|
|
183
|
+
const b = next[0];
|
|
184
|
+
if (a.gapClosure || b.gapClosure || a.hasSummary || b.hasSummary || a.tdd || b.tdd)
|
|
185
|
+
continue;
|
|
186
|
+
if (!resolveDeps(b).has(a.id))
|
|
187
|
+
continue;
|
|
188
|
+
links.push({ a, b });
|
|
189
|
+
}
|
|
190
|
+
// A missing estimate is judged on task count only: a serial pair whose merge fits
|
|
191
|
+
// the ceiling has no visible budget reason, so it is reported (and blocks) rather
|
|
192
|
+
// than passing silently.
|
|
193
|
+
const chainNext = new Map();
|
|
194
|
+
const hasChainPrev = new Set();
|
|
195
|
+
for (const { a, b } of links) {
|
|
196
|
+
chainNext.set(a.id, b);
|
|
197
|
+
hasChainPrev.add(b.id);
|
|
198
|
+
const mergedTasks = a.tasks + b.tasks;
|
|
199
|
+
if (a.estimate === null || b.estimate === null) {
|
|
200
|
+
if (mergedTasks <= ceiling)
|
|
201
|
+
unestimated_serial.push({ plans: [a.id, b.id], merged_tasks: mergedTasks });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const mergedEstimate = a.estimate + b.estimate;
|
|
205
|
+
if (mergedTasks <= ceiling && mergedEstimate <= limit) {
|
|
206
|
+
unjustified_splits.push({ plans: [a.id, b.id], merged_tasks: mergedTasks, merged_estimate: mergedEstimate });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// min_plans: pack each maximal serial chain greedily in wave order (optimal for
|
|
210
|
+
// contiguous grouping; a missing estimate counts its tasks only); a plan in no
|
|
211
|
+
// chain is one plan.
|
|
212
|
+
let groups = 0;
|
|
213
|
+
const inChain = new Set();
|
|
214
|
+
for (const p of plans) {
|
|
215
|
+
if (hasChainPrev.has(p.id) || !chainNext.has(p.id))
|
|
216
|
+
continue;
|
|
217
|
+
let cur = p;
|
|
218
|
+
let tasks = 0;
|
|
219
|
+
let est = 0;
|
|
220
|
+
let open = false;
|
|
221
|
+
while (cur) {
|
|
222
|
+
inChain.add(cur.id);
|
|
223
|
+
const curEst = cur.estimate ?? 0;
|
|
224
|
+
if (open && (tasks + cur.tasks > ceiling || est + curEst > limit)) {
|
|
225
|
+
groups++;
|
|
226
|
+
tasks = 0;
|
|
227
|
+
est = 0;
|
|
228
|
+
}
|
|
229
|
+
tasks += cur.tasks;
|
|
230
|
+
est += curEst;
|
|
231
|
+
open = true;
|
|
232
|
+
cur = chainNext.get(cur.id);
|
|
233
|
+
}
|
|
234
|
+
if (open)
|
|
235
|
+
groups++;
|
|
236
|
+
}
|
|
237
|
+
min_plans = groups + plans.filter((p) => !inChain.has(p.id)).length;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
...budget,
|
|
241
|
+
plans: entries,
|
|
242
|
+
over_ceiling,
|
|
243
|
+
over_budget,
|
|
244
|
+
unjustified_splits,
|
|
245
|
+
unestimated,
|
|
246
|
+
unparseable_estimate,
|
|
247
|
+
unestimated_serial,
|
|
248
|
+
estimate_files_mismatch,
|
|
249
|
+
dependency_cycle: cycle,
|
|
250
|
+
min_plans,
|
|
251
|
+
// Over the ceiling or the budget is the planner's split threshold (D-05), so both
|
|
252
|
+
// block, for plans that are not executed yet.
|
|
253
|
+
block: over_blocking || unjustified_splits.length > 0 || unestimated_serial.length > 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Tasks as `phase-plan-index` counts them (the shared `countPlanTasks`: `<task>`
|
|
258
|
+
* elements, else `## Task N` headings). A `type: tdd` plan holds one `<feature>`
|
|
259
|
+
* block instead (tdd.md), counted as one task.
|
|
260
|
+
*/
|
|
261
|
+
const FEATURE_RE = /<feature[\s>]/gi;
|
|
262
|
+
function planTaskCount(content) {
|
|
263
|
+
return countPlanTasks(content) || (content.match(FEATURE_RE) || []).length;
|
|
264
|
+
}
|
|
265
|
+
/** The raw value of a present, non-empty frontmatter key, as text; else undefined. */
|
|
266
|
+
function rawValue(v) {
|
|
267
|
+
if (typeof v === 'number')
|
|
268
|
+
return String(v);
|
|
269
|
+
if (typeof v === 'string')
|
|
270
|
+
return v.trim() === '' ? undefined : v.trim();
|
|
271
|
+
if (Array.isArray(v))
|
|
272
|
+
return v.length === 0 ? undefined : JSON.stringify(v);
|
|
273
|
+
if (v !== null && typeof v === 'object')
|
|
274
|
+
return Object.keys(v).length === 0 ? undefined : JSON.stringify(v);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
function asStringList(v) {
|
|
278
|
+
if (Array.isArray(v))
|
|
279
|
+
return v.map(String);
|
|
280
|
+
if (typeof v === 'string' && v.trim() !== '')
|
|
281
|
+
return [v];
|
|
282
|
+
return [];
|
|
283
|
+
}
|
|
284
|
+
/** Parse every `*-PLAN.md` in a phase directory. An unreadable directory yields no plans. */
|
|
285
|
+
function readPhasePlans(phaseDirAbs) {
|
|
286
|
+
let names;
|
|
287
|
+
try {
|
|
288
|
+
names = node_fs_1.default.readdirSync(phaseDirAbs);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
const planFiles = names.filter((f) => f.endsWith('-PLAN.md') || f === 'PLAN.md').sort();
|
|
294
|
+
const completed = new Set();
|
|
295
|
+
for (const s of names) {
|
|
296
|
+
if (!(s.endsWith('-SUMMARY.md') || s === 'SUMMARY.md'))
|
|
297
|
+
continue;
|
|
298
|
+
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
|
299
|
+
completed.add(exact);
|
|
300
|
+
completed.add(extractCanonicalPlanId(s));
|
|
301
|
+
}
|
|
302
|
+
const out = [];
|
|
303
|
+
for (const file of planFiles) {
|
|
304
|
+
let content;
|
|
305
|
+
try {
|
|
306
|
+
content = node_fs_1.default.readFileSync(node_path_1.default.join(phaseDirAbs, file), 'utf8');
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const fm = extractFrontmatter(content);
|
|
312
|
+
const id = file.replace('-PLAN.md', '').replace('PLAN.md', '');
|
|
313
|
+
const estimate = parseEstimate(fm['context_estimate']);
|
|
314
|
+
const rawEstimate = estimate === null ? rawValue(fm['context_estimate']) : undefined;
|
|
315
|
+
out.push({
|
|
316
|
+
id,
|
|
317
|
+
dependsOn: asStringList(fm['depends_on']),
|
|
318
|
+
tasks: planTaskCount(content),
|
|
319
|
+
files: asStringList(fm['files_modified'] ?? fm['files-modified']),
|
|
320
|
+
estimate,
|
|
321
|
+
...(rawEstimate === undefined ? {} : { rawEstimate }),
|
|
322
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar compare
|
|
323
|
+
gapClosure: fm['gap_closure'] === 'true' || String(fm['gap_closure']) === 'true',
|
|
324
|
+
hasSummary: completed.has(id) || completed.has(extractCanonicalPlanId(file)),
|
|
325
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- FrontmatterValue scalar compare
|
|
326
|
+
tdd: String(fm['type'] ?? '').trim() === 'tdd',
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* The phase-scoped check: budget from config, plans from the resolved phase directory.
|
|
333
|
+
* An unknown phase blocks (`phase_found: false`, `block: true`) so a wrong phase
|
|
334
|
+
* argument cannot pass the gate; a resolver error propagates instead of reading as
|
|
335
|
+
* "not found".
|
|
336
|
+
*/
|
|
337
|
+
function computePlanSizing(projectDir, phase) {
|
|
338
|
+
const budget = planBudget(loadConfig(projectDir).context_window);
|
|
339
|
+
const result = resolvePhaseForRead(projectDir, phase);
|
|
340
|
+
const relDir = result && typeof result['directory'] === 'string' ? result['directory'] : '';
|
|
341
|
+
if (!relDir) {
|
|
342
|
+
return { phase, phase_found: false, ...analyzePlanSizing([], budget), block: true };
|
|
343
|
+
}
|
|
344
|
+
const phaseDir = node_path_1.default.resolve(projectDir, relDir);
|
|
345
|
+
return { phase, phase_found: true, ...analyzePlanSizing(readPhasePlans(phaseDir), budget) };
|
|
346
|
+
}
|