@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,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* dev-env.cjs — keep a stable pp-core install and a development build apart.
|
|
6
|
+
*
|
|
7
|
+
* stable [<tag>] [--dir <dir>] [--runtime <id>] worktree on the latest release tag, installed --global
|
|
8
|
+
* dev-setup [--config-dir <dir>] [--no-shell] npm ci, install the dev profile, put claude-dev on PATH
|
|
9
|
+
* install [--config-dir <dir>] [--no-build] build, install into a separate Claude profile
|
|
10
|
+
* install --local [<dir>] [--no-build] build, install into a sandbox project
|
|
11
|
+
* claude [--config-dir <dir>] [-- <args>] start the Claude CLI on the dev profile
|
|
12
|
+
*
|
|
13
|
+
* Defaults: the worktree is pp-core-stable next to the main checkout; the profile is
|
|
14
|
+
* PP_DEV_CONFIG_DIR, else ~/.claude-dev; the sandbox is ../pp-sandbox.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const os = require('node:os');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
21
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
22
|
+
|
|
23
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
24
|
+
const INSTALLER = path.join(ROOT, 'bin', 'install.js');
|
|
25
|
+
|
|
26
|
+
const USAGE = [
|
|
27
|
+
'Usage:',
|
|
28
|
+
' npm run setup [-- [<tag>] [--dir <dir>] [--runtime <id>]]',
|
|
29
|
+
' npm run dev:setup [-- [--config-dir <dir>] [--no-shell]]',
|
|
30
|
+
' npm run dev:install [-- [--config-dir <dir>] [--no-build]]',
|
|
31
|
+
' npm run dev:install -- --local [<dir>] [--no-build]',
|
|
32
|
+
' npm run dev:claude [-- <claude args>]',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
function parseStableArgs(rest) {
|
|
36
|
+
const opts = { command: 'stable', tag: null, dir: null, runtime: 'claude' };
|
|
37
|
+
for (let i = 0; i < rest.length; i++) {
|
|
38
|
+
const arg = rest[i];
|
|
39
|
+
if (arg === '--dir') {
|
|
40
|
+
const value = rest[++i];
|
|
41
|
+
if (!value) return { error: '--dir requires a path' };
|
|
42
|
+
opts.dir = value;
|
|
43
|
+
} else if (arg === '--runtime') {
|
|
44
|
+
const value = rest[++i];
|
|
45
|
+
if (!value) return { error: '--runtime requires a runtime id' };
|
|
46
|
+
opts.runtime = value;
|
|
47
|
+
} else if (!arg.startsWith('-') && opts.tag === null) {
|
|
48
|
+
opts.tag = arg;
|
|
49
|
+
} else {
|
|
50
|
+
return { error: `unknown option: ${arg}` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return opts;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseDevSetupArgs(rest) {
|
|
57
|
+
const opts = { command: 'dev-setup', configDir: null, shell: true };
|
|
58
|
+
for (let i = 0; i < rest.length; i++) {
|
|
59
|
+
const arg = rest[i];
|
|
60
|
+
if (arg === '--config-dir') {
|
|
61
|
+
const value = rest[++i];
|
|
62
|
+
if (!value) return { error: '--config-dir requires a path' };
|
|
63
|
+
opts.configDir = value;
|
|
64
|
+
} else if (arg === '--no-shell') {
|
|
65
|
+
opts.shell = false;
|
|
66
|
+
} else {
|
|
67
|
+
return { error: `unknown option: ${arg}` };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return opts;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseArgs(argv) {
|
|
74
|
+
const [command, ...rest] = argv;
|
|
75
|
+
if (command === 'stable') return parseStableArgs(rest);
|
|
76
|
+
if (command === 'dev-setup') return parseDevSetupArgs(rest);
|
|
77
|
+
if (command !== 'install' && command !== 'claude') {
|
|
78
|
+
return { error: `unknown command: ${command === undefined ? '(none)' : command}` };
|
|
79
|
+
}
|
|
80
|
+
const opts = { command, configDir: null, local: false, localDir: null, build: true, passthrough: [] };
|
|
81
|
+
for (let i = 0; i < rest.length; i++) {
|
|
82
|
+
const arg = rest[i];
|
|
83
|
+
if (arg === '--') {
|
|
84
|
+
opts.passthrough = rest.slice(i + 1);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (arg === '--config-dir') {
|
|
88
|
+
const value = rest[++i];
|
|
89
|
+
if (!value) return { error: '--config-dir requires a path' };
|
|
90
|
+
opts.configDir = value;
|
|
91
|
+
} else if (command === 'install' && arg === '--local') {
|
|
92
|
+
opts.local = true;
|
|
93
|
+
if (rest[i + 1] && !rest[i + 1].startsWith('-')) opts.localDir = rest[++i];
|
|
94
|
+
} else if (command === 'install' && arg === '--no-build') {
|
|
95
|
+
opts.build = false;
|
|
96
|
+
} else if (command === 'claude') {
|
|
97
|
+
opts.passthrough = rest.slice(i);
|
|
98
|
+
break;
|
|
99
|
+
} else {
|
|
100
|
+
return { error: `unknown option: ${arg}` };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (opts.local && opts.configDir) return { error: '--local and --config-dir cannot be combined' };
|
|
104
|
+
return opts;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Real path of the longest existing prefix: Windows 8.3 short names and symlinks compare equal to what git prints.
|
|
108
|
+
function canonicalPath(p) {
|
|
109
|
+
const resolved = path.resolve(p);
|
|
110
|
+
try {
|
|
111
|
+
return fs.realpathSync.native(resolved);
|
|
112
|
+
} catch {
|
|
113
|
+
const parent = path.dirname(resolved);
|
|
114
|
+
return parent === resolved ? resolved : path.join(canonicalPath(parent), path.basename(resolved));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function samePath(a, b) {
|
|
119
|
+
const norm = (p) => (process.platform === 'win32' ? canonicalPath(p).toLowerCase() : canonicalPath(p));
|
|
120
|
+
return norm(a) === norm(b);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isInside(child, parent) {
|
|
124
|
+
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
125
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveConfigDir(opts, env = process.env, home = os.homedir()) {
|
|
129
|
+
return path.resolve(opts.configDir || env.PP_DEV_CONFIG_DIR || path.join(home, '.claude-dev'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveLocalDir(opts, root = ROOT) {
|
|
133
|
+
return path.resolve(opts.localDir || path.join(root, '..', 'pp-sandbox'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The stable profile is ~/.claude; overwriting it with a dev build is what this script exists to avoid.
|
|
137
|
+
function assertNotStableProfile(dir, home = os.homedir()) {
|
|
138
|
+
if (samePath(dir, path.join(home, '.claude'))) {
|
|
139
|
+
throw new ExitError(1, `Refusing to install a dev build into the stable profile ${dir}.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A sandbox inside the repo inherits its git root, so it would not behave like a user project.
|
|
144
|
+
function assertOutsideRepo(dir, root = ROOT) {
|
|
145
|
+
if (isInside(dir, root)) {
|
|
146
|
+
throw new ExitError(1, `Sandbox ${dir} is inside the pp-core checkout; pick a directory outside ${root}.`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function npmCliPath() {
|
|
151
|
+
const npmCli = process.env.npm_execpath;
|
|
152
|
+
if (!npmCli) {
|
|
153
|
+
throw new ExitError(1, 'Run this through npm (npm run setup / npm run dev:install), or pass --no-build after building yourself.');
|
|
154
|
+
}
|
|
155
|
+
return npmCli;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function runNpm(args, cwd) {
|
|
159
|
+
execFileSync(process.execPath, [npmCliPath(), ...args], { cwd, stdio: 'inherit' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function runBuild() {
|
|
163
|
+
runNpm(['run', 'build'], ROOT);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function git(args, cwd) {
|
|
167
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Highest vX.Y.Z (or X.Y.Z) tag; pre-release tags are skipped.
|
|
171
|
+
function pickLatestTag(tags) {
|
|
172
|
+
const parsed = tags
|
|
173
|
+
.map((tag) => ({ tag, m: /^v?(\d+)\.(\d+)\.(\d+)$/.exec(tag) }))
|
|
174
|
+
.filter((t) => t.m)
|
|
175
|
+
.map((t) => ({ tag: t.tag, v: t.m.slice(1).map(Number) }));
|
|
176
|
+
parsed.sort((a, b) => b.v[0] - a.v[0] || b.v[1] - a.v[1] || b.v[2] - a.v[2]);
|
|
177
|
+
return parsed.length ? parsed[0].tag : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Next to the main checkout, so running this from any worktree finds the same directory.
|
|
181
|
+
function resolveStableDir(opts, repo = ROOT) {
|
|
182
|
+
if (opts.dir) return path.resolve(opts.dir);
|
|
183
|
+
const commonDir = path.resolve(repo, git(['rev-parse', '--git-common-dir'], repo));
|
|
184
|
+
return path.join(path.dirname(commonDir), '..', 'pp-core-stable');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The runtimes install.js takes as --<id>, read from the capability manifests so no build is needed.
|
|
188
|
+
function listRuntimes(root = ROOT) {
|
|
189
|
+
const capDir = path.join(root, 'capabilities');
|
|
190
|
+
return fs.readdirSync(capDir).flatMap((name) => {
|
|
191
|
+
try {
|
|
192
|
+
const cap = JSON.parse(fs.readFileSync(path.join(capDir, name, 'capability.json'), 'utf8'));
|
|
193
|
+
return cap.role === 'runtime' ? [{ id: cap.id, title: cap.title || cap.id }] : [];
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}).sort((a, b) => a.id.localeCompare(b.id));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveRuntime(id, root = ROOT) {
|
|
201
|
+
const runtimes = listRuntimes(root);
|
|
202
|
+
const found = runtimes.find((r) => r.id === id);
|
|
203
|
+
if (!found) {
|
|
204
|
+
throw new ExitError(2, `Unknown runtime: ${id}. Choose one of: ${runtimes.map((r) => r.id).join(', ')}.`);
|
|
205
|
+
}
|
|
206
|
+
return found;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function listWorktrees(repo) {
|
|
210
|
+
return git(['worktree', 'list', '--porcelain'], repo)
|
|
211
|
+
.split(/\r?\n/)
|
|
212
|
+
.filter((line) => line.startsWith('worktree '))
|
|
213
|
+
.map((line) => path.resolve(line.slice('worktree '.length)));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Adds the worktree at the tag, or moves an existing clean one to it; never touches your own checkout.
|
|
217
|
+
function prepareStableWorktree({ repo, dir, tag }) {
|
|
218
|
+
try {
|
|
219
|
+
git(['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], repo);
|
|
220
|
+
} catch {
|
|
221
|
+
throw new ExitError(1, `Tag ${tag} does not exist in ${repo}.`);
|
|
222
|
+
}
|
|
223
|
+
let known = listWorktrees(repo).some((w) => samePath(w, dir));
|
|
224
|
+
// A worktree folder deleted by hand stays registered; prune it so it is added again.
|
|
225
|
+
if (known && !fs.existsSync(dir)) {
|
|
226
|
+
git(['worktree', 'prune'], repo);
|
|
227
|
+
known = false;
|
|
228
|
+
}
|
|
229
|
+
if (!known) {
|
|
230
|
+
if (fs.existsSync(dir)) {
|
|
231
|
+
throw new ExitError(1, `${dir} exists but is not a worktree of ${repo}; pass --dir <other dir>.`);
|
|
232
|
+
}
|
|
233
|
+
git(['worktree', 'add', '--detach', dir, tag], repo);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (git(['status', '--porcelain'], dir) !== '') {
|
|
237
|
+
throw new ExitError(1, `The stable worktree ${dir} has local changes; commit or discard them first.`);
|
|
238
|
+
}
|
|
239
|
+
git(['checkout', '--quiet', '--detach', tag], dir);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function installStable(opts) {
|
|
243
|
+
const runtime = resolveRuntime(opts.runtime);
|
|
244
|
+
npmCliPath();
|
|
245
|
+
const dir = resolveStableDir(opts);
|
|
246
|
+
try {
|
|
247
|
+
git(['fetch', '--tags', '--quiet', 'origin'], ROOT);
|
|
248
|
+
} catch {
|
|
249
|
+
process.stderr.write('Could not fetch tags from origin; using the tags already in this clone.\n');
|
|
250
|
+
}
|
|
251
|
+
const tag = opts.tag || pickLatestTag(git(['tag', '--list'], ROOT).split(/\r?\n/));
|
|
252
|
+
if (!tag) throw new ExitError(1, 'No release tag found; pass one: npm run setup -- <tag>.');
|
|
253
|
+
prepareStableWorktree({ repo: ROOT, dir, tag });
|
|
254
|
+
process.stdout.write(`Stable worktree ${dir} is on ${tag}.\n`);
|
|
255
|
+
runNpm(['ci'], dir);
|
|
256
|
+
runNpm(['run', 'build'], dir);
|
|
257
|
+
execFileSync(process.execPath, [path.join(dir, 'bin', 'install.js'), `--${runtime.id}`, '--global'], { cwd: dir, stdio: 'inherit' });
|
|
258
|
+
const again = runtime.id === 'claude' ? 'npm run setup' : `npm run setup -- --runtime ${runtime.id}`;
|
|
259
|
+
process.stdout.write(`\npp-core ${tag} is installed as the global pp-core for ${runtime.title}. Update later with: ${again}\n`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// The statusline shows a DEV badge with this build's branch and commit when this file is present.
|
|
263
|
+
function devBuildInfo(root = ROOT, now = new Date()) {
|
|
264
|
+
const info = { checkout: root, installedAt: now.toISOString() };
|
|
265
|
+
try {
|
|
266
|
+
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], root);
|
|
267
|
+
if (branch !== 'HEAD') info.branch = branch;
|
|
268
|
+
info.commit = git(['rev-parse', '--short', 'HEAD'], root);
|
|
269
|
+
info.dirty = git(['status', '--porcelain'], root) !== '';
|
|
270
|
+
} catch {
|
|
271
|
+
// Not a git checkout: the badge still shows DEV, without a ref.
|
|
272
|
+
}
|
|
273
|
+
return info;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function installProfile(opts) {
|
|
277
|
+
const dir = resolveConfigDir(opts);
|
|
278
|
+
assertNotStableProfile(dir);
|
|
279
|
+
if (opts.build) runBuild();
|
|
280
|
+
execFileSync(process.execPath, [INSTALLER, '--claude', '--global', '--config-dir', dir], { stdio: 'inherit' });
|
|
281
|
+
fs.writeFileSync(path.join(dir, 'pp-dev-build.json'), `${JSON.stringify(devBuildInfo(), null, 2)}\n`);
|
|
282
|
+
process.stdout.write(`\nDev profile ready in ${dir}. From a project folder: claude-dev, or npm --prefix "${ROOT}" run dev:claude\n`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function installSandbox(opts) {
|
|
286
|
+
const dir = resolveLocalDir(opts);
|
|
287
|
+
assertOutsideRepo(dir);
|
|
288
|
+
if (opts.build) runBuild();
|
|
289
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
290
|
+
if (!fs.existsSync(path.join(dir, '.git'))) execFileSync('git', ['init', '-q'], { cwd: dir, stdio: 'inherit' });
|
|
291
|
+
execFileSync(process.execPath, [INSTALLER, '--claude', '--local'], { cwd: dir, stdio: 'inherit' });
|
|
292
|
+
process.stdout.write(
|
|
293
|
+
`\nSandbox ready in ${dir}. Skills still come from the global install, and its hooks run alongside the\n` +
|
|
294
|
+
'sandbox hooks; use the dev profile (npm run dev:install) to test skills or hooks.\n'
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// npm runs scripts in the package root; INIT_CWD is the directory the command was typed in.
|
|
299
|
+
function resolveLaunchDir(env = process.env) {
|
|
300
|
+
return path.resolve(env.INIT_CWD || process.cwd());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function launchClaude(opts) {
|
|
304
|
+
const dir = resolveConfigDir(opts);
|
|
305
|
+
assertNotStableProfile(dir);
|
|
306
|
+
if (!fs.existsSync(path.join(dir, 'pp-core', 'VERSION'))) {
|
|
307
|
+
throw new ExitError(1, `No pp-core install in ${dir}; run npm run dev:install first.`);
|
|
308
|
+
}
|
|
309
|
+
const cwd = resolveLaunchDir();
|
|
310
|
+
process.stdout.write(`Starting Claude in ${cwd} on the dev profile ${dir}.\n`);
|
|
311
|
+
const result = spawnSync('claude', opts.passthrough, {
|
|
312
|
+
cwd,
|
|
313
|
+
stdio: 'inherit',
|
|
314
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: dir },
|
|
315
|
+
});
|
|
316
|
+
if (result.error) {
|
|
317
|
+
throw new ExitError(1, `Could not start claude (${result.error.code}); set CLAUDE_CONFIG_DIR=${dir} and start it yourself.`);
|
|
318
|
+
}
|
|
319
|
+
return result.status === null ? 1 : result.status;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const LAUNCHER_START = '# >>> pp-core claude-dev >>>';
|
|
323
|
+
const LAUNCHER_END = '# <<< pp-core claude-dev <<<';
|
|
324
|
+
|
|
325
|
+
// macOS/Linux: a `claude-dev` shell function in the rc file, Claude on the dev profile in any folder.
|
|
326
|
+
function renderRcLauncher(configDir) {
|
|
327
|
+
return [
|
|
328
|
+
`${LAUNCHER_START} (written by npm run dev:setup in the pp-core checkout)`,
|
|
329
|
+
`claude-dev() { CLAUDE_CONFIG_DIR='${configDir.replace(/'/g, "'\\''")}' claude "$@"; }`,
|
|
330
|
+
LAUNCHER_END,
|
|
331
|
+
].join('\n');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Replaces an earlier block in place, so running dev:setup again never adds a second copy.
|
|
335
|
+
function upsertLauncher(text, block) {
|
|
336
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
337
|
+
const existing = new RegExp(`^${esc(LAUNCHER_START)}[^\\n]*\\n[\\s\\S]*?^${esc(LAUNCHER_END)}[^\\n]*`, 'm');
|
|
338
|
+
if (existing.test(text)) return text.replace(existing, () => block);
|
|
339
|
+
if (text === '') return `${block}\n`;
|
|
340
|
+
return `${text}${text.endsWith('\n') ? '' : '\n'}\n${block}\n`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Windows: command files on the user PATH. A shell profile under Documents is often behind
|
|
344
|
+
// Controlled Folder Access, which blocks node.exe and powershell.exe alike.
|
|
345
|
+
function windowsLauncherDir(env = process.env, home = os.homedir()) {
|
|
346
|
+
return path.join(env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'pp-core', 'bin');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// setlocal keeps CLAUDE_CONFIG_DIR inside this one run, for cmd and PowerShell callers alike.
|
|
350
|
+
function renderCmdLauncher(configDir) {
|
|
351
|
+
return ['@echo off', 'setlocal', `set "CLAUDE_CONFIG_DIR=${configDir}"`, 'claude %*', ''].join('\r\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Git Bash on Windows looks up extensionless scripts on PATH, not .cmd files.
|
|
355
|
+
function renderShLauncher(configDir) {
|
|
356
|
+
return ['#!/bin/sh', `CLAUDE_CONFIG_DIR='${configDir.replace(/'/g, "'\\''")}' exec claude "$@"`, ''].join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function pathHasEntry(raw, dir) {
|
|
360
|
+
const norm = (p) => p.trim().replace(/[\\/]+$/, '').toLowerCase();
|
|
361
|
+
return raw.split(';').some((entry) => entry.trim() !== '' && norm(entry) === norm(dir));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function appendPathEntry(raw, dir) {
|
|
365
|
+
const trimmed = raw.replace(/;+$/, '');
|
|
366
|
+
return trimmed === '' ? dir : `${trimmed};${dir}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function powershell(script) {
|
|
370
|
+
return execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
371
|
+
encoding: 'utf8',
|
|
372
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Reads HKCU\Environment\Path unexpanded, so %VAR% entries and the value kind survive the rewrite.
|
|
377
|
+
function addToUserPath(dir) {
|
|
378
|
+
const read = "$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment'); " +
|
|
379
|
+
"$raw = $k.GetValue('Path', '', 'DoNotExpandEnvironmentNames'); " +
|
|
380
|
+
"$kind = if ($k.GetValueNames() -contains 'Path') { $k.GetValueKind('Path') } else { 'ExpandString' }; " +
|
|
381
|
+
'[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("$kind|$raw"))';
|
|
382
|
+
const decoded = Buffer.from(powershell(read).trim(), 'base64').toString('utf8');
|
|
383
|
+
const bar = decoded.indexOf('|');
|
|
384
|
+
const kind = decoded.slice(0, bar);
|
|
385
|
+
const raw = decoded.slice(bar + 1);
|
|
386
|
+
if (pathHasEntry(raw, dir)) return false;
|
|
387
|
+
const value = Buffer.from(appendPathEntry(raw, dir), 'utf8').toString('base64');
|
|
388
|
+
// Setting and clearing a throwaway user variable broadcasts WM_SETTINGCHANGE, so new terminals see the PATH.
|
|
389
|
+
powershell("$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true); " +
|
|
390
|
+
`$k.SetValue('Path', [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${value}')), '${kind === 'String' ? 'String' : 'ExpandString'}'); $k.Close(); ` +
|
|
391
|
+
"[Environment]::SetEnvironmentVariable('PP_CORE_PATH_REFRESH', '1', 'User'); " +
|
|
392
|
+
"[Environment]::SetEnvironmentVariable('PP_CORE_PATH_REFRESH', $null, 'User')");
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function writeWindowsLaunchers(dir, configDir) {
|
|
397
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
398
|
+
fs.writeFileSync(path.join(dir, 'claude-dev.cmd'), renderCmdLauncher(configDir));
|
|
399
|
+
fs.writeFileSync(path.join(dir, 'claude-dev'), renderShLauncher(configDir), { mode: 0o755 });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function installLauncher(configDir) {
|
|
403
|
+
if (process.platform === 'win32') {
|
|
404
|
+
const dir = windowsLauncherDir();
|
|
405
|
+
writeWindowsLaunchers(dir, configDir);
|
|
406
|
+
process.stdout.write(`claude-dev is in ${dir}\n`);
|
|
407
|
+
process.stdout.write(addToUserPath(dir) ? `Added ${dir} to your user PATH.\n` : `${dir} is already on your user PATH.\n`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const rc = path.join(os.homedir(), (process.env.SHELL || '').endsWith('zsh') ? '.zshrc' : '.bashrc');
|
|
411
|
+
const current = fs.existsSync(rc) ? fs.readFileSync(rc, 'utf8') : '';
|
|
412
|
+
fs.writeFileSync(rc, upsertLauncher(current, renderRcLauncher(configDir)));
|
|
413
|
+
process.stdout.write(`claude-dev is defined in ${rc}\n`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function devSetup(opts) {
|
|
417
|
+
runNpm(['ci'], ROOT);
|
|
418
|
+
installProfile({ configDir: opts.configDir, build: true });
|
|
419
|
+
if (opts.shell) installLauncher(resolveConfigDir(opts));
|
|
420
|
+
const start = opts.shell ? 'claude-dev' : `npm --prefix "${ROOT}" run dev:claude`;
|
|
421
|
+
process.stdout.write(
|
|
422
|
+
`\nDone. Open a new terminal window, go to the project you want to work in and run: ${start}\n` +
|
|
423
|
+
'After a change to pp-core, run npm run dev:install to update the dev profile.\n'
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function main(argv = process.argv.slice(2)) {
|
|
428
|
+
const opts = parseArgs(argv);
|
|
429
|
+
if (opts.error) throw new ExitError(2, `${opts.error}\n${USAGE}`);
|
|
430
|
+
if (opts.command === 'claude') return launchClaude(opts);
|
|
431
|
+
if (opts.command === 'dev-setup') devSetup(opts);
|
|
432
|
+
else if (opts.command === 'stable') installStable(opts);
|
|
433
|
+
else if (opts.local) installSandbox(opts);
|
|
434
|
+
else installProfile(opts);
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (require.main === module) runMain(() => main());
|
|
439
|
+
|
|
440
|
+
module.exports = {
|
|
441
|
+
parseArgs,
|
|
442
|
+
resolveConfigDir,
|
|
443
|
+
resolveLocalDir,
|
|
444
|
+
resolveStableDir,
|
|
445
|
+
resolveLaunchDir,
|
|
446
|
+
devBuildInfo,
|
|
447
|
+
renderRcLauncher,
|
|
448
|
+
upsertLauncher,
|
|
449
|
+
windowsLauncherDir,
|
|
450
|
+
renderCmdLauncher,
|
|
451
|
+
renderShLauncher,
|
|
452
|
+
writeWindowsLaunchers,
|
|
453
|
+
pathHasEntry,
|
|
454
|
+
appendPathEntry,
|
|
455
|
+
pickLatestTag,
|
|
456
|
+
listRuntimes,
|
|
457
|
+
resolveRuntime,
|
|
458
|
+
prepareStableWorktree,
|
|
459
|
+
assertNotStableProfile,
|
|
460
|
+
assertOutsideRepo,
|
|
461
|
+
samePath,
|
|
462
|
+
};
|
|
@@ -233,4 +233,106 @@ function assertFileBaseline({ label, current, baseline, fail, updateHint }) {
|
|
|
233
233
|
return { grown, shrunk, added, removed };
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Assert that each artifact's measured size stays within a per-change DELTA
|
|
238
|
+
* BUDGET of its committed baseline (workflow size budget policy, 2026-10-11).
|
|
239
|
+
*
|
|
240
|
+
* Unlike `assertFileBaseline` (exact-byte pins), small growth is absorbed by a
|
|
241
|
+
* slack so routine edits need no baseline commit, while large or cumulative
|
|
242
|
+
* growth still fails by name:
|
|
243
|
+
*
|
|
244
|
+
* - A file may exceed its baseline by at most `perFileSlack` bytes.
|
|
245
|
+
* - The SUM of all positive overages may not exceed `totalSlack` bytes, so many
|
|
246
|
+
* small growths cannot add up to a large one unseen.
|
|
247
|
+
* - Shrinking always passes and never needs a baseline commit.
|
|
248
|
+
* - A file with no baseline entry (ADDED) or a baseline entry with no file
|
|
249
|
+
* (REMOVED) fails: the baseline must name exactly the files on disk.
|
|
250
|
+
* Recording either needs no reason.
|
|
251
|
+
* - An EMPTY baseline fails: with no entries every file is "added", but an
|
|
252
|
+
* explicit message makes the vacuous-pass case impossible to misread.
|
|
253
|
+
*
|
|
254
|
+
* Overage is measured against the committed baseline, not the previous commit,
|
|
255
|
+
* so the slack is a budget that a re-baseline resets — not a per-commit
|
|
256
|
+
* allowance that can be spent again on every commit.
|
|
257
|
+
*
|
|
258
|
+
* @param {object} opts
|
|
259
|
+
* @param {string} opts.label - Guard name (used in messages).
|
|
260
|
+
* @param {Object<string, number>} opts.current - Measured sizes by name.
|
|
261
|
+
* @param {Object<string, number>} opts.baseline - Committed sizes by name
|
|
262
|
+
* (numbers only; strip metadata keys first).
|
|
263
|
+
* @param {number} opts.perFileSlack - Max bytes one file may exceed its baseline.
|
|
264
|
+
* @param {number} opts.totalSlack - Max sum of all positive overages.
|
|
265
|
+
* @param {function(string): void} opts.fail - Invoked once per violation category.
|
|
266
|
+
* @param {string} [opts.updateHint] - Remediation hint appended to growth failures.
|
|
267
|
+
* @returns {{ overages: Array<{name:string,from:number,to:number,delta:number}>,
|
|
268
|
+
* totalOverage: number,
|
|
269
|
+
* shrunk: Array<{name:string,from:number,to:number,delta:number}>,
|
|
270
|
+
* added: string[], removed: string[] }}
|
|
271
|
+
*/
|
|
272
|
+
function assertDeltaBudget({ label, current, baseline, perFileSlack, totalSlack, fail, updateHint }) {
|
|
273
|
+
const currentNames = new Set(Object.keys(current));
|
|
274
|
+
const baselineNames = new Set(Object.keys(baseline));
|
|
275
|
+
const hint = updateHint ? `\n${updateHint}` : '';
|
|
276
|
+
|
|
277
|
+
if (baselineNames.size === 0) {
|
|
278
|
+
fail(
|
|
279
|
+
`[${label}] the committed baseline has no size entries — an empty baseline would make ` +
|
|
280
|
+
`every growth check pass vacuously. Regenerate it with \`npm run size:baseline\`.`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const added = [...currentNames].filter((n) => !baselineNames.has(n)).sort();
|
|
285
|
+
const removed = [...baselineNames].filter((n) => !currentNames.has(n)).sort();
|
|
286
|
+
|
|
287
|
+
const overages = [];
|
|
288
|
+
const shrunk = [];
|
|
289
|
+
for (const name of [...currentNames].filter((n) => baselineNames.has(n)).sort()) {
|
|
290
|
+
const from = baseline[name];
|
|
291
|
+
const to = current[name];
|
|
292
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
293
|
+
fail(`[${label}] baseline entry ${name} is not a non-negative integer byte size: ${JSON.stringify(from)}`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (to > from) overages.push({ name, from, to, delta: to - from });
|
|
297
|
+
else if (to < from) shrunk.push({ name, from, to, delta: from - to });
|
|
298
|
+
}
|
|
299
|
+
const totalOverage = overages.reduce((sum, o) => sum + o.delta, 0);
|
|
300
|
+
const fmt = (o) => ` - ${o.name}: ${o.from} → ${o.to} (+${o.delta})`;
|
|
301
|
+
|
|
302
|
+
const overFile = overages.filter((o) => o.delta > perFileSlack);
|
|
303
|
+
if (overFile.length > 0) {
|
|
304
|
+
fail(
|
|
305
|
+
`[${label}] ${overFile.length} file(s) grew more than the per-file slack of ${perFileSlack} bytes ` +
|
|
306
|
+
`past the committed baseline. Shrink them (extract content lazily), or re-baseline with a ` +
|
|
307
|
+
`reason: npm run size:baseline -- --reason "<why this growth is needed>"\n` +
|
|
308
|
+
`${overFile.map(fmt).join('\n')}${hint}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (totalOverage > totalSlack) {
|
|
313
|
+
fail(
|
|
314
|
+
`[${label}] total growth past the committed baseline is ${totalOverage} bytes, over the total ` +
|
|
315
|
+
`slack of ${totalSlack} bytes (each file may be under its own slack — the sum is what failed). ` +
|
|
316
|
+
`Shrink, or re-baseline with a reason: npm run size:baseline -- --reason "<why>"\n` +
|
|
317
|
+
`${overages.map(fmt).join('\n')}${hint}`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (added.length > 0) {
|
|
322
|
+
fail(
|
|
323
|
+
`[${label}] ${added.length} file(s) are not in the baseline — run \`npm run size:baseline\` ` +
|
|
324
|
+
`to record them (no --reason needed for new files).\n${added.map((n) => ` - ${n}`).join('\n')}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (removed.length > 0) {
|
|
329
|
+
fail(
|
|
330
|
+
`[${label}] ${removed.length} baseline entry(ies) no longer exist — run \`npm run size:baseline\` ` +
|
|
331
|
+
`to drop them (no --reason needed).\n${removed.map((n) => ` - ${n}`).join('\n')}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { overages, totalOverage, shrunk, added, removed };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = { assertWithinAllowlist, assertTightCeiling, assertFileBaseline, assertDeltaBudget };
|