diff --git a/README.md b/README.md index 15b23e6..2179368 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ EPAC is the independent repository for The Interdependency's elementary/particle - Research status: provisional / cross-domain hypothesis unless a narrower artifact says otherwise. - Empirical status: no transfer. Repository independence does not make a physics or chemistry claim true. - Molecular-shape prediction: **FALSIFIED** for the preregistered comparison carried from the incubator; that negative result is preserved as evidence. +- Typed multi-origin join term: native correctness candidate. It preserves authored S0-through-S6 lineage and named seating; its legacy count bag is a demonstrably lossy sidecar. This does not change molecular-shape standing. - UCNS Public Gonol position operations beyond carrier identity: `hmmm`. - Standing-wave / field descriptions: live modeling direction, not established external physics merely by appearing here. @@ -27,6 +28,7 @@ The extraction preserves the stack research artifacts and their epistemic status ## Structure - `epac_*.py` — executable constructors and comparison surfaces migrated from the incubator. +- `epac_join_term.py` — strict EPAC-owned S0-through-S6 join-tree candidate, equality, public recovery, and lineage replay. - `subatomic/` — subatomic construction candidates, receipts, and executable witnesses. - `tests/` — repository-level regression and falsification tests. - `data/` — bounded input/comparison data used by the current experiments. @@ -81,6 +83,28 @@ the archive-coverage gate rejects omitted modules. Current candidates also package `epac_atomic_derivation` and `epac_b_derivation`; the immutable v0.1.0 release retains its original bytes. +The post-v0.1.0 join-term candidate is public representation research, not a +secret-key surface or new release. Its METAPAT license identity, source hashes, +candidate digest, minimal same-bag/non-isomorphic witness, and nonclaims are in +`data/epac-join-term-v0-receipt.json`. + +```bash +python tools/generate_join_term_v0.py --check +python -m pytest -q tests/test_epac_join_term.py +``` + +Public-only recovery does not invoke construction state: + +```python +import json +from importlib.resources import files +from epac_join_term import recover_epac_join_tree, trace_origin_lineage + +receipt = json.loads(files("epac_data").joinpath("epac-join-term-v0-receipt.json").read_text()) +tree = recover_epac_join_tree(json.dumps(receipt["candidate"], sort_keys=True, separators=(",", ":"))) +lineage = trace_origin_lineage(tree, tree.origins[-1].origin_id) +``` + ```bash python -m pip install uv==0.11.18 uv sync --locked --extra test --extra build @@ -151,3 +175,5 @@ subject to that license when distributed. Dependencies retain their own rights and licensing status; this selection grants no rights to UCNS or other upstream projects. [License provenance](docs/license-selection.json) records the owner instruction and its resolution using the organization’s weak-copyleft convention. + +The repaired join-term wire `0.2.0` binds the merged METAPAT license, separates stored state from identified scalar readouts, and preserves authored adjacent-scale relations without claiming Vector, Transformation, or Time. The static candidate provides no resulting state-change or actual occurrence evidence. See `docs/multi-origin-join-term-v0.md` for the explicit pre-merge API migration. diff --git a/data/epac-join-term-v0-receipt.json b/data/epac-join-term-v0-receipt.json new file mode 100644 index 0000000..74230fa --- /dev/null +++ b/data/epac-join-term-v0-receipt.json @@ -0,0 +1 @@ +{"authority":{"epac":{"authority":"schema, constructor, equality, serialization, recovery, and evidence","base_commit":"1e5c999286f12221eff9870d1372203ac7935f2a","base_tree":"ce7781938ff684d826bd91f475b5423bd56b146a","feature_identity":"source-manifest-sha256","repository":"The-Interdependency/epac"},"metapat":{"application_digest":"461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b","application_fixture_path":"src/metapat/fixtures/epac-join-terms-application-v4.json","application_fixture_sha256":"7f3ff74394b48c2162818fad8a373d1b88547b0df73beb8a604376022dcddf07","application_id":"metapat.application.epac_join_terms","application_source_path":"src/metapat/epac_join_terms.py","application_source_sha256":"6cfb4a4109fd43a4f8343b48dd6fd72b74346f4609a65911cbf96188412b6ef0","application_version":"epac-join-terms-application-v4","authority":"semantic spine role license only","authority_transfer":false,"producer_commit":"1cdfb09dd00a451cee30eec2e78624df8c682662","producer_tree":"d946a5a18b0c53fd561dcd9d68fdf36d5dd11638","repository":"The-Interdependency/metapat"},"skill_lib":{"authority":"build and evidence doctrine only","authority_transfer":false,"commit":"516933d98f9de376f4e498f44059043dc0d96470","repository":"The-Interdependency/skill-lib","tree":"a1465ddf1de9519c9abc1a97d058432ba71bb257"},"ucns":{"authority_transfer":false,"consumed":false,"law_modified":false}},"candidate":{"candidate_digest":"11504eb61c4952fc3da04a51d3779484194292db2c8a5c3174b3c3ea540e6442","joins":[{"join_id":"epac.join:fixture-a:S0-S1","order":1,"provenance":["epac.join-term-v0","authored-near-join:S0->S1"],"relation":"epac.join.adjacent-scale","source_origin_id":"epac.origin:fixture-a:S0","target_origin_id":"epac.origin:fixture-a:S1"},{"join_id":"epac.join:fixture-a:S1-S2","order":2,"provenance":["epac.join-term-v0","authored-near-join:S1->S2"],"relation":"epac.join.adjacent-scale","source_origin_id":"epac.origin:fixture-a:S1","target_origin_id":"epac.origin:fixture-a:S2"},{"join_id":"epac.join:fixture-a:S2-S3","order":3,"provenance":["epac.join-term-v0","authored-near-join:S2->S3"],"relation":"epac.join.adjacent-scale","source_origin_id":"epac.origin:fixture-a:S2","target_origin_id":"epac.origin:fixture-a:S3"},{"join_id":"epac.join:fixture-a:S3-S4","order":4,"provenance":["epac.join-term-v0","authored-near-join:S3->S4"],"relation":"epac.join.adjacent-scale","source_origin_id":"epac.origin:fixture-a:S3","target_origin_id":"epac.origin:fixture-a:S4"},{"join_id":"epac.join:fixture-a:S4-S5","order":5,"provenance":["epac.join-term-v0","authored-near-join:S4->S5"],"relation":"epac.join.adjacent-scale","source_origin_id":"epac.origin:fixture-a:S4","target_origin_id":"epac.origin:fixture-a:S5"},{"join_id":"epac.join:fixture-a:S5-S6","order":6,"provenance":["epac.join-term-v0","authored-near-join:S5->S6"],"relation":"epac.join.adjacent-scale","source_origin_id":"epac.origin:fixture-a:S5","target_origin_id":"epac.origin:fixture-a:S6"}],"legacy_bag":{"excluded_from_tensor":true,"kind":"bag","scale_slot_counts":{"S0":2,"S1":2,"S2":3,"S3":3,"S4":2,"S5":4,"S6":2},"slot_kind_counts":{"hole":6,"leftover":6,"member":6}},"object_kind":"multi-origin-join-tree","origins":[{"arity":1,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S0","rule_id":"epac.bearing.slot-facing-sign.v0","value":[-1,0]},"domain_name":"subatomic slot","k":2,"origin_id":"epac.origin:fixture-a:S0","phase":{"denominator":1,"numerator":0},"phase_chart":"visible","provenance":["epac.join-term-v0","declared-scale:S0"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S0","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":1,"numerator":0}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S0","rule_id":"epac.readout.capacity.v0","value":2},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S0","rule_id":"epac.readout.slot-facing.v0","value":-2},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S0","rule_id":"epac.readout.slot-facing.v0","value":0}],"s5_energy_readout":null,"scale":"S0","shape_signature":["0:seed:leftover","1:zero:hole"],"slots":[{"facing":-2,"kind":"leftover","name":"seed","order":0,"participant_id":"epac.leftover:fixture-a:S0:seed","participant_scale":null},{"facing":0,"kind":"hole","name":"zero","order":1,"participant_id":null,"participant_scale":null}]},{"arity":1,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S1","rule_id":"epac.bearing.slot-facing-sign.v0","value":[1,0]},"domain_name":"atomic","k":2,"origin_id":"epac.origin:fixture-a:S1","phase":{"denominator":8,"numerator":1},"phase_chart":"lifted","provenance":["epac.join-term-v0","declared-scale:S1"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S1","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":8,"numerator":1}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S1","rule_id":"epac.readout.capacity.v0","value":2},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S1","rule_id":"epac.readout.slot-facing.v0","value":4},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S1","rule_id":"epac.readout.slot-facing.v0","value":0}],"s5_energy_readout":null,"scale":"S1","shape_signature":["0:subatomic:member","1:zero:hole"],"slots":[{"facing":4,"kind":"member","name":"subatomic","order":0,"participant_id":"epac.origin:fixture-a:S0","participant_scale":"S0"},{"facing":0,"kind":"hole","name":"zero","order":1,"participant_id":null,"participant_scale":null}]},{"arity":2,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S2","rule_id":"epac.bearing.slot-facing-sign.v0","value":[1,-1,0]},"domain_name":"join/arity","k":3,"origin_id":"epac.origin:fixture-a:S2","phase":{"denominator":4,"numerator":1},"phase_chart":"visible","provenance":["epac.join-term-v0","declared-scale:S2","authored-seating-chart"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S2","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":4,"numerator":1}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S2","rule_id":"epac.readout.capacity.v0","value":3},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S2","rule_id":"epac.readout.slot-facing.v0","value":3},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S2","rule_id":"epac.readout.slot-facing.v0","value":-3},{"measured_property":"slots[2].facing","origin_id":"epac.origin:fixture-a:S2","rule_id":"epac.readout.slot-facing.v0","value":0}],"s5_energy_readout":null,"scale":"S2","shape_signature":["0:core:member","1:guest:leftover","2:vacancy:hole"],"slots":[{"facing":3,"kind":"member","name":"core","order":0,"participant_id":"epac.origin:fixture-a:S1","participant_scale":"S1"},{"facing":-3,"kind":"leftover","name":"guest","order":1,"participant_id":"epac.leftover:fixture-a:S2:guest","participant_scale":null},{"facing":0,"kind":"hole","name":"vacancy","order":2,"participant_id":null,"participant_scale":null}]},{"arity":2,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S3","rule_id":"epac.bearing.slot-facing-sign.v0","value":[-1,0,1]},"domain_name":"embed","k":3,"origin_id":"epac.origin:fixture-a:S3","phase":{"denominator":8,"numerator":3},"phase_chart":"lifted","provenance":["epac.join-term-v0","declared-scale:S3"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S3","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":8,"numerator":3}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S3","rule_id":"epac.readout.capacity.v0","value":3},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S3","rule_id":"epac.readout.slot-facing.v0","value":-5},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S3","rule_id":"epac.readout.slot-facing.v0","value":0},{"measured_property":"slots[2].facing","origin_id":"epac.origin:fixture-a:S3","rule_id":"epac.readout.slot-facing.v0","value":2}],"s5_energy_readout":null,"scale":"S3","shape_signature":["0:join:member","1:zero:hole","2:embed-context:leftover"],"slots":[{"facing":-5,"kind":"member","name":"join","order":0,"participant_id":"epac.origin:fixture-a:S2","participant_scale":"S2"},{"facing":0,"kind":"hole","name":"zero","order":1,"participant_id":null,"participant_scale":null},{"facing":2,"kind":"leftover","name":"embed-context","order":2,"participant_id":"epac.leftover:fixture-a:S3:context","participant_scale":null}]},{"arity":2,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S4","rule_id":"epac.bearing.slot-facing-sign.v0","value":[1,-1]},"domain_name":"electronic state","k":2,"origin_id":"epac.origin:fixture-a:S4","phase":{"denominator":2,"numerator":1},"phase_chart":"visible","provenance":["epac.join-term-v0","declared-scale:S4"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S4","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":2,"numerator":1}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S4","rule_id":"epac.readout.capacity.v0","value":2},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S4","rule_id":"epac.readout.slot-facing.v0","value":2},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S4","rule_id":"epac.readout.slot-facing.v0","value":-1}],"s5_energy_readout":null,"scale":"S4","shape_signature":["0:embed:member","1:electronic-context:leftover"],"slots":[{"facing":2,"kind":"member","name":"embed","order":0,"participant_id":"epac.origin:fixture-a:S3","participant_scale":"S3"},{"facing":-1,"kind":"leftover","name":"electronic-context","order":1,"participant_id":"epac.leftover:fixture-a:S4:context","participant_scale":null}]},{"arity":2,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.bearing.slot-facing-sign.v0","value":[1,0,-1,0]},"domain_name":"EPAC energy readout","k":4,"origin_id":"epac.origin:fixture-a:S5","phase":{"denominator":8,"numerator":5},"phase_chart":"lifted","provenance":["epac.join-term-v0","declared-scale:S5"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":8,"numerator":5}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.capacity.v0","value":4},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.slot-facing.v0","value":7},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.slot-facing.v0","value":0},{"measured_property":"slots[2].facing","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.slot-facing.v0","value":-2},{"measured_property":"slots[3].facing","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.slot-facing.v0","value":0}],"s5_energy_readout":{"measured_property":"slots[*].kind,k","origin_id":"epac.origin:fixture-a:S5","rule_id":"epac.readout.occupied-slots-over-k.v0","value":{"denominator":2,"numerator":1}},"scale":"S5","shape_signature":["0:electronic-state:member","1:zero-a:hole","2:occupancy-context:leftover","3:zero-b:hole"],"slots":[{"facing":7,"kind":"member","name":"electronic-state","order":0,"participant_id":"epac.origin:fixture-a:S4","participant_scale":"S4"},{"facing":0,"kind":"hole","name":"zero-a","order":1,"participant_id":null,"participant_scale":null},{"facing":-2,"kind":"leftover","name":"occupancy-context","order":2,"participant_id":"epac.leftover:fixture-a:S5:context","participant_scale":null},{"facing":0,"kind":"hole","name":"zero-b","order":3,"participant_id":null,"participant_scale":null}]},{"arity":2,"bearing":{"measured_property":"slots[*].facing","origin_id":"epac.origin:fixture-a:S6","rule_id":"epac.bearing.slot-facing-sign.v0","value":[1,-1]},"domain_name":"ensemble","k":2,"origin_id":"epac.origin:fixture-a:S6","phase":{"denominator":4,"numerator":3},"phase_chart":"visible","provenance":["epac.join-term-v0","declared-scale:S6"],"readouts":[{"measured_property":"phase","origin_id":"epac.origin:fixture-a:S6","rule_id":"epac.readout.exact-phase.v0","value":{"denominator":4,"numerator":3}},{"measured_property":"k","origin_id":"epac.origin:fixture-a:S6","rule_id":"epac.readout.capacity.v0","value":2},{"measured_property":"slots[0].facing","origin_id":"epac.origin:fixture-a:S6","rule_id":"epac.readout.slot-facing.v0","value":1},{"measured_property":"slots[1].facing","origin_id":"epac.origin:fixture-a:S6","rule_id":"epac.readout.slot-facing.v0","value":-1}],"s5_energy_readout":null,"scale":"S6","shape_signature":["0:energy-readout:member","1:ensemble-context:leftover"],"slots":[{"facing":1,"kind":"member","name":"energy-readout","order":0,"participant_id":"epac.origin:fixture-a:S5","participant_scale":"S5"},{"facing":-1,"kind":"leftover","name":"ensemble-context","order":1,"participant_id":"epac.leftover:fixture-a:S6:context","participant_scale":null}]}],"schema_id":"epac.multi-origin-join-tree","schema_version":"0.2.0","semantic_field_bindings":{"join.join_id":{"application_role":"stored-join-term","catalog_module_id":"metapat.axiom.1.thing","domain_name":"authored join record identity","spine_name":"Thing"},"join.order":{"application_role":"authored-near-join","catalog_module_id":"metapat.axiom.6.relate","domain_name":"authored adjacent-scale relation position","spine_name":"Tensor"},"join.provenance":{"application_role":"recursive-origin","catalog_module_id":"metapat.axiom.7.emerge","domain_name":"authored join provenance","spine_name":"Thing"},"join.relation":{"application_role":"authored-near-join","catalog_module_id":"metapat.axiom.6.relate","domain_name":"authored adjacent-scale join","spine_name":"Tensor"},"join.source_origin_id":{"application_role":"recursive-origin","catalog_module_id":"metapat.axiom.7.emerge","domain_name":"source origin identity","spine_name":"Thing"},"join.target_origin_id":{"application_role":"recursive-origin","catalog_module_id":"metapat.axiom.7.emerge","domain_name":"new target origin identity","spine_name":"Thing"},"legacy_bag.excluded_from_tensor":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"tensor-exclusion marker","spine_name":"Thing"},"legacy_bag.kind":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"legacy sidecar kind","spine_name":"Thing"},"legacy_bag.scale_slot_counts":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"lossy slot-count map by scale","spine_name":"Thing"},"legacy_bag.slot_kind_counts":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"lossy slot-count map by kind","spine_name":"Thing"},"origin.arity":{"application_role":"join-boundary","catalog_module_id":"metapat.axiom.2.boundary","domain_name":"occupied seating arity","spine_name":"Boundary"},"origin.bearing":{"application_role":"state-metric","catalog_module_id":"metapat.axiom.9.scalar","domain_name":"ordered static slot-facing sign readouts","spine_name":"Scalar"},"origin.domain_name":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"EPAC scale name","spine_name":"Simplex"},"origin.k":{"application_role":"join-boundary","catalog_module_id":"metapat.axiom.2.boundary","domain_name":"join window k","spine_name":"Boundary"},"origin.origin_id":{"application_role":"stored-join-term","catalog_module_id":"metapat.axiom.1.thing","domain_name":"origin identity","spine_name":"Thing"},"origin.phase":{"application_role":"origin-state","catalog_module_id":"metapat.axiom.3.state","domain_name":"EPAC exact phase","spine_name":"State"},"origin.phase_chart":{"application_role":"origin-state","catalog_module_id":"metapat.axiom.3.state","domain_name":"visible or lifted phase record","spine_name":"State"},"origin.provenance":{"application_role":"recursive-origin","catalog_module_id":"metapat.axiom.7.emerge","domain_name":"origin provenance","spine_name":"Thing"},"origin.readouts":{"application_role":"state-metric","catalog_module_id":"metapat.axiom.9.scalar","domain_name":"identified phase, capacity, and slot-facing readouts","spine_name":"Scalar"},"origin.s5_energy_readout":{"application_role":"state-metric","catalog_module_id":"metapat.axiom.9.scalar","domain_name":"EPAC S5 occupancy functional","spine_name":"Scalar"},"origin.scale":{"application_role":"scale-origin","catalog_module_id":"metapat.axiom.4.simplex","domain_name":"EPAC scale origin","spine_name":"Simplex"},"origin.shape_signature":{"application_role":"multi-origin-tensor","catalog_module_id":"metapat.axiom.5.tensor","domain_name":"ordered seating shape","spine_name":"Tensor"},"origin.slots":{"application_role":"join-boundary","catalog_module_id":"metapat.axiom.2.boundary","domain_name":"named ordered join slots","spine_name":"Boundary"},"readout.measured_property":{"application_role":"origin-state","catalog_module_id":"metapat.axiom.3.state","domain_name":"named source state property","spine_name":"State"},"readout.origin_id":{"application_role":"stored-join-term","catalog_module_id":"metapat.axiom.1.thing","domain_name":"readout source origin identity","spine_name":"Thing"},"readout.rule_id":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"EPAC readout rule identity","spine_name":"Thing"},"readout.value":{"application_role":"state-metric","catalog_module_id":"metapat.axiom.9.scalar","domain_name":"computed EPAC readout value","spine_name":"Scalar"},"slot.facing":{"application_role":"origin-state","catalog_module_id":"metapat.axiom.3.state","domain_name":"stored slot-facing property","spine_name":"State"},"slot.kind":{"application_role":"join-boundary","catalog_module_id":"metapat.axiom.2.boundary","domain_name":"member, hole, or leftover","spine_name":"Boundary"},"slot.name":{"application_role":"join-boundary","catalog_module_id":"metapat.axiom.2.boundary","domain_name":"slot name","spine_name":"Boundary"},"slot.order":{"application_role":"join-boundary","catalog_module_id":"metapat.axiom.2.boundary","domain_name":"slot order","spine_name":"Boundary"},"slot.participant_id":{"application_role":"stored-join-term","catalog_module_id":"metapat.axiom.1.thing","domain_name":"member or leftover identity","spine_name":"Thing"},"slot.participant_scale":{"application_role":"scale-origin","catalog_module_id":"metapat.axiom.4.simplex","domain_name":"member source scale","spine_name":"Simplex"},"tree.joins":{"application_role":"authored-near-join","catalog_module_id":"metapat.axiom.6.relate","domain_name":"ordered authored adjacent-scale relations","spine_name":"Tensor"},"tree.legacy_bag":{"application_role":"domain-customs","catalog_module_id":"metapat.axiom.12.domain_qualification","domain_name":"non-structural legacy bag sidecar","spine_name":"Thing"},"tree.object_kind":{"application_role":"stored-join-term","catalog_module_id":"metapat.axiom.1.thing","domain_name":"multi-origin join-tree kind","spine_name":"Thing"},"tree.origins":{"application_role":"multi-origin-tensor","catalog_module_id":"metapat.axiom.5.tensor","domain_name":"S0-through-S6 origin tensor","spine_name":"Tensor"}},"semantic_license":{"application_digest":"461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b","application_id":"metapat.application.epac_join_terms","application_version":"epac-join-terms-application-v4","authority":"The-Interdependency/metapat","authority_transfer":false}},"evidence":{"declared_check_count":17,"declared_contract_count":17,"deterministic_regeneration_command":"python tools/generate_join_term_v0.py --check","molecular_shape_standing":"FALSIFIED (preserved; tested separately)","public_recovery_uses_constructor_state":false},"evidence_digest":"215844eda0b1b1f3edf95bf7b360df84deecc97382a88f777c150fa50e359839","evidence_epoch":"2026-09-30","historical_original_authoring_runtime":{"executable":"/usr/bin/python3.12","executable_sha256":"e50d468e8b0adfb05733f5b87b3cff34829c4a8c1aea50c865aa8bdfe4bb150f","implementation":"CPython","version":"3.12.3"},"hmmm":["Authored adjacency and its stored order do not establish state changes or actual occurrence order; Transformation and Time remain unlicensed for this candidate.","Whether a useful operation exists that requires secret EPAC state and cannot be efficiently reproduced from a legitimate public projection remains unestablished; this candidate is wholly public.","Whether exact lineage and seating replay improve an independently preregistered EPAC task remains unmeasured.","Whether any later UCNS correspondence is useful requires a separate license and test without rewriting UCNS law."],"inputs":{"identity_namespace":"fixture-a","input_is_public":true,"phase_chart_rule":"visible on even-numbered scales; lifted on odd-numbered scales","phase_values":[{"denominator":1,"numerator":0},{"denominator":8,"numerator":1},{"denominator":4,"numerator":1},{"denominator":8,"numerator":3},{"denominator":2,"numerator":1},{"denominator":8,"numerator":5},{"denominator":4,"numerator":3}],"private_input_supplied":false,"s2_seating_order":["core","guest","vacancy"]},"nonclaims":["Vector action, resulting state Transformation, or actual Time sequence","chemical or physical truth","molecular geometry or shape prediction","UCNS correspondence, topology, theorem status, or gonol identity","energy coupling, conservation, entropy, force, or field behavior","cryptographic secrecy, confidentiality, public-key separation, or computational hardness","PCEA compatibility","production suitability","release or protocol authority"],"results":{"authored_join_count":6,"candidate_digest":"11504eb61c4952fc3da04a51d3779484194292db2c8a5c3174b3c3ea540e6442","candidate_json_sha256":"821d35a1e42c8d22423f78fdce8449066af40392d47f3e79e5b30b8f993fcbfd","classification":"PUBLIC_ONLY_RECOVERY_SURVIVED","hole_count":6,"leftover_count":6,"legacy_bag_sha256":"ae6e48f3245310cefc233eb091a2cb872ac7ab0302296735d3a1b5fe9e2a2f9f","legacy_projection":"slot-count bag","member_count":6,"native_operation":"exact authored origin-lineage and named-seating replay","origin_count":7,"s6_lineage":["epac.origin:fixture-a:S0","epac.origin:fixture-a:S1","epac.origin:fixture-a:S2","epac.origin:fixture-a:S3","epac.origin:fixture-a:S4","epac.origin:fixture-a:S5","epac.origin:fixture-a:S6"],"same_bag_alpha_renamed_identity_witness":{"bags_equal":true,"baseline_s6_lineage":["epac.origin:fixture-a:S0","epac.origin:fixture-a:S1","epac.origin:fixture-a:S2","epac.origin:fixture-a:S3","epac.origin:fixture-a:S4","epac.origin:fixture-a:S5","epac.origin:fixture-a:S6"],"conclusion":"the legacy bag does not select exact instance identity labels, while structural lineage up to alpha-renaming is unchanged","exact_instance_lineages_equal":false,"join_isomorphic":true,"renamed_candidate_digest":"7e30d6f4942e215462254dec9723a1deeb4e71d680e2cc960f227b02240f420d","renamed_identity_namespace":"fixture-b","renamed_legacy_bag_sha256":"ae6e48f3245310cefc233eb091a2cb872ac7ab0302296735d3a1b5fe9e2a2f9f","renamed_s6_lineage":["epac.origin:fixture-b:S0","epac.origin:fixture-b:S1","epac.origin:fixture-b:S2","epac.origin:fixture-b:S3","epac.origin:fixture-b:S4","epac.origin:fixture-b:S5","epac.origin:fixture-b:S6"]},"same_bag_nonisomorphic_witness":{"bags_equal":true,"baseline_candidate_digest":"11504eb61c4952fc3da04a51d3779484194292db2c8a5c3174b3c3ea540e6442","conclusion":"the legacy bag cannot uniquely recover authored seating or the complete join tree","join_isomorphic":false,"reordered_candidate_digest":"f74f9227a79acf84588b11d153bf66f9490d3aa18d8e899b6266c11ca3594633","reordered_legacy_bag_sha256":"ae6e48f3245310cefc233eb091a2cb872ac7ab0302296735d3a1b5fe9e2a2f9f","reordered_s2_seating_order":["guest","core","vacancy"]},"slot_count":18},"runtime_scope":"Deterministic evidence is runtime-independent; current execution identities belong to the exact-head artifact replay receipts, not this historical authoring record.","schema_id":"epac.join-term-v0-evidence","schema_version":"1.0.0","source_files_sha256":{"README.md":"7e28c7c092f7551b11a03c9db7862e27ec88ff6f81167ed3d3f99db26e29a18b","data/metapat-epac-join-terms-application-v4.json":"7f3ff74394b48c2162818fad8a373d1b88547b0df73beb8a604376022dcddf07","docs/PROVENANCE.md":"b410815a47ea9a8229406241616a9dc143ff09eb8da717456c019379efa6d5ee","docs/domain-claims.md":"f6f7f517e127a517a4acccba5527692239700c128b7bf0ecef9603e8c155c911","docs/multi-origin-join-term-v0.md":"6efb441f4421edf84c22556b46e438cc2da4f51536c522d3b6556af7b1c66a39","docs/research-status.md":"1f5194f0dc44521b6dc81d9a9494c72ff385fd879b8eb2b2849824640c5c2d07","docs/work-graph.json":"4edb37ace798511ff4b5f667afd1f0a3852ce78b7335349aaa9662171de0b9e9","docs/work-graphs/epac-join-term-v0.json":"7487441ec7e3ff3203635e245fccba5d293192cef5201c657946a15788166f16","epac_boundary_probe_completeness.py":"61df1624adc2970fe2e7d9d94ee15359decd42ddb7285ac223bd46eb748541ec","epac_join_term.py":"4697da5c76d5dfc77aedc5c84c71b7175180550824b6a1a2c1bf23a8548839af","pyproject.toml":"414cdc71cdb2e0ff859fcb59a359d57a5327e6b105e0ff010a522216dcd3881f","tests/test_boundary_probe_completeness.py":"254590ac58a54979f0f05534cea357e2369577e96304dda0ae337f3114d13ac4","tests/test_epac_join_term.py":"ed060a6ef7903f178e3da6b94890f088de8b37dc7b94e6e41b4ccc15a3b7f5c8","tools/generate_join_term_v0.py":"1836734e0ba692f23f604838ed3146c811528266a341979366eff6b19447e331"},"source_manifest_sha256":"c3aad9a890e4ea42afcc8118a14c21b8ef10044ec1520a0d32f7daf668fbff0b"} diff --git a/data/metapat-epac-join-terms-application-v4.json b/data/metapat-epac-join-terms-application-v4.json new file mode 100644 index 0000000..15de5b9 --- /dev/null +++ b/data/metapat-epac-join-terms-application-v4.json @@ -0,0 +1 @@ +{"application_digest":"461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b","application_id":"metapat.application.epac_join_terms","application_version":"epac-join-terms-application-v4","catalog_bindings":[{"application_role":"customs-boundary","application_statement":"METAPAT supplies the semantic spine while EPAC retains its domain-qualified names, schemas, constructors, and evidence obligations.","binding_digest":"27af0152ad1f50fa01b38de6f712c0a22abc461a968df879ec2540fbc1e13801","module_claim_status":"ROOT-STIPULATION","module_digest":"f8fc0e86a8a89fac2c82a3be652be5fbe7b8c6d6be390944e1bdad453a6cb3cf","module_id":"metapat.root_spine","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"stored-join-term","application_statement":"EPAC uses Thing as the semantic role of one stored typed join term, not a formula bag, residue list, or count.","binding_digest":"cf9c8b563db8c6bdfe0b5a543580be5dc6e79e6284abcf85eef583ad2782cad0","module_claim_status":"ROOT-STIPULATION","module_digest":"e01a0c12c16667966e2b720198bc7a62fb052c9ce157bca6b2c4716878d882bc","module_id":"metapat.axiom.1.thing","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"join-boundary","application_statement":"EPAC uses Boundary for named ordered slots, explicit holes and leftovers, and the stored window k.","binding_digest":"4c940699402952f0ea3d503cb1549bccb5a02a30f08ede03b237e6c5c82f8dc2","module_claim_status":"ROOT-STIPULATION","module_digest":"428041533a55becd1146c14445de8eefdca03c221d2186abbeef247b93c416bb","module_id":"metapat.axiom.2.boundary","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"origin-state","application_statement":"EPAC uses State for domain-qualified phase and capacity as metricable properties of a declared origin; stored property values are not scalar observations.","binding_digest":"782e221bac0b62b604e2df4fd691ffa22274dcae58df19bed91a1eac206b8a1e","module_claim_status":"ROOT-STIPULATION","module_digest":"a5fc19172877799517432262248350a751980a31d03a7c84c83467dcfc0e1659","module_id":"metapat.axiom.3.state","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"scale-origin","application_statement":"EPAC treats one complete scale origin O(S_n), with its boundary and state, as a Simplex-role object.","binding_digest":"c9047b1e0c76554d3c0529bf137103a9a7741f9bfa1a7ca08f1db523047bd62d","module_claim_status":"ROOT-STIPULATION","module_digest":"ba36e3a72bc20515d4d79007d84c590657ccfa09aa9fc2962505e1ec9bc588b4","module_id":"metapat.axiom.4.simplex","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"multi-origin-tensor","application_statement":"EPAC uses Tensor for the structure formed by all declared S0 through S6 origins and their authored relations; a flattened bag is not that object.","binding_digest":"d5cab46d312e5ef71b084436eb01dd0d80b2058b3067229efed4546712a5497b","module_claim_status":"DEFINITION","module_digest":"f04180257239d082cdd26f140a96522c9b1944db16f77c5a94a6b570b675496c","module_id":"metapat.axiom.5.tensor","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"authored-near-join","application_statement":"EPAC records only authored adjacent-scale joins and does not infer ancestry from analogy, shared counts, or shared geometry.","binding_digest":"29baa32527cbbaa98e13ffb8fbc654841382d827bb9ddad5ad0a96f3c18bb788","module_claim_status":"DEFINITION","module_digest":"69fca315a036822beda3cadbd90e640ea59beaabc902a09a7426af185a280bef","module_id":"metapat.axiom.6.relate","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"recursive-origin","application_statement":"EPAC may store join(O(S_n)) as a new identity O(S_{n+1}) while retaining its ordered constituent identities and provenance.","binding_digest":"fd513671ccf54131d70bf819fad9ad3cf3ae5319167a979e3dd53b2ef186ad7d","module_claim_status":"DEFINITION","module_digest":"80e9cb91322b66f0049c2d8ca92fa3025e7a178a2fa29c2498e48d3eb3ef26b1","module_id":"metapat.axiom.7.emerge","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"state-metric","application_statement":"EPAC uses Scalar for separately identified phase, capacity, slot-facing, and occupancy metrics or readouts of named state properties; a readout names its origin, measured property, and measurement rule and is not the state itself.","binding_digest":"899be80606174f2309b03478b22923da3923da1e05b4abc9e7dc1145ab052945","module_claim_status":"DEFINITION","module_digest":"d9769d495301cee60ad6a9a739fe82ac8bc932851905607717663a84bbbf3be4","module_id":"metapat.axiom.9.scalar","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"join-transformation","application_statement":"EPAC uses Transformation only for the resulting before/after change of a named state property of an identified thing caused by a declared join action; the action, a new origin identity, or different values at unrelated origins alone are not that result.","binding_digest":"602dfadcfebf406b2b0f7cdcf4487cfae0afef150899cc466dde69a6217a70fd","module_claim_status":"DEFINITION","module_digest":"8f66dd4462c075b06679b4a9665c7007d988bb294fcc0adede11de6ab745166c","module_id":"metapat.axiom.10.transformation","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"transformation-sequence","application_statement":"EPAC uses Time only when evidence establishes that at least two resulting state changes actually occurred sequentially; registration may preserve that sequence, but sorting records, sequence numbers, and structural scale order cannot create it or establish physical time.","binding_digest":"b4ead837a4a45c355ea6a0bb52220c98cc8107ba053631b030fa435391b8bcfc","module_claim_status":"DEFINITION","module_digest":"b22289bb49f07621da781e3c27e4394b369b45b28acaf893bc6328a8a5a11429","module_id":"metapat.axiom.11.time","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"},{"application_role":"domain-customs","application_statement":"Every transferred spine role remains paired with an EPAC domain name; structural recurrence alone transfers no chemical, physical, energetic, or UCNS meaning.","binding_digest":"0c54afe4796ff86386479e0112da36b288c2b3d6218ab6ae79364e32dd109a3c","module_claim_status":"DEFINITION","module_digest":"ce530064d74c6813b4687cbb933a443a4f9306306660c8ab09ce80ba9752c22c","module_id":"metapat.axiom.12.domain_qualification","schema_id":"metapat.application-catalog-binding","schema_version":"1.0.0"}],"catalog_digest":"aeffdfb92acf1486f517f149367ee38251c11197f32b01cc02564e7ca804ec69","catalog_version":"metapat-semantic-catalog-v4","claim_status":"CROSS-DOMAIN-HYPOTHESIS","does_not_transfer":["Static bearing inferred from slot-facing signs remains an EPAC readout; no Vector role is licensed without a separately established state-altering operation and measured resulting change.","This license does not transfer a UCNS coordinate, phase law, topology, theorem status, gonol identity, or implementation into EPAC.","EPAC phase is not chemistry phase, and visible or lifted coordinates acquire no physical meaning merely because they are stored.","S5 naming does not establish energy coupling, conservation, entropy, force, field, or empirical physical validity.","Shared shape, count, phase, vocabulary, or geometry does not establish ancestry or authorize an inferred join.","A formula bag, residue collection, contracted cardinality, subset fill, or gravity-point registry is not licensed as the multi-origin tensor-role object.","METAPAT licensing does not validate EPAC implementation correctness, molecular geometry, production suitability, or external-domain truth."],"domain_statements":["An EPAC join term stores named ordered slots, explicit holes and leftovers, a positive window k, exact origin state, and provenance; omission is not a representation of zero.","At each adjacent scale, join(O(S_n)) may be stored as a new identity O(S_{n+1}); recursive construction does not erase or contract the child identities that establish the join.","The EPAC tensor-role object contains every declared origin S0 through S6 and its authored adjacent-scale relations; a formula bag or subset fill cannot substitute for that structure.","EPAC join equality is join-isomorphism over typed ordered structure and state, not equality of counts, formulas, residue bags, or visible projections.","Visible and lifted phase records remain distinct EPAC objects when their declared identities or coordinates differ.","S5 energy readout is an EPAC-qualified functional of occupancy and is not a fourth circle, a universal energy primitive, or evidence of physical energy coupling."],"domain_validity_claim":false,"domains":["EPAC relational representation","typed recursive join terms","multi-origin provenance"],"evidence_boundary":"METAPAT owns this exact semantic license and its catalog bindings. EPAC owns the schema, constructor, equality, serialization, replay, and domain evidence. UCNS retains any UCNS law and EDCM retains measurement authority. Passing deterministic tests establishes contract conformance only, not chemical, physical, empirical, or production validity.","evidence_requirements":["The EPAC schema must represent every scale S0 through S6, a new identity at each recursive join, named ordered slots, positive k, explicit holes and leftovers, exact state properties, separately identified readouts, and provenance.","Closed-schema parsing must reject a bag presented as a tree, missing scales, inferred skip-scale joins, omitted holes, duplicate identities, unlicensed fields, and malformed provenance.","Join-isomorphism tests must distinguish two trees with the same counts but different typed ordered structure and must keep visible and lifted records distinct.","A deterministic public fixture and replay receipt must bind the exact METAPAT application identity and permit independent recovery without private construction state.","Existing EPAC molecular-shape falsification must remain unchanged; subsequent comparisons may compare S3 tree to S3 tree only.","State and scalar evidence must identify the origin and metricable property separately from each readout and its measurement rule; storing a phase or capacity value does not establish observation.","Transformation evidence must identify the affected thing, named property, before and after states, resulting difference, and the join action responsible; an action record or new identity alone is insufficient.","Time evidence must establish occurrence order for at least two evidenced transformations independently of record sorting, serialization, scale labels, or assigned sequence numbers; simultaneous or unordered changes do not qualify."],"measurement_validity_claim":false,"metapat_validity_claim":false,"root_impact":"none","schema_id":"metapat.application-module","schema_version":"2.0.0","selected_scales":["S0 subatomic slot","S1 atomic","S2 join/arity","S3 embed","S4 electronic state","S5 EPAC energy readout","S6 ensemble"],"shared_question_form":["typed origin O(S_n)","-> named ordered slots including holes and leftovers","-> authored adjacent-scale join","-> new identity O(S_{n+1})","-> preserved source identities and provenance","-> complete S0-through-S6 relational structure"],"source_document":"docs/applications/epac-join-terms.md","source_statement_refs":["docs/applications/epac-join-terms.md#application-identity::statement-1","docs/applications/epac-join-terms.md#application-identity::statement-2","docs/applications/epac-join-terms.md#application-identity::statement-3","docs/applications/epac-join-terms.md#catalog-bindings::statement-4","docs/applications/epac-join-terms.md#catalog-bindings::statement-5","docs/applications/epac-join-terms.md#catalog-bindings::statement-6","docs/applications/epac-join-terms.md#catalog-bindings::statement-7","docs/applications/epac-join-terms.md#catalog-bindings::statement-8","docs/applications/epac-join-terms.md#catalog-bindings::statement-9","docs/applications/epac-join-terms.md#catalog-bindings::statement-10","docs/applications/epac-join-terms.md#catalog-bindings::statement-11","docs/applications/epac-join-terms.md#catalog-bindings::statement-12","docs/applications/epac-join-terms.md#catalog-bindings::statement-13","docs/applications/epac-join-terms.md#catalog-bindings::statement-14","docs/applications/epac-join-terms.md#catalog-bindings::statement-15","docs/applications/epac-join-terms.md#epac-scale-map::statement-16","docs/applications/epac-join-terms.md#epac-scale-map::statement-17","docs/applications/epac-join-terms.md#epac-scale-map::statement-18","docs/applications/epac-join-terms.md#epac-scale-map::statement-19","docs/applications/epac-join-terms.md#epac-scale-map::statement-20","docs/applications/epac-join-terms.md#epac-scale-map::statement-21","docs/applications/epac-join-terms.md#epac-scale-map::statement-22","docs/applications/epac-join-terms.md#epac-domain-statements::statement-23","docs/applications/epac-join-terms.md#epac-domain-statements::statement-24","docs/applications/epac-join-terms.md#epac-domain-statements::statement-25","docs/applications/epac-join-terms.md#epac-domain-statements::statement-26","docs/applications/epac-join-terms.md#epac-domain-statements::statement-27","docs/applications/epac-join-terms.md#epac-domain-statements::statement-28","docs/applications/epac-join-terms.md#recursive-question-form::statement-29","docs/applications/epac-join-terms.md#recursive-question-form::statement-30","docs/applications/epac-join-terms.md#recursive-question-form::statement-31","docs/applications/epac-join-terms.md#recursive-question-form::statement-32","docs/applications/epac-join-terms.md#recursive-question-form::statement-33","docs/applications/epac-join-terms.md#recursive-question-form::statement-34","docs/applications/epac-join-terms.md#transfers::statement-35","docs/applications/epac-join-terms.md#transfers::statement-36","docs/applications/epac-join-terms.md#transfers::statement-37","docs/applications/epac-join-terms.md#transfers::statement-38","docs/applications/epac-join-terms.md#does-not-transfer::statement-39","docs/applications/epac-join-terms.md#does-not-transfer::statement-40","docs/applications/epac-join-terms.md#does-not-transfer::statement-41","docs/applications/epac-join-terms.md#does-not-transfer::statement-42","docs/applications/epac-join-terms.md#does-not-transfer::statement-43","docs/applications/epac-join-terms.md#does-not-transfer::statement-44","docs/applications/epac-join-terms.md#does-not-transfer::statement-45","docs/applications/epac-join-terms.md#working-question::statement-46","docs/applications/epac-join-terms.md#evidence-boundary::statement-47","docs/applications/epac-join-terms.md#evidence-boundary::statement-48","docs/applications/epac-join-terms.md#evidence-boundary::statement-49","docs/applications/epac-join-terms.md#evidence-boundary::statement-50","docs/applications/epac-join-terms.md#evidence-boundary::statement-51","docs/applications/epac-join-terms.md#evidence-boundary::statement-52","docs/applications/epac-join-terms.md#evidence-boundary::statement-53","docs/applications/epac-join-terms.md#evidence-boundary::statement-54","docs/applications/epac-join-terms.md#evidence-boundary::statement-55","docs/applications/epac-join-terms.md#hmmm::statement-56","docs/applications/epac-join-terms.md#hmmm::statement-57","docs/applications/epac-join-terms.md#hmmm::statement-58"],"source_statements":["Status: **CROSS-DOMAIN-HYPOTHESIS / EPAC semantic license**","Root impact: **none**","METAPAT owns the spine vocabulary and this license. EPAC owns the typed join-term domain, implementation, and evidence. UCNS and EDCM retain their own law and measurement authority. No validity status transfers among them.","| `metapat.root_spine` | customs-boundary | METAPAT supplies the semantic spine while EPAC retains its domain-qualified names, schemas, constructors, and evidence obligations. |","| `metapat.axiom.1.thing` | stored-join-term | EPAC uses Thing as the semantic role of one stored typed join term, not a formula bag, residue list, or count. |","| `metapat.axiom.2.boundary` | join-boundary | EPAC uses Boundary for named ordered slots, explicit holes and leftovers, and the stored window k. |","| `metapat.axiom.3.state` | origin-state | EPAC uses State for domain-qualified phase and capacity as metricable properties of a declared origin; stored property values are not scalar observations. |","| `metapat.axiom.4.simplex` | scale-origin | EPAC treats one complete scale origin O(S_n), with its boundary and state, as a Simplex-role object. |","| `metapat.axiom.5.tensor` | multi-origin-tensor | EPAC uses Tensor for the structure formed by all declared S0 through S6 origins and their authored relations; a flattened bag is not that object. |","| `metapat.axiom.6.relate` | authored-near-join | EPAC records only authored adjacent-scale joins and does not infer ancestry from analogy, shared counts, or shared geometry. |","| `metapat.axiom.7.emerge` | recursive-origin | EPAC may store join(O(S_n)) as a new identity O(S_{n+1}) while retaining its ordered constituent identities and provenance. |","| `metapat.axiom.9.scalar` | state-metric | EPAC uses Scalar for separately identified phase, capacity, slot-facing, and occupancy metrics or readouts of named state properties; a readout names its origin, measured property, and measurement rule and is not the state itself. |","| `metapat.axiom.10.transformation` | join-transformation | EPAC uses Transformation only for the resulting before/after change of a named state property of an identified thing caused by a declared join action; the action, a new origin identity, or different values at unrelated origins alone are not that result. |","| `metapat.axiom.11.time` | transformation-sequence | EPAC uses Time only when evidence establishes that at least two resulting state changes actually occurred sequentially; registration may preserve that sequence, but sorting records, sequence numbers, and structural scale order cannot create it or establish physical time. |","| `metapat.axiom.12.domain_qualification` | domain-customs | Every transferred spine role remains paired with an EPAC domain name; structural recurrence alone transfers no chemical, physical, energetic, or UCNS meaning. |","| `S0` | subatomic slot |","| `S1` | atomic |","| `S2` | join/arity |","| `S3` | embed |","| `S4` | electronic state |","| `S5` | EPAC energy readout |","| `S6` | ensemble |","An EPAC join term stores named ordered slots, explicit holes and leftovers, a positive window k, exact origin state, and provenance; omission is not a representation of zero.","At each adjacent scale, join(O(S_n)) may be stored as a new identity O(S_{n+1}); recursive construction does not erase or contract the child identities that establish the join.","The EPAC tensor-role object contains every declared origin S0 through S6 and its authored adjacent-scale relations; a formula bag or subset fill cannot substitute for that structure.","EPAC join equality is join-isomorphism over typed ordered structure and state, not equality of counts, formulas, residue bags, or visible projections.","Visible and lifted phase records remain distinct EPAC objects when their declared identities or coordinates differ.","S5 energy readout is an EPAC-qualified functional of occupancy and is not a fourth circle, a universal energy primitive, or evidence of physical energy coupling.","typed origin O(S_n)","-> named ordered slots including holes and leftovers","-> authored adjacent-scale join","-> new identity O(S_{n+1})","-> preserved source identities and provenance","-> complete S0-through-S6 relational structure","- The spine roles Thing, Boundary, State, Simplex, Tensor, and Scalar may label their declared EPAC counterparts; Transformation and Time require their stated result and occurrence evidence, and this application grants no Vector binding.","- A bounded lower-scale whole may participate in an authored adjacent-scale relation and the joined whole may receive a new identity.","- Ordered constituent identity, explicit holes and leftovers, state properties, separately identified readouts, and provenance may remain addressable across recursive construction.","- Exact deterministic serialization and replay may test whether an EPAC implementation preserves the licensed distinctions.","- Static bearing inferred from slot-facing signs remains an EPAC readout; no Vector role is licensed without a separately established state-altering operation and measured resulting change.","- This license does not transfer a UCNS coordinate, phase law, topology, theorem status, gonol identity, or implementation into EPAC.","- EPAC phase is not chemistry phase, and visible or lifted coordinates acquire no physical meaning merely because they are stored.","- S5 naming does not establish energy coupling, conservation, entropy, force, field, or empirical physical validity.","- Shared shape, count, phase, vocabulary, or geometry does not establish ancestry or authorize an inferred join.","- A formula bag, residue collection, contracted cardinality, subset fill, or gravity-point registry is not licensed as the multi-origin tensor-role object.","- METAPAT licensing does not validate EPAC implementation correctness, molecular geometry, production suitability, or external-domain truth.","Can an EPAC-owned constructor preserve a typed, ordered, provenance-bearing S0-through-S6 join tree such that every stored field has both a METAPAT spine role and an EPAC domain name, while bags remain explicitly non-structural sidecars?","METAPAT owns this exact semantic license and its catalog bindings. EPAC owns the schema, constructor, equality, serialization, replay, and domain evidence. UCNS retains any UCNS law and EDCM retains measurement authority. Passing deterministic tests establishes contract conformance only, not chemical, physical, empirical, or production validity.","1. The EPAC schema must represent every scale S0 through S6, a new identity at each recursive join, named ordered slots, positive k, explicit holes and leftovers, exact state properties, separately identified readouts, and provenance.","2. Closed-schema parsing must reject a bag presented as a tree, missing scales, inferred skip-scale joins, omitted holes, duplicate identities, unlicensed fields, and malformed provenance.","3. Join-isomorphism tests must distinguish two trees with the same counts but different typed ordered structure and must keep visible and lifted records distinct.","4. A deterministic public fixture and replay receipt must bind the exact METAPAT application identity and permit independent recovery without private construction state.","5. Existing EPAC molecular-shape falsification must remain unchanged; subsequent comparisons may compare S3 tree to S3 tree only.","6. State and scalar evidence must identify the origin and metricable property separately from each readout and its measurement rule; storing a phase or capacity value does not establish observation.","7. Transformation evidence must identify the affected thing, named property, before and after states, resulting difference, and the join action responsible; an action record or new identity alone is insufficient.","8. Time evidence must establish occurrence order for at least two evidenced transformations independently of record sorting, serialization, scale labels, or assigned sequence numbers; simultaneous or unordered changes do not qualify.","Whether the typed multi-origin representation supports a useful native private operation that its public projection cannot efficiently reproduce remains unestablished.","No chemical or physical interpretation of EPAC phase, bearing, capacity, or S5 occupancy is licensed by this application.","Whether any later UCNS correspondence is useful must be licensed and tested separately without rewriting UCNS law."],"title":"EPAC Typed Multi-Origin Join Terms","transfers":["The spine roles Thing, Boundary, State, Simplex, Tensor, and Scalar may label their declared EPAC counterparts; Transformation and Time require their stated result and occurrence evidence, and this application grants no Vector binding.","A bounded lower-scale whole may participate in an authored adjacent-scale relation and the joined whole may receive a new identity.","Ordered constituent identity, explicit holes and leftovers, state properties, separately identified readouts, and provenance may remain addressable across recursive construction.","Exact deterministic serialization and replay may test whether an EPAC implementation preserves the licensed distinctions."],"ucns_theorem_status_transfer":false,"ucns_topology_claim":false,"unresolved_constraints":["hmmm: Whether the typed multi-origin representation supports a useful native private operation that its public projection cannot efficiently reproduce remains unestablished.","hmmm: No chemical or physical interpretation of EPAC phase, bearing, capacity, or S5 occupancy is licensed by this application.","hmmm: Whether any later UCNS correspondence is useful must be licensed and tested separately without rewriting UCNS law."],"working_question":"Can an EPAC-owned constructor preserve a typed, ordered, provenance-bearing S0-through-S6 join tree such that every stored field has both a METAPAT spine role and an EPAC domain name, while bags remain explicitly non-structural sidecars?"} diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index 19d1992..9820fb7 100644 --- a/docs/PROVENANCE.md +++ b/docs/PROVENANCE.md @@ -108,3 +108,27 @@ event is recorded in [docs/graduation.json](graduation.json) and the byte-identi local copy of the [Stack-owned scoped receipt](https://github.com/The-Interdependency/stack/blob/1c6ec9fb85d2ead58b13d806a5c4a9bc3803c936/integration/epac/authority-transition.json). Use the release tag and SHA256SUMS to inspect the immutable bytes; use the current graduation record to inspect subsequent lifecycle standing. + +## Post-release EPAC join-term candidate — 2026-09-29 + +The typed multi-origin join-term candidate starts from EPAC commit +`1e5c999286f12221eff9870d1372203ac7935f2a` (tree +`ce7781938ff684d826bd91f475b5423bd56b146a`). It consumes the semantic +application `metapat.application.epac_join_terms` from METAPAT commit +`1cdfb09dd00a451cee30eec2e78624df8c682662` (tree +`d946a5a18b0c53fd561dcd9d68fdf36d5dd11638`) with application digest +`461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b`. +Current build/evidence doctrine is identified as skill-lib commit +`516933d98f9de376f4e498f44059043dc0d96470`. + +The deterministic receipt binds exact hashes of every non-generated feature +input because a committed file cannot contain the hash of the commit that +contains itself. The generated JSON receipt and Markdown audit are excluded +from that manifest to avoid a self-hash cycle; their containing Git commit +externally binds all bytes. UCNS is not consumed or +modified by this candidate. METAPAT retains semantic authority; EPAC owns the +schema, constructor, equality, serialization, public recovery, and evidence. +No scientific, geometry, proof, measurement, confidentiality, or release +authority transfers. + +The repaired join-term wire `0.2.0` binds the merged METAPAT license, separates stored state from identified scalar readouts, and preserves authored adjacent-scale relations without claiming Vector, Transformation, or Time. The static candidate provides no resulting state-change or actual occurrence evidence. See `docs/multi-origin-join-term-v0.md` for the explicit pre-merge API migration. diff --git a/docs/domain-claims.md b/docs/domain-claims.md index 4d08584..f042784 100644 --- a/docs/domain-claims.md +++ b/docs/domain-claims.md @@ -20,11 +20,17 @@ EPAC may construct and test relational representations involving atomic, subatom “Standing wave,” “field,” “particle,” “harmonic,” and related terms remain domain-qualified. In particular, METAPAT harmonic language does not itself mean physical vibration or frequency. +The multi-origin join-term candidate uses METAPAT spine names under the exact `metapat.application.epac_join_terms` license while retaining EPAC names for every semantic field. `S5 energy readout` means only the candidate's exact EPAC occupancy functional. `phase` means an EPAC state coordinate. Neither name imports chemistry or physics. A shared bag, count, phase, or geometry does not establish ancestry; only an authored typed join does. + ## Usage guidance Use `EPAC` as the stable project name. When a longer explanation is needed, describe the actual research scope rather than inventing a fixed acronym expansion. Stronger ontology claims require an explicit domain claim and evidence. +Use the complete join tree for exact lineage or seating replay. Treat its `legacy_bag` as a lossy migration sidecar, never as the Tensor-role structure. Public-only recovery establishes no secrecy boundary. + ## hmmm - whether standing-wave language will earn a narrower ratified EPAC ontology claim after explicit external-physics comparison - whether any fixed lexical expansion of EPAC is useful enough to ratify; none is currently required + +The repaired join-term wire `0.2.0` binds the merged METAPAT license, separates stored state from identified scalar readouts, and preserves authored adjacent-scale relations without claiming Vector, Transformation, or Time. The static candidate provides no resulting state-change or actual occurrence evidence. See `docs/multi-origin-join-term-v0.md` for the explicit pre-merge API migration. diff --git a/docs/epac-join-term-v0-audit.md b/docs/epac-join-term-v0-audit.md new file mode 100644 index 0000000..ba3ed21 --- /dev/null +++ b/docs/epac-join-term-v0-audit.md @@ -0,0 +1,90 @@ +# EPAC join-term v0 deterministic audit + +Classification: **PUBLIC_ONLY_RECOVERY_SURVIVED** + +The public receipt recovers one complete typed S0-through-S6 join tree and its exact authored lineage without constructor or private state. The legacy count bag is retained beside the tree and is demonstrably non-injective. + +## Exact authority + +- EPAC base: `1e5c999286f12221eff9870d1372203ac7935f2a` (tree `ce7781938ff684d826bd91f475b5423bd56b146a`) +- EPAC feature source manifest: `c3aad9a890e4ea42afcc8118a14c21b8ef10044ec1520a0d32f7daf668fbff0b` +- METAPAT producer: `1cdfb09dd00a451cee30eec2e78624df8c682662` (tree `d946a5a18b0c53fd561dcd9d68fdf36d5dd11638`) +- METAPAT application: `metapat.application.epac_join_terms@epac-join-terms-application-v4` +- METAPAT application digest: `461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b` +- skill-lib: `516933d98f9de376f4e498f44059043dc0d96470` (tree `a1465ddf1de9519c9abc1a97d058432ba71bb257`) +- UCNS consumed: `false`; UCNS law modified: `false` + +The containing feature commit cannot be embedded in a file inside itself. The evidence therefore binds the exact pre-feature EPAC base plus SHA-256 of every non-generated feature input; the generated receipt and audit are excluded to avoid a self-hash cycle, and the final Git commit binds all bytes externally. + +## Exact result + +- Candidate digest: `11504eb61c4952fc3da04a51d3779484194292db2c8a5c3174b3c3ea540e6442` +- Candidate JSON SHA-256: `821d35a1e42c8d22423f78fdce8449066af40392d47f3e79e5b30b8f993fcbfd` +- Evidence digest: `215844eda0b1b1f3edf95bf7b360df84deecc97382a88f777c150fa50e359839` +- Origins: `7` +- Authored joins (no Transformation or Time claim): `6` +- Slots: `18` (`6` members, `6` holes, `6` leftovers) +- Native operation: exact authored origin-lineage and named-seating replay + +## Minimal lossy-projection witness + +- Baseline digest: `11504eb61c4952fc3da04a51d3779484194292db2c8a5c3174b3c3ea540e6442` +- Reordered digest: `f74f9227a79acf84588b11d153bf66f9490d3aa18d8e899b6266c11ca3594633` +- Shared bag SHA-256: `ae6e48f3245310cefc233eb091a2cb872ac7ab0302296735d3a1b5fe9e2a2f9f` +- Bags equal: `true` +- Join-isomorphic: `false` +- Conclusion: the legacy bag cannot uniquely recover authored seating or the complete join tree. + +## Exact-instance identity projection witness + +- Renamed identity namespace: `fixture-b` +- Renamed candidate digest: `7e30d6f4942e215462254dec9723a1deeb4e71d680e2cc960f227b02240f420d` +- Bags equal: `true` +- Join-isomorphic: `true` +- Exact instance lineages equal: `false` +- Conclusion: the legacy bag does not select exact instance identity labels, while structural lineage up to alpha-renaming is unchanged. + +This is information loss. It is not a secrecy, computational-hardness, or confidentiality result. + +## Source hashes + +- `README.md`: `7e28c7c092f7551b11a03c9db7862e27ec88ff6f81167ed3d3f99db26e29a18b` +- `data/metapat-epac-join-terms-application-v4.json`: `7f3ff74394b48c2162818fad8a373d1b88547b0df73beb8a604376022dcddf07` +- `docs/PROVENANCE.md`: `b410815a47ea9a8229406241616a9dc143ff09eb8da717456c019379efa6d5ee` +- `docs/domain-claims.md`: `f6f7f517e127a517a4acccba5527692239700c128b7bf0ecef9603e8c155c911` +- `docs/multi-origin-join-term-v0.md`: `6efb441f4421edf84c22556b46e438cc2da4f51536c522d3b6556af7b1c66a39` +- `docs/research-status.md`: `1f5194f0dc44521b6dc81d9a9494c72ff385fd879b8eb2b2849824640c5c2d07` +- `docs/work-graph.json`: `4edb37ace798511ff4b5f667afd1f0a3852ce78b7335349aaa9662171de0b9e9` +- `docs/work-graphs/epac-join-term-v0.json`: `7487441ec7e3ff3203635e245fccba5d293192cef5201c657946a15788166f16` +- `epac_boundary_probe_completeness.py`: `61df1624adc2970fe2e7d9d94ee15359decd42ddb7285ac223bd46eb748541ec` +- `epac_join_term.py`: `4697da5c76d5dfc77aedc5c84c71b7175180550824b6a1a2c1bf23a8548839af` +- `pyproject.toml`: `414cdc71cdb2e0ff859fcb59a359d57a5327e6b105e0ff010a522216dcd3881f` +- `tests/test_boundary_probe_completeness.py`: `254590ac58a54979f0f05534cea357e2369577e96304dda0ae337f3114d13ac4` +- `tests/test_epac_join_term.py`: `ed060a6ef7903f178e3da6b94890f088de8b37dc7b94e6e41b4ccc15a3b7f5c8` +- `tools/generate_join_term_v0.py`: `1836734e0ba692f23f604838ed3146c811528266a341979366eff6b19447e331` + +## Explicit nonclaims + +- Vector action, resulting state Transformation, or actual Time sequence +- chemical or physical truth +- molecular geometry or shape prediction +- UCNS correspondence, topology, theorem status, or gonol identity +- energy coupling, conservation, entropy, force, or field behavior +- cryptographic secrecy, confidentiality, public-key separation, or computational hardness +- PCEA compatibility +- production suitability +- release or protocol authority + +## hmmm + +- Authored adjacency and its stored order do not establish state changes or actual occurrence order; Transformation and Time remain unlicensed for this candidate. +- Whether a useful operation exists that requires secret EPAC state and cannot be efficiently reproduced from a legitimate public projection remains unestablished; this candidate is wholly public. +- Whether exact lineage and seating replay improve an independently preregistered EPAC task remains unmeasured. +- Whether any later UCNS correspondence is useful requires a separate license and test without rewriting UCNS law. + +## Replay + +```bash +python tools/generate_join_term_v0.py --check +python -m pytest -q tests/test_epac_join_term.py +``` diff --git a/docs/multi-origin-join-term-v0.md b/docs/multi-origin-join-term-v0.md new file mode 100644 index 0000000..fd6d405 --- /dev/null +++ b/docs/multi-origin-join-term-v0.md @@ -0,0 +1,160 @@ +# EPAC multi-origin join term v0 + +Status: **native correctness candidate / representation evidence only** + +This document specifies one EPAC-owned typed multi-origin join tree. It is the working correctness prototype requested by the nomenclature handoff: METAPAT names the spine, while EPAC owns the domain object, constructor, equality, public recovery, and evidence. + +It does not amend METAPAT or UCNS. It does not repair or reinterpret the preserved `FALSIFIED` molecular-shape result. + +## Authority and customs + +The semantic license is: + +```text +application_id: metapat.application.epac_join_terms +application_version: epac-join-terms-application-v4 +application_digest: 461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b +producer repository: The-Interdependency/metapat +``` + +This candidate uses the licensed roles `Thing`, `Boundary`, `State`, `Simplex`, `Tensor`, and `Scalar`. It claims no `Vector`, `Transformation`, or `Time`: its static bearing does not alter state, and an authored join or scale position establishes neither a resulting state change nor actual occurrence order. EPAC retains the domain names and all implementation/evidence obligations. Every semantic wire path is paired with both names in `semantic_field_bindings`; coverage is derived from the live serialized shape and checked exactly. The only exempt metadata are `schema_id`, `schema_version`, `candidate_digest`, `semantic_license`, and `semantic_field_bindings` itself. The object kind and every legacy-bag leaf remain meaning-bearing and are explicitly licensed. + +Each binding also carries the exact METAPAT `catalog_module_id` selected by its `application_role`. The field's `spine_name` and the application role are two checked axes, not interchangeable labels: for example, an authored relation contributes to the `Tensor` structure under the `metapat.axiom.6.relate` license, while recursive source/target identity and provenance remain `Thing` fields under `metapat.axiom.7.emerge`. Domain qualification may accompany the six roles used here; it does not rename them. The adapter rejects any `(spine_name, application_role)` pair outside this pinned projection. The exact merged METAPAT fixture is preserved byte-for-byte in `data/metapat-epac-join-terms-application-v4.json`; generation verifies its SHA-256 and compares its catalog roles against the consumer projection. This is a pinned evidence snapshot, not an EPAC-owned semantic license. + +The license transfers no UCNS coordinate or law, chemistry-phase meaning, physical-energy interpretation, EDCM measurement validity, theorem standing, or ancestry by analogy. + +## Complete native object + +The object is a tensor-role structure indexed by all seven EPAC scales: + +| scale | EPAC domain name | +|---|---| +| S0 | subatomic slot | +| S1 | atomic | +| S2 | join/arity | +| S3 | embed | +| S4 | electronic state | +| S5 | EPAC energy readout | +| S6 | ensemble | + +There is exactly one origin at each scale in this bounded candidate. Every origin stores: + +- a new origin identity; +- its exact scale and EPAC domain name; +- exact rational phase and an explicit `visible` or `lifted` phase chart; +- positive join window `k`; +- exactly `k` named, ordered slots; +- each slot as `member`, `hole`, or `leftover`; +- an exact stored slot-facing property `facing`; +- separate phase, capacity, and slot-facing readouts, plus static bearing inferred from the ordered facing properties; +- arity and shape signature derived from the explicit seating chart; +- provenance. + +For each `n` from 0 through 5, the `S(n+1)` origin contains exactly one `member` slot referencing `S(n)`. A separately identified `AuthoredJoin` records that adjacent-scale relation in `joins`. Its `order` is the structural position S0–S1 through S5–S6, not Time evidence. No skip-scale join, shared-count ancestry, or geometric analogy is inferred. + +A zero seat is serialized as a `hole` with null participant identity, null participant scale, and facing property zero. It is never represented by an omitted key. + +## State, scalar readouts, and inferred bearing + +Ratios are `{numerator, denominator}` with an integer numerator and strictly positive integer denominator. Typed construction reduces them exactly, but wire parsing rejects unreduced forms (including `0/2`) even when the candidate digest has been recomputed. Floating point is absent. + +`phase`, `phase_chart`, `k`, and each slot’s `facing` remain stored properties. Separately computed scalar readouts name `origin_id`, `measured_property`, `rule_id`, and `value`. Phase uses `epac.readout.exact-phase.v0`; capacity uses `epac.readout.capacity.v0`; each `slots[index].facing` uses `epac.readout.slot-facing.v0`. These rules copy the declared value into an identified readout and do not establish an external observation or measurement validity. Readout metadata and values are validated against the stored state on recovery. + +Bearing rule `epac.bearing.slot-facing-sign.v0` maps each ordered slot-facing property `s` to: + +```text +-1 when s < 0 + 0 when s = 0 ++1 when s > 0 +``` + +The serialized bearing readout identifies its origin, `slots[*].facing`, the sign rule, and an ordered list of component values. Strict recovery recomputes it from the stored properties and rejects disagreement. The readout describes static signs and performs no state-altering operation; it has no Vector binding. + +## S5 energy readout + +Only S5 carries `s5_energy_readout`. Its exact candidate law is: + +```text +occupied slots / k +``` + +Its wire readout names the S5 origin, `slots[*].kind,k`, and rule `epac.readout.occupied-slots-over-k.v0`; the result is stored under `value`. Non-S5 origins store null. Here a hole is unoccupied and a member or leftover is occupied. This is an EPAC-qualified occupancy functional. It is not a fourth circle and establishes no physical energy, conservation, entropy, force, field, or coupling claim. + +## Equality + +`join_isomorphic(left, right)` compares the complete typed ordered structure after a bijective alpha-renaming of instance origin, authored-join, and leftover identities. It preserves identity equality and aliasing patterns as well as scale names, phase values and charts, named slot order, slot kinds, facing properties and identified readouts, inferred bearing, arity, shape, S5 occupancy readout, provenance labels, and authored adjacency. Recovery rejects duplicate declared object identities before comparison, so an alias cannot masquerade as an alpha-renaming. + +Therefore: + +- the same tree under a different identity namespace is isomorphic; +- two seating charts with the same counts but different named order are not isomorphic; +- visible and lifted phase records remain distinct; +- a bag of counts cannot pass as the tree. + +## Native operation and lossy public projection + +The native typed tree supports exact authored lineage replay: for any present target origin, `trace_origin_lineage` returns the unique stored sequence from S0 through that target. Named seating is available at every step. + +The legacy projection is deliberately typed `bag` and kept beside the tree. It retains only slot counts by scale and total counts by slot kind. It is excluded from the tensor-role object and from join-isomorphism. The minimal committed non-isomorphic witness keeps the identity namespace fixed and changes only S2 seating order, proving that the bag cannot uniquely reproduce authored seating or the complete tree. A separate alpha-renamed witness keeps structure and seating fixed: its bag is identical and it remains join-isomorphic, while its exact instance lineage identifiers differ. Thus the bag does not select instance labels; structural lineage shape up to alpha-renaming is unchanged. This is information loss, not a computational-hardness or confidentiality claim. + +The complete canonical JSON receipt is public. Recovery from those public bytes reconstructs the tree and lineage without invoking the constructor or consulting private state. No secret key, cryptographic private structure, or confidentiality property exists here. + +## Wire and fail-closed recovery + +The canonical wire is sorted-key compact UTF-8 JSON under: + +```text +schema_id: epac.multi-origin-join-tree +schema_version: 0.2.0 +object_kind: multi-origin-join-tree +``` + +`candidate_digest` is SHA-256 of the canonical payload excluding only that digest field. Strict recovery rejects duplicate JSON keys, unknown or missing fields, invalid or unreduced ratios, omitted explicit slot state, duplicate identities, incomplete scales, invalid k, noncontiguous slot order, malformed holes, skip-scale references, inconsistent joins, changed semantic bindings, changed license identity, bag masquerading, and digest mismatch. Origin, authored-join, and leftover object identities occupy one globally unique declared-object scope. A member `participant_id` is a reference to an already declared origin and therefore intentionally repeats that origin identity; it does not declare a new object. + +Mapping parsing also requires the supplied payload to equal the canonical serialization of the recovered object before its digest is accepted; normalization cannot hide changed wire data. JSON recovery additionally compares the original text with the canonical tree serialization. Whitespace, a trailing newline, reordered keys, alternate numeric spellings such as `-0`, and unnecessary Unicode escapes are rejected even if they parse to the same values. The newline terminating the outer evidence receipt is not part of the embedded candidate wire. + +Canonical evidence: + +```text +data/epac-join-term-v0-receipt.json +docs/epac-join-term-v0-audit.md +``` + +Regenerate and verify: + +```bash +python tools/generate_join_term_v0.py +python tools/generate_join_term_v0.py --check +python -m pytest -q tests/test_epac_join_term.py +``` + +The generator works directly from an uninstalled checkout, including from a different working directory. `--root` may only identify the checkout containing that generator. Imported generation during installed-artifact replay additionally requires the loaded module’s import-time source hash to match the archived source. Identity failures occur before any output is written. + +## Migration rule + +Wire version `0.2.0` deliberately rejects `0.1.0` candidates carrying the superseded license. The pre-merge candidate API `JoinTransformation`/`transformations`/`transformation_id`/`sequence` becomes `AuthoredJoin`/`joins`/`join_id`/`order`, and `facing_scalar` becomes stored `facing` plus separate readouts. All six authored relations, seven origins, exact state, seating, provenance, equality, and public recovery mechanisms are retained. No historical state-change or temporal evidence is fabricated during migration. The released package version is unchanged; this is not a new release. + +Existing formula/count collections remain explicitly typed bags. The candidate constructor writes the tree once and stores the lossy bag beside it. A bag is never promoted to a tree by inference. Molecular comparisons remain `FALSIFIED`; any future structural comparison must compare an S3 tree to an S3 tree under a separately preregistered question. + +## Nonclaims + +This candidate does not establish: + +- Vector action, resulting Transformation, or actual Time sequence; +- chemical or physical truth; +- molecular geometry or shape prediction; +- UCNS correspondence, topology, theorem status, or gonol identity; +- energy coupling, conservation, entropy, force, or field behavior; +- a cryptographic secret, confidentiality, public-key separation, or computational hardness; +- PCEA compatibility; +- production suitability; +- release or protocol authority. + +## hmmm + +- Transformation requires an identified thing and property, before/after state, resulting difference, and responsible action. Time additionally requires at least two actual sequential changes with occurrence evidence independent of sorting or scale labels. This static candidate supplies neither; those uses remain unlicensed. + +- Whether a useful operation exists that requires secret EPAC state and cannot be efficiently reproduced from a legitimate public projection remains unestablished; this candidate is wholly public. +- Whether exact lineage and seating replay improve an independently preregistered EPAC task remains unmeasured. +- Whether any later UCNS correspondence is useful requires a separate license and test without rewriting UCNS law. +- Nomenclature is not decoration: it determines which domain object a consumer is permitted to open. diff --git a/docs/research-status.md b/docs/research-status.md index c02586e..67b5bb1 100644 --- a/docs/research-status.md +++ b/docs/research-status.md @@ -15,6 +15,10 @@ This means the current construction is not evidence that EPAC predicts empirical The extraction carries atomic, periodic, molecular, dimensional-arity, Public Gonol, subatomic, comparison, receipt, and regression-test surfaces from the pinned stack source. +The post-v0.1.0 typed multi-origin join-term candidate preserves one explicit origin at every S0-through-S6 scale, adjacent authored joins, named ordered slots, holes, leftovers, exact state, inferred bearing, and provenance. Deterministic public bytes recover the complete tree and exact origin lineage without constructor state. Its legacy bag preserves counts but loses seating and is not the tree. + +This is representation correctness only. It does not provide a cryptographic private structure, confidentiality, chemical or physical phase, physical energy, UCNS correspondence, molecular geometry, or evidence that the representation improves an external task. + ## Usage guidance Treat constructor success as reproducibility of a declared structure. Treat a comparison standing only as evidence for the exact preregistered claim it scores. Do not infer physics or chemistry validation from either. @@ -25,3 +29,7 @@ Treat constructor success as reproducibility of a declared structure. Treat a co - a non-imported mapping from EPAC relational structure to empirical molecular angles - standing-wave/field ontology beyond provisional modeling language - release packaging and downstream artifact consumption +- whether exact lineage and seating improve a separately preregistered EPAC task +- whether any genuinely secret native structure enables a useful operation that a legitimate public projection cannot efficiently reproduce + +The repaired join-term wire `0.2.0` binds the merged METAPAT license, separates stored state from identified scalar readouts, and preserves authored adjacent-scale relations without claiming Vector, Transformation, or Time. The static candidate provides no resulting state-change or actual occurrence evidence. See `docs/multi-origin-join-term-v0.md` for the explicit pre-merge API migration. diff --git a/docs/work-graph.json b/docs/work-graph.json index 6af0bc3..7b78456 100644 --- a/docs/work-graph.json +++ b/docs/work-graph.json @@ -4,7 +4,8 @@ "authority_transfer": false, "hmmm": [ "Geometry ratification, canonicality/compositionality, PCEA application, and unmeasured operation effects remain unresolved.", - "No PostgreSQL fresh-making acceptance is asserted." + "No PostgreSQL fresh-making acceptance is asserted.", + "Compatibility beyond the exact METAPAT application producer recorded in docs/work-graphs/epac-join-term-v0.json is unestablished and requires an explicit rebind." ], "measurement_status_transfer": false, "proof_status_transfer": false, @@ -25,8 +26,8 @@ }, { "authority": "semantic authority for METAPAT application terms", - "commit": "d6699e21b11c8f8394998efc34a468e2d6efc8b0", - "relation": "semantic source consumed by EPAC", + "commit": "1cdfb09dd00a451cee30eec2e78624df8c682662", + "relation": "semantic source consumed by EPAC; join-term v0 binds metapat.application.epac_join_terms@epac-join-terms-application-v4 with digest 461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b and delegates its scoped graph to docs/work-graphs/epac-join-term-v0.json", "repository": "The-Interdependency/metapat" }, { @@ -38,5 +39,5 @@ ], "schema": "the-interdependency.stack-manifest", "version": "1.0.0", - "work_graph_sha256": "9e079e25fa2021f83a17171db4be157b43aced0aa6fbbf4729fc9ce71ff4f4f3" + "work_graph_sha256": "3359501225382845edf34312b4acd864a51a8204617016bae8979f449ab4a7b5" } diff --git a/docs/work-graphs/epac-join-term-v0.json b/docs/work-graphs/epac-join-term-v0.json new file mode 100644 index 0000000..0515906 --- /dev/null +++ b/docs/work-graphs/epac-join-term-v0.json @@ -0,0 +1,77 @@ +{ + "active_frontier": [ + "Measure whether exact lineage and named seating improve a separately preregistered EPAC task.", + "Identify, if one exists, a native secret-dependent operation whose legitimate public projection does not efficiently reproduce it." + ], + "authority": { + "does_not_own": [ + "METAPAT root or catalog authority", + "UCNS geometry, topology, theorem, or gonol authority", + "EDCM measurement authority", + "external physics or chemistry truth" + ], + "non_transfer": [ + "METAPAT licenses semantic roles but does not validate the EPAC implementation", + "typed join-tree correctness does not transfer chemistry, physics, energy-coupling, molecular-shape, or production validity", + "shared vocabulary, counts, or geometry do not establish ancestry" + ], + "owns": [ + "EPAC join-term schema, constructor, equality, serialization, recovery, and deterministic evidence", + "classification of the legacy bag as a lossy non-structural sidecar" + ] + }, + "blocked": [], + "cross_repository_relations": [ + { + "authority_transfer": false, + "identity": "metapat.application.epac_join_terms@epac-join-terms-application-v4#461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b", + "producer_commit": "1cdfb09dd00a451cee30eec2e78624df8c682662", + "producer_tree": "d946a5a18b0c53fd561dcd9d68fdf36d5dd11638", + "relation": "exact semantic license for spine-to-EPAC role names", + "repository": "The-Interdependency/metapat" + }, + { + "authority_transfer": false, + "identity": "516933d98f9de376f4e498f44059043dc0d96470", + "relation": "current build, evidence, risk-boundary, documentation, test, and work-graph doctrine", + "repository": "The-Interdependency/skill-lib" + } + ], + "delivered": [ + { + "boundary": "complete public representation; no secret or confidentiality claim", + "status": "implemented candidate", + "surface": "typed S0-through-S6 join tree" + }, + { + "boundary": "counts only; excluded from tensor and equality", + "status": "preserved migration sidecar", + "surface": "legacy bag projection" + }, + { + "boundary": "representation correctness only; molecular-shape evidence remains FALSIFIED", + "status": "deterministic public-only recovery evidence", + "surface": "canonical JSON receipt and Markdown audit" + } + ], + "hmmm": [ + "No native secret-dependent useful operation is established.", + "The downstream value of exact lineage and seating replay remains unmeasured.", + "No UCNS correspondence is selected or licensed.", + "Static bearing and authored adjacency supply no state-changing Vector, resulting Transformation, or actual temporal sequence. These roles are not claimed." + ], + "machine_entrypoints": { + "candidate": "epac_join_term.py", + "generator": "tools/generate_join_term_v0.py", + "human_specification": "docs/multi-origin-join-term-v0.md", + "producer_fixture": "data/metapat-epac-join-terms-application-v4.json", + "public_receipt": "data/epac-join-term-v0-receipt.json", + "tests": "tests/test_epac_join_term.py" + }, + "repository": "The-Interdependency/epac", + "status": { + "current_claim": "A strict typed S0-through-S6 join tree preserves exact authored adjacency, named ordered seating, explicit holes and leftovers, exact state, separately identified scalar readouts, static inferred bearing, and provenance. Its count bag is a provably lossy sidecar. Public receipt bytes recover the complete candidate without private state.", + "state": "native correctness candidate" + }, + "version": "0.1.0" +} diff --git a/epac_boundary_probe_completeness.py b/epac_boundary_probe_completeness.py index 0199d0f..30cba2b 100644 --- a/epac_boundary_probe_completeness.py +++ b/epac_boundary_probe_completeness.py @@ -126,6 +126,7 @@ "viz/__main__.py", "epac_public_gonol.py", "epac_dimensional_arity.py", + "epac_join_term.py", "epac_periodic.py", "epac_molecular.py", "epac_cross_scale_closure.py", @@ -355,6 +356,11 @@ def _declared_operations() -> tuple[dict[str, str], ...]: def _classify_operation(module: str, name: str) -> str: + if module == "epac_join_term": + # The join-tree candidate is not part of the frozen 27-state boundary + # quotient surface. Its possible boundary relevance remains unresolved + # until a separately preregistered measurement selects an operation. + return AMBIGUOUS if module in { "epac_atomic", "epac_atomic_derivation", diff --git a/epac_join_term.py b/epac_join_term.py new file mode 100644 index 0000000..3f70bb9 --- /dev/null +++ b/epac_join_term.py @@ -0,0 +1,1088 @@ +"""Typed S0-through-S6 EPAC multi-origin join terms. + +This module is a representation and correctness prototype. It preserves an +authored recursive join tree, exact state properties and separate readouts, named ordered slots, holes, +leftovers, inferred bearing, and provenance. It does not establish chemistry, +physics, energy coupling, UCNS correspondence, or production suitability. + +Usage guidance +-------------- + from epac_join_term import construct_epac_join_tree, trace_origin_lineage + + tree = construct_epac_join_tree() + assert trace_origin_lineage(tree, tree.origins[-1].origin_id)[0].endswith("S0") +""" + +# === MODULE_BUILD === +# id: epac_multi_origin_join_term_v0 +# module_name: epac_join_term +# module_kind: schema +# summary: strict public prototype for a typed ordered provenance-bearing S0-through-S6 EPAC join tree and its lossy legacy bag projection +# owner: The Interdependency/epac +# public_surface: ExactRatio, JoinSlot, ScaleOrigin, AuthoredJoin, LegacyBag, EPACJoinTree, construct_epac_join_tree, recover_epac_join_tree, join_isomorphic, trace_origin_lineage, legacy_bag_projection +# internal_surface: canonical JSON, strict validators, structural signature, fixture declarations +# auth_boundary: none +# storage_boundary: read own source for evidence identity and caller-provided public wire; no secrets or mutable persistence +# network_boundary: none +# user_data_boundary: public deterministic research fixture only +# admin_only: false +# tests: tests.test_epac_join_term +# rollout: research candidate and packaged deterministic receipt; not a release or protocol profile +# rollback: remove this candidate module, its receipt, tests, and documentation without changing existing EPAC constructors or falsification evidence +# requires: metapat.application.epac_join_terms@epac-join-terms-application-v4 +# since: 2026-09-29 +# unresolved: no native secret operation, external-domain interpretation, or useful physical coupling is established +# === END MODULE_BUILD === + +# === DOCS === +# id: epac_multi_origin_join_term_v0_docs +# summary: specifies the typed join tree, role licensing, exact wire, equality, lossy bag projection, recovery, and nonclaims +# audience: developer, agent, independent replayer, domain reviewer +# source: docs/multi-origin-join-term-v0.md +# covers: epac_multi_origin_join_term_v0, data/epac-join-term-v0-receipt.json, tools/generate_join_term_v0.py +# status: candidate +# === END DOCS === + +# === CAPABILITIES === +# id: epac_multi_origin_join_recovery +# summary: recovers and validates the complete typed S0-through-S6 join tree from public deterministic JSON and replays exact authored lineage +# exposes: epac_join_term.recover_epac_join_tree, epac_join_term.trace_origin_lineage +# inputs: public UTF-8 JSON bytes under epac.multi-origin-join-tree version 0.2.0 +# outputs: immutable validated join tree and exact S0-through-target origin lineage +# boundaries: auth:none, storage:none, network:none, user_data:public fixture only +# === END CAPABILITIES === + +# === BOUNDARIES === +# id: epac_multi_origin_join_term_v0_boundary +# summary: representation evidence only; no confidentiality, cryptographic private structure, chemistry phase, physical energy, UCNS law, molecular-shape repair, PCEA compatibility, or release authority +# auth_boundary: none +# storage_boundary: no mutable persistence +# network_boundary: none +# user_data_boundary: public deterministic research fixture only +# admin_only: false +# === END BOUNDARIES === + +# === CONTRACTS === +# id: epac_join_tree_complete_scale_tensor +# given: an EPAC join tree is constructed or recovered +# then: it contains exactly one typed origin at every scale S0 through S6 and exactly six authored adjacent-scale joins +# class: schema +# +# id: epac_join_tree_explicit_boundary +# given: an origin boundary is serialized +# then: positive k, named ordered slots, holes, leftovers, arity, and shape signature are explicit and mutually consistent +# class: correctness +# +# id: epac_join_tree_inferred_bearing +# given: stored slot-facing properties are present +# then: ordered bearing components are derived by the declared sign rule and a mismatching serialized bearing is rejected +# class: correctness +# +# id: epac_join_tree_recursive_identity +# given: an origin above S0 is inspected +# then: its member slot references only the immediately preceding origin and its authored join records exact source and new target identity +# class: construction +# +# id: epac_join_tree_s5_energy_qualified +# given: the S5 origin is serialized +# then: its exact occupancy functional equals occupied slots over k while every non-S5 origin carries no S5 readout +# class: boundary_contract +# +# id: epac_join_tree_semantic_customs +# given: any semantic wire field is inspected +# then: its schema path is paired with an exact METAPAT spine name, EPAC domain name, and application role under the pinned license +# class: provenance +# +# id: epac_join_tree_join_isomorphism +# given: two trees are compared +# then: bijectively alpha-renamed identities may be isomorphic while identity aliasing, named order, holes, leftovers, state, phase chart, shape, or authored relation changes remain distinct +# class: correctness +# +# id: epac_join_tree_bag_is_lossy +# given: two same-count non-isomorphic join trees +# then: their legacy bag projections may be equal but the bag cannot be parsed as or uniquely recover the tree +# class: safety +# +# id: epac_join_tree_public_recovery +# given: only canonical public receipt bytes +# then: strict recovery verifies the digest and reconstructs exact tree state and lineage without constructor or private state +# class: evidence +# +# id: epac_join_tree_strict_rejection +# given: duplicate keys, unknown or missing fields, invalid modulus, omitted explicit slot state, duplicate identities, skip-scale references, or inconsistent joins +# then: recovery fails closed +# class: safety +# +# id: epac_join_tree_deterministic_serialization +# given: the same typed tree is serialized repeatedly +# then: canonical bytes and candidate digest are identical +# class: evidence +# +# id: epac_join_tree_existing_falsification_preserved +# given: this candidate is added to EPAC +# then: the preregistered molecular-shape standings remain FALSIFIED and are neither deleted nor reinterpreted +# class: boundary_contract +# +# id: epac_join_tree_readout_boundary +# given: stored origin properties and derived readouts +# then: each readout identifies its origin, measured property, and rule and cannot replace or alter the stored state +# class: boundary_contract +# +# id: epac_join_tree_conditional_roles +# given: static bearing and authored adjacent-scale joins without result or occurrence evidence +# then: no field claims Vector, Transformation, or Time and the producer role projection matches the exact merged fixture +# class: provenance +# +# id: epac_join_tree_canonical_wire +# given: an equivalent unreduced ratio or other normalized wire value with an old or recomputed digest +# then: recovery rejects it rather than silently normalizing the digest-bearing payload +# class: safety +# === END CONTRACTS === + +from __future__ import annotations + +import hashlib +import json +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from fractions import Fraction +from pathlib import Path +from types import MappingProxyType +from typing import Any + +_MODULE_SOURCE_SHA256 = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() + +JOIN_TREE_SCHEMA_ID = "epac.multi-origin-join-tree" +JOIN_TREE_SCHEMA_VERSION = "0.2.0" +BEARING_RULE_ID = "epac.bearing.slot-facing-sign.v0" +ADJACENT_JOIN_RELATION = "epac.join.adjacent-scale" +METAPAT_APPLICATION_ID = "metapat.application.epac_join_terms" +METAPAT_APPLICATION_VERSION = "epac-join-terms-application-v4" +METAPAT_APPLICATION_DIGEST = "461ef7e059aa65b514017683ba9b57058ee9f582bc63748fabd021cdeb660b4b" + +SCALE_DOMAIN_NAMES: tuple[str, ...] = ( + "subatomic slot", + "atomic", + "join/arity", + "embed", + "electronic state", + "EPAC energy readout", + "ensemble", +) +SCALE_IDS: tuple[str, ...] = tuple(f"S{index}" for index in range(7)) + +# Exact role-to-catalog projection from the pinned METAPAT application. An +# application role identifies the licensing clause; spine_name identifies the +# EPAC field's structural role. Those axes are related but not synonymous. +_METAPAT_APPLICATION_ROLE_MODULES = MappingProxyType({ + "customs-boundary": "metapat.root_spine", + "stored-join-term": "metapat.axiom.1.thing", + "join-boundary": "metapat.axiom.2.boundary", + "origin-state": "metapat.axiom.3.state", + "scale-origin": "metapat.axiom.4.simplex", + "multi-origin-tensor": "metapat.axiom.5.tensor", + "authored-near-join": "metapat.axiom.6.relate", + "recursive-origin": "metapat.axiom.7.emerge", + "state-metric": "metapat.axiom.9.scalar", + "join-transformation": "metapat.axiom.10.transformation", + "transformation-sequence": "metapat.axiom.11.time", + "domain-customs": "metapat.axiom.12.domain_qualification", +}) +_SPINE_NAMES = frozenset( + { + "Thing", + "Boundary", + "State", + "Simplex", + "Tensor", + "Scalar", + } +) +_APPLICATION_ROLE_ALLOWED_SPINES = MappingProxyType({ + "customs-boundary": _SPINE_NAMES, + "stored-join-term": frozenset({"Thing"}), + "join-boundary": frozenset({"Boundary"}), + "origin-state": frozenset({"State"}), + "scale-origin": frozenset({"Simplex"}), + "multi-origin-tensor": frozenset({"Tensor"}), + "authored-near-join": frozenset({"Tensor"}), + "recursive-origin": frozenset({"Thing"}), + "state-metric": frozenset({"Scalar"}), + "join-transformation": frozenset({"Transformation"}), + "transformation-sequence": frozenset({"Time"}), + "domain-customs": _SPINE_NAMES, +}) + +# Exact metadata subtrees identify the wire or carry the semantic license and +# are not domain observations. Every other serialized field path is licensed +# here; _semantic_wire_field_paths derives that coverage from live wire data. +_WIRE_METADATA_FIELDS = frozenset( + { + "candidate_digest", + "schema_id", + "schema_version", + "semantic_field_bindings", + "semantic_license", + } +) +_SEMANTIC_FIELD_BINDING_ROWS = { + "tree.object_kind": {"spine_name": "Thing", "domain_name": "multi-origin join-tree kind", "application_role": "stored-join-term"}, + "origin.origin_id": {"spine_name": "Thing", "domain_name": "origin identity", "application_role": "stored-join-term"}, + "origin.scale": {"spine_name": "Simplex", "domain_name": "EPAC scale origin", "application_role": "scale-origin"}, + "origin.domain_name": {"spine_name": "Simplex", "domain_name": "EPAC scale name", "application_role": "domain-customs"}, + "origin.phase": {"spine_name": "State", "domain_name": "EPAC exact phase", "application_role": "origin-state"}, + "origin.phase_chart": {"spine_name": "State", "domain_name": "visible or lifted phase record", "application_role": "origin-state"}, + "origin.k": {"spine_name": "Boundary", "domain_name": "join window k", "application_role": "join-boundary"}, + "origin.slots": {"spine_name": "Boundary", "domain_name": "named ordered join slots", "application_role": "join-boundary"}, + "slot.name": {"spine_name": "Boundary", "domain_name": "slot name", "application_role": "join-boundary"}, + "slot.order": {"spine_name": "Boundary", "domain_name": "slot order", "application_role": "join-boundary"}, + "slot.kind": {"spine_name": "Boundary", "domain_name": "member, hole, or leftover", "application_role": "join-boundary"}, + "slot.participant_id": {"spine_name": "Thing", "domain_name": "member or leftover identity", "application_role": "stored-join-term"}, + "slot.participant_scale": {"spine_name": "Simplex", "domain_name": "member source scale", "application_role": "scale-origin"}, + "slot.facing": {"spine_name": "State", "domain_name": "stored slot-facing property", "application_role": "origin-state"}, + "origin.bearing": {"spine_name": "Scalar", "domain_name": "ordered static slot-facing sign readouts", "application_role": "state-metric"}, + "origin.arity": {"spine_name": "Boundary", "domain_name": "occupied seating arity", "application_role": "join-boundary"}, + "origin.shape_signature": {"spine_name": "Tensor", "domain_name": "ordered seating shape", "application_role": "multi-origin-tensor"}, + "origin.s5_energy_readout": {"spine_name": "Scalar", "domain_name": "EPAC S5 occupancy functional", "application_role": "state-metric"}, + "origin.readouts": {"spine_name": "Scalar", "domain_name": "identified phase, capacity, and slot-facing readouts", "application_role": "state-metric"}, + "readout.origin_id": {"spine_name": "Thing", "domain_name": "readout source origin identity", "application_role": "stored-join-term"}, + "readout.measured_property": {"spine_name": "State", "domain_name": "named source state property", "application_role": "origin-state"}, + "readout.rule_id": {"spine_name": "Thing", "domain_name": "EPAC readout rule identity", "application_role": "domain-customs"}, + "readout.value": {"spine_name": "Scalar", "domain_name": "computed EPAC readout value", "application_role": "state-metric"}, + "origin.provenance": {"spine_name": "Thing", "domain_name": "origin provenance", "application_role": "recursive-origin"}, + "join.join_id": {"spine_name": "Thing", "domain_name": "authored join record identity", "application_role": "stored-join-term"}, + "join.order": {"spine_name": "Tensor", "domain_name": "authored adjacent-scale relation position", "application_role": "authored-near-join"}, + "join.source_origin_id": {"spine_name": "Thing", "domain_name": "source origin identity", "application_role": "recursive-origin"}, + "join.target_origin_id": {"spine_name": "Thing", "domain_name": "new target origin identity", "application_role": "recursive-origin"}, + "join.relation": {"spine_name": "Tensor", "domain_name": "authored adjacent-scale join", "application_role": "authored-near-join"}, + "join.provenance": {"spine_name": "Thing", "domain_name": "authored join provenance", "application_role": "recursive-origin"}, + "tree.origins": {"spine_name": "Tensor", "domain_name": "S0-through-S6 origin tensor", "application_role": "multi-origin-tensor"}, + "tree.joins": {"spine_name": "Tensor", "domain_name": "ordered authored adjacent-scale relations", "application_role": "authored-near-join"}, + "tree.legacy_bag": {"spine_name": "Thing", "domain_name": "non-structural legacy bag sidecar", "application_role": "domain-customs"}, + "legacy_bag.kind": {"spine_name": "Thing", "domain_name": "legacy sidecar kind", "application_role": "domain-customs"}, + "legacy_bag.excluded_from_tensor": {"spine_name": "Thing", "domain_name": "tensor-exclusion marker", "application_role": "domain-customs"}, + "legacy_bag.scale_slot_counts": {"spine_name": "Thing", "domain_name": "lossy slot-count map by scale", "application_role": "domain-customs"}, + "legacy_bag.slot_kind_counts": {"spine_name": "Thing", "domain_name": "lossy slot-count map by kind", "application_role": "domain-customs"}, +} +SEMANTIC_FIELD_BINDINGS: Mapping[str, Mapping[str, str]] = MappingProxyType( + { + key: MappingProxyType(value) + for key, value in _SEMANTIC_FIELD_BINDING_ROWS.items() + } +) +del _SEMANTIC_FIELD_BINDING_ROWS + +REQUIRED_SEMANTIC_FIELD_PATHS = frozenset(SEMANTIC_FIELD_BINDINGS) +SLOT_KINDS = frozenset({"member", "hole", "leftover"}) +PHASE_CHARTS = frozenset({"visible", "lifted"}) + + +def _canonical_json(value: Any) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def _sha256(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _strict_keys(data: Mapping[str, Any], expected: set[str], name: str) -> None: + unknown = set(data) - expected + missing = expected - set(data) + if unknown: + raise ValueError(f"unknown {name} fields: {sorted(unknown)!r}") + if missing: + raise ValueError(f"missing {name} fields: {sorted(missing)!r}") + + +def _semantic_wire_field_paths(data: Mapping[str, Any]) -> frozenset[str]: + """Derive every meaning-bearing schema path from one serialized tree.""" + if not isinstance(data, Mapping): + raise ValueError("semantic wire coverage requires an object") + paths = { + f"tree.{key}" + for key in data + if key not in _WIRE_METADATA_FIELDS + } + origins = data.get("origins", ()) + for origin in origins if isinstance(origins, (list, tuple)) else (): + if not isinstance(origin, Mapping): + continue + paths.update(f"origin.{key}" for key in origin) + slots = origin.get("slots", ()) + for slot in slots if isinstance(slots, (list, tuple)) else (): + if isinstance(slot, Mapping): + paths.update(f"slot.{key}" for key in slot) + readouts = origin.get("readouts", ()) + records = list(readouts) if isinstance(readouts, (list, tuple)) else [] + records.extend((origin.get("bearing"), origin.get("s5_energy_readout"))) + for readout in records: + if isinstance(readout, Mapping): + paths.update(f"readout.{key}" for key in readout) + joins = data.get("joins", ()) + for join in ( + joins if isinstance(joins, (list, tuple)) else () + ): + if isinstance(join, Mapping): + paths.update(f"join.{key}" for key in join) + legacy_bag = data.get("legacy_bag") + if isinstance(legacy_bag, Mapping): + paths.update(f"legacy_bag.{key}" for key in legacy_bag) + return frozenset(paths) + + +def _text(value: Any, name: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} must be a non-empty string") + return value + + +def _integer(value: Any, name: str, *, minimum: int | None = None) -> int: + if isinstance(value, bool) or not isinstance(value, int): + raise ValueError(f"{name} must be an integer") + if minimum is not None and value < minimum: + raise ValueError(f"{name} must be at least {minimum}") + return value + + +def _optional_text(value: Any, name: str) -> str | None: + if value is None: + return None + return _text(value, name) + + +def _text_tuple(value: Any, name: str, *, minimum: int = 0) -> tuple[str, ...]: + if not isinstance(value, (list, tuple)): + raise ValueError(f"{name} must be an array") + result = tuple(_text(item, f"{name} item") for item in value) + if len(result) < minimum: + raise ValueError(f"{name} must contain at least {minimum} items") + return result + + +def _reject_duplicate_keys(pairs: Sequence[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +@dataclass(frozen=True, slots=True) +class ExactRatio: + numerator: int + denominator: int + + def __post_init__(self) -> None: + numerator = _integer(self.numerator, "ratio numerator") + denominator = _integer(self.denominator, "ratio denominator", minimum=1) + reduced = Fraction(numerator, denominator) + object.__setattr__(self, "numerator", reduced.numerator) + object.__setattr__(self, "denominator", reduced.denominator) + + def to_dict(self) -> dict[str, int]: + return {"numerator": self.numerator, "denominator": self.denominator} + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> ExactRatio: + if not isinstance(data, Mapping): + raise ValueError("exact ratio must be an object") + _strict_keys(data, {"numerator", "denominator"}, "exact-ratio") + ratio = cls( + _integer(data["numerator"], "ratio numerator"), + _integer(data["denominator"], "ratio denominator", minimum=1), + ) + if ratio.to_dict() != data: + raise ValueError("exact ratio must be reduced on the wire") + return ratio + + +@dataclass(frozen=True, slots=True) +class JoinSlot: + name: str + order: int + kind: str + participant_id: str | None + participant_scale: str | None + facing: int + + def __post_init__(self) -> None: + _text(self.name, "slot name") + _integer(self.order, "slot order", minimum=0) + if self.kind not in SLOT_KINDS: + raise ValueError(f"unsupported slot kind {self.kind!r}") + _integer(self.facing, "slot facing") + if self.kind == "hole": + if self.participant_id is not None or self.participant_scale is not None: + raise ValueError("hole must store null participant identity and scale") + if self.facing != 0: + raise ValueError("hole facing must be zero") + elif self.kind == "member": + _text(self.participant_id, "member participant_id") + if self.participant_scale not in SCALE_IDS: + raise ValueError("member participant_scale must be S0 through S6") + else: + _text(self.participant_id, "leftover participant_id") + if self.participant_scale is not None: + raise ValueError("leftover participant_scale must be null") + + @property + def bearing_component(self) -> int: + return (self.facing > 0) - (self.facing < 0) + + def to_dict(self) -> dict[str, Any]: + return { + "name": self.name, + "order": self.order, + "kind": self.kind, + "participant_id": self.participant_id, + "participant_scale": self.participant_scale, + "facing": self.facing, + } + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> JoinSlot: + if not isinstance(data, Mapping): + raise ValueError("join slot must be an object") + _strict_keys( + data, + {"name", "order", "kind", "participant_id", "participant_scale", "facing"}, + "join-slot", + ) + return cls( + name=_text(data["name"], "slot name"), + order=_integer(data["order"], "slot order", minimum=0), + kind=_text(data["kind"], "slot kind"), + participant_id=_optional_text(data["participant_id"], "participant_id"), + participant_scale=_optional_text(data["participant_scale"], "participant_scale"), + facing=_integer(data["facing"], "slot facing"), + ) + + +@dataclass(frozen=True, slots=True) +class ScaleOrigin: + origin_id: str + scale: str + domain_name: str + phase: ExactRatio + phase_chart: str + k: int + slots: tuple[JoinSlot, ...] + provenance: tuple[str, ...] + + def __post_init__(self) -> None: + _text(self.origin_id, "origin_id") + if self.scale not in SCALE_IDS: + raise ValueError("origin scale must be S0 through S6") + expected_name = SCALE_DOMAIN_NAMES[int(self.scale[1:])] + if self.domain_name != expected_name: + raise ValueError(f"{self.scale} domain_name must be {expected_name!r}") + if not isinstance(self.phase, ExactRatio): + raise ValueError("origin phase must be ExactRatio") + if self.phase_chart not in PHASE_CHARTS: + raise ValueError("phase_chart must be visible or lifted") + _integer(self.k, "origin k", minimum=1) + slots = tuple(self.slots) + if len(slots) != self.k: + raise ValueError("origin k must equal the explicit slot count") + if any(not isinstance(slot, JoinSlot) for slot in slots): + raise ValueError("origin slots must contain JoinSlot values") + if tuple(slot.order for slot in slots) != tuple(range(self.k)): + raise ValueError("origin slot order must be explicit, unique, and contiguous") + if len({slot.name for slot in slots}) != len(slots): + raise ValueError("origin slot names must be unique") + provenance = _text_tuple(self.provenance, "origin provenance", minimum=1) + object.__setattr__(self, "slots", slots) + object.__setattr__(self, "provenance", provenance) + + @property + def bearing(self) -> tuple[int, ...]: + return tuple(slot.bearing_component for slot in self.slots) + + @property + def arity(self) -> int: + return sum(slot.kind != "hole" for slot in self.slots) + + @property + def shape_signature(self) -> tuple[str, ...]: + return tuple(f"{slot.order}:{slot.name}:{slot.kind}" for slot in self.slots) + + @property + def s5_energy_readout(self) -> ExactRatio | None: + if self.scale != "S5": + return None + return ExactRatio(self.arity, self.k) + + def _readout(self, measured_property: str, rule_id: str, value: Any) -> dict[str, Any]: + return {"origin_id": self.origin_id, "measured_property": measured_property, + "rule_id": rule_id, "value": value} + + @property + def readouts(self) -> list[dict[str, Any]]: + return [ + self._readout("phase", "epac.readout.exact-phase.v0", self.phase.to_dict()), + self._readout("k", "epac.readout.capacity.v0", self.k), + *[self._readout(f"slots[{slot.order}].facing", "epac.readout.slot-facing.v0", slot.facing) + for slot in self.slots], + ] + + def to_dict(self) -> dict[str, Any]: + return { + "origin_id": self.origin_id, + "scale": self.scale, + "domain_name": self.domain_name, + "phase": self.phase.to_dict(), + "phase_chart": self.phase_chart, + "k": self.k, + "slots": [slot.to_dict() for slot in self.slots], + "bearing": self._readout("slots[*].facing", BEARING_RULE_ID, list(self.bearing)), + "readouts": self.readouts, + "arity": self.arity, + "shape_signature": list(self.shape_signature), + "s5_energy_readout": ( + self._readout("slots[*].kind,k", "epac.readout.occupied-slots-over-k.v0", + self.s5_energy_readout.to_dict()) + if self.s5_energy_readout is not None else None + ), + "provenance": list(self.provenance), + } + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> ScaleOrigin: + if not isinstance(data, Mapping): + raise ValueError("scale origin must be an object") + expected = { + "origin_id", "scale", "domain_name", "phase", "phase_chart", "k", "slots", + "bearing", "readouts", "arity", "shape_signature", "s5_energy_readout", "provenance", + } + _strict_keys(data, expected, "scale-origin") + if not isinstance(data["slots"], list): + raise ValueError("origin slots must be an array") + origin = cls( + origin_id=_text(data["origin_id"], "origin_id"), + scale=_text(data["scale"], "origin scale"), + domain_name=_text(data["domain_name"], "origin domain_name"), + phase=ExactRatio.from_dict(data["phase"]), + phase_chart=_text(data["phase_chart"], "phase_chart"), + k=_integer(data["k"], "origin k", minimum=1), + slots=tuple(JoinSlot.from_dict(item) for item in data["slots"]), + provenance=_text_tuple(data["provenance"], "origin provenance", minimum=1), + ) + expected_wire = origin.to_dict() + for field in ("bearing", "readouts", "s5_energy_readout"): + if _canonical_json(data[field]) != _canonical_json(expected_wire[field]): + raise ValueError(f"serialized {field} must match its identified state and readout rule") + if _integer(data["arity"], "origin arity", minimum=0) != origin.arity: + raise ValueError("serialized arity does not match explicit slots") + if _text_tuple(data["shape_signature"], "shape_signature") != origin.shape_signature: + raise ValueError("serialized shape_signature does not match explicit slots") + return origin + + +@dataclass(frozen=True, slots=True) +class AuthoredJoin: + """An authored relation and its structural position, not a state change.""" + join_id: str + order: int + source_origin_id: str + target_origin_id: str + relation: str + provenance: tuple[str, ...] + + def __post_init__(self) -> None: + _text(self.join_id, "join_id") + _integer(self.order, "authored join order", minimum=1) + _text(self.source_origin_id, "source_origin_id") + _text(self.target_origin_id, "target_origin_id") + if self.source_origin_id == self.target_origin_id: + raise ValueError("join source and target identities must differ") + if self.relation != ADJACENT_JOIN_RELATION: + raise ValueError("only the authored adjacent-scale join relation is supported") + object.__setattr__( + self, "provenance", _text_tuple(self.provenance, "join provenance", minimum=1) + ) + + def to_dict(self) -> dict[str, Any]: + return { + "join_id": self.join_id, + "order": self.order, + "source_origin_id": self.source_origin_id, + "target_origin_id": self.target_origin_id, + "relation": self.relation, + "provenance": list(self.provenance), + } + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> AuthoredJoin: + if not isinstance(data, Mapping): + raise ValueError("authored join must be an object") + _strict_keys( + data, + {"join_id", "order", "source_origin_id", "target_origin_id", "relation", "provenance"}, + "authored-join", + ) + return cls( + join_id=_text(data["join_id"], "join_id"), + order=_integer(data["order"], "authored join order", minimum=1), + source_origin_id=_text(data["source_origin_id"], "source_origin_id"), + target_origin_id=_text(data["target_origin_id"], "target_origin_id"), + relation=_text(data["relation"], "join relation"), + provenance=_text_tuple(data["provenance"], "join provenance", minimum=1), + ) + + +@dataclass(frozen=True, slots=True) +class LegacyBag: + scale_slot_counts: tuple[tuple[str, int], ...] + slot_kind_counts: tuple[tuple[str, int], ...] + kind: str = "bag" + excluded_from_tensor: bool = True + + def __post_init__(self) -> None: + if self.kind != "bag" or self.excluded_from_tensor is not True: + raise ValueError("legacy bag must remain typed bag and excluded from tensor") + if tuple(scale for scale, _count in self.scale_slot_counts) != SCALE_IDS: + raise ValueError("legacy bag must count every scale S0 through S6") + for scale, count in self.scale_slot_counts: + _text(scale, "bag scale") + _integer(count, "bag slot count", minimum=0) + expected_kinds = tuple(sorted(SLOT_KINDS)) + if tuple(kind for kind, _count in self.slot_kind_counts) != expected_kinds: + raise ValueError("legacy bag slot kinds must be complete and sorted") + for kind, count in self.slot_kind_counts: + _text(kind, "bag slot kind") + _integer(count, "bag kind count", minimum=0) + + def to_dict(self) -> dict[str, Any]: + return { + "kind": self.kind, + "excluded_from_tensor": self.excluded_from_tensor, + "scale_slot_counts": {key: value for key, value in self.scale_slot_counts}, + "slot_kind_counts": {key: value for key, value in self.slot_kind_counts}, + } + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> LegacyBag: + if not isinstance(data, Mapping): + raise ValueError("legacy bag must be an object") + _strict_keys(data, {"kind", "excluded_from_tensor", "scale_slot_counts", "slot_kind_counts"}, "legacy-bag") + scale_counts = data["scale_slot_counts"] + kind_counts = data["slot_kind_counts"] + if not isinstance(scale_counts, Mapping) or not isinstance(kind_counts, Mapping): + raise ValueError("legacy bag counts must be objects") + return cls( + kind=_text(data["kind"], "legacy bag kind"), + excluded_from_tensor=data["excluded_from_tensor"], + scale_slot_counts=tuple((key, _integer(value, f"{key} slot count", minimum=0)) for key, value in scale_counts.items()), + slot_kind_counts=tuple((key, _integer(value, f"{key} kind count", minimum=0)) for key, value in kind_counts.items()), + ) + + +def _license_record() -> dict[str, Any]: + return { + "application_id": METAPAT_APPLICATION_ID, + "application_version": METAPAT_APPLICATION_VERSION, + "application_digest": METAPAT_APPLICATION_DIGEST, + "authority": "The-Interdependency/metapat", + "authority_transfer": False, + } + + +def _field_binding_record() -> dict[str, dict[str, str]]: + records: dict[str, dict[str, str]] = {} + for key, value in sorted(SEMANTIC_FIELD_BINDINGS.items()): + role = value["application_role"] + spine = value["spine_name"] + module_id = _METAPAT_APPLICATION_ROLE_MODULES.get(role) + allowed_spines = _APPLICATION_ROLE_ALLOWED_SPINES.get(role, frozenset()) + if module_id is None or spine not in allowed_spines: + raise RuntimeError(f"unlicensed METAPAT semantic pair for {key}") + records[key] = {**value, "catalog_module_id": module_id} + return records + + +def _bag_for(origins: Sequence[ScaleOrigin]) -> LegacyBag: + scale_counts = tuple((origin.scale, len(origin.slots)) for origin in origins) + kind_counts = tuple( + (kind, sum(slot.kind == kind for origin in origins for slot in origin.slots)) + for kind in sorted(SLOT_KINDS) + ) + return LegacyBag(scale_counts, kind_counts) + + +@dataclass(frozen=True, slots=True) +class EPACJoinTree: + origins: tuple[ScaleOrigin, ...] + joins: tuple[AuthoredJoin, ...] + legacy_bag: LegacyBag + object_kind: str = "multi-origin-join-tree" + schema_id: str = JOIN_TREE_SCHEMA_ID + schema_version: str = JOIN_TREE_SCHEMA_VERSION + + def __post_init__(self) -> None: + if self.object_kind != "multi-origin-join-tree": + raise ValueError("a bag cannot pass as a multi-origin join tree") + if self.schema_id != JOIN_TREE_SCHEMA_ID or self.schema_version != JOIN_TREE_SCHEMA_VERSION: + raise ValueError("unsupported EPAC join-tree schema") + origins = tuple(self.origins) + joins = tuple(self.joins) + if any(not isinstance(item, ScaleOrigin) for item in origins): + raise ValueError("origins must contain ScaleOrigin values") + if any(not isinstance(item, AuthoredJoin) for item in joins): + raise ValueError("joins must contain AuthoredJoin values") + if tuple(origin.scale for origin in origins) != SCALE_IDS: + raise ValueError("tensor must contain exactly one ordered origin S0 through S6") + origin_ids = tuple(origin.origin_id for origin in origins) + if len(set(origin_ids)) != len(origin_ids): + raise ValueError("origin identities must be unique") + if len(joins) != 6: + raise ValueError("tensor must contain exactly six adjacent-scale joins") + join_ids = tuple(item.join_id for item in joins) + if len(set(join_ids)) != 6: + raise ValueError("join identities must be unique") + leftover_ids = tuple( + slot.participant_id + for origin in origins + for slot in origin.slots + if slot.kind == "leftover" + ) + declared_object_ids = origin_ids + join_ids + leftover_ids + if len(set(declared_object_ids)) != len(declared_object_ids): + raise ValueError( + "origin, join, and leftover object identities must be globally unique" + ) + if tuple(item.order for item in joins) != tuple(range(1, 7)): + raise ValueError("authored join order must be exactly 1 through 6") + for index, origin in enumerate(origins): + members = tuple(slot for slot in origin.slots if slot.kind == "member") + if index == 0: + if members: + raise ValueError("S0 cannot contain a lower-scale member reference") + continue + previous = origins[index - 1] + if len(members) != 1: + raise ValueError(f"{origin.scale} must contain exactly one adjacent-scale member") + member = members[0] + if member.participant_id != previous.origin_id or member.participant_scale != previous.scale: + raise ValueError(f"{origin.scale} member must reference the immediately preceding origin") + event = joins[index - 1] + if event.source_origin_id != previous.origin_id or event.target_origin_id != origin.origin_id: + raise ValueError(f"join {event.order} does not match adjacent origins") + if not isinstance(self.legacy_bag, LegacyBag) or self.legacy_bag != _bag_for(origins): + raise ValueError("legacy bag must be the exact lossy projection of the tree") + object.__setattr__(self, "origins", origins) + object.__setattr__(self, "joins", joins) + if _semantic_wire_field_paths(self._payload()) != REQUIRED_SEMANTIC_FIELD_PATHS: + raise RuntimeError("semantic field bindings do not cover the live wire schema") + + def _payload(self) -> dict[str, Any]: + return { + "schema_id": self.schema_id, + "schema_version": self.schema_version, + "object_kind": self.object_kind, + "semantic_license": _license_record(), + "semantic_field_bindings": _field_binding_record(), + "origins": [origin.to_dict() for origin in self.origins], + "joins": [item.to_dict() for item in self.joins], + "legacy_bag": self.legacy_bag.to_dict(), + } + + @property + def candidate_digest(self) -> str: + return _sha256(_canonical_json(self._payload()).encode("utf-8")) + + def to_dict(self) -> dict[str, Any]: + return {**self._payload(), "candidate_digest": self.candidate_digest} + + def to_json(self) -> str: + return _canonical_json(self.to_dict()) + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> EPACJoinTree: + if not isinstance(data, Mapping): + raise ValueError("EPAC join tree must be an object") + expected = { + "schema_id", "schema_version", "object_kind", "semantic_license", + "semantic_field_bindings", "origins", "joins", "legacy_bag", + "candidate_digest", + } + _strict_keys(data, expected, "EPAC-join-tree") + if data["semantic_license"] != _license_record(): + raise ValueError("semantic license identity mismatch") + if data["semantic_field_bindings"] != _field_binding_record(): + raise ValueError("semantic field bindings are incomplete or changed") + if set(data["semantic_field_bindings"]) != REQUIRED_SEMANTIC_FIELD_PATHS: + raise ValueError("semantic field bindings do not cover the complete declared wire") + if not isinstance(data["origins"], list) or not isinstance(data["joins"], list): + raise ValueError("origins and joins must be arrays") + tree = cls( + schema_id=_text(data["schema_id"], "schema_id"), + schema_version=_text(data["schema_version"], "schema_version"), + object_kind=_text(data["object_kind"], "object_kind"), + origins=tuple(ScaleOrigin.from_dict(item) for item in data["origins"]), + joins=tuple(AuthoredJoin.from_dict(item) for item in data["joins"]), + legacy_bag=LegacyBag.from_dict(data["legacy_bag"]), + ) + raw_payload = {key: value for key, value in data.items() if key != "candidate_digest"} + if _canonical_json(raw_payload) != _canonical_json(tree._payload()): + raise ValueError("non-canonical EPAC join-tree payload") + if _text(data["candidate_digest"], "candidate_digest") != tree.candidate_digest: + raise ValueError("candidate_digest mismatch") + return tree + + @classmethod + def from_json(cls, text: str) -> EPACJoinTree: + try: + data = json.loads(text, object_pairs_hook=_reject_duplicate_keys) + except json.JSONDecodeError as exc: + raise ValueError("invalid EPAC join-tree JSON") from exc + tree = cls.from_dict(data) + if text != tree.to_json(): + raise ValueError("EPAC join-tree JSON must use its canonical encoding") + return tree + + +def _slot( + name: str, + order: int, + kind: str, + facing: int, + participant_id: str | None = None, + participant_scale: str | None = None, +) -> JoinSlot: + return JoinSlot(name, order, kind, participant_id, participant_scale, facing) + + +def construct_epac_join_tree( + *, + identity_namespace: str = "fixture-a", + s2_seating_order: tuple[str, str, str] = ("core", "guest", "vacancy"), + phase_chart_overrides: Mapping[str, str] | None = None, +) -> EPACJoinTree: + """Construct the bounded public candidate with one origin at every scale. + + ``s2_seating_order`` may reorder the same three S2 seats. It exists to + exhibit that a bag of counts is not a join tree. It does not infer a join. + """ + namespace = _text(identity_namespace, "identity_namespace") + if set(s2_seating_order) != {"core", "guest", "vacancy"} or len(s2_seating_order) != 3: + raise ValueError("s2_seating_order must be a permutation of core, guest, vacancy") + charts = {scale: ("visible" if index % 2 == 0 else "lifted") for index, scale in enumerate(SCALE_IDS)} + for scale, chart in dict(phase_chart_overrides or {}).items(): + if scale not in SCALE_IDS or chart not in PHASE_CHARTS: + raise ValueError("phase chart overrides must map S0 through S6 to visible or lifted") + charts[scale] = chart + origin_ids = {scale: f"epac.origin:{namespace}:{scale}" for scale in SCALE_IDS} + phases = ( + ExactRatio(0, 1), ExactRatio(1, 8), ExactRatio(1, 4), ExactRatio(3, 8), + ExactRatio(1, 2), ExactRatio(5, 8), ExactRatio(3, 4), + ) + + origins: list[ScaleOrigin] = [] + origins.append( + ScaleOrigin( + origin_ids["S0"], "S0", SCALE_DOMAIN_NAMES[0], phases[0], charts["S0"], 2, + ( + _slot("seed", 0, "leftover", -2, f"epac.leftover:{namespace}:S0:seed"), + _slot("zero", 1, "hole", 0), + ), + ("epac.join-term-v0", "declared-scale:S0"), + ) + ) + origins.append( + ScaleOrigin( + origin_ids["S1"], "S1", SCALE_DOMAIN_NAMES[1], phases[1], charts["S1"], 2, + ( + _slot("subatomic", 0, "member", 4, origin_ids["S0"], "S0"), + _slot("zero", 1, "hole", 0), + ), + ("epac.join-term-v0", "declared-scale:S1"), + ) + ) + s2_templates = { + "core": ("member", 3, origin_ids["S1"], "S1"), + "guest": ("leftover", -3, f"epac.leftover:{namespace}:S2:guest", None), + "vacancy": ("hole", 0, None, None), + } + s2_slots = tuple( + _slot(name, order, *s2_templates[name]) + for order, name in enumerate(s2_seating_order) + ) + origins.append( + ScaleOrigin( + origin_ids["S2"], "S2", SCALE_DOMAIN_NAMES[2], phases[2], charts["S2"], 3, + s2_slots, + ("epac.join-term-v0", "declared-scale:S2", "authored-seating-chart"), + ) + ) + fixed_slots = { + "S3": ( + _slot("join", 0, "member", -5, origin_ids["S2"], "S2"), + _slot("zero", 1, "hole", 0), + _slot("embed-context", 2, "leftover", 2, f"epac.leftover:{namespace}:S3:context"), + ), + "S4": ( + _slot("embed", 0, "member", 2, origin_ids["S3"], "S3"), + _slot("electronic-context", 1, "leftover", -1, f"epac.leftover:{namespace}:S4:context"), + ), + "S5": ( + _slot("electronic-state", 0, "member", 7, origin_ids["S4"], "S4"), + _slot("zero-a", 1, "hole", 0), + _slot("occupancy-context", 2, "leftover", -2, f"epac.leftover:{namespace}:S5:context"), + _slot("zero-b", 3, "hole", 0), + ), + "S6": ( + _slot("energy-readout", 0, "member", 1, origin_ids["S5"], "S5"), + _slot("ensemble-context", 1, "leftover", -1, f"epac.leftover:{namespace}:S6:context"), + ), + } + for index in range(3, 7): + scale = f"S{index}" + slots = fixed_slots[scale] + origins.append( + ScaleOrigin( + origin_ids[scale], scale, SCALE_DOMAIN_NAMES[index], phases[index], charts[scale], + len(slots), slots, ("epac.join-term-v0", f"declared-scale:{scale}"), + ) + ) + joins = tuple( + AuthoredJoin( + join_id=f"epac.join:{namespace}:S{index - 1}-S{index}", + order=index, + source_origin_id=origin_ids[f"S{index - 1}"], + target_origin_id=origin_ids[f"S{index}"], + relation=ADJACENT_JOIN_RELATION, + provenance=("epac.join-term-v0", f"authored-near-join:S{index - 1}->S{index}"), + ) + for index in range(1, 7) + ) + origin_tuple = tuple(origins) + return EPACJoinTree(origin_tuple, joins, _bag_for(origin_tuple)) + + +def recover_epac_join_tree(public_bytes: bytes | str) -> EPACJoinTree: + """Recover and validate the tree from public bytes only.""" + if isinstance(public_bytes, bytes): + try: + text = public_bytes.decode("utf-8") + except UnicodeDecodeError as exc: + raise ValueError("EPAC join-tree bytes must be UTF-8") from exc + elif isinstance(public_bytes, str): + text = public_bytes + else: + raise TypeError("public_bytes must be bytes or str") + return EPACJoinTree.from_json(text) + + +def legacy_bag_projection(tree: EPACJoinTree) -> dict[str, Any]: + """Return the explicitly lossy count bag kept beside, not as, the tree.""" + if not isinstance(tree, EPACJoinTree): + raise TypeError("tree must be EPACJoinTree") + return tree.legacy_bag.to_dict() + + +def _structural_signature(tree: EPACJoinTree) -> tuple[Any, ...]: + by_id = {origin.origin_id: origin.scale for origin in tree.origins} + leftover_by_id = { + slot.participant_id: index + for index, slot in enumerate( + slot + for origin in tree.origins + for slot in origin.slots + if slot.kind == "leftover" + ) + } + origin_rows = [] + for origin in tree.origins: + slot_rows = tuple( + ( + slot.name, + slot.order, + slot.kind, + ( + by_id.get(slot.participant_id) + if slot.kind == "member" + else ("leftover", leftover_by_id[slot.participant_id]) + if slot.kind == "leftover" + else None + ), + slot.participant_scale, + slot.facing, + ) + for slot in origin.slots + ) + origin_rows.append( + ( + origin.scale, + origin.domain_name, + origin.phase.numerator, + origin.phase.denominator, + origin.phase_chart, + origin.k, + slot_rows, + origin.bearing, + origin.arity, + origin.shape_signature, + origin.s5_energy_readout.to_dict() if origin.s5_energy_readout else None, + origin.provenance, + ) + ) + event_rows = tuple( + ( + item.order, + by_id[item.source_origin_id], + by_id[item.target_origin_id], + item.relation, + item.provenance, + ) + for item in tree.joins + ) + return tuple(origin_rows), event_rows + + +def join_isomorphic(left: EPACJoinTree, right: EPACJoinTree) -> bool: + """Return label-preserving join isomorphism, excluding instance IDs/bag.""" + if not isinstance(left, EPACJoinTree) or not isinstance(right, EPACJoinTree): + raise TypeError("join_isomorphic requires two EPACJoinTree values") + return _structural_signature(left) == _structural_signature(right) + + +def trace_origin_lineage(tree: EPACJoinTree, target_origin_id: str) -> tuple[str, ...]: + """Replay exact authored ancestry from S0 through the requested origin.""" + if not isinstance(tree, EPACJoinTree): + raise TypeError("tree must be EPACJoinTree") + _text(target_origin_id, "target_origin_id") + by_target = {item.target_origin_id: item.source_origin_id for item in tree.joins} + known = {origin.origin_id for origin in tree.origins} + if target_origin_id not in known: + raise ValueError("target origin is not present in the tree") + reversed_path = [target_origin_id] + while reversed_path[-1] in by_target: + reversed_path.append(by_target[reversed_path[-1]]) + return tuple(reversed(reversed_path)) + + +__all__ = [ + "ADJACENT_JOIN_RELATION", + "BEARING_RULE_ID", + "JOIN_TREE_SCHEMA_ID", + "JOIN_TREE_SCHEMA_VERSION", + "METAPAT_APPLICATION_DIGEST", + "METAPAT_APPLICATION_ID", + "METAPAT_APPLICATION_VERSION", + "REQUIRED_SEMANTIC_FIELD_PATHS", + "SCALE_DOMAIN_NAMES", + "SCALE_IDS", + "SEMANTIC_FIELD_BINDINGS", + "EPACJoinTree", + "ExactRatio", + "JoinSlot", + "AuthoredJoin", + "LegacyBag", + "ScaleOrigin", + "construct_epac_join_tree", + "join_isomorphic", + "legacy_bag_projection", + "recover_epac_join_tree", + "trace_origin_lineage", +] diff --git a/pyproject.toml b/pyproject.toml index e08ee5b..16b5933 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ test = ["pytest==9.1.1"] build = ["build==1.5.0", "twine==7.0.0", "setuptools==84.0.0", "wheel==0.48.0"] [tool.setuptools] -py-modules = ["epac_evidence_cache", "epac_atomic", "epac_atomic_derivation", "epac_b_derivation", "epac_comparison", "epac_dimensional_arity", "epac_molecular", "epac_periodic", "epac_public_gonol", "epac_ucns_provenance", "epac_boundary_minimal_refinement", "epac_boundary_nondegeneracy", "epac_boundary_probe_completeness", "epac_boundary_quotient", "epac_cross_scale_closure"] +py-modules = ["epac_evidence_cache", "epac_atomic", "epac_atomic_derivation", "epac_b_derivation", "epac_comparison", "epac_dimensional_arity", "epac_join_term", "epac_molecular", "epac_periodic", "epac_public_gonol", "epac_ucns_provenance", "epac_boundary_minimal_refinement", "epac_boundary_nondegeneracy", "epac_boundary_probe_completeness", "epac_boundary_quotient", "epac_cross_scale_closure"] packages = ["epac_data", "epac_subatomic", "epac_viz"] package-dir = {epac_data = "data", epac_subatomic = "subatomic", epac_viz = "viz"} diff --git a/tests/test_boundary_probe_completeness.py b/tests/test_boundary_probe_completeness.py index 28f3e8f..30eaa1c 100644 --- a/tests/test_boundary_probe_completeness.py +++ b/tests/test_boundary_probe_completeness.py @@ -143,9 +143,9 @@ def test_inventory_covers_packaged_execution_modules(self) -> None: def test_declared_operations_preserve_unresolved_semantics(self) -> None: inventory = self.report["operation_inventory"] - self.assertEqual(inventory["operation_count"], 153) + self.assertEqual(inventory["operation_count"], 164) self.assertEqual(inventory["boundary_relevant_count"], 58) - self.assertEqual(inventory["ambiguous_count"], 56) + self.assertEqual(inventory["ambiguous_count"], 67) self.assertEqual(inventory["omitted_boundary_relevant_count"], 14) for row in self.report["operation_ledger"]: if row["name"] in self.report["unmapped_operation_probes"]: @@ -167,6 +167,22 @@ def test_declared_operations_preserve_unresolved_semantics(self) -> None: "epac_b_derivation.ligand_field_spin_from_energies", {row["operation"] for row in ambiguous}, ) + self.assertEqual( + {row["name"] for row in ambiguous if row["module"] == "epac_join_term"}, + { + "EPACJoinTree", + "ExactRatio", + "JoinSlot", + "AuthoredJoin", + "LegacyBag", + "ScaleOrigin", + "construct_epac_join_tree", + "join_isomorphic", + "legacy_bag_projection", + "recover_epac_join_tree", + "trace_origin_lineage", + }, + ) for operation in ("epac_boundary_minimal_refinement.boundary_minimal_refinement_report", "epac_boundary_probe_completeness.boundary_probe_completeness_report", "epac_boundary_probe_completeness.declared_operation_ledger", diff --git a/tests/test_epac_join_term.py b/tests/test_epac_join_term.py new file mode 100644 index 0000000..2122bd4 --- /dev/null +++ b/tests/test_epac_join_term.py @@ -0,0 +1,574 @@ +"""Correctness and public-only recovery witnesses for EPAC join term v0.""" + +# === CHECKS === +# id: check_epac_join_tree_complete_scale_tensor +# proves: epac_join_tree_complete_scale_tensor +# call: self::test_complete_s0_through_s6_tensor +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_explicit_boundary +# proves: epac_join_tree_explicit_boundary +# call: self::test_boundaries_store_named_ordered_holes_and_leftovers +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_inferred_bearing +# proves: epac_join_tree_inferred_bearing +# call: self::test_bearing_is_inferred_and_tamper_rejected +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_recursive_identity +# proves: epac_join_tree_recursive_identity +# call: self::test_recursive_origins_and_joins_are_adjacent +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_s5_energy_qualified +# proves: epac_join_tree_s5_energy_qualified +# call: self::test_s5_readout_is_exact_occupancy_only +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_semantic_customs +# proves: epac_join_tree_semantic_customs +# call: self::test_every_declared_semantic_field_is_dual_named +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_join_isomorphism +# proves: epac_join_tree_join_isomorphism +# call: self::test_join_isomorphism_preserves_structure_not_instance_ids +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_bag_is_lossy +# proves: epac_join_tree_bag_is_lossy +# call: self::test_same_bag_different_tree_and_bag_cannot_parse +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_public_recovery +# proves: epac_join_tree_public_recovery +# call: self::test_public_receipt_recovers_without_constructor_state +# mutates: filesystem_read +# cleanup: none +# +# id: check_epac_join_tree_strict_rejection +# proves: epac_join_tree_strict_rejection +# call: self::test_malformed_wires_fail_closed +# mutates: none +# cleanup: none +# +# id: check_epac_join_tree_deterministic_serialization +# proves: epac_join_tree_deterministic_serialization +# call: self::test_serialization_and_receipt_regeneration_are_deterministic +# mutates: filesystem_read +# cleanup: none +# +# id: check_epac_join_tree_existing_falsification_preserved +# proves: epac_join_tree_existing_falsification_preserved +# call: self::test_molecular_shape_falsification_remains_unchanged +# mutates: filesystem_read +# cleanup: none +# +# id: check_epac_join_tree_readout_boundary +# proves: epac_join_tree_readout_boundary +# call: self::test_readouts_identify_state_without_replacing_it +# mutates: temporary_files +# cleanup: pytest_tmp_path +# +# id: check_epac_join_tree_conditional_roles +# proves: epac_join_tree_conditional_roles +# call: self::test_merged_producer_and_conditional_roles +# mutates: temporary_files +# cleanup: pytest_tmp_path +# +# id: check_epac_join_tree_canonical_wire +# proves: epac_join_tree_canonical_wire +# call: self::test_noncanonical_ratios_rejected_even_with_recomputed_digest +# mutates: temporary_files +# cleanup: pytest_tmp_path +# +# id: check_epac_join_generator_direct_invocation +# proves: epac_join_generator_direct_invocation +# call: self::test_generator_runs_directly_without_installation +# mutates: temporary_files +# cleanup: pytest_tmp_path +# +# id: check_epac_join_generator_source_identity +# proves: epac_join_generator_source_identity +# call: self::test_generator_rejects_mismatched_source_before_writing +# mutates: temporary_files +# cleanup: pytest_tmp_path +# === END CHECKS === + +import json +import hashlib +import os +import subprocess +import sys +from copy import deepcopy +from importlib.resources import files +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +import pytest + +import epac_join_term +from epac_comparison import compare_after_construction +from epac_join_term import ( + METAPAT_APPLICATION_DIGEST, + REQUIRED_SEMANTIC_FIELD_PATHS, + SCALE_IDS, + EPACJoinTree, + ExactRatio, + construct_epac_join_tree, + join_isomorphic, + legacy_bag_projection, + recover_epac_join_tree, + trace_origin_lineage, +) + +ROOT = Path(__file__).resolve().parents[1] + + +def _fixture_receipt() -> dict: + return json.loads( + files("epac_data").joinpath("epac-join-term-v0-receipt.json").read_text(encoding="utf-8") + ) + + +def test_complete_s0_through_s6_tensor() -> None: + tree = construct_epac_join_tree() + assert tuple(origin.scale for origin in tree.origins) == SCALE_IDS + assert len(tree.origins) == 7 + assert len(tree.joins) == 6 + assert tuple(item.order for item in tree.joins) == tuple(range(1, 7)) + assert len({origin.origin_id for origin in tree.origins}) == 7 + + +def test_boundaries_store_named_ordered_holes_and_leftovers() -> None: + tree = construct_epac_join_tree() + for origin in tree.origins: + assert origin.k == len(origin.slots) + assert tuple(slot.order for slot in origin.slots) == tuple(range(origin.k)) + assert len({slot.name for slot in origin.slots}) == origin.k + assert origin.arity == sum(slot.kind != "hole" for slot in origin.slots) + assert origin.shape_signature == tuple( + f"{slot.order}:{slot.name}:{slot.kind}" for slot in origin.slots + ) + holes = [slot for origin in tree.origins for slot in origin.slots if slot.kind == "hole"] + leftovers = [slot for origin in tree.origins for slot in origin.slots if slot.kind == "leftover"] + assert holes and leftovers + assert all(slot.participant_id is None and slot.participant_scale is None for slot in holes) + assert all(slot.participant_id and slot.participant_scale is None for slot in leftovers) + declared_object_ids = ( + [origin.origin_id for origin in tree.origins] + + [event.join_id for event in tree.joins] + + [slot.participant_id for slot in leftovers] + ) + assert len(declared_object_ids) == len(set(declared_object_ids)) + + +def test_bearing_is_inferred_and_tamper_rejected() -> None: + tree = construct_epac_join_tree() + assert all( + origin.bearing + == tuple((slot.facing > 0) - (slot.facing < 0) for slot in origin.slots) + for origin in tree.origins + ) + tampered = tree.to_dict() + tampered["origins"][2]["bearing"]["value"][0] *= -1 + with pytest.raises(ValueError, match="bearing"): + EPACJoinTree.from_dict(tampered) + + +def test_recursive_origins_and_joins_are_adjacent() -> None: + tree = construct_epac_join_tree() + for index in range(1, 7): + source = tree.origins[index - 1] + target = tree.origins[index] + members = [slot for slot in target.slots if slot.kind == "member"] + assert len(members) == 1 + assert (members[0].participant_id, members[0].participant_scale) == ( + source.origin_id, + source.scale, + ) + event = tree.joins[index - 1] + assert (event.source_origin_id, event.target_origin_id) == ( + source.origin_id, + target.origin_id, + ) + assert trace_origin_lineage(tree, tree.origins[-1].origin_id) == tuple( + origin.origin_id for origin in tree.origins + ) + + +def test_s5_readout_is_exact_occupancy_only() -> None: + tree = construct_epac_join_tree() + for origin in tree.origins: + if origin.scale == "S5": + assert origin.s5_energy_readout == ExactRatio(2, 4) == ExactRatio(1, 2) + else: + assert origin.s5_energy_readout is None + text = (ROOT / "docs/multi-origin-join-term-v0.md").read_text(encoding="utf-8") + assert "establishes no physical energy" in text + + +def test_every_declared_semantic_field_is_dual_named() -> None: + tree = construct_epac_join_tree() + original_digest = tree.candidate_digest + wire = tree.to_dict() + assert set(wire["semantic_field_bindings"]) == REQUIRED_SEMANTIC_FIELD_PATHS + assert epac_join_term._semantic_wire_field_paths(wire) == REQUIRED_SEMANTIC_FIELD_PATHS + assert epac_join_term._WIRE_METADATA_FIELDS == { + "candidate_digest", + "schema_id", + "schema_version", + "semantic_field_bindings", + "semantic_license", + } + assert wire["semantic_license"]["application_digest"] == METAPAT_APPLICATION_DIGEST + for binding in wire["semantic_field_bindings"].values(): + assert set(binding) == { + "spine_name", + "domain_name", + "application_role", + "catalog_module_id", + } + assert all(isinstance(value, str) and value for value in binding.values()) + assert binding["catalog_module_id"] == epac_join_term._METAPAT_APPLICATION_ROLE_MODULES[ + binding["application_role"] + ] + assert binding["spine_name"] in epac_join_term._APPLICATION_ROLE_ALLOWED_SPINES[ + binding["application_role"] + ] + assert set(epac_join_term._METAPAT_APPLICATION_ROLE_MODULES) == { + "customs-boundary", + "stored-join-term", + "join-boundary", + "origin-state", + "scale-origin", + "multi-origin-tensor", + "authored-near-join", + "recursive-origin", + "state-metric", + "join-transformation", + "transformation-sequence", + "domain-customs", + } + with pytest.raises(TypeError): + epac_join_term.SEMANTIC_FIELD_BINDINGS["origin.phase"]["spine_name"] = "MUTATED" + with pytest.raises(TypeError): + epac_join_term.SEMANTIC_FIELD_BINDINGS["new.path"] = {} + with pytest.raises(TypeError): + epac_join_term._METAPAT_APPLICATION_ROLE_MODULES["origin-state"] = "MUTATED" + with pytest.raises(TypeError): + epac_join_term._APPLICATION_ROLE_ALLOWED_SPINES["origin-state"] = frozenset() + wire["semantic_field_bindings"]["origin.phase"]["spine_name"] = "MUTATED" + assert tree.candidate_digest == original_digest + assert tree.to_dict()["semantic_field_bindings"]["origin.phase"]["spine_name"] == "State" + + +def test_join_isomorphism_preserves_structure_not_instance_ids() -> None: + original = construct_epac_join_tree(identity_namespace="first") + renamed = construct_epac_join_tree(identity_namespace="second") + reordered = construct_epac_join_tree(s2_seating_order=("guest", "core", "vacancy")) + lifted_s0 = construct_epac_join_tree(phase_chart_overrides={"S0": "lifted"}) + assert join_isomorphic(original, renamed) + assert not join_isomorphic(original, reordered) + assert not join_isomorphic(original, lifted_s0) + + +def test_same_bag_different_tree_and_bag_cannot_parse() -> None: + first = construct_epac_join_tree(s2_seating_order=("core", "guest", "vacancy")) + second = construct_epac_join_tree(s2_seating_order=("guest", "core", "vacancy")) + renamed = construct_epac_join_tree(identity_namespace="renamed") + assert legacy_bag_projection(first) == legacy_bag_projection(second) + assert not join_isomorphic(first, second) + assert legacy_bag_projection(first) == legacy_bag_projection(renamed) + assert join_isomorphic(first, renamed) + assert trace_origin_lineage(first, first.origins[-1].origin_id) != trace_origin_lineage( + renamed, renamed.origins[-1].origin_id + ) + with pytest.raises(ValueError): + recover_epac_join_tree(json.dumps(legacy_bag_projection(first))) + + +def test_public_receipt_recovers_without_constructor_state(monkeypatch: pytest.MonkeyPatch) -> None: + receipt = _fixture_receipt() + public_tree_json = json.dumps( + receipt["candidate"], ensure_ascii=False, sort_keys=True, separators=(",", ":") + ) + monkeypatch.setattr( + epac_join_term, + "construct_epac_join_tree", + lambda **_kwargs: (_ for _ in ()).throw(AssertionError("constructor state consulted")), + ) + recovered = recover_epac_join_tree(public_tree_json.encode("utf-8")) + assert recovered.candidate_digest == receipt["results"]["candidate_digest"] + assert trace_origin_lineage(recovered, recovered.origins[-1].origin_id) == tuple( + receipt["results"]["s6_lineage"] + ) + + +def test_malformed_wires_fail_closed() -> None: + tree = construct_epac_join_tree() + duplicate = '{"schema_id":"first","schema_id":"second"}' + with pytest.raises(ValueError, match="duplicate JSON key"): + recover_epac_join_tree(duplicate) + + mutations = [] + unknown = tree.to_dict() + unknown["circle_count"] = 4 + mutations.append(unknown) + missing = tree.to_dict() + del missing["origins"][0]["slots"][1]["participant_id"] + mutations.append(missing) + invalid_modulus = tree.to_dict() + invalid_modulus["origins"][0]["phase"]["denominator"] = 0 + mutations.append(invalid_modulus) + duplicate_origin = tree.to_dict() + duplicate_origin["origins"][1]["origin_id"] = duplicate_origin["origins"][0]["origin_id"] + mutations.append(duplicate_origin) + duplicate_leftover = tree.to_dict() + leftover_rows = [ + slot + for origin in duplicate_leftover["origins"] + for slot in origin["slots"] + if slot["kind"] == "leftover" + ] + leftover_rows[1]["participant_id"] = leftover_rows[0]["participant_id"] + mutations.append(duplicate_leftover) + leftover_origin_collision = tree.to_dict() + next( + slot + for origin in leftover_origin_collision["origins"] + for slot in origin["slots"] + if slot["kind"] == "leftover" + )["participant_id"] = leftover_origin_collision["origins"][0]["origin_id"] + mutations.append(leftover_origin_collision) + join_origin_collision = tree.to_dict() + join_origin_collision["joins"][0]["join_id"] = ( + join_origin_collision["origins"][0]["origin_id"] + ) + mutations.append(join_origin_collision) + duplicate_join = tree.to_dict() + duplicate_join["joins"][1]["join_id"] = ( + duplicate_join["joins"][0]["join_id"] + ) + mutations.append(duplicate_join) + leftover_join_collision = tree.to_dict() + next( + slot + for origin in leftover_join_collision["origins"] + for slot in origin["slots"] + if slot["kind"] == "leftover" + )["participant_id"] = leftover_join_collision["joins"][0][ + "join_id" + ] + mutations.append(leftover_join_collision) + skip_scale = tree.to_dict() + member = next(slot for slot in skip_scale["origins"][2]["slots"] if slot["kind"] == "member") + member["participant_id"] = skip_scale["origins"][0]["origin_id"] + member["participant_scale"] = "S0" + mutations.append(skip_scale) + inconsistent_event = tree.to_dict() + inconsistent_event["joins"][0]["target_origin_id"] = inconsistent_event["origins"][2]["origin_id"] + mutations.append(inconsistent_event) + incomplete_license = tree.to_dict() + del incomplete_license["semantic_field_bindings"]["origin.phase"] + mutations.append(incomplete_license) + bag_masquerade = tree.to_dict() + bag_masquerade["object_kind"] = "bag" + mutations.append(bag_masquerade) + boolean_bearing = tree.to_dict() + boolean_bearing["origins"][1]["bearing"]["value"][0] = True + mutations.append(boolean_bearing) + + for mutation in mutations: + with pytest.raises(ValueError): + EPACJoinTree.from_dict(deepcopy(mutation)) + + +def test_serialization_and_receipt_regeneration_are_deterministic() -> None: + first = construct_epac_join_tree() + second = construct_epac_join_tree() + assert first.to_json() == second.to_json() + assert first.candidate_digest == second.candidate_digest + receipt_path = ROOT / "data/epac-join-term-v0-receipt.json" + audit_path = ROOT / "docs/epac-join-term-v0-audit.md" + assert receipt_path.read_bytes().endswith(b"\n") + assert audit_path.read_bytes().endswith(b"\n") + generator_path = ROOT / "tools/generate_join_term_v0.py" + spec = spec_from_file_location("epac_join_term_v0_generator_fixture", generator_path) + assert spec is not None and spec.loader is not None + generator = module_from_spec(spec) + spec.loader.exec_module(generator) + + rendered_receipt = generator.render_receipt(ROOT) + assert receipt_path.read_text(encoding="utf-8") == rendered_receipt + assert audit_path.read_text(encoding="utf-8") == generator.render_audit( + json.loads(rendered_receipt) + ) + + +def test_molecular_shape_falsification_remains_unchanged() -> None: + standings = compare_after_construction()["standings"] + for name in ( + "charged_3_structure_as_sealed_shape_prediction", + "topology_3_structure_as_sealed_shape_prediction", + "ucns_mobius_as_sealed_shape_prediction", + "atomic_shells_as_sealed_shape_prediction", + ): + assert standings[name] == "FALSIFIED" + + +def _generator(): + spec = spec_from_file_location("epac_join_generator_identity_test", ROOT / "tools/generate_join_term_v0.py") + assert spec is not None and spec.loader is not None + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _rehash(wire): + payload = {key: value for key, value in wire.items() if key != "candidate_digest"} + wire["candidate_digest"] = hashlib.sha256( + json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() + ).hexdigest() + + +def test_readouts_identify_state_without_replacing_it() -> None: + tree = construct_epac_join_tree() + original_bytes = tree.to_json() + for origin in tree.origins: + wire = origin.to_dict() + assert wire["phase"] == origin.phase.to_dict() + assert wire["k"] == origin.k + assert wire["readouts"][0] == { + "origin_id": origin.origin_id, "measured_property": "phase", + "rule_id": "epac.readout.exact-phase.v0", "value": origin.phase.to_dict(), + } + assert wire["readouts"][1]["measured_property"] == "k" + assert wire["readouts"][1]["value"] == origin.k + for slot, readout in zip(origin.slots, wire["readouts"][2:]): + assert readout["measured_property"] == f"slots[{slot.order}].facing" + assert readout["value"] == slot.facing + records = wire["readouts"] + [wire["bearing"]] + if wire["s5_energy_readout"] is not None: + records.append(wire["s5_energy_readout"]) + for readout in records: + assert readout["origin_id"] == origin.origin_id + assert readout["measured_property"] and readout["rule_id"] + wire["readouts"][0]["value"]["numerator"] += 1 + assert tree.to_json() == original_bytes + for field in ("origin_id", "measured_property", "rule_id", "value"): + wire = tree.to_dict() + wire["origins"][0]["readouts"][0][field] = "unrelated" + _rehash(wire) + with pytest.raises(ValueError, match="readouts"): + EPACJoinTree.from_dict(wire) + wire = tree.to_dict() + wire["origins"][0]["readouts"][0]["value"]["numerator"] = False + _rehash(wire) + with pytest.raises(ValueError, match="readouts"): + EPACJoinTree.from_dict(wire) + + +def test_merged_producer_and_conditional_roles() -> None: + producer_bytes = files("epac_data").joinpath("metapat-epac-join-terms-application-v4.json").read_bytes() + producer = json.loads(producer_bytes) + generator = _generator() + assert hashlib.sha256(producer_bytes).hexdigest() == generator.METAPAT_APPLICATION_FIXTURE_SHA256 + assert producer["application_digest"] == METAPAT_APPLICATION_DIGEST + assert {item["application_role"]: item["module_id"] for item in producer["catalog_bindings"]} == epac_join_term._METAPAT_APPLICATION_ROLE_MODULES + tree = construct_epac_join_tree() + bindings = tree.to_dict()["semantic_field_bindings"] + assert {row["spine_name"] for row in bindings.values()}.isdisjoint({"Vector", "Transformation", "Time"}) + assert bindings["origin.phase"]["spine_name"] == "State" + assert bindings["slot.facing"]["spine_name"] == "State" + assert bindings["origin.readouts"]["spine_name"] == "Scalar" + assert bindings["origin.bearing"]["spine_name"] == "Scalar" + assert bindings["tree.joins"]["application_role"] == "authored-near-join" + assert "joins" in tree.to_dict() and "transformations" not in tree.to_dict() + for path, role, spine in [("origin.bearing", "inferred-bearing", "Vector"), + ("join.join_id", "join-transformation", "Transformation"), + ("join.order", "transformation-sequence", "Time")]: + wire = tree.to_dict() + wire["semantic_field_bindings"][path].update(application_role=role, spine_name=spine) + _rehash(wire) + with pytest.raises(ValueError, match="semantic field bindings"): + EPACJoinTree.from_dict(wire) + + +def test_noncanonical_ratios_rejected_even_with_recomputed_digest() -> None: + tree = construct_epac_join_tree() + # Nonzero phase, zero phase, phase readout, and occupancy readout. + for origin_index, field in [(1, "phase"), (0, "phase"), (1, "readouts"), (5, "s5_energy_readout")]: + for rehash in (False, True): + wire = tree.to_dict() + ratio = wire["origins"][origin_index][field] + if field == "readouts": + ratio = ratio[0]["value"] + elif field == "s5_energy_readout": + ratio = ratio["value"] + ratio["numerator"] *= 2 + ratio["denominator"] *= 2 + if rehash: + _rehash(wire) + with pytest.raises(ValueError): + EPACJoinTree.from_dict(wire) + assert ExactRatio(2, 16) == ExactRatio(1, 8) # typed construction still reduces + assert EPACJoinTree.from_json(tree.to_json()) == tree + # JSON parsing must not erase a different encoding under the same digest. + canonical = tree.to_json() + variants = [ + canonical + "\n", + " " + canonical, + json.dumps(tree.to_dict(), sort_keys=True, indent=2), + json.dumps(tree.to_dict(), sort_keys=False, separators=(",", ":")), + canonical.replace('"numerator":0', '"numerator":-0', 1), + ] + unicode_tree = construct_epac_join_tree(identity_namespace="fixture-é") + variants.append(json.dumps(unicode_tree.to_dict(), sort_keys=True, separators=(",", ":"))) + for encoded in variants: + assert encoded != EPACJoinTree.from_dict(json.loads(encoded)).to_json() + with pytest.raises(ValueError, match="canonical encoding"): + recover_epac_join_tree(encoded.encode("utf-8")) + assert recover_epac_join_tree(unicode_tree.to_json().encode("utf-8")) == unicode_tree + + +def test_generator_runs_directly_without_installation(tmp_path) -> None: + env = dict(os.environ) + env.pop("PYTHONPATH", None) + env.pop("PYTHONHOME", None) + # -S disables site packages and editable-install hooks. CWD is outside source. + result = subprocess.run( + [sys.executable, "-S", str(ROOT / "tools/generate_join_term_v0.py"), "--check"], + cwd=tmp_path, env=env, capture_output=True, text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert result.stdout.count("CURRENT") == 2 + + +def test_generator_rejects_mismatched_source_before_writing(tmp_path, monkeypatch) -> None: + generator = _generator() + with pytest.raises(ValueError, match="checkout containing this generator"): + generator.write_outputs(tmp_path) + assert not list(tmp_path.iterdir()) + # Also reject a copied source tree, even when its receipt could be recomputed. + for relative in generator.SOURCE_PATHS: + target = tmp_path / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((ROOT / relative).read_bytes()) + source = tmp_path / "epac_join_term.py" + source.write_text(source.read_text().replace("slot-facing-sign.v0", "slot-facing-sign.other")) + with pytest.raises(ValueError, match="checkout containing this generator"): + generator.write_outputs(tmp_path) + assert not (tmp_path / generator.RECEIPT_PATH).exists() + monkeypatch.setattr(epac_join_term, "_MODULE_SOURCE_SHA256", "different-loaded-source") + with pytest.raises(ValueError, match="loaded epac_join_term source"): + generator.build_receipt(ROOT) diff --git a/tools/generate_join_term_v0.py b/tools/generate_join_term_v0.py new file mode 100644 index 0000000..4a588dd --- /dev/null +++ b/tools/generate_join_term_v0.py @@ -0,0 +1,402 @@ +"""Generate or verify deterministic EPAC join-term v0 evidence.""" + +# === MODULE_BUILD === +# id: epac_join_term_v0_evidence_generator +# module_name: tools.generate_join_term_v0 +# module_kind: instrument +# summary: renders the canonical public join-tree receipt and its Markdown audit from the live candidate constructor and exact source identities +# owner: The Interdependency/epac +# public_surface: build_receipt, render_receipt, render_audit, write_outputs, main +# internal_surface: fixed authority and runtime identities, canonical JSON and source hashing +# auth_boundary: none +# storage_boundary: read feature sources; write or compare two declared generated artifacts +# network_boundary: none +# user_data_boundary: public deterministic research fixture only +# admin_only: false +# tests: tests.test_epac_join_term +# rollout: explicit generation and CI drift check +# rollback: restore prior evidence only with matching constructor and source-manifest identity +# requires: epac_multi_origin_join_term_v0 +# since: 2026-09-29 +# unresolved: the receipt records contract evidence, not external-domain validity or release authority +# === END MODULE_BUILD === + +# === CONTRACTS === +# id: epac_join_generator_direct_invocation +# given: the documented command is run from an uninstalled checkout +# then: the generator loads its own checkout and verifies the committed evidence +# class: correctness +# +# id: epac_join_generator_source_identity +# given: an alternate root or a loaded implementation with different source bytes +# then: generation and verification fail before writing evidence +# class: provenance +# === END CONTRACTS === + +from __future__ import annotations + +import argparse +import hashlib +import json +import sys +from collections.abc import Mapping +from pathlib import Path +from typing import Any + +# Direct invocation must select this checkout, even without an installed EPAC. +# Imported use deliberately preserves the installed module for artifact replay. +GENERATOR_ROOT = Path(__file__).resolve().parents[1] +if __name__ == "__main__": + sys.path.insert(0, str(GENERATOR_ROOT)) + +import epac_join_term +from epac_join_term import ( + METAPAT_APPLICATION_DIGEST, + METAPAT_APPLICATION_ID, + METAPAT_APPLICATION_VERSION, + construct_epac_join_tree, + join_isomorphic, + legacy_bag_projection, + trace_origin_lineage, +) + +RECEIPT_PATH = Path("data/epac-join-term-v0-receipt.json") +AUDIT_PATH = Path("docs/epac-join-term-v0-audit.md") +SOURCE_PATHS = ( + Path("data/metapat-epac-join-terms-application-v4.json"), + Path("README.md"), + Path("docs/PROVENANCE.md"), + Path("docs/domain-claims.md"), + Path("docs/work-graph.json"), + Path("epac_join_term.py"), + Path("epac_boundary_probe_completeness.py"), + Path("docs/multi-origin-join-term-v0.md"), + Path("docs/research-status.md"), + Path("docs/work-graphs/epac-join-term-v0.json"), + Path("pyproject.toml"), + Path("tests/test_boundary_probe_completeness.py"), + Path("tests/test_epac_join_term.py"), + Path("tools/generate_join_term_v0.py"), +) + +EPAC_BASE_COMMIT = "1e5c999286f12221eff9870d1372203ac7935f2a" +EPAC_BASE_TREE = "ce7781938ff684d826bd91f475b5423bd56b146a" +METAPAT_PRODUCER_COMMIT = "1cdfb09dd00a451cee30eec2e78624df8c682662" +METAPAT_PRODUCER_TREE = "d946a5a18b0c53fd561dcd9d68fdf36d5dd11638" +METAPAT_APPLICATION_FIXTURE_SHA256 = "7f3ff74394b48c2162818fad8a373d1b88547b0df73beb8a604376022dcddf07" +METAPAT_APPLICATION_SOURCE_SHA256 = "6cfb4a4109fd43a4f8343b48dd6fd72b74346f4609a65911cbf96188412b6ef0" +SKILL_LIB_COMMIT = "516933d98f9de376f4e498f44059043dc0d96470" +SKILL_LIB_TREE = "a1465ddf1de9519c9abc1a97d058432ba71bb257" + +ORIGINAL_AUTHORING_RUNTIME = { + "implementation": "CPython", + "version": "3.12.3", + "executable": "/usr/bin/python3.12", + "executable_sha256": "e50d468e8b0adfb05733f5b87b3cff34829c4a8c1aea50c865aa8bdfe4bb150f", +} + +NONCLAIMS = ( + "Vector action, resulting state Transformation, or actual Time sequence", + "chemical or physical truth", + "molecular geometry or shape prediction", + "UCNS correspondence, topology, theorem status, or gonol identity", + "energy coupling, conservation, entropy, force, or field behavior", + "cryptographic secrecy, confidentiality, public-key separation, or computational hardness", + "PCEA compatibility", + "production suitability", + "release or protocol authority", +) + +HMMM = ( + "Authored adjacency and its stored order do not establish state changes or actual occurrence order; Transformation and Time remain unlicensed for this candidate.", + "Whether a useful operation exists that requires secret EPAC state and cannot be efficiently reproduced from a legitimate public projection remains unestablished; this candidate is wholly public.", + "Whether exact lineage and seating replay improve an independently preregistered EPAC task remains unmeasured.", + "Whether any later UCNS correspondence is useful requires a separate license and test without rewriting UCNS law.", +) + + +def _canonical_json(value: Mapping[str, Any]) -> str: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + +def _sha256(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _source_hashes(root: Path) -> dict[str, str]: + return { + path.as_posix(): _sha256((root / path).read_bytes()) + for path in SOURCE_PATHS + } + + +def build_receipt(root: Path) -> dict[str, Any]: + root = root.resolve() + if root != GENERATOR_ROOT: + raise ValueError("--root must identify the checkout containing this generator") + # Installed replay is allowed only for the same source bytes. Never hash B + # while constructing with a module loaded from A (including a stale import). + if epac_join_term._MODULE_SOURCE_SHA256 != _sha256((root / "epac_join_term.py").read_bytes()): + raise ValueError("loaded epac_join_term source does not match --root") + fixture_bytes = (root / "data/metapat-epac-join-terms-application-v4.json").read_bytes() + if _sha256(fixture_bytes) != METAPAT_APPLICATION_FIXTURE_SHA256: + raise ValueError("METAPAT producer fixture digest mismatch") + application = json.loads(fixture_bytes) + if application["application_digest"] != METAPAT_APPLICATION_DIGEST: + raise ValueError("METAPAT application identity mismatch") + producer_roles = {item["application_role"]: item["module_id"] for item in application["catalog_bindings"]} + if producer_roles != epac_join_term._METAPAT_APPLICATION_ROLE_MODULES: + raise ValueError("consumer role projection differs from merged METAPAT producer") + tree = construct_epac_join_tree() + reordered = construct_epac_join_tree( + s2_seating_order=("guest", "core", "vacancy"), + ) + renamed = construct_epac_join_tree(identity_namespace="fixture-b") + source_hashes = _source_hashes(root) + candidate_json = tree.to_json().encode("utf-8") + bag_json = _canonical_json(legacy_bag_projection(tree)).encode("utf-8") + reordered_bag_json = _canonical_json(legacy_bag_projection(reordered)).encode("utf-8") + renamed_bag_json = _canonical_json(legacy_bag_projection(renamed)).encode("utf-8") + if bag_json != reordered_bag_json: + raise RuntimeError("same-count witness no longer has identical bag bytes") + if join_isomorphic(tree, reordered): + raise RuntimeError("same-count witness unexpectedly became join-isomorphic") + if renamed_bag_json != bag_json or not join_isomorphic(tree, renamed): + raise RuntimeError("alpha-renamed identity witness no longer preserves structure and bag") + receipt: dict[str, Any] = { + "schema_id": "epac.join-term-v0-evidence", + "schema_version": "1.0.0", + "evidence_epoch": "2026-09-30", + "authority": { + "epac": { + "repository": "The-Interdependency/epac", + "base_commit": EPAC_BASE_COMMIT, + "base_tree": EPAC_BASE_TREE, + "feature_identity": "source-manifest-sha256", + "authority": "schema, constructor, equality, serialization, recovery, and evidence", + }, + "metapat": { + "repository": "The-Interdependency/metapat", + "producer_commit": METAPAT_PRODUCER_COMMIT, + "producer_tree": METAPAT_PRODUCER_TREE, + "application_id": METAPAT_APPLICATION_ID, + "application_version": METAPAT_APPLICATION_VERSION, + "application_digest": METAPAT_APPLICATION_DIGEST, + "application_fixture_sha256": METAPAT_APPLICATION_FIXTURE_SHA256, + "application_source_sha256": METAPAT_APPLICATION_SOURCE_SHA256, + "application_source_path": "src/metapat/epac_join_terms.py", + "application_fixture_path": "src/metapat/fixtures/epac-join-terms-application-v4.json", + "authority": "semantic spine role license only", + "authority_transfer": False, + }, + "skill_lib": { + "repository": "The-Interdependency/skill-lib", + "commit": SKILL_LIB_COMMIT, + "tree": SKILL_LIB_TREE, + "authority": "build and evidence doctrine only", + "authority_transfer": False, + }, + "ucns": { + "consumed": False, + "law_modified": False, + "authority_transfer": False, + }, + }, + "historical_original_authoring_runtime": ORIGINAL_AUTHORING_RUNTIME, + "runtime_scope": "Deterministic evidence is runtime-independent; current execution identities belong to the exact-head artifact replay receipts, not this historical authoring record.", + "inputs": { + "identity_namespace": "fixture-a", + "s2_seating_order": ["core", "guest", "vacancy"], + "phase_chart_rule": "visible on even-numbered scales; lifted on odd-numbered scales", + "phase_values": [origin.phase.to_dict() for origin in tree.origins], + "input_is_public": True, + "private_input_supplied": False, + }, + "source_files_sha256": source_hashes, + "source_manifest_sha256": _sha256(_canonical_json(source_hashes).encode("utf-8")), + "candidate": tree.to_dict(), + "results": { + "classification": "PUBLIC_ONLY_RECOVERY_SURVIVED", + "candidate_digest": tree.candidate_digest, + "candidate_json_sha256": _sha256(candidate_json), + "origin_count": len(tree.origins), + "authored_join_count": len(tree.joins), + "slot_count": sum(origin.k for origin in tree.origins), + "hole_count": sum(slot.kind == "hole" for origin in tree.origins for slot in origin.slots), + "leftover_count": sum(slot.kind == "leftover" for origin in tree.origins for slot in origin.slots), + "member_count": sum(slot.kind == "member" for origin in tree.origins for slot in origin.slots), + "s6_lineage": list(trace_origin_lineage(tree, tree.origins[-1].origin_id)), + "native_operation": "exact authored origin-lineage and named-seating replay", + "legacy_projection": "slot-count bag", + "legacy_bag_sha256": _sha256(bag_json), + "same_bag_nonisomorphic_witness": { + "baseline_candidate_digest": tree.candidate_digest, + "reordered_candidate_digest": reordered.candidate_digest, + "reordered_s2_seating_order": ["guest", "core", "vacancy"], + "reordered_legacy_bag_sha256": _sha256(reordered_bag_json), + "bags_equal": True, + "join_isomorphic": False, + "conclusion": "the legacy bag cannot uniquely recover authored seating or the complete join tree", + }, + "same_bag_alpha_renamed_identity_witness": { + "renamed_identity_namespace": "fixture-b", + "renamed_candidate_digest": renamed.candidate_digest, + "renamed_legacy_bag_sha256": _sha256(renamed_bag_json), + "bags_equal": True, + "join_isomorphic": True, + "baseline_s6_lineage": list( + trace_origin_lineage(tree, tree.origins[-1].origin_id) + ), + "renamed_s6_lineage": list( + trace_origin_lineage(renamed, renamed.origins[-1].origin_id) + ), + "exact_instance_lineages_equal": False, + "conclusion": "the legacy bag does not select exact instance identity labels, while structural lineage up to alpha-renaming is unchanged", + }, + }, + "evidence": { + "declared_contract_count": 17, + "declared_check_count": 17, + "public_recovery_uses_constructor_state": False, + "molecular_shape_standing": "FALSIFIED (preserved; tested separately)", + "deterministic_regeneration_command": "python tools/generate_join_term_v0.py --check", + }, + "nonclaims": list(NONCLAIMS), + "hmmm": list(HMMM), + } + receipt["evidence_digest"] = _sha256(_canonical_json(receipt).encode("utf-8")) + return receipt + + +def render_receipt(root: Path) -> str: + return _canonical_json(build_receipt(root)) + "\n" + + +def render_audit(receipt: Mapping[str, Any]) -> str: + authority = receipt["authority"] + results = receipt["results"] + witness = results["same_bag_nonisomorphic_witness"] + identity_witness = results["same_bag_alpha_renamed_identity_witness"] + lines = [ + "# EPAC join-term v0 deterministic audit", + "", + f"Classification: **{results['classification']}**", + "", + "The public receipt recovers one complete typed S0-through-S6 join tree and its exact authored lineage without constructor or private state. The legacy count bag is retained beside the tree and is demonstrably non-injective.", + "", + "## Exact authority", + "", + f"- EPAC base: `{authority['epac']['base_commit']}` (tree `{authority['epac']['base_tree']}`)", + f"- EPAC feature source manifest: `{receipt['source_manifest_sha256']}`", + f"- METAPAT producer: `{authority['metapat']['producer_commit']}` (tree `{authority['metapat']['producer_tree']}`)", + f"- METAPAT application: `{authority['metapat']['application_id']}@{authority['metapat']['application_version']}`", + f"- METAPAT application digest: `{authority['metapat']['application_digest']}`", + f"- skill-lib: `{authority['skill_lib']['commit']}` (tree `{authority['skill_lib']['tree']}`)", + "- UCNS consumed: `false`; UCNS law modified: `false`", + "", + "The containing feature commit cannot be embedded in a file inside itself. The evidence therefore binds the exact pre-feature EPAC base plus SHA-256 of every non-generated feature input; the generated receipt and audit are excluded to avoid a self-hash cycle, and the final Git commit binds all bytes externally.", + "", + "## Exact result", + "", + f"- Candidate digest: `{results['candidate_digest']}`", + f"- Candidate JSON SHA-256: `{results['candidate_json_sha256']}`", + f"- Evidence digest: `{receipt['evidence_digest']}`", + f"- Origins: `{results['origin_count']}`", + f"- Authored joins (no Transformation or Time claim): `{results['authored_join_count']}`", + f"- Slots: `{results['slot_count']}` (`{results['member_count']}` members, `{results['hole_count']}` holes, `{results['leftover_count']}` leftovers)", + f"- Native operation: {results['native_operation']}", + "", + "## Minimal lossy-projection witness", + "", + f"- Baseline digest: `{witness['baseline_candidate_digest']}`", + f"- Reordered digest: `{witness['reordered_candidate_digest']}`", + f"- Shared bag SHA-256: `{results['legacy_bag_sha256']}`", + "- Bags equal: `true`", + "- Join-isomorphic: `false`", + f"- Conclusion: {witness['conclusion']}.", + "", + "## Exact-instance identity projection witness", + "", + f"- Renamed identity namespace: `{identity_witness['renamed_identity_namespace']}`", + f"- Renamed candidate digest: `{identity_witness['renamed_candidate_digest']}`", + "- Bags equal: `true`", + "- Join-isomorphic: `true`", + "- Exact instance lineages equal: `false`", + f"- Conclusion: {identity_witness['conclusion']}.", + "", + "This is information loss. It is not a secrecy, computational-hardness, or confidentiality result.", + "", + "## Source hashes", + "", + ] + lines.extend( + f"- `{path}`: `{digest}`" + for path, digest in sorted(receipt["source_files_sha256"].items()) + ) + lines.extend(["", "## Explicit nonclaims", ""]) + lines.extend(f"- {item}" for item in receipt["nonclaims"]) + lines.extend(["", "## hmmm", ""]) + lines.extend(f"- {item}" for item in receipt["hmmm"]) + lines.extend( + [ + "", + "## Replay", + "", + "```bash", + "python tools/generate_join_term_v0.py --check", + "python -m pytest -q tests/test_epac_join_term.py", + "```", + "", + ] + ) + return "\n".join(lines) + + +def write_outputs(root: Path) -> tuple[Path, Path]: + root = root.resolve() + receipt_text = render_receipt(root) + receipt = json.loads(receipt_text) + receipt_path = root / RECEIPT_PATH + audit_path = root / AUDIT_PATH + receipt_path.parent.mkdir(parents=True, exist_ok=True) + audit_path.parent.mkdir(parents=True, exist_ok=True) + receipt_path.write_text(receipt_text, encoding="utf-8") + audit_path.write_text(render_audit(receipt), encoding="utf-8") + return receipt_path, audit_path + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=GENERATOR_ROOT) + parser.add_argument("--check", action="store_true") + args = parser.parse_args(argv) + root = args.root.resolve() + try: + receipt_text = render_receipt(root) + except (ValueError, OSError) as exc: + parser.error(str(exc)) + audit_text = render_audit(json.loads(receipt_text)) + failures = 0 + for relative, expected in ((RECEIPT_PATH, receipt_text), (AUDIT_PATH, audit_text)): + path = root / relative + if args.check: + try: + actual = path.read_text(encoding="utf-8") + except OSError: + print(f"GAP missing generated evidence: {path}") + failures += 1 + continue + if actual != expected: + print(f"GAP stale generated evidence: {path}") + failures += 1 + continue + print(f"CURRENT {relative}") + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(expected, encoding="utf-8") + print(path) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main())