返回 CodeWhale
check-runtime-contract-budget.py
根目录 / scripts / check-runtime-contract-budget.py
1 #!/usr/bin/env python3
2 """Enforce one-way ceilings on Codewhale's provider-free runtime contract.
3
4 The default invocation runs ``measure-runtime-contract.py`` with Cargo forced
5 offline. Pass ``--receipt`` to check an existing JSON receipt without compiling,
6 which also keeps this checker's unit tests hermetic.
7
8 Usage:
9 python3 scripts/check-runtime-contract-budget.py
10 python3 scripts/check-runtime-contract-budget.py --receipt receipt.json
11 python3 scripts/check-runtime-contract-budget.py --update
12 python3 scripts/check-runtime-contract-budget.py --update --allow-increase
13
14 ``--update`` alone only locks in decreases and refuses growth. A PR whose
15 change intentionally grows the contract, or changes a structural identity,
16 runs ``--update --allow-increase`` to rewrite the budget from its own
17 measurement so the fix lands in the same PR instead of turning main red after
18 merge. Existing ``_``-prefixed history notes are preserved either way.
19 """
20
21 from __future__ import annotations
22
23 import argparse
24 import copy
25 import hashlib
26 import json
27 import os
28 import re
29 import shlex
30 import stat
31 import subprocess
32 import sys
33 import tempfile
34 from pathlib import Path
35 from typing import Any, Sequence
36
37 REPO_ROOT = Path(__file__).resolve().parent.parent
38 BUDGET_PATH = REPO_ROOT / "scripts" / "runtime-contract-budget.json"
39 MEASURE_SCRIPT = REPO_ROOT / "scripts" / "measure-runtime-contract.py"
40 RECEIPT_KIND = "codewhale.runtime_contract_receipt"
41 BUDGET_KIND = "codewhale.runtime_contract_budget"
42 SCHEMA_VERSION = 1
43 REPRESENTATIVE_FIXTURE_ID = "representative-v1"
44 TOOL_SURFACE_PROFILE = "production-default-builtins-no-mcp-no-host-interpreters-bash-v2"
45
46 MetricPath = tuple[str, ...]
47 MetricResult = tuple[str, str, int, int]
48
49 VISIBLE_MODES = (("plan", "Plan"), ("act", "Act"), ("operate", "Operate"))
50 REPRESENTATIVE_STAGES = (
51 ("base", "base"),
52 ("project", "project-authority"),
53 ("instructions", "configured-instructions"),
54 ("skill", "skill"),
55 ("memory", "memory"),
56 ("goal", "goal"),
57 ("handoff", "handoff"),
58 )
59 TOOL_SURFACES = (("full", "full"), ("active", "active"))
60
61 METRICS: tuple[tuple[MetricPath, str], ...] = (
62 *(
63 (("system_prompt", "modes", mode, field), f"{label} {description}")
64 for mode, label in VISIBLE_MODES
65 for field, description in (
66 ("system_prompt_bytes", "system-prompt bytes"),
67 ("system_prompt_tokens_est", "system-prompt estimated tokens"),
68 ("system_prompt_blocks", "system-prompt blocks"),
69 ("mode_instructions_bytes", "mode-instruction bytes"),
70 ("mode_instructions_tokens_est", "mode-instruction estimated tokens"),
71 )
72 ),
73 *(
74 (
75 ("representative_context", "stages", stage, "bytes"),
76 f"representative {label} stage bytes",
77 )
78 for stage, label in REPRESENTATIVE_STAGES
79 ),
80 *(
81 (
82 ("representative_context", "stages", stage, "delta_bytes"),
83 f"representative {label} stage delta bytes",
84 )
85 for stage, label in REPRESENTATIVE_STAGES[1:]
86 ),
87 (
88 ("representative_context", "total_bytes"),
89 "representative total bytes",
90 ),
91 (
92 ("representative_context", "total_tokens_est"),
93 "representative estimated tokens",
94 ),
95 (
96 ("representative_context", "system_prompt_blocks"),
97 "representative system-prompt blocks",
98 ),
99 *(
100 (
101 ("tool_catalog", "modes", mode, surface, field),
102 f"{label} {surface_label} {description}",
103 )
104 for mode, label in VISIBLE_MODES
105 for surface, surface_label in TOOL_SURFACES
106 for field, description in (
107 ("tools", "tool count"),
108 ("bytes", "tool-schema bytes"),
109 ("tokens_est", "tool-schema estimated tokens"),
110 )
111 ),
112 (
113 ("skill_discovery", "first_delta", "root_discovery_calls"),
114 "first unchanged-turn root discovery calls",
115 ),
116 (
117 ("skill_discovery", "first_delta", "directories_visited"),
118 "first unchanged-turn directories visited",
119 ),
120 (
121 ("skill_discovery", "first_delta", "skill_md_read_attempts"),
122 "first unchanged-turn SKILL.md read attempts",
123 ),
124 (
125 ("skill_discovery", "second_delta", "root_discovery_calls"),
126 "second unchanged-turn root discovery calls",
127 ),
128 (
129 ("skill_discovery", "second_delta", "directories_visited"),
130 "second unchanged-turn directories visited",
131 ),
132 (
133 ("skill_discovery", "second_delta", "skill_md_read_attempts"),
134 "second unchanged-turn SKILL.md read attempts",
135 ),
136 )
137
138 IDENTITIES: tuple[tuple[MetricPath, str], ...] = (
139 (("tool_catalog", "surface_profile"), "tool surface profile"),
140 (("tool_catalog", "execution_shell"), "tool fixture shell"),
141 *(
142 (
143 ("tool_catalog", "modes", mode, surface, field),
144 f"{label} {surface_label} tool {description}",
145 )
146 for mode, label in VISIBLE_MODES
147 for surface, surface_label in TOOL_SURFACES
148 for field, description in (
149 ("tool_names", "names"),
150 ("identity_sha256", "identity digest"),
151 )
152 ),
153 *(
154 (
155 ("representative_context", "stages", stage, "identity_sha256"),
156 f"representative {label} stage identity digest",
157 )
158 for stage, label in REPRESENTATIVE_STAGES
159 ),
160 )
161
162
163 class RuntimeContractError(ValueError):
164 """A receipt or budget is missing a required, well-typed metric."""
165
166
167 def load_json(path: Path, kind: str) -> dict[str, Any]:
168 try:
169 document = json.loads(path.read_text(encoding="utf-8"))
170 except FileNotFoundError as error:
171 raise RuntimeContractError(f"missing {kind}: {path}") from error
172 except (OSError, json.JSONDecodeError) as error:
173 raise RuntimeContractError(f"invalid {kind} {path}: {error}") from error
174 if not isinstance(document, dict):
175 raise RuntimeContractError(f"invalid {kind} {path}: top level must be an object")
176 return document
177
178
179 def validate_document(
180 document: dict[str, Any], expected_kind: str, source: str
181 ) -> None:
182 actual_kind = document.get("document_kind")
183 if actual_kind != expected_kind:
184 raise RuntimeContractError(
185 f"{source} document_kind must be `{expected_kind}`, got {actual_kind!r}"
186 )
187 version = document.get("schema_version")
188 if (
189 isinstance(version, bool)
190 or not isinstance(version, int)
191 or version != SCHEMA_VERSION
192 ):
193 raise RuntimeContractError(
194 f"{source} schema_version must be {SCHEMA_VERSION}, got {version!r}"
195 )
196
197
198 def required_value(document: dict[str, Any], path: MetricPath, kind: str) -> Any:
199 value: Any = document
200 dotted = ".".join(path)
201 for part in path:
202 if not isinstance(value, dict) or part not in value:
203 raise RuntimeContractError(f"{kind} is missing required field `{dotted}`")
204 value = value[part]
205 return value
206
207
208 def tool_identity_digest(names: list[str]) -> str:
209 return hashlib.sha256("\0".join(names).encode("utf-8")).hexdigest()
210
211
212 def validate_identity_structure(document: dict[str, Any], kind: str) -> None:
213 profile = required_value(document, ("tool_catalog", "surface_profile"), kind)
214 if profile != TOOL_SURFACE_PROFILE:
215 raise RuntimeContractError(
216 f"{kind} tool surface_profile must be `{TOOL_SURFACE_PROFILE}`, "
217 f"got {profile!r}"
218 )
219
220 shell = required_value(document, ("tool_catalog", "execution_shell"), kind)
221 if shell != "bash":
222 raise RuntimeContractError(
223 f"{kind} tool execution_shell must be `bash`, got {shell!r}"
224 )
225
226 for mode, _label in VISIBLE_MODES:
227 for surface, _surface_label in TOOL_SURFACES:
228 base = ("tool_catalog", "modes", mode, surface)
229 names = required_value(document, (*base, "tool_names"), kind)
230 dotted_names = ".".join((*base, "tool_names"))
231 if (
232 not isinstance(names, list)
233 or any(not isinstance(name, str) or not name for name in names)
234 or names != sorted(set(names))
235 ):
236 raise RuntimeContractError(
237 f"{kind} field `{dotted_names}` must be sorted unique non-empty strings"
238 )
239 count = metric_value(document, (*base, "tools"), kind)
240 if count != len(names):
241 raise RuntimeContractError(
242 f"{kind} metric `{'.'.join((*base, 'tools'))}` must equal the "
243 f"owned tool_names length ({len(names)})"
244 )
245 digest = required_value(document, (*base, "identity_sha256"), kind)
246 expected = tool_identity_digest(names)
247 if digest != expected:
248 raise RuntimeContractError(
249 f"{kind} field `{'.'.join((*base, 'identity_sha256'))}` must "
250 "match the owned sorted tool_names"
251 )
252
253 for stage, _label in REPRESENTATIVE_STAGES:
254 path = ("representative_context", "stages", stage, "identity_sha256")
255 digest = required_value(document, path, kind)
256 if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None:
257 raise RuntimeContractError(
258 f"{kind} field `{'.'.join(path)}` must be a lowercase SHA-256 digest"
259 )
260
261
262 def validate_receipt(receipt: dict[str, Any]) -> None:
263 validate_document(receipt, RECEIPT_KIND, "receipt")
264 skill_discovery = receipt.get("skill_discovery")
265 identical = (
266 skill_discovery.get("prompts_byte_identical")
267 if isinstance(skill_discovery, dict)
268 else None
269 )
270 if identical is not True:
271 raise RuntimeContractError(
272 "receipt metric `skill_discovery.prompts_byte_identical` must be true"
273 )
274 representative = receipt.get("representative_context")
275 fixture_id = (
276 representative.get("fixture_id")
277 if isinstance(representative, dict)
278 else None
279 )
280 if fixture_id != REPRESENTATIVE_FIXTURE_ID:
281 raise RuntimeContractError(
282 "receipt metric `representative_context.fixture_id` must be "
283 f"`{REPRESENTATIVE_FIXTURE_ID}`, got {fixture_id!r}"
284 )
285 representative_identical = representative.get("prompts_byte_identical")
286 if representative_identical is not True:
287 raise RuntimeContractError(
288 "receipt metric `representative_context.prompts_byte_identical` must be true"
289 )
290 validate_identity_structure(receipt, "receipt")
291
292
293 def validate_budget(budget: dict[str, Any]) -> None:
294 validate_document(budget, BUDGET_KIND, "budget")
295 representative = budget.get("representative_context")
296 fixture_id = (
297 representative.get("fixture_id")
298 if isinstance(representative, dict)
299 else None
300 )
301 if fixture_id != REPRESENTATIVE_FIXTURE_ID:
302 raise RuntimeContractError(
303 "budget metric `representative_context.fixture_id` must be "
304 f"`{REPRESENTATIVE_FIXTURE_ID}`, got {fixture_id!r}"
305 )
306 validate_identity_structure(budget, "budget")
307
308
309 def metric_value(document: dict[str, Any], path: MetricPath, kind: str) -> int:
310 value = required_value(document, path, kind)
311 dotted = ".".join(path)
312 if isinstance(value, bool) or not isinstance(value, int) or value < 0:
313 raise RuntimeContractError(
314 f"{kind} metric `{dotted}` must be a non-negative integer"
315 )
316 return value
317
318
319 def compare(
320 receipt: dict[str, Any], budget: dict[str, Any]
321 ) -> tuple[list[MetricResult], list[MetricResult]]:
322 """Return (increases, decreases) as path/label/current/ceiling tuples."""
323 validate_receipt(receipt)
324 validate_budget(budget)
325 for path, label in IDENTITIES:
326 receipt_value = required_value(receipt, path, "receipt")
327 budget_value = required_value(budget, path, "budget")
328 if receipt_value != budget_value:
329 detail = ""
330 if isinstance(receipt_value, list) and isinstance(budget_value, list):
331 added = [str(item) for item in receipt_value if item not in budget_value]
332 removed = [
333 str(item) for item in budget_value if item not in receipt_value
334 ]
335 detail = f" (added={added} removed={removed})"
336 raise RuntimeContractError(
337 f"identity changed for {label} [`{'.'.join(path)}`]{detail}"
338 )
339 increases: list[MetricResult] = []
340 decreases: list[MetricResult] = []
341 for path, label in METRICS:
342 current = metric_value(receipt, path, "receipt")
343 ceiling = metric_value(budget, path, "budget")
344 result = (".".join(path), label, current, ceiling)
345 if current > ceiling:
346 increases.append(result)
347 elif current < ceiling:
348 decreases.append(result)
349 return increases, decreases
350
351
352 def set_path_value(document: dict[str, Any], path: MetricPath, value: Any) -> None:
353 target = document
354 for part in path[:-1]:
355 target = target.setdefault(part, {})
356 target[path[-1]] = value
357
358
359 def budget_from_receipt(receipt: dict[str, Any]) -> dict[str, Any]:
360 validate_receipt(receipt)
361 budget: dict[str, Any] = {
362 "_comment": (
363 "One-way numeric ceilings and exact structural identities for the "
364 "provider-free runtime contract. Decreases pass; increases or identity "
365 "changes fail. Lock in decreases with: python3 "
366 "scripts/check-runtime-contract-budget.py --update"
367 ),
368 "document_kind": BUDGET_KIND,
369 "schema_version": SCHEMA_VERSION,
370 "representative_context": {
371 "fixture_id": REPRESENTATIVE_FIXTURE_ID,
372 },
373 }
374 for path, _label in METRICS:
375 set_path_value(budget, path, metric_value(receipt, path, "receipt"))
376 for path, _label in IDENTITIES:
377 set_path_value(
378 budget,
379 path,
380 copy.deepcopy(required_value(receipt, path, "receipt")),
381 )
382 return budget
383
384
385 def write_budget_atomic(path: Path, budget: dict[str, Any]) -> None:
386 """Replace an existing budget atomically without changing its mode bits."""
387 original_mode = stat.S_IMODE(path.stat().st_mode)
388 payload = json.dumps(budget, indent=2, sort_keys=True) + "\n"
389 file_descriptor, temporary_name = tempfile.mkstemp(
390 prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
391 )
392 temporary_path = Path(temporary_name)
393 try:
394 with os.fdopen(file_descriptor, "w", encoding="utf-8") as handle:
395 handle.write(payload)
396 handle.flush()
397 os.fsync(handle.fileno())
398 os.chmod(temporary_path, original_mode)
399 os.replace(temporary_path, path)
400 except BaseException:
401 temporary_path.unlink(missing_ok=True)
402 raise
403
404
405 def run_measurement() -> dict[str, Any]:
406 env = os.environ.copy()
407 env["CARGO_NET_OFFLINE"] = "true"
408 proc = subprocess.run(
409 [sys.executable, str(MEASURE_SCRIPT)],
410 cwd=REPO_ROOT,
411 env=env,
412 capture_output=True,
413 text=True,
414 check=False,
415 )
416 sys.stderr.write(proc.stderr)
417 if proc.returncode != 0:
418 sys.stdout.write(proc.stdout)
419 raise RuntimeContractError(
420 f"runtime-contract measurement failed with exit code {proc.returncode}"
421 )
422 try:
423 receipt = json.loads(proc.stdout)
424 except json.JSONDecodeError as error:
425 raise RuntimeContractError(f"measurement emitted invalid JSON: {error}") from error
426 if not isinstance(receipt, dict):
427 raise RuntimeContractError("measurement top level must be an object")
428 validate_receipt(receipt)
429 return receipt
430
431
432 def update_command(
433 receipt_path: Path | None, budget_path: Path, *, allow_increase: bool = False
434 ) -> str:
435 parts = ["python3", "scripts/check-runtime-contract-budget.py"]
436 if receipt_path is not None:
437 parts.extend(["--receipt", str(receipt_path)])
438 if budget_path != BUDGET_PATH:
439 parts.extend(["--budget", str(budget_path)])
440 parts.append("--update")
441 if allow_increase:
442 parts.append("--allow-increase")
443 return shlex.join(parts)
444
445
446 def rebased_budget(receipt: dict[str, Any], previous: dict[str, Any]) -> dict[str, Any]:
447 """Budget from ``receipt`` that keeps ``previous``'s ``_`` history notes."""
448 budget = budget_from_receipt(receipt)
449 for key, value in previous.items():
450 if key.startswith("_"):
451 budget[key] = copy.deepcopy(value)
452 return budget
453
454
455 FRAGMENT_MODULE = REPO_ROOT / "crates" / "core" / "src" / "fragments.rs"
456 FRAGMENT_MAX_TOKENS_CEILING = 10_000
457 FRAGMENT_MAX_BYTES_CEILING = FRAGMENT_MAX_TOKENS_CEILING * 4
458 FRAGMENT_DEFAULT_MAX_BYTES_CEILING = 4 * 1024
459 FRAGMENT_MAX_COUNT_CEILING = 16
460
461
462 def check_fragment_caps() -> None:
463 """Gate the bounded fragment hard caps (issue #5264).
464
465 Static check — no cargo needed. Fails closed if the fragment module is
466 missing, if any cap has been raised without review, or if the
467 project-instruction import is absent.
468 """
469 try:
470 text = FRAGMENT_MODULE.read_text(encoding="utf-8")
471 except FileNotFoundError as error:
472 raise RuntimeContractError(
473 f"missing bounded fragment module: {FRAGMENT_MODULE} ({error})"
474 ) from error
475
476 def const_value(pattern: str) -> int:
477 match = re.search(pattern, text)
478 if not match:
479 raise RuntimeContractError(f"fragment cap missing: {pattern}")
480 try:
481 return int(match.group(1).replace("_", ""))
482 except ValueError as error:
483 raise RuntimeContractError(f"fragment cap not an int: {pattern}") from error
484
485 max_tokens = const_value(r"pub const MAX_FRAGMENT_TOKENS:\s*usize\s*=\s*([0-9_]+)")
486 if max_tokens != FRAGMENT_MAX_TOKENS_CEILING:
487 raise RuntimeContractError(
488 f"MAX_FRAGMENT_TOKENS must be {FRAGMENT_MAX_TOKENS_CEILING}, got {max_tokens}"
489 )
490 # MAX_FRAGMENT_BYTES must be defined as MAX_FRAGMENT_TOKENS * 4 (canonical)
491 # or as a literal 40000. Either way the derived ceiling is 40_000.
492 has_multiplication = re.search(
493 r"pub const MAX_FRAGMENT_BYTES:\s*usize\s*=\s*MAX_FRAGMENT_TOKENS\s*\*\s*4", text
494 )
495 bytes_literal = re.search(
496 r"pub const MAX_FRAGMENT_BYTES:\s*usize\s*=\s*([0-9_]+)", text
497 )
498 if bytes_literal:
499 literal = int(bytes_literal.group(1).replace("_", ""))
500 if literal != FRAGMENT_MAX_BYTES_CEILING:
501 raise RuntimeContractError(
502 f"MAX_FRAGMENT_BYTES must be {FRAGMENT_MAX_BYTES_CEILING}, got {literal}"
503 )
504 elif not has_multiplication:
505 raise RuntimeContractError(
506 "MAX_FRAGMENT_BYTES must be defined as MAX_FRAGMENT_TOKENS * 4 or as 40000"
507 )
508 # DEFAULT is defined as 4 * 1024 (canonical) or 4096 literal
509 has_default_multiplication = re.search(
510 r"pub const DEFAULT_FRAGMENT_MAX_BYTES:\s*usize\s*=\s*4\s*\*\s*1024", text
511 )
512 default_literal = re.search(
513 r"pub const DEFAULT_FRAGMENT_MAX_BYTES:\s*usize\s*=\s*([0-9_]+)", text
514 )
515 if has_default_multiplication:
516 # canonical 4*1024 == 4096, which equals ceiling
517 pass
518 elif default_literal:
519 default_bytes = int(default_literal.group(1).replace("_", ""))
520 if default_bytes != FRAGMENT_DEFAULT_MAX_BYTES_CEILING:
521 raise RuntimeContractError(
522 f"DEFAULT_FRAGMENT_MAX_BYTES must be {FRAGMENT_DEFAULT_MAX_BYTES_CEILING}, got {default_bytes}"
523 )
524 if default_bytes > FRAGMENT_MAX_BYTES_CEILING:
525 raise RuntimeContractError(
526 f"DEFAULT_FRAGMENT_MAX_BYTES ({default_bytes}) must not exceed MAX_FRAGMENT_BYTES ({FRAGMENT_MAX_BYTES_CEILING})"
527 )
528 else:
529 raise RuntimeContractError("DEFAULT_FRAGMENT_MAX_BYTES definition not found")
530
531 max_count = const_value(
532 r"pub const MAX_FRAGMENTS_PER_CONTEXT:\s*usize\s*=\s*([0-9_]+)"
533 )
534 if max_count != FRAGMENT_MAX_COUNT_CEILING:
535 raise RuntimeContractError(
536 f"MAX_FRAGMENTS_PER_CONTEXT must be {FRAGMENT_MAX_COUNT_CEILING}, got {max_count}"
537 )
538 if max_count > FRAGMENT_MAX_COUNT_CEILING:
539 raise RuntimeContractError(
540 f"MAX_FRAGMENTS_PER_CONTEXT ({max_count}) must not exceed {FRAGMENT_MAX_COUNT_CEILING}"
541 )
542
543 # Ensure every injection type is in FragmentId::all() and the
544 # project-instruction import is present as a typed fragment.
545 required_fragments = [
546 "Workspace",
547 "Permissions",
548 "Route",
549 "AgentTopology",
550 "SkillsTools",
551 "TokenBudget",
552 "ProjectInstructions",
553 "Constitution",
554 ]
555 for name in required_fragments:
556 if f"Self::{name}" not in text and f"{name} =>" not in text and f'"{name.lower()}"' not in text.lower():
557 # Fallback: search for enum variant declaration
558 if not re.search(rf"\b{name}\b", text):
559 raise RuntimeContractError(
560 f"FragmentId missing required variant {name}"
561 )
562 # Marker stability — these strings are pinned by tests / prefix cache
563 required_markers = [
564 "<!-- cw:ctx:workspace -->",
565 "<!-- cw:ctx:project_instructions -->",
566 "<!-- cw:ctx:constitution -->",
567 ]
568 for marker in required_markers:
569 if marker not in text:
570 raise RuntimeContractError(
571 f"bounded fragment module missing required marker {marker!r}"
572 )
573
574 # Project-instruction import must be a typed fragment, not ad-hoc
575 if "load_project_instruction_fragment" not in text:
576 raise RuntimeContractError(
577 "bounded fragment module must expose load_project_instruction_fragment (project-instruction import as typed fragment)"
578 )
579 if "PROJECT_INSTRUCTION_CANDIDATES" not in text:
580 raise RuntimeContractError(
581 "bounded fragment module must define PROJECT_INSTRUCTION_CANDIDATES"
582 )
583 # Required candidate files from #3978
584 required_candidates = [
585 ".cursorrules",
586 ".clinerules",
587 ".windsurf/rules",
588 ".gemini",
589 ".github/copilot-instructions.md",
590 ]
591 for candidate in required_candidates:
592 if candidate not in text:
593 raise RuntimeContractError(
594 f"PROJECT_INSTRUCTION_CANDIDATES missing required entry {candidate!r}"
595 )
596
597 # matches_text recognizer must exist on the fragment trait
598 if "fn matches_text" not in text:
599 raise RuntimeContractError(
600 "bounded fragment module must define a matches_text recognizer on the fragment trait"
601 )
602 if "trait ContextFragment" not in text:
603 raise RuntimeContractError(
604 "bounded fragment module must define trait ContextFragment with matches_text"
605 )
606
607 # No unbounded fragment — enforce that creation clamps to MAX_FRAGMENT_BYTES
608 if "MAX_FRAGMENT_BYTES" not in text or "enforce_byte_cap" not in text:
609 raise RuntimeContractError(
610 "bounded fragment module must enforce byte caps via enforce_byte_cap and MAX_FRAGMENT_BYTES"
611 )
612
613 # TUI must be unified with the core boundary (shared crates/core module)
614 tui_fragment = REPO_ROOT / "crates" / "runtime" / "src" / "model_context" / "fragment.rs"
615 try:
616 tui_text = tui_fragment.read_text(encoding="utf-8")
617 except FileNotFoundError as error:
618 raise RuntimeContractError(
619 f"missing TUI fragment module: {tui_fragment} ({error})"
620 ) from error
621 if "codewhale_core::fragments" not in tui_text:
622 raise RuntimeContractError(
623 "TUI model_context/fragment.rs must re-export caps from codewhale_core::fragments (shared crates/core boundary)"
624 )
625 if "ProjectInstructions" not in tui_text:
626 raise RuntimeContractError(
627 "TUI fragment module must include ProjectInstructions variant (unified with core)"
628 )
629 if "MAX_FRAGMENT_BYTES" not in tui_text:
630 raise RuntimeContractError(
631 "TUI fragment module must enforce MAX_FRAGMENT_BYTES (10K-token ceiling)"
632 )
633 if "matches_text" not in tui_text:
634 raise RuntimeContractError(
635 "TUI fragment module must expose a matches_text recognizer"
636 )
637
638
639 def main(argv: Sequence[str] | None = None) -> int:
640 parser = argparse.ArgumentParser(description=__doc__)
641 parser.add_argument(
642 "--receipt",
643 type=Path,
644 help="check an existing measurement JSON instead of compiling",
645 )
646 parser.add_argument(
647 "--budget",
648 type=Path,
649 default=BUDGET_PATH,
650 help=argparse.SUPPRESS,
651 )
652 parser.add_argument(
653 "--update",
654 action="store_true",
655 help="tighten all ceilings to the current receipt; refuses increases",
656 )
657 parser.add_argument(
658 "--allow-increase",
659 action="store_true",
660 help=(
661 "with --update, also accept increases and identity changes: rewrite "
662 "the budget from the receipt so the change lands in the same PR"
663 ),
664 )
665 args = parser.parse_args(argv)
666 if args.allow_increase and not args.update:
667 parser.error("--allow-increase requires --update")
668 grow_command = update_command(args.receipt, args.budget, allow_increase=True)
669
670 try:
671 check_fragment_caps()
672 if args.receipt is not None and args.receipt.resolve() == args.budget.resolve():
673 raise RuntimeContractError(
674 "receipt and budget must resolve to distinct filesystem paths"
675 )
676 budget = load_json(args.budget, "budget")
677 receipt = (
678 load_json(args.receipt, "receipt")
679 if args.receipt is not None
680 else run_measurement()
681 )
682 if args.allow_increase:
683 validate_receipt(receipt)
684 validate_budget(budget)
685 write_budget_atomic(args.budget, rebased_budget(receipt, budget))
686 print(
687 f"[runtime-contract-budget] wrote {args.budget} from the current "
688 f"measurement ({len(METRICS)} metrics, {len(IDENTITIES)} identities). "
689 "Record why it grew in the budget's _comment or the PR description."
690 )
691 return 0
692 increases, decreases = compare(receipt, budget)
693 except RuntimeContractError as error:
694 print(f"[runtime-contract-budget] ERROR: {error}", file=sys.stderr)
695 if str(error).startswith("identity changed"):
696 print(
697 "If the identity change is intended, land the new budget in this PR:\n"
698 f" {grow_command}",
699 file=sys.stderr,
700 )
701 return 2
702 except OSError as error:
703 print(
704 f"[runtime-contract-budget] ERROR: failed to update budget: {error}",
705 file=sys.stderr,
706 )
707 return 2
708
709 if increases:
710 print("[runtime-contract-budget] FAIL: runtime contract grew:", file=sys.stderr)
711 for path, label, current, ceiling in increases:
712 print(
713 f" {label}: {current} > {ceiling} (+{current - ceiling}) [{path}]",
714 file=sys.stderr,
715 )
716 print(
717 "\nReduce the model-facing surface, or if the growth is intended land "
718 f"the higher ceiling in this PR:\n {grow_command}",
719 file=sys.stderr,
720 )
721 return 1
722
723 if args.update:
724 try:
725 write_budget_atomic(args.budget, rebased_budget(receipt, budget))
726 except OSError as error:
727 print(
728 f"[runtime-contract-budget] ERROR: failed to update budget: {error}",
729 file=sys.stderr,
730 )
731 return 2
732 print(
733 f"[runtime-contract-budget] wrote {args.budget}: "
734 f"{len(decreases)} decreased ceilings locked in "
735 f"({len(METRICS)} total)"
736 )
737 return 0
738
739 if decreases:
740 print(
741 f"[runtime-contract-budget] PASS: {len(METRICS)} ceilings respected; "
742 f"{len(decreases)} can be tightened."
743 )
744 for path, label, current, ceiling in decreases:
745 print(f" {label}: {current} < {ceiling} (-{ceiling - current}) [{path}]")
746 print(f"Tighten with:\n {update_command(args.receipt, args.budget)}")
747 return 0
748
749 print(
750 f"[runtime-contract-budget] PASS: all {len(METRICS)} metrics are exactly at budget."
751 )
752 return 0
753
754
755 if __name__ == "__main__":
756 raise SystemExit(main())
757
757 lines PYTHON