Add the TypeScript mirror of engine/modules/_constraint_math.py with identical semantics, validated against the SAME shared corpus (tests/fixtures/constraint-corpus.json) so Py === TS parity holds. Vitest covers 2 unit cases (multi-tag multiply, exclude-absorbing) plus the 4 corpus cases (multi-tag->1, multi-pick->6, exclude->EXCLUDE, exception->0.25). Also bump bundle TOTAL_LIMIT 405 -> 430 KB for the upcoming SP3 constraint reach UI; entry/per-file limits unchanged.
227 lines
12 KiB
JavaScript
227 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
import { gzipSync } from "node:zlib";
|
||
import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs";
|
||
import { dirname, join } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
|
||
const JS_DIR = fileURLToPath(new URL("../js", import.meta.url));
|
||
|
||
// `--json <path>` writes a machine-readable manifest alongside the human
|
||
// console report. Used by .github/workflows/bundle-size-pr.yml to compute
|
||
// the delta between base and head.
|
||
const args = process.argv.slice(2);
|
||
const jsonIdx = args.indexOf("--json");
|
||
const jsonPath = jsonIdx >= 0 ? args[jsonIdx + 1] : null;
|
||
|
||
// Budgets (gzipped bytes) — tune only with explicit review
|
||
// 2026-05-10: total bumped 250 → 256 KB to fit Phase B duplicate/fork
|
||
// system (per-kind save-to-library fork handlers + supporting helpers
|
||
// + flash/shake CSS). Cycle was explicitly scoped + approved.
|
||
// 2026-05-12: total bumped 256 → 300 KB to fit Batch 2 bundle drag/drop
|
||
// overhaul + headroom for follow-up batches without per-task contortion.
|
||
// Cycle was explicitly scoped + approved.
|
||
// 2026-05-20: total bumped 300 → 312 KB to fit RichTextInput atomic-chip
|
||
// editor reused inside the canvas combine TemplateSection (lazy chunk
|
||
// ~9 KB gzipped). The textarea-with-pills pattern was replaced with the
|
||
// SPA's chip editor so canvas + SPA share one $var/@ref editing surface.
|
||
// Explicitly approved feature request.
|
||
const ENTRY_LIMIT = 30 * 1024; // 30 KB
|
||
// 316 KB — bumped from 312 KB during the nested-bundle drag redesign.
|
||
// The container-scoped resolver (drop-zone.ts), pure applier (drop.ts),
|
||
// floating indicator SFC (BundleDropBar.vue), zoom-aware offsetTop
|
||
// walker, and moving-range filter for bundle drags added ~1 KB net.
|
||
// Bump approved alongside the feature land.
|
||
// 321 KB — bumped from 316 KB for the bundle edit modal
|
||
// (BundleInstanceModal + IdentitySection + RuntimeSection). Modal is
|
||
// lazy-loaded so the entry chunk is unaffected; total grows ~5 KB
|
||
// gzipped for the once-per-bundle edit surface (display name + frame
|
||
// color + master Lock/Hide cascades + Save/Reset-to-library footer).
|
||
// Mirrors the per-kind module modal shell users already know.
|
||
// 323 KB — bumped from 321 KB to fit the HSV color popover inside
|
||
// the bundle modal (shared HsvPicker.vue + hsv.ts moved to
|
||
// `src/components/shared/` so SPA's ColorPicker / TweaksPanel and
|
||
// the canvas bundle modal both drive one picker). Adds ~2 KB
|
||
// gzipped in the lazy modal chunk; entry stays untouched.
|
||
// 324 KB — bumped from 323 KB for the bundle-modal polish round:
|
||
// SPA link in footer, popover Teleport + viewport-aware positioning,
|
||
// always-on "clear color" affordance, stableStringify-based bundle
|
||
// fingerprint that folds meta + instance overrides into the MOD
|
||
// signal, and child-uid preservation on reset-to-library so the
|
||
// outer bundle's MOD evaluates against its existing snapshot rather
|
||
// than flipping to "modified" via spurious uid churn. ~0.5 KB net.
|
||
// 325 KB — bumped from 324 KB for the bundle detach + master toggle
|
||
// rework + true-duplicate-with-local-edits. Adds `setBundleInternal`
|
||
// / `setBundleLock` helpers that delegate to inner-bundle masters
|
||
// (skipping inner-owned rows + recursing into them) and the smart
|
||
// OFF cascade, plus the per-instance deep-clone duplicate that
|
||
// rewrites bundle_origin + parent_uid through a uid map. Net
|
||
// growth ~0.2 KB gzip.
|
||
// 327 KB — bumped from 325 KB for the rule matrix redesign. Replaces
|
||
// the 4-state click-cycle + cog-anchored CellFactorPopover with a
|
||
// click-driven CellRulePopover (4 labeled state buttons + numeric
|
||
// factor input) plus a new collapsible MatrixLegend. Net growth
|
||
// ~1.2 KB gzip across the lazy ContextWidget chunk; entry untouched.
|
||
// 328 KB — bumped from 327 KB for the rule matrix polish pass: popover
|
||
// now Teleports to body (escapes the cell's hover / clipping context),
|
||
// adds a reset-to-library button + native-styled stepper input (mirror
|
||
// of OptionRow's spinner), and the watcher narrows so factor typing
|
||
// no longer select-all-clobbers each keystroke. ~0.4 KB net gzip in
|
||
// the lazy ContextWidget chunk; entry stays put.
|
||
// 330 KB — bumped from 328 KB for the null-wildcard-option feature
|
||
// (2026-05-24). Adds `is_null` option flag + "+ Add null" button on
|
||
// the wildcard editor, pi-ban chip rendering across canvas OptionRow
|
||
// + ExceptionsSection + TestRunner + WildcardEditor, "Include null"
|
||
// checkbox on the nested-ref sub-cat picker, new is_empty /
|
||
// is_not_empty derivation operators + flipped "is empty" tick UI,
|
||
// and engine-side validation + nested-ref filter keyword support.
|
||
// ~0.7 KB net gzip across the lazy ContextWidget chunk; entry stays
|
||
// put. See docs/superpowers/specs/2026-05-24-null-wildcard-option-design.md.
|
||
// 340 KB — bumped from 330 KB during the polish-cleanup cycle.
|
||
// Absorbs the count-aware cross-node downstream scanner + per-instance
|
||
// wildcard uuid collection (graph.ts), library-fallback RichTextPreview
|
||
// wired into the preview-resolver cache (so constraint-modal exception
|
||
// chips + the test-runner constraint table resolve `@{uuid}` against
|
||
// the wildcard library, not just the local node), sync `consumedBy`
|
||
// claim that fixes the cross-node MOVE-becomes-COPY race in
|
||
// ContextWidget's onDrop, plus the constraint pairing badge work
|
||
// landing alongside on the same branch. Approved with the bundle of
|
||
// bug fixes — see the polish-cleanup PR description.
|
||
// 360 KB — bumped from 340 KB for the WP_PromptCleaner widget
|
||
// (2026-05-25). Adds the CleanerWidget spectrum-dial SFC, the
|
||
// BlocklistModal body-teleported editor, the cleaner mount glue
|
||
// chunk, and shared types/intensity helpers. ~9 KB net gzip across
|
||
// the lazy cleaner chunks; entry stays put. See
|
||
// docs/superpowers/specs/2026-05-25-prompt-cleaner-node-design.md.
|
||
// Bumped 360 -> 380 KB on 2026-05-26 to accommodate the WP_VarToInt /
|
||
// WP_VarToFloat / WP_VarToBool converter nodes. New chunks: var-picker
|
||
// SFC + TS parser mirror + widget glue. ~4 KB net gzip across the lazy
|
||
// chunks; entry stays put. See
|
||
// docs/superpowers/specs/2026-05-26-converter-nodes-design.md.
|
||
// Bumped 380 -> 385 KB on 2026-05-27 for the prompt-template-library
|
||
// round-trip modals. LoadTemplateModal gains the ModulePicker-style
|
||
// chrome (search + category/tags filter popover + scrollable rows) and
|
||
// SaveTemplateModal gains the push-to-library layout (category select +
|
||
// Update-existing / Save-as-new dual action). ~3 KB net gzip across the
|
||
// two lazy assembler modal chunks; entry stays put.
|
||
// NOTE: this gate measures the EXTENSION bundle (`js/`, from
|
||
// build:extension) only — NOT the manager SPA (`web_dist/`). The 2026-05-27
|
||
// Documentation tab lives entirely in src/manager → web_dist, so it does
|
||
// not affect this budget; no bump was needed for it.
|
||
// Bumped 385 -> 387 KB on 2026-05-28 for the injector two-tier row model.
|
||
// Adds the durable general-template row to the InjectorWidget lazy chunk:
|
||
// the inline binding+template editor + refs hint in InjectorRow.vue, the
|
||
// `kind` discriminator + addGeneralRow + general-aware reconcile in
|
||
// InjectorWidget.vue, and the general-row branch in the conflict scanner.
|
||
// ~1.5 KB net gzip in the lazy InjectorWidget chunk (baseline was already
|
||
// at 393968/394240 — only 272 B free — so the bump is required); entry
|
||
// stays put.
|
||
// Bumped 387 -> 388 KB on 2026-05-28 for the kind-aware RefChip. Engine
|
||
// warnings like `constraint_never_applied` wrap NON-wildcard ids (the
|
||
// constraint module's own id + the target wildcard id) as `@{uuid}` in
|
||
// the message text. Pre-fix, RefChip painted every `@{uuid}` as a
|
||
// wildcard chip — the constraint id fell through as a red `?` because
|
||
// the resolver only consulted the wildcard catalog. Adds the
|
||
// `moduleKind` prop + `--wp-refchip-tone` inline style + per-kind
|
||
// PrimeIcon switch in RefChip.vue, the `resolveModuleChip` library-
|
||
// wide search in resolveChip.ts, a `uuidToKind` prop on RichTextPreview
|
||
// wiring through the preview-resolver `kind` field, and a trace-derived
|
||
// `uuidToKindMap` in DebugViewer. ~0.3 KB net gzip across the lazy
|
||
// DebugViewer + _shared + preview-resolver chunks; entry stays put.
|
||
// Bumped 388 -> 393 KB on 2026-05-28 for the WP_SeedList DOM widget.
|
||
// Replaces the three stock widgets (strategy Combo + 2× Boolean
|
||
// override toggles) with a single socketless DOM widget owning the
|
||
// strategy chip group + both override toggles inside one SFC. Adds
|
||
// the SeedListWidget Vue chunk (~4.2 KB gzip) + the seed_list glue
|
||
// chunk (~0.7 KB gzip) for a ~4.9 KB total bump; entry stays put.
|
||
// Same cycle also prefixed every custom DOM widget input name with
|
||
// `wp_*` (modules → wp_modules, rows → wp_rows, viewer → wp_viewer,
|
||
// etc.) so the canvas socket labels read as branded WP widgets — no
|
||
// bundle impact from the rename alone.
|
||
// 2026-05-28: bumped 393 -> 395 KB for the SeedListWidget override
|
||
// lock UI (disabled-attr on strategy chips + `--locked` wrapper class +
|
||
// strategyLocked computed + two new lock-state Vitest cases). Plus the
|
||
// host glue (`setStockWidgetDisabled` + `syncStockWidgetGates` callers
|
||
// in onUpdate/onValueRestored/microtask). Net ~0.5 KB gzip; 2 KB
|
||
// headroom buffer to avoid the "17 bytes from budget" tightness from
|
||
// the previous bump.
|
||
// Bumped 395 -> 405 KB on 2026-06-07 for SP1 Chunk G (in-graph multi-tag
|
||
// pool editor). PoolSection gains grouped quick pills + an ƒ(x) Advanced
|
||
// boolean-expression editor; OptionRow gains per-tag CATEGORY chips; and
|
||
// the shared engine-parity subcat_filter parser (parse/matches/readsAs)
|
||
// is now imported into the wildcard graph editor chunk. ~4 KB net gzip
|
||
// across the lazy ContextWidget chunk; entry stays put. Approved with the
|
||
// SP1 multi-tag + boolean-filter feature (docs/superpowers/specs/
|
||
// 2026-06-06-wildcard-multi-subcategory-boolean-filter-design.md §4.2).
|
||
// Bumped 405 -> 430 KB on 2026-06-11 for SP3 constraint reach UI headroom.
|
||
// SP3 Task 13 lands the TS combineConstraintFactor mirror (+ shared corpus
|
||
// parity test) now; the upcoming reach/constraint-matrix UI phases need the
|
||
// total budget headroom in the lazy ContextWidget chunk. Approved alongside
|
||
// the SP3 constraint feature.
|
||
const TOTAL_LIMIT = 430 * 1024; // 430 KB
|
||
|
||
function gzipSize(path) {
|
||
return gzipSync(readFileSync(path)).length;
|
||
}
|
||
|
||
function allJsFiles(dir) {
|
||
const out = [];
|
||
for (const name of readdirSync(dir)) {
|
||
const full = join(dir, name);
|
||
if (statSync(full).isDirectory()) {
|
||
out.push(...allJsFiles(full));
|
||
} else if (name.endsWith(".js")) {
|
||
out.push(full);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
let failed = false;
|
||
|
||
const files = allJsFiles(JS_DIR);
|
||
let total = 0;
|
||
const manifest = { entry: null, total: 0, files: {} };
|
||
console.log(`\nBundle size report (gzipped):\n`);
|
||
for (const file of files) {
|
||
const size = gzipSize(file);
|
||
total += size;
|
||
const rel = file.substring(JS_DIR.length + 1);
|
||
// Strip the rolled-up content hash so PR ↔ base file names match. Vite
|
||
// emits `assets/<name>-<hash>.js` per chunk; the hash changes with any
|
||
// content edit and would otherwise show every chunk as added/removed.
|
||
// Normalize Windows backslashes so the manifest diffs cleanly against
|
||
// CI runs (which always emit forward slashes on Linux).
|
||
const stable = rel.replace(/-[A-Za-z0-9_-]{8,}\.js$/, ".js").replaceAll("\\", "/");
|
||
manifest.files[stable] = size;
|
||
console.log(` ${rel.padEnd(40)} ${(size / 1024).toFixed(2).padStart(7)} KB`);
|
||
}
|
||
manifest.total = total;
|
||
console.log(` ${"TOTAL".padEnd(40)} ${(total / 1024).toFixed(2).padStart(7)} KB`);
|
||
|
||
const entry = files.find((f) => f.endsWith("main.js"));
|
||
if (entry) {
|
||
const entrySize = gzipSize(entry);
|
||
manifest.entry = entrySize;
|
||
if (entrySize > ENTRY_LIMIT) {
|
||
console.error(`\n✗ Entry main.js gzip ${entrySize} bytes exceeds budget ${ENTRY_LIMIT}`);
|
||
failed = true;
|
||
} else {
|
||
console.log(`\n✓ Entry within budget (${entrySize} / ${ENTRY_LIMIT} bytes gzipped)`);
|
||
}
|
||
}
|
||
|
||
if (total > TOTAL_LIMIT) {
|
||
console.error(`✗ Total js/ gzip ${total} bytes exceeds budget ${TOTAL_LIMIT}`);
|
||
failed = true;
|
||
} else {
|
||
console.log(`✓ Total within budget (${total} / ${TOTAL_LIMIT} bytes gzipped)`);
|
||
}
|
||
|
||
if (jsonPath) {
|
||
mkdirSync(dirname(jsonPath), { recursive: true });
|
||
writeFileSync(jsonPath, JSON.stringify(manifest, null, 2));
|
||
console.log(`\nWrote size manifest to ${jsonPath}`);
|
||
}
|
||
|
||
process.exit(failed ? 1 : 0);
|