@pareto4/pp-core 1.11.0 → 1.12.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/README.md +31 -0
- package/agents/pp-code-fixer.md +1 -1
- package/agents/pp-debug-session-manager.md +1 -1
- package/agents/pp-debugger.md +1 -1
- package/agents/pp-executor.md +20 -10
- package/agents/pp-intel-updater.md +1 -1
- package/agents/pp-phase-researcher.md +1 -1
- package/agents/pp-plan-checker.md +5 -5
- package/agents/pp-planner.md +10 -24
- package/agents/pp-project-researcher.md +1 -1
- package/agents/pp-research-synthesizer.md +1 -1
- package/agents/pp-ui-researcher.md +1 -1
- package/agents/pp-verifier.md +8 -4
- package/bin/install.js +429 -82
- package/commands/pp-autonomous.md +8 -6
- package/commands/pp-discovery-to-build.md +1 -1
- package/commands/pp-discovery.md +3 -2
- package/commands/pp-discuss-phase.md +2 -2
- package/commands/pp-execute-phase.md +3 -1
- package/commands/pp-graphify.md +5 -5
- package/commands/pp-import.md +1 -1
- package/commands/pp-map-codebase.md +1 -1
- package/commands/pp-ns-project.md +2 -3
- package/commands/pp-ns-workflow.md +1 -1
- package/commands/pp-plan-phase.md +2 -1
- package/commands/pp-verify-work.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-cursor-session-start.js +2 -2
- package/hooks/dist/pp-session-state.sh +1 -1
- package/hooks/dist/pp-statusline.js +21 -2
- package/hooks/pp-cursor-session-start.js +2 -2
- package/hooks/pp-session-state.sh +1 -1
- package/hooks/pp-statusline.js +21 -2
- package/package.json +18 -5
- package/pareto-skills/pp-project-setup/SKILL.md +5 -63
- package/pp-core/bin/detect-build-commands.sh +182 -0
- package/pp-core/bin/lib/capability-registry.cjs +65 -7
- package/pp-core/bin/lib/check-command-router.cjs +79 -38
- package/pp-core/bin/lib/clusters.cjs +2 -3
- package/pp-core/bin/lib/command-aliases.cjs +36 -0
- package/pp-core/bin/lib/command-arg-projection.cjs +99 -1
- package/pp-core/bin/lib/commands.cjs +21 -6
- package/pp-core/bin/lib/compose-bindings.cjs +128 -15
- package/pp-core/bin/lib/config-schema.cjs +16 -0
- package/pp-core/bin/lib/config.cjs +142 -27
- package/pp-core/bin/lib/core-utils.cjs +43 -7
- package/pp-core/bin/lib/discovery-input-check.cjs +218 -0
- package/pp-core/bin/lib/discovery-rebaseline.cjs +1372 -0
- package/pp-core/bin/lib/discovery-research-track.cjs +979 -0
- package/pp-core/bin/lib/discovery-rollup.cjs +476 -0
- package/pp-core/bin/lib/discovery-screens.cjs +1995 -0
- package/pp-core/bin/lib/discovery-sweep.cjs +390 -0
- package/pp-core/bin/lib/discovery-table.cjs +98 -0
- package/pp-core/bin/lib/dispatch-plan.cjs +472 -0
- package/pp-core/bin/lib/frontmatter.cjs +17 -2
- package/pp-core/bin/lib/init-command-router.cjs +8 -5
- package/pp-core/bin/lib/init-contract.cjs +213 -33
- package/pp-core/bin/lib/init-interactive.cjs +20 -1
- package/pp-core/bin/lib/init.cjs +124 -134
- package/pp-core/bin/lib/install-profiles.cjs +5 -0
- package/pp-core/bin/lib/install-run.cjs +536 -0
- package/pp-core/bin/lib/io.cjs +15 -0
- package/pp-core/bin/lib/loop-resolver.cjs +24 -0
- package/pp-core/bin/lib/milestone.cjs +345 -48
- package/pp-core/bin/lib/phase-command-router.cjs +23 -1
- package/pp-core/bin/lib/phase-locator.cjs +15 -6
- package/pp-core/bin/lib/phase-scope.cjs +91 -0
- package/pp-core/bin/lib/phase-tier.cjs +467 -0
- package/pp-core/bin/lib/phase.cjs +247 -21
- package/pp-core/bin/lib/phases-command-router.cjs +2 -0
- package/pp-core/bin/lib/plan-graph.cjs +368 -0
- package/pp-core/bin/lib/planning-workspace.cjs +53 -0
- package/pp-core/bin/lib/platform-bindings.cjs +32 -2
- package/pp-core/bin/lib/profile-output.cjs +11 -3
- package/pp-core/bin/lib/prohibition-enforcement.cjs +112 -0
- package/pp-core/bin/lib/roadmap-parser.cjs +366 -99
- package/pp-core/bin/lib/roadmap.cjs +141 -39
- package/pp-core/bin/lib/runtime-hooks-surface.cjs +1 -1
- package/pp-core/bin/lib/shell-command-projection.cjs +24 -0
- package/pp-core/bin/lib/state-command-router.cjs +82 -13
- package/pp-core/bin/lib/state-document.cjs +124 -7
- package/pp-core/bin/lib/state.cjs +784 -257
- package/pp-core/bin/lib/template.cjs +22 -6
- package/pp-core/bin/lib/vault-command-router.cjs +57 -6
- package/pp-core/bin/lib/vault-practices.cjs +6 -36
- package/pp-core/bin/lib/vault-write-back.cjs +20 -4
- package/pp-core/bin/lib/vault.cjs +555 -23
- package/pp-core/bin/lib/verify.cjs +53 -31
- package/pp-core/bin/lib/workstream.cjs +1 -1
- package/pp-core/bin/lib/worktree-safety.cjs +26 -15
- package/pp-core/bin/portable-timeout.sh +89 -0
- package/pp-core/bin/pp-tools.cjs +158 -8
- package/pp-core/bin/shared/config-defaults.manifest.json +7 -0
- package/pp-core/bin/shared/config-schema.manifest.json +3 -0
- package/pp-core/references/autonomous-best-effort.md +461 -0
- package/pp-core/references/autonomous-lifecycle.md +146 -0
- package/pp-core/references/autonomous-smart-discuss.md +16 -12
- package/pp-core/references/checkpoints.md +1 -1
- package/pp-core/references/discovery-id-conventions.md +202 -0
- package/pp-core/references/discovery-rebaseline.md +254 -0
- package/pp-core/references/discovery-research-track.md +283 -0
- package/pp-core/references/discovery-traceability.md +400 -0
- package/pp-core/references/loop-hook-dispatch.md +7 -0
- package/pp-core/references/mandatory-initial-read.md +1 -1
- package/pp-core/references/planner-gap-closure.md +1 -0
- package/pp-core/references/planner-human-verify-mode.md +1 -1
- package/pp-core/references/planner-load-graph-context.md +1 -1
- package/pp-core/references/planner-plan-graph.md +66 -0
- package/pp-core/references/planner-tdd.md +1 -0
- package/pp-core/references/planning-config.md +8 -0
- package/pp-core/references/sketch-tooling.md +4 -0
- package/pp-core/references/tdd.md +1 -0
- package/pp-core/templates/claude-md.md +1 -1
- package/pp-core/templates/phase-prompt.md +6 -2
- package/pp-core/templates/prototype-review/review.js +303 -0
- package/pp-core/templates/state.md +6 -0
- package/pp-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/pp-core/workflows/add-backlog.md +1 -1
- package/pp-core/workflows/add-phase.md +1 -1
- package/pp-core/workflows/add-tests.md +1 -1
- package/pp-core/workflows/add-todo.md +1 -1
- package/pp-core/workflows/ai-integration-phase.md +2 -2
- package/pp-core/workflows/audit-fix.md +12 -16
- package/pp-core/workflows/audit-milestone.md +1 -1
- package/pp-core/workflows/audit-uat.md +1 -1
- package/pp-core/workflows/autonomous.md +68 -306
- package/pp-core/workflows/check-todos.md +1 -1
- package/pp-core/workflows/cleanup.md +1 -1
- package/pp-core/workflows/code-review-fix.md +1 -1
- package/pp-core/workflows/code-review.md +19 -7
- package/pp-core/workflows/complete-milestone.md +41 -60
- package/pp-core/workflows/debug.md +1 -1
- package/pp-core/workflows/diagnose-issues.md +1 -1
- package/pp-core/workflows/discovery-sprint/gap-analysis.md +140 -0
- package/pp-core/workflows/discovery-sprint/input-package.md +140 -0
- package/pp-core/workflows/discovery-sprint/prefill.md +92 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +257 -0
- package/pp-core/workflows/discovery-sprint/rebaseline.md +276 -0
- package/pp-core/workflows/discovery-sprint/session-13-gates.md +24 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/02-as-is.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/03-to-be.md +215 -0
- package/pp-core/workflows/discovery-sprint/sessions/04-ux-design.md +146 -0
- package/pp-core/workflows/discovery-sprint/sessions/05-architecture.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/06-security.md +107 -0
- package/pp-core/workflows/discovery-sprint/sessions/07-identity.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/08-integration.md +85 -0
- package/pp-core/workflows/discovery-sprint/sessions/09-qa.md +109 -0
- package/pp-core/workflows/discovery-sprint/sessions/10-infrastructure.md +119 -0
- package/pp-core/workflows/discovery-sprint/sessions/11-roadmap.md +92 -0
- package/pp-core/workflows/discovery-sprint/sessions/12-delivery-model.md +98 -0
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +205 -0
- package/pp-core/workflows/discovery-sprint/templates/context-template.md +266 -0
- package/pp-core/workflows/discovery-sprint.md +79 -1714
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +5 -5
- package/pp-core/workflows/discovery-to-build/steps/screen-inventory-gate.md +70 -0
- package/pp-core/workflows/discovery-to-build/templates/discovery-references.md +54 -0
- package/pp-core/workflows/discovery-to-build/templates/pp-handoff.md +184 -0
- package/pp-core/workflows/discovery-to-build.md +21 -238
- package/pp-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/pp-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/pp-core/workflows/discuss-phase-assumptions.md +1 -1
- package/pp-core/workflows/discuss-phase.md +9 -2
- package/pp-core/workflows/do.md +4 -4
- package/pp-core/workflows/docs-update.md +1 -1
- package/pp-core/workflows/edit-phase.md +1 -1
- package/pp-core/workflows/eval-review.md +1 -1
- package/pp-core/workflows/execute-phase/steps/checkpoint-handling.md +45 -0
- package/pp-core/workflows/execute-phase/steps/client-context-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/pp-core/workflows/execute-phase/steps/cross-ai-delegation.md +90 -0
- package/pp-core/workflows/execute-phase/steps/install-run.md +29 -0
- package/pp-core/workflows/execute-phase/steps/phase-close-rebaseline.md +23 -0
- package/pp-core/workflows/execute-phase/steps/post-merge-gate.md +76 -77
- package/pp-core/workflows/execute-phase/steps/serial-group-dispatch.md +72 -0
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +5 -5
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +19 -112
- package/pp-core/workflows/execute-phase.md +102 -216
- package/pp-core/workflows/execute-plan.md +6 -2
- package/pp-core/workflows/explore.md +1 -1
- package/pp-core/workflows/extract-learnings.md +1 -1
- package/pp-core/workflows/forensics.md +4 -4
- package/pp-core/workflows/graduation.md +1 -1
- package/pp-core/workflows/health.md +1 -1
- package/pp-core/workflows/help/modes/brief.md +1 -0
- package/pp-core/workflows/help/modes/default.md +2 -1
- package/pp-core/workflows/help/modes/full.md +12 -5
- package/pp-core/workflows/import.md +1 -1
- package/pp-core/workflows/ingest-docs.md +1 -1
- package/pp-core/workflows/init.md +18 -2
- package/pp-core/workflows/insert-phase.md +1 -1
- package/pp-core/workflows/list-workspaces.md +1 -1
- package/pp-core/workflows/manager.md +3 -3
- package/pp-core/workflows/map-codebase.md +2 -2
- package/pp-core/workflows/milestone-summary.md +4 -4
- package/pp-core/workflows/mvp-phase.md +1 -1
- package/pp-core/workflows/new-milestone.md +7 -8
- package/pp-core/workflows/new-project.md +6 -1
- package/pp-core/workflows/new-workspace.md +1 -1
- package/pp-core/workflows/next.md +2 -2
- package/pp-core/workflows/note.md +2 -2
- package/pp-core/workflows/pause-work.md +1 -1
- package/pp-core/workflows/plan-milestone-gaps.md +1 -1
- package/pp-core/workflows/plan-phase/steps/chunked-planning.md +111 -0
- package/pp-core/workflows/plan-phase/steps/phase-tier.md +34 -0
- package/pp-core/workflows/plan-phase.md +43 -167
- package/pp-core/workflows/plan-review-convergence.md +2 -2
- package/pp-core/workflows/plant-seed.md +1 -1
- package/pp-core/workflows/pr-branch.md +1 -1
- package/pp-core/workflows/profile-user.md +1 -1
- package/pp-core/workflows/progress.md +3 -3
- package/pp-core/workflows/quick.md +3 -1
- package/pp-core/workflows/remove-phase.md +1 -1
- package/pp-core/workflows/remove-workspace.md +1 -1
- package/pp-core/workflows/resume-project.md +6 -12
- package/pp-core/workflows/review.md +1 -1
- package/pp-core/workflows/scan.md +1 -1
- package/pp-core/workflows/secure-phase.md +1 -1
- package/pp-core/workflows/settings-advanced.md +1 -1
- package/pp-core/workflows/settings-integrations.md +1 -1
- package/pp-core/workflows/settings.md +1 -1
- package/pp-core/workflows/ship.md +25 -8
- package/pp-core/workflows/sketch-wrap-up.md +1 -1
- package/pp-core/workflows/sketch.md +2 -1
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/spike-wrap-up.md +1 -1
- package/pp-core/workflows/spike.md +1 -1
- package/pp-core/workflows/stats.md +2 -2
- package/pp-core/workflows/thread.md +1 -1
- package/pp-core/workflows/transition.md +5 -9
- package/pp-core/workflows/ui-phase.md +23 -2
- package/pp-core/workflows/ui-review.md +1 -1
- package/pp-core/workflows/ultraplan-phase.md +2 -2
- package/pp-core/workflows/validate-phase.md +1 -1
- package/pp-core/workflows/vault-write-back.md +1 -1
- package/pp-core/workflows/verify-phase.md +29 -26
- package/pp-core/workflows/verify-work.md +14 -10
- package/scripts/base64-scan.sh +7 -41
- package/scripts/changeset/cli.cjs +25 -5
- package/scripts/dev-env.cjs +462 -0
- package/scripts/lib/allowlist-ratchet.cjs +103 -1
- package/scripts/lib/size-budget.cjs +311 -0
- package/scripts/lint-neutrality-names.cjs +694 -0
- package/scripts/lint-org-coordinates.allowlist.json +35 -0
- package/scripts/lint-org-coordinates.cjs +426 -0
- package/scripts/lint-skill-deps.cjs +48 -9
- package/scripts/lint-test-file-count.cjs +17 -2
- package/scripts/prompt-injection-scan.sh +41 -20
- package/scripts/sync-manifest-versions.cjs +41 -7
- package/scripts/sync-runtime-launcher.cjs +106 -1
- package/scripts/update-size-baseline.cjs +274 -30
- package/scripts/workflow-size.cjs +61 -9
- package/commands/pp-build-handoff.md +0 -46
- package/pp-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +0 -94
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file size-budget.cjs
|
|
5
|
+
*
|
|
6
|
+
* The per-commit size budget for workflow and agent markdown (pack 9, item 4).
|
|
7
|
+
*
|
|
8
|
+
* ## What is measured
|
|
9
|
+
*
|
|
10
|
+
* Every CHANGE since the baseline file was last committed, one at a time:
|
|
11
|
+
* - each commit in `<last baseline commit>..HEAD` (at most MAX_COMMITS), against
|
|
12
|
+
* its parent — for a merge commit, against the larger of each file's sizes in
|
|
13
|
+
* its parents, so a merge adds nothing of its own and each plan commit is
|
|
14
|
+
* judged on its own branch;
|
|
15
|
+
* - the uncommitted edits under the guarded directory, against HEAD.
|
|
16
|
+
* A file's allowance base is the LARGER of its size before the change and its
|
|
17
|
+
* size in the committed baseline file. A change may grow one file by at most
|
|
18
|
+
* `per_file_bytes` and all files together by at most `per_commit_bytes`. So:
|
|
19
|
+
* - a plan that grows a few files a little passes without touching the
|
|
20
|
+
* baseline, however much the phase has grown in earlier plans;
|
|
21
|
+
* - growth already recorded in the baseline (a justified re-baseline) passes;
|
|
22
|
+
* - one change growing past the budget fails, by file and by total, and stays
|
|
23
|
+
* red until it is shrunk or recorded — a later commit does not hide it.
|
|
24
|
+
* A file that is in neither the baseline nor the parent (a new file) is not
|
|
25
|
+
* budgeted; the tier, nested and new-file hard caps in the guard tests bound it.
|
|
26
|
+
*
|
|
27
|
+
* The cumulative growth of a phase is recorded once, at phase close:
|
|
28
|
+
* `npm run size:baseline -- --reason "phase NN close"` in the phase's closing
|
|
29
|
+
* commit, which also starts the next window. The tier hard caps stay absolute
|
|
30
|
+
* red lines.
|
|
31
|
+
*
|
|
32
|
+
* ## Why per commit, not against the baseline alone
|
|
33
|
+
*
|
|
34
|
+
* Measured against the committed baseline alone, the slack is spent cumulatively:
|
|
35
|
+
* a long phase whose plans each grow a different file by a few hundred bytes goes
|
|
36
|
+
* red part-way through, and from then on every plan re-baselines — the baseline
|
|
37
|
+
* becomes a file every plan touches, which serialises plans that are otherwise
|
|
38
|
+
* independent.
|
|
39
|
+
*
|
|
40
|
+
* ## Fallback
|
|
41
|
+
*
|
|
42
|
+
* Not a git checkout, or no HEAD: the check is the previous one — growth
|
|
43
|
+
* against the committed baseline with the same two numbers (`assertDeltaBudget`),
|
|
44
|
+
* new and removed files included. In a shallow clone, commits whose parent is
|
|
45
|
+
* missing are skipped (CI's `test` job clones with GitLab's default depth).
|
|
46
|
+
*
|
|
47
|
+
* ## The numbers
|
|
48
|
+
*
|
|
49
|
+
* `size_budget.per_file_bytes` (default 1024) and `size_budget.per_commit_bytes`
|
|
50
|
+
* (default 4096) — the schema defaults in
|
|
51
|
+
* `pp-core/bin/shared/config-defaults.manifest.json`, overridable in the
|
|
52
|
+
* repository's `.planning/config.json`. Nothing else holds them.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
const fs = require('fs');
|
|
56
|
+
const path = require('path');
|
|
57
|
+
const { execFileSync } = require('child_process');
|
|
58
|
+
const { assertDeltaBudget } = require('./allowlist-ratchet.cjs');
|
|
59
|
+
|
|
60
|
+
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
61
|
+
const DEFAULTS_MANIFEST = path.join(REPO_ROOT, 'pp-core', 'bin', 'shared', 'config-defaults.manifest.json');
|
|
62
|
+
|
|
63
|
+
function isPositiveInt(v) {
|
|
64
|
+
return Number.isInteger(v) && v > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The budget numbers: project `.planning/config.json` `size_budget.*` when set to
|
|
69
|
+
* positive integers, else the schema defaults from the manifest.
|
|
70
|
+
*
|
|
71
|
+
* @param {{ repoRoot?: string, manifestPath?: string }} [opts]
|
|
72
|
+
* @returns {{ perFileBytes: number, perCommitBytes: number, source: { perFileBytes: string, perCommitBytes: string } }}
|
|
73
|
+
*/
|
|
74
|
+
function readSizeBudget({ repoRoot = REPO_ROOT, manifestPath = DEFAULTS_MANIFEST } = {}) {
|
|
75
|
+
const defaults = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')).size_budget;
|
|
76
|
+
if (!defaults || !isPositiveInt(defaults.per_file_bytes) || !isPositiveInt(defaults.per_commit_bytes)) {
|
|
77
|
+
throw new Error(`${manifestPath} must define size_budget.per_file_bytes and size_budget.per_commit_bytes as positive integers`);
|
|
78
|
+
}
|
|
79
|
+
let project = {};
|
|
80
|
+
try {
|
|
81
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(repoRoot, '.planning', 'config.json'), 'utf-8'));
|
|
82
|
+
if (cfg && typeof cfg.size_budget === 'object' && cfg.size_budget !== null) project = cfg.size_budget;
|
|
83
|
+
} catch {
|
|
84
|
+
// no project config: schema defaults apply
|
|
85
|
+
}
|
|
86
|
+
const pick = (key) =>
|
|
87
|
+
isPositiveInt(project[key])
|
|
88
|
+
? { value: project[key], source: '.planning/config.json' }
|
|
89
|
+
: { value: defaults[key], source: 'schema default' };
|
|
90
|
+
const perFile = pick('per_file_bytes');
|
|
91
|
+
const perCommit = pick('per_commit_bytes');
|
|
92
|
+
return {
|
|
93
|
+
perFileBytes: perFile.value,
|
|
94
|
+
perCommitBytes: perCommit.value,
|
|
95
|
+
source: { perFileBytes: perFile.source, perCommitBytes: perCommit.source },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE inherited from a hook would point git at
|
|
100
|
+
// another repository; the guarded directory decides which repository is meant.
|
|
101
|
+
function git(cwd, args) {
|
|
102
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('GIT_')));
|
|
103
|
+
return execFileSync('git', args, { cwd, env, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Cap on how many commits back the guard looks (a re-baseline resets the window). */
|
|
107
|
+
const MAX_COMMITS = 300;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The changes to judge, oldest first, or null when git cannot provide them (see
|
|
111
|
+
* the Fallback section above). A change is one commit since the baseline file
|
|
112
|
+
* was last committed — `before` is, per file, the larger of its sizes in the
|
|
113
|
+
* commit's parents (so a merge commit adds nothing of its own) — plus the
|
|
114
|
+
* uncommitted edits under `dir` against HEAD.
|
|
115
|
+
*
|
|
116
|
+
* @param {{ dir: string, include: (rel: string) => boolean, baselinePath?: string }} opts
|
|
117
|
+
* `dir` the guarded directory; `include` filters POSIX paths relative to it
|
|
118
|
+
* (the same keys the measurer produces); `baselinePath` the committed baseline
|
|
119
|
+
* (its last commit starts the window; untracked → the last MAX_COMMITS commits).
|
|
120
|
+
* @returns {{ commits: Array<{ label: string, before: Object<string, number>, after: Object<string, number> }>,
|
|
121
|
+
* head: Object<string, number>, dirty: boolean, since: string | null } | null}
|
|
122
|
+
*/
|
|
123
|
+
function commitChanges({ dir, include, baselinePath }) {
|
|
124
|
+
let top;
|
|
125
|
+
try {
|
|
126
|
+
top = git(dir, ['rev-parse', '--show-toplevel']).trim();
|
|
127
|
+
git(top, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const realTop = fs.realpathSync(top);
|
|
132
|
+
const rel = path.relative(realTop, fs.realpathSync(dir)).split(path.sep).join('/');
|
|
133
|
+
const pathspec = rel === '' ? '.' : rel;
|
|
134
|
+
const prefix = rel === '' ? '' : `${rel}/`;
|
|
135
|
+
|
|
136
|
+
const cache = new Map();
|
|
137
|
+
const sizesAt = (rev) => {
|
|
138
|
+
if (cache.has(rev)) return cache.get(rev);
|
|
139
|
+
let listing = null;
|
|
140
|
+
try {
|
|
141
|
+
listing = git(top, ['ls-tree', '-r', '-l', '--full-tree', rev, '--', pathspec]);
|
|
142
|
+
} catch {
|
|
143
|
+
listing = null; // object missing (shallow boundary)
|
|
144
|
+
}
|
|
145
|
+
let sizes = null;
|
|
146
|
+
if (listing !== null) {
|
|
147
|
+
sizes = {};
|
|
148
|
+
for (const line of listing.split('\n')) {
|
|
149
|
+
const m = /^\d+ blob [0-9a-f]+\s+(\d+)\t(.+)$/.exec(line);
|
|
150
|
+
if (!m || !m[2].startsWith(prefix)) continue;
|
|
151
|
+
const key = m[2].slice(prefix.length);
|
|
152
|
+
if (include(key)) sizes[key] = Number(m[1]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
cache.set(rev, sizes);
|
|
156
|
+
return sizes;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let since = null;
|
|
160
|
+
if (baselinePath) {
|
|
161
|
+
try {
|
|
162
|
+
const relBaseline = path.relative(realTop, fs.realpathSync(baselinePath)).split(path.sep).join('/');
|
|
163
|
+
since = git(top, ['log', '-1', '--format=%H', 'HEAD', '--', relBaseline]).trim() || null;
|
|
164
|
+
} catch {
|
|
165
|
+
since = null; // baseline outside the repository or not there
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const range = since ? [`${since}..HEAD`] : ['HEAD'];
|
|
169
|
+
// No pathspec here: path-limited rev-list rewrites parents and prunes merges.
|
|
170
|
+
const lines = git(top, ['rev-list', '--reverse', `--max-count=${MAX_COMMITS}`, '--parents', ...range])
|
|
171
|
+
.split('\n')
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
const commits = [];
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
const [sha, ...parents] = line.split(' ');
|
|
176
|
+
const after = sizesAt(sha);
|
|
177
|
+
const parentSizes = parents.map(sizesAt);
|
|
178
|
+
if (after === null || parents.length === 0 || parentSizes.some((p) => p === null)) continue;
|
|
179
|
+
const before = {};
|
|
180
|
+
for (const p of parentSizes) {
|
|
181
|
+
for (const [k, v] of Object.entries(p)) before[k] = Math.max(before[k] ?? -1, v);
|
|
182
|
+
}
|
|
183
|
+
const subject = git(top, ['log', '-1', '--format=%s', sha]).trim();
|
|
184
|
+
commits.push({ label: `commit ${sha.slice(0, 9)} "${subject}"`, before, after });
|
|
185
|
+
}
|
|
186
|
+
const dirty = git(top, ['status', '--porcelain', '--untracked-files=all', '--', pathspec]).trim() !== '';
|
|
187
|
+
return { commits, head: sizesAt('HEAD') ?? {}, dirty, since };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The per-commit budget check. Reports through `fail`; returns the measurement.
|
|
192
|
+
*
|
|
193
|
+
* @param {object} o
|
|
194
|
+
* @param {string} o.label - prefix for messages, e.g. 'workflow-size'
|
|
195
|
+
* @param {Object<string, number>} o.current - sizes now (working tree)
|
|
196
|
+
* @param {Object<string, number>} o.baseline - committed baseline sizes
|
|
197
|
+
* @param {ReturnType<typeof commitChanges>} o.changes - from `commitChanges`; null → baseline-only fallback
|
|
198
|
+
* @param {{ perFileBytes: number, perCommitBytes: number }} o.budget - from `readSizeBudget`
|
|
199
|
+
* @param {(msg: string) => void} o.fail
|
|
200
|
+
* @param {string} [o.updateHint]
|
|
201
|
+
*/
|
|
202
|
+
function assertCommitDeltaBudget({ label, current, baseline, changes, budget, fail, updateHint }) {
|
|
203
|
+
const hint = updateHint ? `\n${updateHint}` : '';
|
|
204
|
+
if (changes === null) {
|
|
205
|
+
return {
|
|
206
|
+
mode: 'baseline',
|
|
207
|
+
...assertDeltaBudget({
|
|
208
|
+
label,
|
|
209
|
+
current,
|
|
210
|
+
baseline,
|
|
211
|
+
perFileSlack: budget.perFileBytes,
|
|
212
|
+
totalSlack: budget.perCommitBytes,
|
|
213
|
+
fail,
|
|
214
|
+
updateHint: `(No git history available: measured against the committed baseline only.)${hint}`,
|
|
215
|
+
}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Object.keys(baseline).length === 0) {
|
|
220
|
+
fail(
|
|
221
|
+
`[${label}] the committed baseline has no size entries — an empty baseline would make ` +
|
|
222
|
+
`every growth check pass vacuously. Regenerate it with \`npm run size:baseline\`.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
for (const [name, v] of Object.entries(baseline)) {
|
|
226
|
+
if (!Number.isInteger(v) || v < 0) fail(`[${label}] baseline entry ${name} is not a non-negative integer byte size: ${JSON.stringify(v)}`);
|
|
227
|
+
}
|
|
228
|
+
const base = (name) => (Number.isInteger(baseline[name]) && baseline[name] >= 0 ? baseline[name] : -1);
|
|
229
|
+
const todo =
|
|
230
|
+
`Shrink it — move content into a lazily-read steps/ or references/ file — or, if the growth is needed, ` +
|
|
231
|
+
`record it: npm run size:baseline -- --reason "<why this growth is needed>" and commit the baseline. ` +
|
|
232
|
+
`Growth within the budget needs no baseline commit: the phase's closing commit re-baselines once ` +
|
|
233
|
+
`with --reason "phase NN close".`;
|
|
234
|
+
const fmt = (o) => ` - ${o.name}: ${o.from} → ${o.to} (+${o.delta})`;
|
|
235
|
+
|
|
236
|
+
const judged = [...changes.commits];
|
|
237
|
+
if (changes.dirty) judged.push({ label: 'the uncommitted changes', before: changes.head, after: current });
|
|
238
|
+
const results = [];
|
|
239
|
+
for (const change of judged) {
|
|
240
|
+
const overages = [];
|
|
241
|
+
for (const name of Object.keys(change.after).sort()) {
|
|
242
|
+
const from = Math.max(base(name), change.before[name] ?? -1);
|
|
243
|
+
if (from < 0) continue; // new file: bounded by the hard caps, recorded at phase close
|
|
244
|
+
const to = change.after[name];
|
|
245
|
+
if (to > from) overages.push({ name, from, to, delta: to - from });
|
|
246
|
+
}
|
|
247
|
+
const totalOverage = overages.reduce((sum, o) => sum + o.delta, 0);
|
|
248
|
+
const where = `in ${change.label} (measured against its parent, or the committed baseline when larger)`;
|
|
249
|
+
const overFile = overages.filter((o) => o.delta > budget.perFileBytes);
|
|
250
|
+
if (overFile.length > 0) {
|
|
251
|
+
fail(
|
|
252
|
+
`[${label}] ${overFile.length} file(s) grew more than the per-file slack of ${budget.perFileBytes} bytes ` +
|
|
253
|
+
`(size_budget.per_file_bytes) ${where}. ${todo}\n${overFile.map(fmt).join('\n')}${hint}`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (totalOverage > budget.perCommitBytes) {
|
|
257
|
+
fail(
|
|
258
|
+
`[${label}] total growth ${where} is ${totalOverage} bytes, over the total slack of ` +
|
|
259
|
+
`${budget.perCommitBytes} bytes (size_budget.per_commit_bytes). ${todo}\n${overages.map(fmt).join('\n')}${hint}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
results.push({ change: change.label, overages, totalOverage });
|
|
263
|
+
}
|
|
264
|
+
const unrecorded = Object.keys(current).filter((n) => base(n) < 0).sort();
|
|
265
|
+
const removed = Object.keys(baseline).filter((n) => !Object.prototype.hasOwnProperty.call(current, n)).sort();
|
|
266
|
+
return { mode: 'commit', since: changes.since, results, unrecorded, removed };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The whole guard for one tree, as the size-budget tests run it: read the
|
|
271
|
+
* committed baseline, measure, list the changes, check the budget and the
|
|
272
|
+
* `_rebaselines` log. Returns failure messages (empty = green).
|
|
273
|
+
*
|
|
274
|
+
* @param {object} o
|
|
275
|
+
* @param {string} o.label - 'workflow-size' / 'agent-size'
|
|
276
|
+
* @param {string} o.dir - guarded directory
|
|
277
|
+
* @param {string} o.baselinePath - committed baseline JSON
|
|
278
|
+
* @param {(dir: string) => Object<string, number>} o.measure - the measurer
|
|
279
|
+
* @param {(rel: string) => boolean} o.include - which repository paths (relative to dir) the measurer covers
|
|
280
|
+
* @param {{ perFileBytes: number, perCommitBytes: number }} [o.budget] - default: readSizeBudget()
|
|
281
|
+
* @param {object|null} [o.changes] - default: commitChanges({ dir, include, baselinePath }); null = baseline-only
|
|
282
|
+
* @param {string} [o.updateHint]
|
|
283
|
+
* @returns {string[]}
|
|
284
|
+
*/
|
|
285
|
+
function checkSizeBudget({ label, dir, baselinePath, measure, include, budget, changes, updateHint }) {
|
|
286
|
+
// Lazy: update-size-baseline.cjs is the baseline file format's owner.
|
|
287
|
+
const { readBaseline, splitBaseline, validateRebaselineLog, REBASELINES_KEY } = require('../update-size-baseline.cjs');
|
|
288
|
+
const failures = [];
|
|
289
|
+
const raw = readBaseline(baselinePath);
|
|
290
|
+
if (raw === null) {
|
|
291
|
+
failures.push(`baseline ${baselinePath} does not exist — run \`npm run size:baseline\``);
|
|
292
|
+
} else if (!(REBASELINES_KEY in raw)) {
|
|
293
|
+
failures.push(`baseline ${baselinePath} has no ${REBASELINES_KEY} log — run \`npm run size:baseline\``);
|
|
294
|
+
}
|
|
295
|
+
const { sizes, rebaselines } = splitBaseline(raw);
|
|
296
|
+
assertCommitDeltaBudget({
|
|
297
|
+
label,
|
|
298
|
+
current: measure(dir),
|
|
299
|
+
baseline: sizes,
|
|
300
|
+
changes: changes === undefined ? commitChanges({ dir, include, baselinePath }) : changes,
|
|
301
|
+
budget: budget ?? readSizeBudget(),
|
|
302
|
+
fail: (m) => failures.push(m),
|
|
303
|
+
updateHint,
|
|
304
|
+
});
|
|
305
|
+
for (const problem of validateRebaselineLog(rebaselines)) {
|
|
306
|
+
failures.push(`[${label}] invalid ${REBASELINES_KEY} log: ${problem}`);
|
|
307
|
+
}
|
|
308
|
+
return failures;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { readSizeBudget, commitChanges, assertCommitDeltaBudget, checkSizeBudget, DEFAULTS_MANIFEST, MAX_COMMITS };
|