@pareto4/pp-core 1.11.1 → 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 +70 -6
- 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 +93 -4
- 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 +149 -9
- 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
|
@@ -64,31 +64,46 @@ PATTERNS=(
|
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
# ─── Allowlist ───────────────────────────────────────────────────────────────
|
|
67
|
-
# Files that legitimately
|
|
67
|
+
# Files that legitimately contain injection-shaped text (pattern sources, negative
|
|
68
|
+
# test fixtures, adversarial corpora, security documentation).
|
|
69
|
+
#
|
|
70
|
+
# FORMAT (HINST-03, D-09-09): every entry is 'path::reason'.
|
|
71
|
+
# - `path` is matched as a SUBSTRING of the scanned file path, not as a prefix
|
|
72
|
+
# and not as a glob: entry `SECURITY.md` exempts every path ending in it.
|
|
73
|
+
# - `reason` states WHY that path legitimately carries injection-shaped text,
|
|
74
|
+
# and the hit count measured when it was written. It must be at least 80
|
|
75
|
+
# characters, and it must contain neither a single quote (the entry is a
|
|
76
|
+
# single-quoted bash literal) nor the two-character delimiter that opens it.
|
|
77
|
+
# - `is_allowlisted` strips the reason before matching, so the reason never
|
|
78
|
+
# participates in the substring rule.
|
|
79
|
+
#
|
|
80
|
+
# Membership is PINNED EXACTLY by tests/security-scan.security.test.cjs
|
|
81
|
+
# (describe 'prompt-injection-scan.sh ALLOWLIST is pinned'). That test sources
|
|
82
|
+
# THIS file and reads the array bash actually expands — adding, removing or
|
|
83
|
+
# un-reasoning an entry here without editing the test turns the suite red.
|
|
68
84
|
ALLOWLIST=(
|
|
69
|
-
'scripts/prompt-injection-scan.sh'
|
|
70
|
-
'scripts/base64-scan.sh'
|
|
71
|
-
'scripts/secret-scan.sh'
|
|
72
|
-
'tests/security-scan.security.test.cjs'
|
|
73
|
-
'tests/security.test.cjs'
|
|
74
|
-
'tests/prompt-injection-scan.security.test.cjs'
|
|
75
|
-
'tests/
|
|
76
|
-
'
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'tests/
|
|
80
|
-
'tests/security-
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
|
|
84
|
-
# and are not attack vectors — they explain/demonstrate injection patterns.
|
|
85
|
-
'TEST-EXAMPLES.md'
|
|
86
|
-
'explanation/security-model.md'
|
|
85
|
+
'scripts/prompt-injection-scan.sh::the PATTERNS array above is the literal source of every phrase this scanner looks for, so the file matches itself by construction; measured 4 pattern hits at HEAD on 2026-10-11'
|
|
86
|
+
'scripts/base64-scan.sh::decodes base64 blobs and re-scans the recovered plaintext against the same injection vocabulary, so it names those phrases inline; measured 2 pattern hits at HEAD on 2026-10-11'
|
|
87
|
+
'scripts/secret-scan.sh::third of the three diff-mode scan scripts CI runs together in the security-scan job of .gitlab-ci.yml; measured 0 pattern hits at HEAD on 2026-10-11, retained because its own exemption list at line 223 already names security-prompt-injection.security.test.cjs and grows with that vocabulary'
|
|
88
|
+
'tests/security-scan.security.test.cjs::the behavioural suite for this very scanner, so every positive case embeds a real injection phrase as fixture input; measured 28 pattern hits at HEAD on 2026-10-11'
|
|
89
|
+
'tests/security.test.cjs::the adversarial suite for the library sanitisers, whose fixtures are real override and role-manipulation strings asserted to be neutralised; measured 54 pattern hits at HEAD on 2026-10-11'
|
|
90
|
+
'tests/prompt-injection-scan.security.test.cjs::drives scanForInjection in the compiled security library with crafted payloads, and carries a second allowlist of its own for that separate scanner; measured 11 pattern hits at HEAD on 2026-10-11'
|
|
91
|
+
'tests/vault-security.test.cjs::the vault fencing suite plants an instruction-override heading in a fixture vault file and asserts SUMMARY.md fences and defangs it rather than passing it to the agent as instructions; measured 1 pattern hit at HEAD on 2026-10-11'
|
|
92
|
+
'tests/verify.test.cjs::the verification suite plants an instruction-override line in a phase artifact and asserts it is rejected, so the line is present as a negative fixture; measured 1 pattern hit at HEAD on 2026-10-11'
|
|
93
|
+
'pp-core/bin/lib/security.cjs::gitignored build output compiled from src/security.cts per .gitignore line 117, so it is unreachable in diff mode and matters only to a local dir-mode run; it carries the library injection patterns inline; measured 8 pattern hits at HEAD on 2026-10-11'
|
|
94
|
+
'tests/read-injection-scanner.security.test.cjs::exercises the read-time injection scanner hook with the payloads a hostile file would carry, each asserted to be blocked rather than obeyed; measured 14 pattern hits at HEAD on 2026-10-11'
|
|
95
|
+
'tests/security-prompt-injection.security.test.cjs::carries two deliberate NUL bytes at lines 631 and 881 so grep classifies it as binary and reports whole-file matches with no line numbers; 10 binary pattern matches measured at HEAD on 2026-10-11'
|
|
96
|
+
'tests/fixtures/adversarial/security/::a directory of adversarial corpus files whose entire purpose is to BE injection payloads for the suites above; measured 20 pattern hits across its 7 tracked files at HEAD on 2026-10-11'
|
|
97
|
+
'SECURITY.md::the substring rule makes this exempt every path ending in SECURITY.md, including pp-core/templates/SECURITY.md and every per-phase threat register generated from that template; the breadth is accepted because a security policy is exactly where injection phrasing is legitimately quoted; measured 1 pattern hit across all 13 such files on disk at HEAD on 2026-10-11, a quoted attack shape in a phase threat register'
|
|
98
|
+
'TEST-EXAMPLES.md::documentation that demonstrates injection examples in order to explain what these scanners catch, so the example text is the point of the file rather than an attack; measured 1 pattern hit at HEAD on 2026-10-11'
|
|
99
|
+
'explanation/security-model.md::matches docs/explanation/security-model.md through the substring rule; it is the prose that explains the injection threat model and quotes the attack shapes pp-core defends against; measured 2 pattern hits at HEAD on 2026-10-11'
|
|
87
100
|
)
|
|
88
101
|
|
|
89
102
|
is_allowlisted() {
|
|
90
103
|
local file="$1"
|
|
91
104
|
for allowed in "${ALLOWLIST[@]}"; do
|
|
105
|
+
# Strip the 'path::reason' suffix — the reason never takes part in matching.
|
|
106
|
+
allowed="${allowed%%::*}"
|
|
92
107
|
if [[ "$file" == *"$allowed"* ]]; then
|
|
93
108
|
return 0
|
|
94
109
|
fi
|
|
@@ -204,4 +219,10 @@ main() {
|
|
|
204
219
|
exit 0
|
|
205
220
|
}
|
|
206
221
|
|
|
207
|
-
|
|
222
|
+
# Sourceable for the test that pins the ALLOWLIST (D-09-10): sourcing this
|
|
223
|
+
# script must expand the array WITHOUT running the scanner. Direct execution
|
|
224
|
+
# (bash scripts/prompt-injection-scan.sh ... , or ./scripts/...) is unchanged —
|
|
225
|
+
# there BASH_SOURCE[0] and $0 are the same string. Verified on bash 3.2.57.
|
|
226
|
+
if [[ "${BASH_SOURCE[0]:-}" == "${0}" ]]; then
|
|
227
|
+
main "$@"
|
|
228
|
+
fi
|
|
@@ -4,10 +4,17 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* sync-manifest-versions.cjs
|
|
6
6
|
*
|
|
7
|
-
* Stamps the package.json version into every
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Stamps the package.json version into every versioned manifest. Called
|
|
8
|
+
* automatically by the `version` npm lifecycle script so that `npm version X.Y.Z`
|
|
9
|
+
* keeps all registered manifests in sync. Two kinds:
|
|
10
|
+
*
|
|
11
|
+
* - VERSIONED_MANIFESTS — JSON runtime-integration manifests whose top-level
|
|
12
|
+
* `version` field MUST track the package version.
|
|
13
|
+
* - VERSIONED_TEXT_FILES — plain-text manifests whose ENTIRE content is the
|
|
14
|
+
* version followed by one newline. Root `VERSION` is the repo layout's
|
|
15
|
+
* version source for `resolveCreatedWith` (src/compose-bindings.cts), so a
|
|
16
|
+
* repo build stamps `createdWith` from it. As a plain-text file it could not
|
|
17
|
+
* join the JSON loop, so nothing kept it in step with package.json.
|
|
11
18
|
*
|
|
12
19
|
* Usage:
|
|
13
20
|
* node scripts/sync-manifest-versions.cjs # stamp + report
|
|
@@ -29,6 +36,22 @@ const VERSIONED_MANIFESTS = [
|
|
|
29
36
|
'gemini-extension.json',
|
|
30
37
|
];
|
|
31
38
|
|
|
39
|
+
// Plain-text manifests: the whole file is the version plus one newline. Stamped,
|
|
40
|
+
// drift-checked, staged and counted exactly like VERSIONED_MANIFESTS.
|
|
41
|
+
const VERSIONED_TEXT_FILES = [
|
|
42
|
+
'VERSION',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// A text manifest's trimmed content, or null when the file is missing.
|
|
46
|
+
function readTextVersion(abs) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readFileSync(abs, 'utf8').trim();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err && err.code === 'ENOENT') return null;
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
function readJson(p) {
|
|
33
56
|
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
34
57
|
}
|
|
@@ -53,6 +76,13 @@ function syncManifestVersions(opts) {
|
|
|
53
76
|
changed.push(rel);
|
|
54
77
|
}
|
|
55
78
|
}
|
|
79
|
+
for (const rel of VERSIONED_TEXT_FILES) {
|
|
80
|
+
const abs = path.join(root, rel);
|
|
81
|
+
if ((readTextVersion(abs) || '') !== v) {
|
|
82
|
+
fs.writeFileSync(abs, v + '\n');
|
|
83
|
+
changed.push(rel);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
56
86
|
return changed;
|
|
57
87
|
}
|
|
58
88
|
|
|
@@ -65,6 +95,10 @@ function findDrift(opts) {
|
|
|
65
95
|
const found = readJson(path.join(root, rel)).version;
|
|
66
96
|
if (found !== v) drift.push({ manifest: rel, found, expected: v });
|
|
67
97
|
}
|
|
98
|
+
for (const rel of VERSIONED_TEXT_FILES) {
|
|
99
|
+
const found = readTextVersion(path.join(root, rel));
|
|
100
|
+
if (found !== v) drift.push({ manifest: rel, found, expected: v });
|
|
101
|
+
}
|
|
68
102
|
return drift;
|
|
69
103
|
}
|
|
70
104
|
|
|
@@ -84,14 +118,14 @@ function stageManifests(opts) {
|
|
|
84
118
|
return;
|
|
85
119
|
}
|
|
86
120
|
try {
|
|
87
|
-
execFileSync('git', ['add', '--', ...VERSIONED_MANIFESTS], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
121
|
+
execFileSync('git', ['add', '--', ...VERSIONED_MANIFESTS, ...VERSIONED_TEXT_FILES], { cwd: root, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
88
122
|
} catch (err) {
|
|
89
123
|
const detail = err && err.stderr ? err.stderr.toString().trim() : (err && err.message) || 'unknown error';
|
|
90
124
|
throw new Error(`sync-manifest-versions: failed to git-add manifests inside a work tree: ${detail}`);
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
|
|
94
|
-
module.exports = { VERSIONED_MANIFESTS, syncManifestVersions, findDrift, getPackageVersion, stageManifests };
|
|
128
|
+
module.exports = { VERSIONED_MANIFESTS, VERSIONED_TEXT_FILES, syncManifestVersions, findDrift, getPackageVersion, stageManifests };
|
|
95
129
|
|
|
96
130
|
if (require.main === module) {
|
|
97
131
|
const args = process.argv.slice(2);
|
|
@@ -105,7 +139,7 @@ if (require.main === module) {
|
|
|
105
139
|
console.error('Run `node scripts/sync-manifest-versions.cjs` to fix.');
|
|
106
140
|
process.exitCode = 1;
|
|
107
141
|
} else {
|
|
108
|
-
console.log('All ' + VERSIONED_MANIFESTS.length + ' versioned manifests in sync at ' + version + '.');
|
|
142
|
+
console.log('All ' + (VERSIONED_MANIFESTS.length + VERSIONED_TEXT_FILES.length) + ' versioned manifests in sync at ' + version + '.');
|
|
109
143
|
}
|
|
110
144
|
} else {
|
|
111
145
|
const changed = syncManifestVersions({ version });
|
|
@@ -23,6 +23,75 @@ const WORKFLOWS_DIR = path.join(__dirname, '..', 'pp-core', 'workflows');
|
|
|
23
23
|
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
24
24
|
const SNIPPET_FILE = path.join(WORKFLOWS_DIR, '_runtime-launcher.snippet.sh');
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The prefix every launcher line starts with (after trimming leading indentation).
|
|
28
|
+
*/
|
|
29
|
+
const LAUNCHER_HEAD = '_PP_SHIM_NAME="pp-tools.cjs"; ';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Launcher carriers that live OUTSIDE this script's two roots
|
|
33
|
+
* (pp-core/workflows/ and agents/).
|
|
34
|
+
*
|
|
35
|
+
* Why a list rather than another walked root: these are commands, docs and
|
|
36
|
+
* references whose block structure must survive untouched. transformFile()
|
|
37
|
+
* strips every resolver line from every shell block and re-inserts ONE preamble
|
|
38
|
+
* into the first pp_run block — correct for a workflow, wrong here, because
|
|
39
|
+
* commands/pp-graphify.md legitimately carries five copies (one per Bash-tool
|
|
40
|
+
* block a reader may run in isolation) and docs/DEBUGGING.md carries one inside
|
|
41
|
+
* an explanatory fence. They get line-level in-place replacement instead:
|
|
42
|
+
* same count, same positions, same indentation.
|
|
43
|
+
*
|
|
44
|
+
* Adding a launcher line to a file outside the roots without listing it here is
|
|
45
|
+
* caught by the repo-wide sweep in tests/runtime-launcher-parity.test.cjs (W6),
|
|
46
|
+
* which reads this array — the sweep is deliberately wider than the generator.
|
|
47
|
+
*/
|
|
48
|
+
const EXTRA_CARRIERS = Object.freeze([
|
|
49
|
+
'commands/pp-discuss-phase.md',
|
|
50
|
+
'commands/pp-graphify.md',
|
|
51
|
+
'commands/pp-import.md',
|
|
52
|
+
'docs/DEBUGGING.md',
|
|
53
|
+
'pp-core/references/planner-load-graph-context.md',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True when `line` is a launcher line: it starts with the launcher head
|
|
58
|
+
* (ignoring indentation) and closes the one-line if/elif/else chain with `fi`.
|
|
59
|
+
*
|
|
60
|
+
* A prose line that merely mentions the head in backticks does not close with
|
|
61
|
+
* `fi`, so it is rejected. This is the SINGLE detector shared by the generator
|
|
62
|
+
* and the parity sweep.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} line
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
function isLauncherLine(line) {
|
|
68
|
+
if (typeof line !== 'string') return false;
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
return trimmed.startsWith(LAUNCHER_HEAD) && /\bfi$/.test(trimmed);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Replace every launcher line in `content` with `canonicalLine`, in place:
|
|
75
|
+
* the line count, each line's position and each line's leading whitespace are
|
|
76
|
+
* preserved. Nothing else in the document is read or written.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} content
|
|
79
|
+
* @param {string} canonicalLine
|
|
80
|
+
* @returns {string|null} the new content, or null when nothing changed
|
|
81
|
+
*/
|
|
82
|
+
function replaceCanonicalLines(content, canonicalLine) {
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
let changed = false;
|
|
85
|
+
const out = lines.map((line) => {
|
|
86
|
+
if (!isLauncherLine(line)) return line;
|
|
87
|
+
const indent = line.slice(0, line.length - line.trimStart().length);
|
|
88
|
+
const replacement = indent + canonicalLine;
|
|
89
|
+
if (replacement !== line) changed = true;
|
|
90
|
+
return replacement;
|
|
91
|
+
});
|
|
92
|
+
return changed ? out.join('\n') : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
26
95
|
// Read canonical preamble (full content of snippet file)
|
|
27
96
|
function loadPreamble() {
|
|
28
97
|
const raw = fs.readFileSync(SNIPPET_FILE, 'utf8');
|
|
@@ -413,12 +482,48 @@ function main() {
|
|
|
413
482
|
}
|
|
414
483
|
}
|
|
415
484
|
|
|
485
|
+
// Process the launcher carriers outside the two roots (in-place line mode).
|
|
486
|
+
if (preamble.length !== 1) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`_runtime-launcher.snippet.sh must be exactly ONE line (found ${preamble.length}). ` +
|
|
489
|
+
'The in-place carrier mode, the repo-wide parity sweep and the one-line extractor in ' +
|
|
490
|
+
'tests/bug-3668-workflow-runtime-resolution.test.cjs all depend on that contract.',
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const repoRoot = path.join(__dirname, '..');
|
|
494
|
+
for (const rel of EXTRA_CARRIERS) {
|
|
495
|
+
const full = path.join(repoRoot, rel);
|
|
496
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
497
|
+
if (!content.split('\n').some((line) => isLauncherLine(line))) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`EXTRA_CARRIERS lists ${rel} but that file carries no launcher line. ` +
|
|
500
|
+
'A stale list must fail loudly rather than silently skip a carrier — ' +
|
|
501
|
+
'remove the entry or restore the launcher line.',
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const result = replaceCanonicalLines(content, preamble[0]);
|
|
505
|
+
if (result !== null) {
|
|
506
|
+
fs.writeFileSync(full, result, 'utf8');
|
|
507
|
+
transformedCount++;
|
|
508
|
+
console.log(`transformed (carrier): ${rel}`);
|
|
509
|
+
} else {
|
|
510
|
+
unchangedCount++;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
416
514
|
console.log(`\nDone. ${transformedCount} files transformed, ${unchangedCount} unchanged.`);
|
|
417
515
|
}
|
|
418
516
|
|
|
419
517
|
// Export the pure transform for tests; only run the filesystem walk when invoked
|
|
420
518
|
// directly (`node scripts/sync-runtime-launcher.cjs`), never on require().
|
|
421
|
-
module.exports = {
|
|
519
|
+
module.exports = {
|
|
520
|
+
transformFile,
|
|
521
|
+
loadPreamble,
|
|
522
|
+
isLauncherLine,
|
|
523
|
+
replaceCanonicalLines,
|
|
524
|
+
EXTRA_CARRIERS,
|
|
525
|
+
LAUNCHER_HEAD,
|
|
526
|
+
};
|
|
422
527
|
|
|
423
528
|
if (require.main === module) {
|
|
424
529
|
main();
|
|
@@ -4,65 +4,309 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* @file update-size-baseline.cjs
|
|
6
6
|
*
|
|
7
|
-
* Regenerates the committed per-file
|
|
8
|
-
* (`tests/workflow-size-baseline.json`) from
|
|
7
|
+
* Regenerates the committed per-file size baselines
|
|
8
|
+
* (`tests/workflow-size-baseline.json`, `tests/agent-size-baseline.json`) from
|
|
9
|
+
* the current files.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Workflow size budget policy (2026-10-11; per commit since pack 9): the guards
|
|
12
|
+
* allow each commit a small DELTA BUDGET (config size_budget.*, see
|
|
13
|
+
* scripts/lib/size-budget.cjs), so plans never run this script. It runs once per
|
|
14
|
+
* phase, in the phase's closing commit (`--reason "phase NN close"`, from
|
|
15
|
+
* execute-phase), and otherwise only when one commit must grow past the budget.
|
|
16
|
+
* Because a raise is the review record of growth, RAISING any entry requires a
|
|
17
|
+
* reason:
|
|
13
18
|
*
|
|
14
|
-
*
|
|
19
|
+
* npm run size:baseline # lower / add / remove only
|
|
20
|
+
* npm run size:baseline -- --reason "<why>" # also allowed to raise
|
|
21
|
+
*
|
|
22
|
+
* Without `--reason`, a run that would raise any entry is REFUSED: it exits
|
|
23
|
+
* non-zero and writes nothing (neither baseline). With a reason, every raise is
|
|
24
|
+
* recorded in the baseline file's `_rebaselines` array (newest last) as
|
|
25
|
+
* `{ date, reason, deltas: [{ file, from, to, delta }] }` — the committed
|
|
26
|
+
* review record of a deliberate growth.
|
|
27
|
+
*
|
|
28
|
+
* Idempotent: running it twice with no file changes produces no diff (a run
|
|
29
|
+
* with nothing to raise never appends a `_rebaselines` entry).
|
|
15
30
|
*/
|
|
16
31
|
|
|
17
32
|
const fs = require('fs');
|
|
18
33
|
const path = require('path');
|
|
19
|
-
const { measureMdFiles, WORKFLOWS_DIR } = require('./workflow-size.cjs');
|
|
34
|
+
const { measureMdFiles, measureMdTree, WORKFLOWS_DIR } = require('./workflow-size.cjs');
|
|
20
35
|
|
|
21
36
|
const BASELINE_PATH = path.join(__dirname, '..', 'tests', 'workflow-size-baseline.json');
|
|
22
37
|
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
|
23
38
|
const AGENT_BASELINE_PATH = path.join(__dirname, '..', 'tests', 'agent-size-baseline.json');
|
|
24
|
-
const
|
|
39
|
+
const isPpAgent = (f) => f.startsWith('pp-');
|
|
40
|
+
|
|
41
|
+
/** Key of the raise log inside a baseline file. Never a measured filename. */
|
|
42
|
+
const REBASELINES_KEY = '_rebaselines';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Split a parsed baseline file into its size entries and its raise log.
|
|
46
|
+
* Size entries are the numeric values; metadata keys (`_rebaselines`) are not
|
|
47
|
+
* sizes and must never reach a guard as one.
|
|
48
|
+
*
|
|
49
|
+
* @param {object|null} raw - Parsed baseline JSON (null when absent).
|
|
50
|
+
* @returns {{ sizes: Object<string, number>, rebaselines: Array<object> }}
|
|
51
|
+
*/
|
|
52
|
+
function splitBaseline(raw) {
|
|
53
|
+
const sizes = {};
|
|
54
|
+
let rebaselines = [];
|
|
55
|
+
if (raw && typeof raw === 'object') {
|
|
56
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
57
|
+
if (key === REBASELINES_KEY) rebaselines = value;
|
|
58
|
+
else sizes[key] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { sizes, rebaselines };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read and parse a baseline file; `null` when it does not exist.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} filePath
|
|
68
|
+
* @returns {object|null}
|
|
69
|
+
*/
|
|
70
|
+
function readBaseline(filePath) {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = fs.readFileSync(filePath, 'utf-8');
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.code === 'ENOENT') return null;
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
return JSON.parse(text);
|
|
79
|
+
}
|
|
25
80
|
|
|
26
81
|
/**
|
|
27
|
-
* Serialize
|
|
28
|
-
* indent, trailing newline (
|
|
82
|
+
* Serialize to the on-disk baseline format: size keys sorted, then the
|
|
83
|
+
* `_rebaselines` log last; 2-space indent, trailing newline (a stable,
|
|
84
|
+
* minimal-diff artifact).
|
|
29
85
|
*
|
|
30
86
|
* @param {Object<string, number>} sizes
|
|
87
|
+
* @param {Array<object>} [rebaselines]
|
|
31
88
|
* @returns {string}
|
|
32
89
|
*/
|
|
33
|
-
function serializeBaseline(sizes) {
|
|
90
|
+
function serializeBaseline(sizes, rebaselines = []) {
|
|
34
91
|
const sorted = {};
|
|
35
92
|
for (const key of Object.keys(sizes).sort()) sorted[key] = sizes[key];
|
|
93
|
+
sorted[REBASELINES_KEY] = rebaselines;
|
|
36
94
|
return JSON.stringify(sorted, null, 2) + '\n';
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
/**
|
|
40
|
-
*
|
|
98
|
+
* Classify the difference between an old and a new size map.
|
|
99
|
+
*
|
|
100
|
+
* @param {Object<string, number>} oldSizes
|
|
101
|
+
* @param {Object<string, number>} newSizes
|
|
102
|
+
* @returns {{ raised: Array<{file:string,from:number,to:number,delta:number}>,
|
|
103
|
+
* lowered: Array<{file:string,from:number,to:number,delta:number}>,
|
|
104
|
+
* added: string[], removed: string[] }}
|
|
105
|
+
*/
|
|
106
|
+
function diffSizes(oldSizes, newSizes) {
|
|
107
|
+
const raised = [];
|
|
108
|
+
const lowered = [];
|
|
109
|
+
const added = [];
|
|
110
|
+
for (const file of Object.keys(newSizes).sort()) {
|
|
111
|
+
if (!(file in oldSizes)) {
|
|
112
|
+
added.push(file);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const from = oldSizes[file];
|
|
116
|
+
const to = newSizes[file];
|
|
117
|
+
if (to > from) raised.push({ file, from, to, delta: to - from });
|
|
118
|
+
else if (to < from) lowered.push({ file, from, to, delta: from - to });
|
|
119
|
+
}
|
|
120
|
+
const removed = Object.keys(oldSizes).filter((f) => !(f in newSizes)).sort();
|
|
121
|
+
return { raised, lowered, added, removed };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validate a `_rebaselines` log. Returns a list of problems (empty = valid).
|
|
126
|
+
* Every entry must carry an ISO date, a non-empty reason and a non-empty list
|
|
127
|
+
* of positive deltas whose arithmetic holds; dates must be non-decreasing
|
|
128
|
+
* (newest last).
|
|
129
|
+
*
|
|
130
|
+
* @param {unknown} log
|
|
131
|
+
* @returns {string[]}
|
|
132
|
+
*/
|
|
133
|
+
function validateRebaselineLog(log) {
|
|
134
|
+
if (!Array.isArray(log)) return [`${REBASELINES_KEY} must be an array, got ${JSON.stringify(log)}`];
|
|
135
|
+
const problems = [];
|
|
136
|
+
const isSize = (n) => Number.isInteger(n) && n >= 0;
|
|
137
|
+
let prevDate = '';
|
|
138
|
+
log.forEach((entry, i) => {
|
|
139
|
+
const at = `${REBASELINES_KEY}[${i}]`;
|
|
140
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
141
|
+
problems.push(`${at} must be an object`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (typeof entry.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.date)) {
|
|
145
|
+
problems.push(`${at}.date must be YYYY-MM-DD, got ${JSON.stringify(entry.date)}`);
|
|
146
|
+
} else {
|
|
147
|
+
if (entry.date < prevDate) problems.push(`${at}.date ${entry.date} is older than the entry before it (newest must be last)`);
|
|
148
|
+
prevDate = entry.date;
|
|
149
|
+
}
|
|
150
|
+
if (typeof entry.reason !== 'string' || entry.reason.trim() === '') {
|
|
151
|
+
problems.push(`${at}.reason must be a non-empty string`);
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(entry.deltas) || entry.deltas.length === 0) {
|
|
154
|
+
problems.push(`${at}.deltas must be a non-empty array`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
entry.deltas.forEach((d, j) => {
|
|
158
|
+
const dat = `${at}.deltas[${j}]`;
|
|
159
|
+
if (!d || typeof d.file !== 'string' || d.file === '') problems.push(`${dat}.file must be a non-empty string`);
|
|
160
|
+
if (!d || !isSize(d.from) || !isSize(d.to)) problems.push(`${dat}.from/.to must be non-negative integers`);
|
|
161
|
+
else if (!Number.isInteger(d.delta) || d.delta <= 0 || d.delta !== d.to - d.from) {
|
|
162
|
+
problems.push(`${dat}.delta must be the positive difference to - from (${d.to - d.from}), got ${JSON.stringify(d.delta)}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
return problems;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Compute (but do not write) a regenerated baseline.
|
|
41
171
|
*
|
|
42
172
|
* @param {object} [opts]
|
|
43
173
|
* @param {string} [opts.dir] - Directory to measure (default: workflows).
|
|
44
|
-
* @param {string} [opts.outPath] - Baseline file
|
|
45
|
-
* @param {function(string): boolean} [opts.predicate] -
|
|
46
|
-
* @
|
|
174
|
+
* @param {string} [opts.outPath] - Baseline file (default: workflow baseline).
|
|
175
|
+
* @param {function(string): boolean} [opts.predicate] - File filter.
|
|
176
|
+
* @param {function(string, function): Object<string, number>} [opts.measure] -
|
|
177
|
+
* Measurer (default: recursive `measureMdTree`; agents pass `measureMdFiles`).
|
|
178
|
+
* @param {string} [opts.reason] - Justification; required to raise any entry.
|
|
179
|
+
* @param {string} [opts.today] - Date stamp for a raise (default: today, UTC).
|
|
180
|
+
* @returns {{ outPath: string, refused: boolean, count: number, content: string|null,
|
|
181
|
+
* change: ReturnType<typeof diffSizes> }}
|
|
47
182
|
*/
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
183
|
+
function planBaseline({
|
|
184
|
+
dir = WORKFLOWS_DIR,
|
|
185
|
+
outPath = BASELINE_PATH,
|
|
186
|
+
predicate,
|
|
187
|
+
measure = measureMdTree,
|
|
188
|
+
reason,
|
|
189
|
+
today = new Date().toISOString().slice(0, 10),
|
|
190
|
+
} = {}) {
|
|
191
|
+
const sizes = measure(dir, predicate);
|
|
192
|
+
const { sizes: oldSizes, rebaselines: oldLog } = splitBaseline(readBaseline(outPath));
|
|
193
|
+
const change = diffSizes(oldSizes, sizes);
|
|
194
|
+
const count = Object.keys(sizes).length;
|
|
195
|
+
const hasReason = typeof reason === 'string' && reason.trim() !== '';
|
|
196
|
+
if (change.raised.length > 0 && !hasReason) {
|
|
197
|
+
return { outPath, refused: true, count, content: null, change };
|
|
198
|
+
}
|
|
199
|
+
const log = Array.isArray(oldLog) ? [...oldLog] : [];
|
|
200
|
+
if (change.raised.length > 0) {
|
|
201
|
+
log.push({ date: today, reason: reason.trim(), deltas: change.raised });
|
|
202
|
+
}
|
|
203
|
+
return { outPath, refused: false, count, content: serializeBaseline(sizes, log), change };
|
|
53
204
|
}
|
|
54
205
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Regenerate one baseline file. A refused plan (a raise without a reason)
|
|
208
|
+
* writes nothing.
|
|
209
|
+
*
|
|
210
|
+
* @param {Parameters<typeof planBaseline>[0]} [opts]
|
|
211
|
+
* @returns {ReturnType<typeof planBaseline>}
|
|
212
|
+
*/
|
|
213
|
+
function generateBaseline(opts = {}) {
|
|
214
|
+
const plan = planBaseline(opts);
|
|
215
|
+
if (!plan.refused) fs.writeFileSync(plan.outPath, plan.content);
|
|
216
|
+
return plan;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse CLI arguments: only `--reason <text>` / `--reason=<text>`.
|
|
221
|
+
*
|
|
222
|
+
* @param {string[]} argv
|
|
223
|
+
* @returns {{ reason?: string }}
|
|
224
|
+
*/
|
|
225
|
+
function parseArgs(argv) {
|
|
226
|
+
const out = {};
|
|
227
|
+
for (let i = 0; i < argv.length; i++) {
|
|
228
|
+
const arg = argv[i];
|
|
229
|
+
if (arg === '--reason') {
|
|
230
|
+
out.reason = argv[++i];
|
|
231
|
+
if (out.reason === undefined) throw new Error('--reason needs a value');
|
|
232
|
+
} else if (arg.startsWith('--reason=')) {
|
|
233
|
+
out.reason = arg.slice('--reason='.length);
|
|
234
|
+
} else {
|
|
235
|
+
throw new Error(`unknown argument: ${arg} (only --reason "<text>" is accepted)`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (out.reason !== undefined && out.reason.trim() === '') throw new Error('--reason must not be empty');
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const TARGETS = [
|
|
243
|
+
{ label: 'workflow', dir: WORKFLOWS_DIR, outPath: BASELINE_PATH, measure: measureMdTree },
|
|
244
|
+
{ label: 'agent', dir: AGENTS_DIR, outPath: AGENT_BASELINE_PATH, predicate: isPpAgent, measure: measureMdFiles },
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CLI entry. Plans every target first and writes only when none is refused,
|
|
249
|
+
* so a refusal leaves BOTH baselines untouched.
|
|
250
|
+
*
|
|
251
|
+
* @param {string[]} argv
|
|
252
|
+
* @param {object} [io]
|
|
253
|
+
* @param {Array<object>} [io.targets]
|
|
254
|
+
* @param {string} [io.today]
|
|
255
|
+
* @param {NodeJS.WritableStream} [io.stdout]
|
|
256
|
+
* @param {NodeJS.WritableStream} [io.stderr]
|
|
257
|
+
* @returns {number} exit code
|
|
258
|
+
*/
|
|
259
|
+
function main(argv, { targets = TARGETS, today, stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
260
|
+
let args;
|
|
261
|
+
try {
|
|
262
|
+
args = parseArgs(argv);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
stderr.write(`size:baseline: ${err.message}\n`);
|
|
265
|
+
return 2;
|
|
266
|
+
}
|
|
267
|
+
const plans = targets.map((t) => ({ t, plan: planBaseline({ ...t, reason: args.reason, today }) }));
|
|
268
|
+
const refused = plans.filter((p) => p.plan.refused);
|
|
269
|
+
if (refused.length > 0) {
|
|
270
|
+
const lines = refused.flatMap(({ t, plan }) =>
|
|
271
|
+
plan.change.raised.map((r) => ` - ${t.label}: ${r.file}: ${r.from} → ${r.to} (+${r.delta})`)
|
|
272
|
+
);
|
|
273
|
+
stderr.write(
|
|
274
|
+
'size:baseline: REFUSED — this run would RAISE baseline entries, which needs a reason:\n' +
|
|
275
|
+
`${lines.join('\n')}\n` +
|
|
276
|
+
'Shrink the file(s), or re-run with: npm run size:baseline -- --reason "<why this growth is needed>"\n' +
|
|
277
|
+
'Nothing was written.\n'
|
|
64
278
|
);
|
|
279
|
+
return 1;
|
|
65
280
|
}
|
|
281
|
+
for (const { t, plan } of plans) {
|
|
282
|
+
fs.writeFileSync(plan.outPath, plan.content);
|
|
283
|
+
const c = plan.change;
|
|
284
|
+
stdout.write(
|
|
285
|
+
`Wrote ${plan.count} ${t.label} sizes to ${path.relative(process.cwd(), plan.outPath)} ` +
|
|
286
|
+
`(raised ${c.raised.length}, lowered ${c.lowered.length}, added ${c.added.length}, removed ${c.removed.length})\n`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
if (args.reason !== undefined && plans.every((p) => p.plan.change.raised.length === 0)) {
|
|
290
|
+
stdout.write('Note: nothing was raised, so --reason was not recorded.\n');
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (require.main === module) {
|
|
296
|
+
process.exitCode = main(process.argv.slice(2));
|
|
66
297
|
}
|
|
67
298
|
|
|
68
|
-
module.exports = {
|
|
299
|
+
module.exports = {
|
|
300
|
+
generateBaseline,
|
|
301
|
+
planBaseline,
|
|
302
|
+
serializeBaseline,
|
|
303
|
+
splitBaseline,
|
|
304
|
+
readBaseline,
|
|
305
|
+
diffSizes,
|
|
306
|
+
validateRebaselineLog,
|
|
307
|
+
parseArgs,
|
|
308
|
+
main,
|
|
309
|
+
REBASELINES_KEY,
|
|
310
|
+
BASELINE_PATH,
|
|
311
|
+
AGENT_BASELINE_PATH,
|
|
312
|
+
};
|