{"record":{"id":"e82ca4903cf483af","repo":"Hmbown/CodeWhale","slug":"kind-metric-join-base-tools-must","errorCode":null,"errorMessage":"{kind} metric `{'.'.join((*base, 'tools'))}` must equal the owned tool_names length ({len(names)})","messagePattern":"(.+?) metric `(.+?)` must equal the owned tool_names length \\((.+?)\\)","errorType":"exception","errorClass":"RuntimeContractError","httpStatus":null,"severity":"error","filePath":"scripts/check-runtime-contract-budget.py","lineNumber":227,"sourceCode":"            f\"got {profile!r}\"\n        )\n\n    for mode, _label in VISIBLE_MODES:\n        for surface, _surface_label in TOOL_SURFACES:\n            base = (\"tool_catalog\", \"modes\", mode, surface)\n            names = required_value(document, (*base, \"tool_names\"), kind)\n            dotted_names = \".\".join((*base, \"tool_names\"))\n            if (\n                not isinstance(names, list)\n                or any(not isinstance(name, str) or not name for name in names)\n                or names != sorted(set(names))\n            ):\n                raise RuntimeContractError(\n                    f\"{kind} field `{dotted_names}` must be sorted unique non-empty strings\"\n                )\n            count = metric_value(document, (*base, \"tools\"), kind)\n            if count != len(names):\n                raise RuntimeContractError(\n                    f\"{kind} metric `{'.'.join((*base, 'tools'))}` must equal the \"\n                    f\"owned tool_names length ({len(names)})\"\n                )\n            digest = required_value(document, (*base, \"identity_sha256\"), kind)\n            expected = tool_identity_digest(names)\n            if digest != expected:\n                raise RuntimeContractError(\n                    f\"{kind} field `{'.'.join((*base, 'identity_sha256'))}` must \"\n                    \"match the owned sorted tool_names\"\n                )\n\n    for stage, _label in REPRESENTATIVE_STAGES:\n        path = (\"representative_context\", \"stages\", stage, \"identity_sha256\")\n        digest = required_value(document, path, kind)\n        if not isinstance(digest, str) or re.fullmatch(r\"[0-9a-f]{64}\", digest) is None:\n            raise RuntimeContractError(\n                f\"{kind} field `{'.'.join(path)}` must be a lowercase SHA-256 digest\"\n            )","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/check-runtime-contract-budget.py#L209-L245","documentation":"For each mode x surface, the integer metric tool_catalog.modes.<mode>.<surface>.tools must exactly equal len(tool_names) in the same node. The count is deliberately redundant with the names list, and validate_identity_structure cross-checks the redundancy so a hand-edited budget cannot hide a tool-count change behind stale names. Raised from the same pass as the sorted-names check (validate_receipt, validate_budget, compare, budget_from_receipt, run_measurement).","triggerScenarios":"A document where `tools` was edited (or freshly measured) without the matching tool_names edit - budget says \"tools\": 33 while tool_names lists 34 entries, or a name was appended without bumping the count. The message names the exact node and the expected length.","commonSituations":"Hand-applying a tool addition to the budget JSON and forgetting the count; copying a tools block between modes/surfaces whose name lists differ; a receipt from a measure-script version that counts differently (for example counting disabled tools).","solutions":["Set the named tools metric to exactly len(tool_names) for that node","If a tool was genuinely added, update tool_names, tools, bytes, tokens_est, and identity_sha256 as one unit in scripts/runtime-contract-budget.json","Or re-measure with scripts/measure-runtime-contract.py and check the fresh receipt via --receipt so the harness computes all fields consistently"],"exampleFix":"// before\n\"tool_catalog\": { \"modes\": { \"act\": { \"full\": {\n  \"tool_names\": [\"agent\", \"apply_patch\", \"bash\"],\n  \"tools\": 2 } } } }\n\n// after\n\"tool_catalog\": { \"modes\": { \"act\": { \"full\": {\n  \"tool_names\": [\"agent\", \"apply_patch\", \"bash\"],\n  \"tools\": 3 } } } }","handlingStrategy":"validation","validationCode":"def tool_counts_consistent(doc: dict) -> bool:\n    for mode in (\"plan\", \"act\", \"operate\"):\n        for surface in (\"full\", \"active\"):\n            node = (\n                doc.get(\"tool_catalog\", {})\n                .get(\"modes\", {})\n                .get(mode, {})\n                .get(surface, {})\n            )\n            names = node.get(\"tool_names\")\n            count = node.get(\"tools\")\n            if not isinstance(names, list) or count != len(names):\n                return False\n    return True","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Edit tools count and tool_names only together, in the same commit hunk, so review shows them adjacent","Prefer regenerating the budget via --update over hand-patching numbers","Grep your diff for `\"tools\":` changes and verify each has a matching tool_names diff hunk"],"tags":["python","json","validation","ratchet","tool-catalog"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}