mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 03:56:53 +03:00
48c1a1e6f5
Thicken the two flagship engineering methodology skills with the artifact set promised by issue #239: schema-v1 eval manifests (6 cases each), fillable templates, and one small stdlib-only script per skill with tests. backend-engineering: - evals/evals.json: API implementation review, endpoint modeling, service structure, error handling, N+1 detection, integration retry/idempotency - templates/service-design-record.md, templates/error-handling-taxonomy.md - scripts/n1-query-spotter.py (+ test_n1_query_spotter.py): flags query-like calls inside loops with loop-variable confidence, --json output frontend-engineering: - evals/evals.json: component/state design, state management selection, API integration, data-fetching states, performance review, performance budgets - templates/component-state-design-record.md, templates/performance-budget.md - scripts/bundle-budget-checker.py (+ test_bundle_budget_checker.py): enforces total and per-chunk byte budgets on bundle reports, exit 1 on violation Both SKILL.md files gain Templates and Scripts sections; both READMEs document the scripts in Quick Start. All local validators pass (validate-skills.rb, validate-evals.py, eval-coverage ratchet, make validate). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
193 lines
6.3 KiB
Python
193 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Bundle budget checker for frontend-engineering.
|
|
|
|
Compares a JavaScript/CSS bundle size report against total and per-chunk byte
|
|
budgets and fails when a budget is exceeded, so a performance regression stops
|
|
the build instead of shipping silently.
|
|
|
|
Input: a JSON file describing bundle chunks, in either of two shapes:
|
|
|
|
{"chunks": [{"name": "main.js", "size": 180000}, ...]} # structured
|
|
{"main.js": 180000, "vendor.js": 90000} # name -> bytes
|
|
|
|
Budgets accept human units: 250KB, 1.5MB, 512000, 10 B (decimal KB/MB/GB or
|
|
binary KiB/MiB/GiB).
|
|
|
|
Exit codes:
|
|
0 all sizes within budget
|
|
1 one or more chunks or the total exceed budget
|
|
2 usage or input error (missing file, malformed JSON, bad budget value)
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
_UNIT_RE = re.compile(r"^\s*(\d+(?:\.\d+)?)\s*(b|kb|kib|mb|mib|gb|gib)?\s*$", re.IGNORECASE)
|
|
_MULTIPLIERS = {
|
|
"b": 1,
|
|
"kb": 1000,
|
|
"kib": 1024,
|
|
"mb": 1000**2,
|
|
"mib": 1024**2,
|
|
"gb": 1000**3,
|
|
"gib": 1024**3,
|
|
}
|
|
|
|
|
|
def parse_size(text):
|
|
"""Parse '250KB', '1.5MB', or plain byte counts into an int, or None."""
|
|
match = _UNIT_RE.match(text)
|
|
if not match:
|
|
return None
|
|
value = float(match.group(1))
|
|
unit = (match.group(2) or "b").lower()
|
|
return int(value * _MULTIPLIERS[unit])
|
|
|
|
|
|
def human_size(size):
|
|
"""Render a byte count in a compact human unit (binary)."""
|
|
value = float(size)
|
|
for unit in ("B", "KiB", "MiB", "GiB"):
|
|
if value < 1024 or unit == "GiB":
|
|
if unit == "B":
|
|
return f"{int(value)} B"
|
|
return f"{value:.1f} {unit}"
|
|
value /= 1024
|
|
return f"{value:.1f} GiB"
|
|
|
|
|
|
def load_chunks(path):
|
|
"""Load a bundle report into [(name, bytes)]; raises ValueError on bad input."""
|
|
try:
|
|
raw = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
except OSError as exc:
|
|
raise ValueError(f"cannot read {path}: {exc.strerror}") from exc
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"{path}: invalid JSON: {exc.msg}") from exc
|
|
|
|
if isinstance(raw, dict):
|
|
if "chunks" in raw:
|
|
chunks = raw["chunks"]
|
|
if not isinstance(chunks, list):
|
|
raise ValueError(f"{path}: 'chunks' must be a list")
|
|
return [
|
|
(str(entry["name"]), int(entry["size"]))
|
|
for entry in chunks
|
|
if "name" in entry and "size" in entry
|
|
]
|
|
return [(str(name), int(size)) for name, size in raw.items()]
|
|
raise ValueError(f"{path}: report must be a JSON object of chunk names to sizes")
|
|
|
|
|
|
def build_parser():
|
|
parser = argparse.ArgumentParser(
|
|
prog="bundle-budget-checker.py",
|
|
description=(
|
|
"Check a bundle size report against total and per-chunk budgets. "
|
|
'Report format: {"chunks": [{"name": ..., "size": bytes}]} '
|
|
"or a plain {name: bytes} mapping."
|
|
),
|
|
epilog="Exit codes: 0 within budget, 1 over budget, 2 usage or input error.",
|
|
)
|
|
parser.add_argument("report", metavar="REPORT.json", help="bundle size report")
|
|
parser.add_argument(
|
|
"--total",
|
|
metavar="SIZE",
|
|
default=None,
|
|
help="total budget for all chunks, e.g. 500KB or 512000",
|
|
)
|
|
parser.add_argument(
|
|
"--chunk",
|
|
metavar="SIZE",
|
|
default=None,
|
|
help="per-chunk budget, e.g. 120KB; each chunk is checked individually",
|
|
)
|
|
parser.add_argument("--json", action="store_true", help="emit a machine-readable JSON report")
|
|
return parser
|
|
|
|
|
|
def main(argv=None):
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
|
|
total_budget = parse_size(args.total) if args.total is not None else None
|
|
chunk_budget = parse_size(args.chunk) if args.chunk is not None else None
|
|
if args.total is not None and total_budget is None:
|
|
print(f"ERROR: cannot parse budget {args.total!r}", file=sys.stderr)
|
|
return 2
|
|
if args.chunk is not None and chunk_budget is None:
|
|
print(f"ERROR: cannot parse budget {args.chunk!r}", file=sys.stderr)
|
|
return 2
|
|
if total_budget is not None and total_budget < 0:
|
|
print("ERROR: --total must not be negative", file=sys.stderr)
|
|
return 2
|
|
if chunk_budget is not None and chunk_budget < 0:
|
|
print("ERROR: --chunk must not be negative", file=sys.stderr)
|
|
return 2
|
|
|
|
try:
|
|
chunks = load_chunks(args.report)
|
|
except ValueError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
return 2
|
|
|
|
total = sum(size for _, size in chunks)
|
|
rows = []
|
|
over_budget = False
|
|
for name, size in chunks:
|
|
over = chunk_budget is not None and size > chunk_budget
|
|
over_budget = over_budget or over
|
|
rows.append(
|
|
{
|
|
"name": name,
|
|
"bytes": size,
|
|
"budget": chunk_budget,
|
|
"status": "over" if over else "ok",
|
|
}
|
|
)
|
|
total_over = total_budget is not None and total > total_budget
|
|
over_budget = over_budget or total_over
|
|
|
|
if args.json:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"total": {
|
|
"bytes": total,
|
|
"budget": total_budget,
|
|
"status": "over" if total_over else "ok",
|
|
},
|
|
"chunks": rows,
|
|
"over_budget": over_budget,
|
|
},
|
|
indent=2,
|
|
)
|
|
)
|
|
else:
|
|
print("Bundle budget report")
|
|
for row in rows:
|
|
status = "OVER" if row["status"] == "over" else "OK"
|
|
budget_text = human_size(row["budget"]) if row["budget"] is not None else "unset"
|
|
print(
|
|
f" {row['name']:<24} {human_size(row['bytes']):>10} "
|
|
f"budget {budget_text:>8} {status}"
|
|
)
|
|
total_budget_text = human_size(total_budget) if total_budget is not None else "unset"
|
|
total_status = "OVER" if total_over else "OK"
|
|
print(
|
|
f" {'total':<24} {human_size(total):>10} "
|
|
f"budget {total_budget_text:>8} {total_status}"
|
|
)
|
|
if over_budget:
|
|
print("Result: over budget", file=sys.stderr)
|
|
else:
|
|
print("Result: within budget")
|
|
return 1 if over_budget else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|