mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-21 00:26:23 +03:00
feat(skill): add scripts, templates, and evals to backend-engineering and frontend-engineering (#256)
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>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
parent
abe1ab3a00
commit
48c1a1e6f5
@@ -0,0 +1,192 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Tests for bundle-budget-checker.py.
|
||||
|
||||
Covers: total and per-chunk budget enforcement, both input shapes (structured
|
||||
chunks list and name->bytes mapping), human unit parsing (250KB, 1.5MB), --json
|
||||
output, no-budget report mode, --help, and error paths (missing file, malformed
|
||||
JSON, bad budget value).
|
||||
|
||||
Discoverable by both pytest and unittest (unittest.TestCase classes).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import suppress
|
||||
|
||||
SCRIPTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
||||
CHECKER = os.path.join(SCRIPTS_DIR, "bundle-budget-checker.py")
|
||||
|
||||
|
||||
def run_checker(args):
|
||||
cmd = [sys.executable, CHECKER, *args]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
return proc.returncode, proc.stdout, proc.stderr
|
||||
|
||||
|
||||
def write_report(data):
|
||||
"""Write a JSON bundle report to a temp file; return its path."""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(data, handle)
|
||||
path = handle.name
|
||||
return path
|
||||
|
||||
|
||||
def cleanup(path):
|
||||
with suppress(OSError):
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class TestBundleBudgetBudgets(unittest.TestCase):
|
||||
def test_within_total_budget(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 100000}]})
|
||||
try:
|
||||
rc, _, _ = run_checker([path, "--total", "250KB"])
|
||||
self.assertEqual(rc, 0)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_over_total_budget(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 300000}]})
|
||||
try:
|
||||
rc, _, stderr = run_checker([path, "--total", "250KB"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("over budget", stderr)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_over_chunk_budget(self):
|
||||
path = write_report(
|
||||
{"chunks": [{"name": "main.js", "size": 180000}, {"name": "vendor.js", "size": 90000}]}
|
||||
)
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path, "--chunk", "120KB"])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertIn("main.js", stdout)
|
||||
self.assertIn("OVER", stdout)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_all_chunks_within_chunk_budget(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 100000}]})
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path, "--chunk", "200KB"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("OK", stdout)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_total_and_chunk_combined(self):
|
||||
path = write_report(
|
||||
{"chunks": [{"name": "main.js", "size": 80000}, {"name": "vendor.js", "size": 80000}]}
|
||||
)
|
||||
try:
|
||||
# Within both budgets: total 160 KB <= 200 KB, each chunk <= 100 KB.
|
||||
rc_ok, _, _ = run_checker([path, "--total", "200KB", "--chunk", "100KB"])
|
||||
self.assertEqual(rc_ok, 0)
|
||||
# Over chunk budget but within total: one chunk over 100 KB.
|
||||
rc_chunk, _, _ = run_checker([path, "--total", "300KB", "--chunk", "75KB"])
|
||||
self.assertEqual(rc_chunk, 1)
|
||||
# Over total but within chunk budget.
|
||||
rc_total, _, _ = run_checker([path, "--total", "100KB", "--chunk", "100KB"])
|
||||
self.assertEqual(rc_total, 1)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_no_budget_reports_only(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 180000}]})
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("unset", stdout)
|
||||
self.assertIn("within budget", stdout)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
|
||||
class TestBundleBudgetFormats(unittest.TestCase):
|
||||
def test_mapping_input_shape(self):
|
||||
path = write_report({"main.js": 180000, "vendor.js": 90000})
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path, "--total", "300KB"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("main.js", stdout)
|
||||
self.assertIn("vendor.js", stdout)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_empty_chunks_list(self):
|
||||
path = write_report({"chunks": []})
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path, "--total", "100KB"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("within budget", stdout)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
|
||||
class TestBundleBudgetParsing(unittest.TestCase):
|
||||
def test_unit_parsing_variants(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 1024}]})
|
||||
try:
|
||||
rc_ok, _, _ = run_checker([path, "--total", "1.5KB"])
|
||||
self.assertEqual(rc_ok, 0)
|
||||
rc_bin, _, _ = run_checker([path, "--total", "1KiB"])
|
||||
self.assertEqual(rc_bin, 0)
|
||||
rc_over, _, _ = run_checker([path, "--total", "512B"])
|
||||
self.assertEqual(rc_over, 1)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_plain_byte_budget(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 512000}]})
|
||||
try:
|
||||
rc, _, _ = run_checker([path, "--total", "512000"])
|
||||
self.assertEqual(rc, 0)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_mib_budget(self):
|
||||
path = write_report({"chunks": [{"name": "app.js", "size": 1500000}]})
|
||||
try:
|
||||
rc, _, _ = run_checker([path, "--total", "2MiB"])
|
||||
self.assertEqual(rc, 0)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
|
||||
class TestBundleBudgetCli(unittest.TestCase):
|
||||
def test_help_exits_zero(self):
|
||||
rc, stdout, _ = run_checker(["--help"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("budget", stdout)
|
||||
|
||||
def test_json_output_parseable(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 300000}]})
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path, "--total", "250KB", "--json"])
|
||||
self.assertEqual(rc, 1)
|
||||
report = json.loads(stdout)
|
||||
self.assertTrue(report["over_budget"])
|
||||
self.assertEqual(report["total"]["status"], "over")
|
||||
self.assertEqual(report["chunks"][0]["name"], "main.js")
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_json_within_budget(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 100000}]})
|
||||
try:
|
||||
rc, stdout, _ = run_checker([path, "--total", "250KB", "--json"])
|
||||
self.assertEqual(rc, 0)
|
||||
report = json.loads(stdout)
|
||||
self.assertFalse(report["over_budget"])
|
||||
self.assertEqual(report["chunks"][0]["status"], "ok")
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_missing_file_exit_two(self):
|
||||
rc, _, stderr = run_checker(["/nonexistent/report.json", "--total", "100KB"])
|
||||
self.assertEqual(rc, 2)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
def test_malformed_json_exit_two(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", delete=False, encoding="utf-8"
|
||||
) as handle:
|
||||
handle.write("{not json")
|
||||
path = handle.name
|
||||
try:
|
||||
rc, _, stderr = run_checker([path, "--total", "100KB"])
|
||||
self.assertEqual(rc, 2)
|
||||
self.assertIn("invalid JSON", stderr)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_bad_budget_value_exit_two(self):
|
||||
path = write_report({"chunks": [{"name": "main.js", "size": 1000}]})
|
||||
try:
|
||||
rc, _, stderr = run_checker([path, "--total", "lots"])
|
||||
self.assertEqual(rc, 2)
|
||||
self.assertIn("cannot parse budget", stderr)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
def test_wrong_top_level_shape_exit_two(self):
|
||||
path = write_report(["main.js", "vendor.js"])
|
||||
try:
|
||||
rc, _, stderr = run_checker([path, "--total", "100KB"])
|
||||
self.assertEqual(rc, 2)
|
||||
self.assertIn("must be a JSON object", stderr)
|
||||
finally:
|
||||
cleanup(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user