{"record":{"id":"8171fed962295db4","repo":"Hmbown/CodeWhale","slug":"identity-changed-for-label-join-path","errorCode":null,"errorMessage":"identity changed for {label} [`{'.'.join(path)}`]{detail}","messagePattern":"identity changed for (.+?) \\[`(.+?)`\\](.+?)","errorType":"exception","errorClass":"RuntimeContractError","httpStatus":null,"severity":"error","filePath":"scripts/check-runtime-contract-budget.py","lineNumber":322,"sourceCode":"\ndef compare(\n    receipt: dict[str, Any], budget: dict[str, Any]\n) -> tuple[list[MetricResult], list[MetricResult]]:\n    \"\"\"Return (increases, decreases) as path/label/current/ceiling tuples.\"\"\"\n    validate_receipt(receipt)\n    validate_budget(budget)\n    for path, label in IDENTITIES:\n        receipt_value = required_value(receipt, path, \"receipt\")\n        budget_value = required_value(budget, path, \"budget\")\n        if receipt_value != budget_value:\n            detail = \"\"\n            if isinstance(receipt_value, list) and isinstance(budget_value, list):\n                added = [str(item) for item in receipt_value if item not in budget_value]\n                removed = [\n                    str(item) for item in budget_value if item not in receipt_value\n                ]\n                detail = f\" (added={added} removed={removed})\"\n            raise RuntimeContractError(\n                f\"identity changed for {label} [`{'.'.join(path)}`]{detail}\"\n            )\n    increases: list[MetricResult] = []\n    decreases: list[MetricResult] = []\n    for path, label in METRICS:\n        current = metric_value(receipt, path, \"receipt\")\n        ceiling = metric_value(budget, path, \"budget\")\n        result = (\".\".join(path), label, current, ceiling)\n        if current > ceiling:\n            increases.append(result)\n        elif current < ceiling:\n            decreases.append(result)\n    return increases, decreases\n\n\ndef set_path_value(document: dict[str, Any], path: MetricPath, value: Any) -> None:\n    target = document\n    for part in path[:-1]:","sourceCodeStart":304,"sourceCodeEnd":340,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/check-runtime-contract-budget.py#L304-L340","documentation":"The core ratchet trip in compare(): for every identity path (tool surface_profile, per-mode/surface tool_names and identity_sha256, per-stage digests) the receipt must equal the budget exactly. Any add/remove/rename of a tool or any edit to prompt text that feeds a stage digest breaks byte equality and fails the run even when all numeric ceilings are respected. List values get an (added=... removed=...) detail naming the moved tool names; the CLI exits non-zero, so CI fails.","triggerScenarios":"Adding, removing, or renaming a tool in the production catalog (the budget _comment records exactly this for the v0.9.8 `agent` tool); editing BASE_PROMPT in crates/tui/src/prompts/text.rs, which re-hashes every representative stage; measuring on a platform whose rendering differs from the Linux CI that produced the budget.","commonSituations":"A PR that legitimately extends the tool surface and forgets this is a two-part decision (code + budget); cross-platform measurement drift (the budget _comment explicitly says re-measure on Linux CI if Lint reports identity drift); refactors that rename a FragmentId or stage key.","solutions":["Read the (added=... removed=...) detail - it names exactly which tool names moved","If the change is unintended, revert the tool-catalog or prompt edit that altered the surface","If intended, hand-update the affected tool_names, tools/bytes/tokens_est ceilings, and identity_sha256 values together in scripts/runtime-contract-budget.json and justify the raise in _comment (the file's existing convention for explicit maintainer decisions)","Re-measure on Linux CI before committing the budget change so platform drift cannot fake an identity change"],"exampleFix":"// recording an intentional tool addition in scripts/runtime-contract-budget.json:\n// tool_names: [... \"agent\" ...]  (kept sorted)\n// tools/bytes/tokens_est: raised to the re-measured values\n// identity_sha256: recomputed over the new sorted names\n// _comment: extend with the rationale, mirroring the existing v0.9.8 entries","handlingStrategy":"validation","validationCode":"def identity_paths():\n    paths = [(\"tool_catalog\", \"surface_profile\")]\n    for mode in (\"plan\", \"act\", \"operate\"):\n        for surface in (\"full\", \"active\"):\n            paths.append((\"tool_catalog\", \"modes\", mode, surface, \"tool_names\"))\n            paths.append((\"tool_catalog\", \"modes\", mode, surface, \"identity_sha256\"))\n    for stage in (\"base\", \"project\", \"instructions\", \"skill\", \"memory\", \"goal\", \"handoff\"):\n        paths.append((\"representative_context\", \"stages\", stage, \"identity_sha256\"))\n    return paths\n\n\ndef get(doc, path):\n    for part in path:\n        doc = doc.get(part, {}) if isinstance(doc, dict) else {}\n    return doc\n\n\ndef identity_drift(receipt: dict, budget: dict) -> list[str]:\n    drifted = []\n    for path in identity_paths():\n        if get(receipt, path) != get(budget, path):\n            drifted.append(\".\".join(path))\n    return drifted  # empty means compare() will not raise error 209","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Any PR that adds, removes, or renames a tool must touch scripts/runtime-contract-budget.json in the same commit","Re-measure on Linux CI before updating identities; other platforms can render different bytes","Edits to BASE_PROMPT re-hash every representative stage - budget the digest updates alongside the prompt change"],"tags":["python","ratchet","ci","identity","tool-catalog"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}