{"record":{"id":"278c6ca765f215bb","repo":"Hmbown/CodeWhale","slug":"kind-field-join-base-identity-sha256","errorCode":null,"errorMessage":"{kind} field `{'.'.join((*base, 'identity_sha256'))}` must match the owned sorted tool_names","messagePattern":"(.+?) field `(.+?)` must match the owned sorted tool_names","errorType":"exception","errorClass":"RuntimeContractError","httpStatus":null,"severity":"error","filePath":"scripts/check-runtime-contract-budget.py","lineNumber":234,"sourceCode":"            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            )\n\n\ndef validate_receipt(receipt: dict[str, Any]) -> None:\n    validate_document(receipt, RECEIPT_KIND, \"receipt\")\n    skill_discovery = receipt.get(\"skill_discovery\")\n    identical = (\n        skill_discovery.get(\"prompts_byte_identical\")","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/check-runtime-contract-budget.py#L216-L252","documentation":"Each tool surface stores identity_sha256 = tool_identity_digest(names) = hashlib.sha256 of the NUL-joined (\"\\0\".join) sorted tool_names, hex-encoded. This error fires when the stored digest does not match the digest recomputed from the document's own tool_names - names and digest are out of sync within one document. It protects the identity half of the ratchet: compare() later requires receipt and budget digests to be byte-equal (error 209).","triggerScenarios":"Editing tool_names (add/remove/rename/reorder) in the budget JSON or a receipt without recomputing identity_sha256; changing the digest algorithm, join separator, or sort order in a fork; a partial hand-merge that updates names but restores the old digest.","commonSituations":"Same hand-edit workflow as errors 200/201: a maintainer records a new tool in the budget, updates the count, but pastes the old digest. Also seen when someone recomputes the digest with a newline or comma join instead of the NUL separator.","solutions":["Recompute the digest for the named node: python3 -c \"import hashlib;print(hashlib.sha256('\\0'.join(sorted(names)).encode()).hexdigest())\" using the exact sorted tool_names from the document","Prefer regenerating all identity fields at once: re-run scripts/measure-runtime-contract.py and record its receipt values into the budget as one maintainer decision","If the names were edited by mistake, revert them so the stored digest becomes correct again"],"exampleFix":"# before - names edited, digest stale\n\"tool_names\": [\"agent\", \"apply_patch\", \"bash\"],\n\"identity_sha256\": \"<digest of the old name set>\"\n\n# after\nimport hashlib\nnames = [\"agent\", \"apply_patch\", \"bash\"]\ndigest = hashlib.sha256(\"\\0\".join(names).encode(\"utf-8\")).hexdigest()\n# paste `digest` into identity_sha256","handlingStrategy":"validation","validationCode":"import hashlib\n\n\ndef expected_digest(names: list[str]) -> str:\n    return hashlib.sha256(\"\\0\".join(names).encode(\"utf-8\")).hexdigest()\n\n\ndef digests_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            if not isinstance(names, list):\n                return False\n            if node.get(\"identity_sha256\") != expected_digest(names):\n                return False\n    return True","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When names change, recompute identity_sha256 in the same edit with the NUL-join recipe above","Add a pre-commit hook that runs digests_consistent() on runtime-contract-budget.json","Record intentional identity changes with rationale in the budget _comment so future drift is explainable"],"tags":["python","sha256","identity","validation","tool-catalog"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}