feat(hololake): validate construction batch handoffs

This commit is contained in:
冰朔 2026-09-10 01:32:53 +08:00
commit 2b9c39d57a
8 changed files with 253 additions and 5 deletions

View file

@ -87,7 +87,7 @@
"human_review":"ICE-GL∞_FINAL_PRODUCT_CHOICE_ONLY",
"learning_observer":"HLP-TDEV-QWEN-0001",
"state":"CURRENT_ONLY_ACTIVE_ARCHITECTURE_TASK_FAIL_REWORK_ENTRY_NOT_ISSUED",
"open_defects":["SELF_OBSERVATION_SUPPORTING_OBJECT_SCHEMAS_AND_ENUM_CONDITIONS_NEED_HARDENING","STATE_VALIDATION_RECEIPT_MUST_BIND_SIGNED_POLICY_CLOCK_AND_DURABLE_HEAD","BATCH_HANDOFF_SCHEMA_TEST_VECTORS_AND_NEGATIVE_VALIDATION_PENDING","FOUNDATION_DRAWING_HASH_REBIND_AFTER_FINAL_REVIEW","NATIVE_TCS_BRAIN_STANDALONE_CONTAINER_EMBEDDING_SITE_PROBE_PENDING"],
"open_defects":["SELF_OBSERVATION_ENUM_CONDITIONS_AND_TYPED_ARRAY_ITEMS_NEED_FINAL_HARDENING","NATIVE_TCS_BRAIN_STANDALONE_CONTAINER_EMBEDDING_SITE_PROBE_PENDING"],
"acceptance":["六批施工顺序冻结", "HLP-BATCH-00-FOUNDATION-DRAWINGS-001接口图冻结", "hololake.public-snapshot/v1唯一Schema", "HLP-BATCH-HANDOFF-CONTRACT-001入退场回执冻结", "HLP-MODULE-SELF-OBSERVATION-CONTRACT-001机器接口冻结", "公共更新与私人Git现场边界可施工", "材料样件采用淘汰清单", "独立图纸审查PASS"]
}
],

View file

@ -25,7 +25,7 @@
"BATCH_GATED_CONSTRUCTION_SEQUENCE_REGISTERED",
"QWEN_ROLE_MOVED_TO_LEARNING_AND_EXPERIENCE"
],
"next_action": "CLOSE_BATCH_00_SCHEMA_HARDENING_HANDOFF_NEGATIVE_TESTS_DRAWING_REBIND_AND_NATIVE_TCS_CONTAINER_SITE_PROBE; KEEP_BATCH_01_ENTRY_CLOSED",
"next_action": "CLOSE_BATCH_00_SELF_OBSERVATION_ENUM_CONDITIONS_AND_NATIVE_TCS_CONTAINER_SITE_PROBE; KEEP_BATCH_01_ENTRY_CLOSED",
"blocked_routes": ["OLD_MODULE_001_PRODUCT_RESUME", "FIXED_PAGE_FIRST", "REPO014_1_3_AS_CURRENT_ARCHITECTURE", "LEGACY_PRODUCT_DONOR_WRITE", "PUBLIC_RELEASE_BEFORE_OFFICIAL_REPO012_CHANNEL_BOUNDARY_PUBLICATION", "UNREGISTERED_OFFICE_OR_MODULE"],
"time_control": "CH-GLW-TIME-0001",
"multipath_console": {

View file

@ -0,0 +1,14 @@
{
"schema":"guanghu.hololake-batch00-handoff-validator-test-receipt/v1",
"receipt_id":"HLP-BATCH-00-HANDOFF-VALIDATOR-TEST-001",
"state":"ARCHITECTURE_TOOL_TESTED_NOT_AN_EXIT_OR_ENTRY_RECEIPT",
"outcome":"PASS",
"test_count":15,
"positive":["ARCHITECTURE_EXIT_SHAPE","CONSTRUCTION_ENTRY_SHAPE","CONSTRUCTION_EXIT_SHAPE"],
"negative":["MISSING_FIELD","UNKNOWN_FIELD","BAD_SHA256","WELL_FORMED_BUT_WRONG_RECEIPT_SHA256","NAIVE_TIMESTAMP","FUTURE_TIMESTAMP","ARCHITECTURE_PASS_WITH_OPEN_DEFECT","ARCHITECTURE_PASS_WITH_EMPTY_DRAWING_OR_SITE_EVIDENCE","ARCHITECTURE_REWORK_OPENS_NEXT_BATCH","CONSTRUCTION_REWORK_WITHOUT_ORDER_OR_CLOSED_NEXT_BATCH","REWORK_ORDER_DOES_NOT_MATCH_EXACT_DEFECTS","UNKNOWN_BATCH","EMPTY_ENTRY_EVIDENCE_OR_ASSIGNMENT","ENTRY_NOT_OPEN","SELF_GRANTED_AUTHORITY"],
"hashes":{"validator":"fd8c6817f42fada4d8d2b56f3d1ced9628e29de7a7c84ee4a82008abe82433a5","tests":"9a142b3ae5106666a0bcf206d22661eeda281fef9e37451206780bd7edf65e62","handoff_contract":"e636ea7a670dfa854da19e885b8a57446c278992e5badb516ed61f2ca9b5573a","foundation_schema":"bbf639886bc14a4cc1667bd29cdef2705a1b90e8b8c524e9307b8767b2ee1a08"},
"receipt_written_by_validator":false,
"batch_00_exit_issued":false,
"batch_01_entry_issued":false,
"authority_granted":false
}

View file

@ -1,14 +1,16 @@
{
"schema":"guanghu.hololake-construction-batch-handoff-contract/v1",
"contract_id":"HLP-BATCH-HANDOFF-CONTRACT-001",
"state":"CURRENT_BATCH_00_MACHINE_CONTRACT_DEFINED_NO_ENTRY_RECEIPT_ISSUED",
"entry_receipt":{"schema":"hololake.construction-batch-entry/v1","required_fields":["receipt_id","batch_id","previous_batch_exit_receipt_sha256","construction_drawing_id","construction_drawing_sha256","accepted_input_artifacts","crew_admissions","inspector_assignment","scope","forbidden_actions","issued_by_role","issued_at","state","authority_granted"],"state_required":"ENTRY_OPEN","rule":"BATCH-00_USES_NULL_PREVIOUS_EXIT; ALL_OTHER_BATCHES_REQUIRE_PREVIOUS_EXIT_PASS_HASH"},
"state":"CURRENT_BATCH_00_MACHINE_CONTRACT_AND_NEGATIVE_VALIDATOR_TESTED_NO_ENTRY_RECEIPT_ISSUED",
"entry_receipt":{"schema":"hololake.construction-batch-entry/v1","required_fields":["receipt_id","batch_id","previous_batch_exit_receipt_sha256","construction_drawing_id","construction_drawing_sha256","accepted_input_artifacts","crew_admissions","inspector_assignment","scope","forbidden_actions","issued_by_role","issued_at","state","receipt_sha256","authority_granted"],"state_required":"ENTRY_OPEN","rule":"CONSTRUCTION_ENTRY_EXISTS_ONLY_FOR_BATCH-01_TO_05_AND_REQUIRES_PREVIOUS_EXIT_PASS_HASH; BATCH-00_USES_SEPARATE_ARCHITECTURE_EXIT_WITH_NO_ENTRY_RECEIPT"},
"batch_00_architecture_exit":{"schema":"hololake.architecture-batch-exit/v1","required_fields":["receipt_id","batch_id","architecture_charter_sha256","drawing_artifacts","site_survey_receipt","independent_inspector","inspection_outcome","open_defects","rework_order","architect_role_released","batch_01_entry_allowed","inspected_at","receipt_sha256","authority_granted"],"rule":"NO_ENTRY_RECEIPT_SHA_REQUIRED; PASS_REQUIRES_ALL_DRAWING_HASHES_SITE_SURVEY_AND_OPEN_DEFECTS_EMPTY"},
"exit_receipt":{"schema":"hololake.construction-batch-exit/v1","required_fields":["receipt_id","batch_id","entry_receipt_sha256","output_artifacts","module_self_report_receipts","test_receipts","cumulative_regression_receipts","independent_inspector","inspection_outcome","open_defects","rework_order","builder_roles_released","next_batch_entry_allowed","inspected_at","receipt_sha256","authority_granted"],"pass_rule":"inspection_outcome=PASS AND open_defects=[] AND next_batch_entry_allowed=true","fail_rule":"inspection_outcome=FAIL_REWORK AND next_batch_entry_allowed=false AND rework_order references exact defects"},
"role_separation":{"architect":"MAY_AUTHOR_DRAWING_CANNOT_ISSUE_ITS_OWN_INDEPENDENT_INSPECTION_PASS","builder":"MAY_BUILD_ONLY_WITH_CURRENT_ENTRY_RECEIPT_CANNOT_SELF_ACCEPT","inspector":"READ_ONLY_DURING_INSPECTION_CANNOT_MODIFY_INSPECTED_ARTIFACT","next_crew":"NO_ADMISSION_BEFORE_PREVIOUS_EXIT_PASS"},
"hash_rule":"EVERY_ACCEPTED_INPUT_AND_OUTPUT_IS_BOUND_BY_ID_VERSION_PATH_SHA256_AND_FACT_STATE",
"canonical_hash":"RFC8785_JSON_CANONICALIZATION_EXCLUDING_THE_OBJECTS_OWN_RECEIPT_SHA256_FIELD",
"machine_schema_refs":{"entry":"schemas/HLP-FOUNDATION-CONTRACTS-v1.schema.json#constructionEntry","exit":"schemas/HLP-FOUNDATION-CONTRACTS-v1.schema.json#constructionExit","batch_00_architecture_exit":"schemas/HLP-FOUNDATION-CONTRACTS-v1.schema.json#architectureExit"},
"validator":"../../smart-computer/batch_handoff_validator.py",
"test_receipt":"BATCH-00-HANDOFF-VALIDATOR-TEST-RECEIPT.json",
"current_receipts":{"HLP-BATCH-00":"NOT_ISSUED","HLP-BATCH-01":"ENTRY_CLOSED","HLP-BATCH-02":"ENTRY_CLOSED","HLP-BATCH-03":"ENTRY_CLOSED","HLP-BATCH-04":"ENTRY_CLOSED","HLP-BATCH-05":"ENTRY_CLOSED"},
"authority_granted":false
}

View file

@ -17,7 +17,7 @@
"aggregateGraph":{"$anchor":"aggregateGraph","type":"object","additionalProperties":false,"required":["schema","inventory_id","inventory_version","expected_instances","validated_instances","coverage","missing_instances","stale_instances","invalid_instances","forked_instances","unregistered_instances","validated_local_states","dependency_edges","source_heads","validation_receipts","graph_generated_at","graph_sha256","authority_granted"],"properties":{"schema":{"const":"hololake.module-state-aggregate-graph/v1"},"inventory_id":{"type":"string"},"inventory_version":{"type":"string"},"expected_instances":{"type":"array"},"validated_instances":{"type":"array"},"coverage":{"type":"number","minimum":0,"maximum":1},"missing_instances":{"type":"array"},"stale_instances":{"type":"array"},"invalid_instances":{"type":"array"},"forked_instances":{"type":"array"},"unregistered_instances":{"type":"array"},"validated_local_states":{"type":"array"},"dependency_edges":{"type":"array"},"source_heads":{"type":"array"},"validation_receipts":{"type":"array"},"graph_generated_at":{"$ref":"#/$defs/timestamp"},"graph_sha256":{"$ref":"#/$defs/sha256"},"authority_granted":{"const":false}}},
"brainJudgment":{"$anchor":"brainJudgment","type":"object","additionalProperties":false,"required":["schema","persona_subject_id","brain_instance_id","brain_binding_receipt","brain_cycle_receipt","input_graph_sha256","facts","inferences","unknowns","whole_operational_assessment","whole_can_continue","next_system_action","human_review_required","judged_at","judgment_sha256","authority_evidence_refs"],"properties":{"schema":{"const":"hololake.native-tcs-brain-whole-judgment/v1"},"persona_subject_id":{"type":"string"},"brain_instance_id":{"type":"string"},"brain_binding_receipt":{"$ref":"#/$defs/sha256"},"brain_cycle_receipt":{"$ref":"#/$defs/sha256"},"input_graph_sha256":{"$ref":"#/$defs/sha256"},"facts":{"type":"array"},"inferences":{"type":"array"},"unknowns":{"type":"array"},"whole_operational_assessment":{"type":"string"},"whole_can_continue":{"type":"boolean"},"next_system_action":{"type":"string"},"human_review_required":{"type":"boolean"},"judged_at":{"$ref":"#/$defs/timestamp"},"judgment_sha256":{"$ref":"#/$defs/sha256"},"authority_evidence_refs":{"type":"array","items":{"$ref":"#/$defs/evidenceRef"}}}},
"raiseHand":{"$anchor":"raiseHand","type":"object","additionalProperties":false,"required":["schema","issue_id","persona_subject_id","brain_instance_id","judgment_sha256","brain_binding_receipt","brain_cycle_receipt","input_graph_sha256","affected_module_ids","affected_instance_ids","symptom","cause_level","facts","inferences","unknowns","evidence_refs","blocked_action","repair_request","can_self_repair","human_review_required","raised_at","envelope_sha256"],"properties":{"schema":{"const":"hololake.persona-container-raise-hand/v1"},"issue_id":{"type":"string"},"persona_subject_id":{"type":"string"},"brain_instance_id":{"type":"string"},"judgment_sha256":{"$ref":"#/$defs/sha256"},"brain_binding_receipt":{"$ref":"#/$defs/sha256"},"brain_cycle_receipt":{"$ref":"#/$defs/sha256"},"input_graph_sha256":{"$ref":"#/$defs/sha256"},"affected_module_ids":{"type":"array","items":{"type":"string"}},"affected_instance_ids":{"type":"array","items":{"type":"string"}},"symptom":{"type":"string"},"cause_level":{"enum":["LOCAL_MODULE","DEPENDENCY_CHAIN","CONTAINER_WHOLE","EXTERNAL_HOST_OR_AUTHORITY","UNKNOWN"]},"facts":{"type":"array"},"inferences":{"type":"array"},"unknowns":{"type":"array"},"evidence_refs":{"type":"array","items":{"$ref":"#/$defs/evidenceRef"}},"blocked_action":{"type":"string"},"repair_request":{"type":"string"},"can_self_repair":{"type":"boolean"},"human_review_required":{"type":"boolean"},"raised_at":{"$ref":"#/$defs/timestamp"},"envelope_sha256":{"$ref":"#/$defs/sha256"}}},
"constructionEntry":{"$anchor":"constructionEntry","type":"object","additionalProperties":false,"required":["schema","receipt_id","batch_id","previous_batch_exit_receipt_sha256","construction_drawing_id","construction_drawing_sha256","accepted_input_artifacts","crew_admissions","inspector_assignment","scope","forbidden_actions","issued_by_role","issued_at","state","authority_granted"],"properties":{"schema":{"const":"hololake.construction-batch-entry/v1"},"receipt_id":{"type":"string"},"batch_id":{"type":"string"},"previous_batch_exit_receipt_sha256":{"$ref":"#/$defs/sha256"},"construction_drawing_id":{"type":"string"},"construction_drawing_sha256":{"$ref":"#/$defs/sha256"},"accepted_input_artifacts":{"type":"array"},"crew_admissions":{"type":"array"},"inspector_assignment":{"type":"object"},"scope":{"type":"array"},"forbidden_actions":{"type":"array"},"issued_by_role":{"type":"string"},"issued_at":{"$ref":"#/$defs/timestamp"},"state":{"const":"ENTRY_OPEN"},"authority_granted":{"const":false}}},
"constructionEntry":{"$anchor":"constructionEntry","type":"object","additionalProperties":false,"required":["schema","receipt_id","batch_id","previous_batch_exit_receipt_sha256","construction_drawing_id","construction_drawing_sha256","accepted_input_artifacts","crew_admissions","inspector_assignment","scope","forbidden_actions","issued_by_role","issued_at","state","receipt_sha256","authority_granted"],"properties":{"schema":{"const":"hololake.construction-batch-entry/v1"},"receipt_id":{"type":"string"},"batch_id":{"type":"string"},"previous_batch_exit_receipt_sha256":{"$ref":"#/$defs/sha256"},"construction_drawing_id":{"type":"string"},"construction_drawing_sha256":{"$ref":"#/$defs/sha256"},"accepted_input_artifacts":{"type":"array"},"crew_admissions":{"type":"array"},"inspector_assignment":{"type":"object"},"scope":{"type":"array"},"forbidden_actions":{"type":"array"},"issued_by_role":{"type":"string"},"issued_at":{"$ref":"#/$defs/timestamp"},"state":{"const":"ENTRY_OPEN"},"receipt_sha256":{"$ref":"#/$defs/sha256"},"authority_granted":{"const":false}}},
"constructionExit":{"$anchor":"constructionExit","type":"object","additionalProperties":false,"required":["schema","receipt_id","batch_id","entry_receipt_sha256","output_artifacts","module_self_report_receipts","test_receipts","cumulative_regression_receipts","independent_inspector","inspection_outcome","open_defects","rework_order","builder_roles_released","next_batch_entry_allowed","inspected_at","receipt_sha256","authority_granted"],"properties":{"schema":{"const":"hololake.construction-batch-exit/v1"},"receipt_id":{"type":"string"},"batch_id":{"type":"string"},"entry_receipt_sha256":{"$ref":"#/$defs/sha256"},"output_artifacts":{"type":"array"},"module_self_report_receipts":{"type":"array"},"test_receipts":{"type":"array"},"cumulative_regression_receipts":{"type":"array"},"independent_inspector":{"type":"string"},"inspection_outcome":{"enum":["PASS","FAIL_REWORK"]},"open_defects":{"type":"array"},"rework_order":{"type":["object","null"]},"builder_roles_released":{"type":"array"},"next_batch_entry_allowed":{"type":"boolean"},"inspected_at":{"$ref":"#/$defs/timestamp"},"receipt_sha256":{"$ref":"#/$defs/sha256"},"authority_granted":{"const":false}}},
"architectureExit":{"$anchor":"architectureExit","type":"object","additionalProperties":false,"required":["schema","receipt_id","batch_id","architecture_charter_sha256","drawing_artifacts","site_survey_receipt","independent_inspector","inspection_outcome","open_defects","rework_order","architect_role_released","batch_01_entry_allowed","inspected_at","receipt_sha256","authority_granted"],"properties":{"schema":{"const":"hololake.architecture-batch-exit/v1"},"receipt_id":{"type":"string"},"batch_id":{"const":"HLP-BATCH-00"},"architecture_charter_sha256":{"$ref":"#/$defs/sha256"},"drawing_artifacts":{"type":"array"},"site_survey_receipt":{"type":"object"},"independent_inspector":{"type":"string"},"inspection_outcome":{"enum":["PASS","FAIL_REWORK"]},"open_defects":{"type":"array"},"rework_order":{"type":["object","null"]},"architect_role_released":{"type":"boolean"},"batch_01_entry_allowed":{"type":"boolean"},"inspected_at":{"$ref":"#/$defs/timestamp"},"receipt_sha256":{"$ref":"#/$defs/sha256"},"authority_granted":{"const":false}}}
}

View file

@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""Read-only validator for HLP-BATCH-HANDOFF-CONTRACT-001 receipt shapes."""
from __future__ import annotations
import argparse
import hashlib
import json
import re
from datetime import datetime
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
STAGE1 = HERE.parent / "smart-bookshelf/009-stage1-language-world-native"
CONTRACT = STAGE1 / "BATCH-HANDOFF-CONTRACT.json"
SCHEMA = STAGE1 / "schemas/HLP-FOUNDATION-CONTRACTS-v1.schema.json"
KINDS = {
"entry": ("entry_receipt", "constructionEntry"),
"exit": ("exit_receipt", "constructionExit"),
"architecture-exit": ("batch_00_architecture_exit", "architectureExit"),
}
def load(path: Path) -> dict[str, Any]:
value = json.loads(path.read_text())
if not isinstance(value, dict):
raise ValueError("JSON_OBJECT_REQUIRED")
return value
def canonical_receipt_sha256(value: dict[str, Any]) -> str:
payload = {key: item for key, item in value.items() if key != "receipt_sha256"}
return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def type_ok(value: Any, expected: Any) -> bool:
values = expected if isinstance(expected, list) else [expected]
mapping = {"object": dict, "array": list, "string": str, "boolean": bool, "null": type(None), "number": (int, float), "integer": int}
return any(isinstance(value, mapping[item]) and not (item in {"integer", "number"} and isinstance(value, bool)) for item in values)
def validate_schema_value(value: Any, rule: dict[str, Any], root: dict[str, Any], path: str = "$") -> list[str]:
errors: list[str] = []
if "$ref" in rule:
ref = rule["$ref"]
if not ref.startswith("#/$defs/"):
return [f"UNSUPPORTED_REF:{path}:{ref}"]
return validate_schema_value(value, root["$defs"][ref.rsplit("/", 1)[1]], root, path)
if "anyOf" in rule:
if not any(not validate_schema_value(value, option, root, path) for option in rule["anyOf"]):
errors.append(f"ANY_OF_REJECT:{path}")
return errors
if "type" in rule and not type_ok(value, rule["type"]):
return [f"TYPE:{path}:{rule['type']}"]
if "const" in rule and value != rule["const"]: errors.append(f"CONST:{path}")
if "enum" in rule and value not in rule["enum"]: errors.append(f"ENUM:{path}")
if isinstance(value, str):
if len(value) < rule.get("minLength", 0): errors.append(f"MIN_LENGTH:{path}")
if "pattern" in rule and not re.fullmatch(rule["pattern"], value): errors.append(f"PATTERN:{path}")
if rule.get("format") == "date-time":
try:
instant = datetime.fromisoformat(value.replace("Z", "+00:00"))
if instant.tzinfo is None: raise ValueError
except ValueError: errors.append(f"DATETIME:{path}")
if isinstance(value, dict):
required = set(rule.get("required", []))
errors.extend(f"REQUIRED:{path}.{key}" for key in sorted(required - set(value)))
properties = rule.get("properties", {})
if rule.get("additionalProperties") is False:
errors.extend(f"UNKNOWN:{path}.{key}" for key in sorted(set(value) - set(properties)))
for key in set(value) & set(properties): errors.extend(validate_schema_value(value[key], properties[key], root, f"{path}.{key}"))
if isinstance(value, list):
if len(value) < rule.get("minItems", 0): errors.append(f"MIN_ITEMS:{path}")
if "items" in rule:
for index, item in enumerate(value): errors.extend(validate_schema_value(item, rule["items"], root, f"{path}[{index}]"))
return errors
def validate(kind: str, receipt: dict[str, Any], now: datetime | None = None) -> dict[str, Any]:
contract, bundle = load(CONTRACT), load(SCHEMA)
contract_key, schema_key = KINDS[kind]
definition = bundle["$defs"][schema_key]
expected = {"schema", *contract[contract_key]["required_fields"]}
errors = []
if expected != set(definition["required"]): errors.append("CONTRACT_SCHEMA_REQUIRED_FIELDS_DRIFT")
errors.extend(validate_schema_value(receipt, definition, bundle))
if receipt.get("receipt_sha256") != canonical_receipt_sha256(receipt): errors.append("RECEIPT_SHA256_MISMATCH")
time_field = "issued_at" if kind == "entry" else "inspected_at"
try:
observed = datetime.fromisoformat(str(receipt.get(time_field)).replace("Z", "+00:00"))
current = now or datetime.now().astimezone()
if observed > current.astimezone(observed.tzinfo): errors.append("FUTURE_TIMESTAMP")
except (TypeError, ValueError):
pass
outcome = receipt.get("inspection_outcome")
if kind == "architecture-exit":
if outcome == "PASS" and (receipt.get("open_defects") != [] or not receipt.get("drawing_artifacts") or not receipt.get("site_survey_receipt") or receipt.get("architect_role_released") is not True or receipt.get("batch_01_entry_allowed") is not True or receipt.get("rework_order") is not None): errors.append("ARCHITECTURE_PASS_SEMANTICS_INVALID")
if outcome == "FAIL_REWORK":
order = receipt.get("rework_order")
if receipt.get("batch_01_entry_allowed") is not False or not isinstance(order, dict) or set(order.get("defects", [])) != set(receipt.get("open_defects", [])): errors.append("ARCHITECTURE_REWORK_SEMANTICS_INVALID")
elif kind == "exit":
evidence_ready = all(receipt.get(key) for key in ["output_artifacts", "module_self_report_receipts", "test_receipts", "cumulative_regression_receipts"])
if outcome == "PASS" and (receipt.get("open_defects") != [] or not evidence_ready or receipt.get("next_batch_entry_allowed") is not True or receipt.get("rework_order") is not None): errors.append("CONSTRUCTION_PASS_SEMANTICS_INVALID")
if outcome == "FAIL_REWORK":
order = receipt.get("rework_order")
if receipt.get("next_batch_entry_allowed") is not False or not isinstance(order, dict) or set(order.get("defects", [])) != set(receipt.get("open_defects", [])): errors.append("CONSTRUCTION_REWORK_SEMANTICS_INVALID")
else:
if receipt.get("state") != "ENTRY_OPEN": errors.append("ENTRY_STATE_MUST_BE_OPEN")
if receipt.get("batch_id") not in {f"HLP-BATCH-{number:02d}" for number in range(1, 6)}: errors.append("CONSTRUCTION_BATCH_ID_NOT_ADMISSIBLE")
if not all(receipt.get(key) for key in ["accepted_input_artifacts", "crew_admissions", "inspector_assignment", "scope", "forbidden_actions"]): errors.append("ENTRY_EVIDENCE_OR_ASSIGNMENT_EMPTY")
return {"outcome":"PASS" if not errors else "REJECT", "kind":kind, "errors":errors, "receipt_written":False, "authority_granted":False}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("kind", choices=KINDS)
parser.add_argument("receipt", type=Path)
args = parser.parse_args()
result = validate(args.kind, load(args.receipt))
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0 if result["outcome"] == "PASS" else 2
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -32,6 +32,7 @@ FOUNDATION_DRAWINGS = STAGE1 / "BATCH-00-FOUNDATION-INTERFACE-DRAWINGS.json"
BATCH_HANDOFF = STAGE1 / "BATCH-HANDOFF-CONTRACT.json"
PUBLIC_SNAPSHOT_SCHEMA = STAGE1 / "schemas/HLP-PUBLIC-SNAPSHOT-v1.schema.json"
SELF_SUPPORT_SCHEMA = STAGE1 / "schemas/HLP-SELF-OBSERVATION-SUPPORT-v1.schema.json"
HANDOFF_TEST_RECEIPT = STAGE1 / "BATCH-00-HANDOFF-VALIDATOR-TEST-RECEIPT.json"
MULTIPATH_CONSOLE = ROOT / "server-tools/heartbeat-multipath-console/console.py"
EPOCH_UTC = datetime(2025, 4, 25, 16, 0, 0, tzinfo=timezone.utc)
ZHUYUAN_BIRTH = date(2026, 3, 5)
@ -112,6 +113,7 @@ def hashes() -> dict:
"batch_handoff": BATCH_HANDOFF,
"public_snapshot_schema": PUBLIC_SNAPSHOT_SCHEMA,
"self_observation_support_schema": SELF_SUPPORT_SCHEMA,
"handoff_validator_test_receipt": HANDOFF_TEST_RECEIPT,
"time_map": TIME_MAP,
}
values = {key: {"path": str(path), "sha256": sha(path)} for key, path in paths.items()}
@ -319,6 +321,15 @@ def audit() -> dict:
errors.append("BATCH01_HANDOFF_NOT_CLOSED")
if "batch_00_architecture_exit" not in handoff or not handoff.get("canonical_hash"):
errors.append("BATCH00_EXIT_OR_HANDOFF_HASH_RULE_MISSING")
handoff_test = load(HANDOFF_TEST_RECEIPT)
expected_handoff_hashes = {
"validator": sha(Path(__file__).resolve().parent / "batch_handoff_validator.py"),
"tests": sha(Path(__file__).resolve().parent / "tests/test_batch_handoff_validator.py"),
"handoff_contract": sha(BATCH_HANDOFF),
"foundation_schema": sha(STAGE1 / "schemas/HLP-FOUNDATION-CONTRACTS-v1.schema.json"),
}
if handoff_test.get("outcome") != "PASS" or handoff_test.get("hashes") != expected_handoff_hashes:
errors.append("BATCH_HANDOFF_VALIDATOR_RECEIPT_MISMATCH")
if dependencies.get("HLP-NATIVE-MOD-003") != {"HLP-NATIVE-MOD-001", "HLP-NATIVE-MOD-002"}:
errors.append("MOD003_FOUNDATION_DEPENDENCY_MISMATCH")
module7 = next((item for item in stage1.get("modules", []) if item.get("module_id") == "HLP-NATIVE-MOD-007"), {})

View file

@ -0,0 +1,96 @@
import copy
import hashlib
import importlib.util
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location("handoff", ROOT / "batch_handoff_validator.py")
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
H = "a" * 64
AT = "2026-09-10T01:20:00+08:00"
def seal(value):
value["receipt_sha256"] = MODULE.canonical_receipt_sha256(value)
return value
def architecture(outcome="PASS"):
return seal({"schema":"hololake.architecture-batch-exit/v1","receipt_id":"R-ARCH-001","batch_id":"HLP-BATCH-00","architecture_charter_sha256":H,"drawing_artifacts":[{"id":"DRAWING","sha256":H}],"site_survey_receipt":{"outcome":"PASS","sha256":H},"independent_inspector":"CODEX-CHILD-001","inspection_outcome":outcome,"open_defects":[] if outcome == "PASS" else ["D1"],"rework_order":None if outcome == "PASS" else {"defects":["D1"]},"architect_role_released":outcome == "PASS","batch_01_entry_allowed":outcome == "PASS","inspected_at":AT,"receipt_sha256":"","authority_granted":False})
def entry():
return seal({"schema":"hololake.construction-batch-entry/v1","receipt_id":"R-ENTRY-001","batch_id":"HLP-BATCH-01","previous_batch_exit_receipt_sha256":H,"construction_drawing_id":"HLP-BATCH-00-FOUNDATION-DRAWINGS-001","construction_drawing_sha256":H,"accepted_input_artifacts":[{"id":"INPUT","sha256":H}],"crew_admissions":[{"id":"CREW"}],"inspector_assignment":{"id":"INSPECTOR"},"scope":["MOD-001"],"forbidden_actions":["PUBLISH"],"issued_by_role":"ENGINEERING_LEAD","issued_at":AT,"state":"ENTRY_OPEN","authority_granted":False})
def exit_receipt(outcome="PASS"):
return seal({"schema":"hololake.construction-batch-exit/v1","receipt_id":"R-EXIT-001","batch_id":"HLP-BATCH-01","entry_receipt_sha256":H,"output_artifacts":[{"id":"OUT","sha256":H}],"module_self_report_receipts":[H],"test_receipts":[H],"cumulative_regression_receipts":[H],"independent_inspector":"CODEX-CHILD-002","inspection_outcome":outcome,"open_defects":[] if outcome == "PASS" else ["D1"],"rework_order":None if outcome == "PASS" else {"defects":["D1"]},"builder_roles_released":[],"next_batch_entry_allowed":outcome == "PASS","inspected_at":AT,"receipt_sha256":"","authority_granted":False})
class HandoffTest(unittest.TestCase):
def test_three_positive_shapes(self):
for kind, value in (("architecture-exit", architecture()), ("entry", entry()), ("exit", exit_receipt())):
self.assertEqual(MODULE.validate(kind, value)["outcome"], "PASS")
def test_missing_field_rejects(self):
value = architecture(); del value["drawing_artifacts"]
self.assertEqual(MODULE.validate("architecture-exit", value)["outcome"], "REJECT")
def test_unknown_field_rejects(self):
value = entry(); value["surprise"] = True
self.assertEqual(MODULE.validate("entry", value)["outcome"], "REJECT")
def test_bad_hash_rejects(self):
value = exit_receipt(); value["receipt_sha256"] = "fake"
self.assertIn("PATTERN:$.receipt_sha256", MODULE.validate("exit", value)["errors"])
def test_different_well_formed_hash_rejects(self):
value = exit_receipt(); value["receipt_sha256"] = "b" * 64
self.assertIn("RECEIPT_SHA256_MISMATCH", MODULE.validate("exit", value)["errors"])
def test_naive_timestamp_rejects(self):
value = entry(); value["issued_at"] = "2026-09-10T01:20:00"
self.assertIn("DATETIME:$.issued_at", MODULE.validate("entry", value)["errors"])
def test_architecture_pass_with_defect_rejects(self):
value = architecture(); value["open_defects"] = ["D1"]
self.assertIn("ARCHITECTURE_PASS_SEMANTICS_INVALID", MODULE.validate("architecture-exit", value)["errors"])
def test_architecture_rework_cannot_open_batch1(self):
value = architecture("FAIL_REWORK"); value["batch_01_entry_allowed"] = True
self.assertIn("ARCHITECTURE_REWORK_SEMANTICS_INVALID", MODULE.validate("architecture-exit", value)["errors"])
def test_construction_rework_requires_order_and_closed_next_entry(self):
value = exit_receipt("FAIL_REWORK"); value["rework_order"] = None; value["next_batch_entry_allowed"] = True
self.assertIn("CONSTRUCTION_REWORK_SEMANTICS_INVALID", MODULE.validate("exit", value)["errors"])
def test_rework_order_must_match_exact_defects(self):
value = exit_receipt("FAIL_REWORK"); value["rework_order"] = {"defects":["OTHER"]}; seal(value)
self.assertIn("CONSTRUCTION_REWORK_SEMANTICS_INVALID", MODULE.validate("exit", value)["errors"])
def test_empty_architecture_evidence_rejects(self):
value = architecture(); value["drawing_artifacts"] = []; value["site_survey_receipt"] = {}; seal(value)
self.assertIn("ARCHITECTURE_PASS_SEMANTICS_INVALID", MODULE.validate("architecture-exit", value)["errors"])
def test_unknown_batch_and_empty_entry_evidence_reject(self):
value = entry(); value["batch_id"] = "HLP-BATCH-99"; value["accepted_input_artifacts"] = []; seal(value)
errors = MODULE.validate("entry", value)["errors"]
self.assertIn("CONSTRUCTION_BATCH_ID_NOT_ADMISSIBLE", errors)
self.assertIn("ENTRY_EVIDENCE_OR_ASSIGNMENT_EMPTY", errors)
def test_future_timestamp_rejects(self):
value = architecture(); value["inspected_at"] = "2999-01-01T00:00:00+00:00"; seal(value)
self.assertIn("FUTURE_TIMESTAMP", MODULE.validate("architecture-exit", value)["errors"])
def test_entry_state_closed_rejects(self):
value = entry(); value["state"] = "CLOSED"
self.assertEqual(MODULE.validate("entry", value)["outcome"], "REJECT")
def test_authority_true_rejects(self):
value = architecture(); value["authority_granted"] = True
self.assertEqual(MODULE.validate("architecture-exit", value)["outcome"], "REJECT")
if __name__ == "__main__": unittest.main()