diff --git a/tools/fiber_projection.py b/tools/fiber_projection.py index af05f02..be62cea 100644 --- a/tools/fiber_projection.py +++ b/tools/fiber_projection.py @@ -40,6 +40,10 @@ # --- evidence / provenance axis (SP_RETR_FIBER_001_axis_binding §2.3) --------- # EPISTEMIC_DERIVED = "derived" # recovered via projection, not observed as source authorship +# Claim atoms are stored as value-envelope keys `attr:claim:` (§3.4.1); to_bundle emits +# them as K lines. Mirrors fiber_retrieval.CLAIM_PREFIX. +CLAIM_PREFIX = "attr:claim:" + # Crystal Atlas required fields (verified against the real schemas, BINDING.md §1). _GRAPH_NODE_REQUIRED = ("node_id", "tenant_id", "node_kind", "display_name") _EVIDENCE_REQUIRED = ("evidence_id", "tenant_id", "source_ref", "anchor_ref") @@ -299,20 +303,27 @@ def unanchored_relational_endpoints(g: ProjectedGraph) -> list: def to_bundle(g: ProjectedGraph) -> str: """Serialize the composite graph H to a tab-delimited, node_id-keyed bundle: - Nnode_idnode_kind + Nnode_idnode_kind (atom) Cparent_node_idchild_node_id (E^⊑, containment) Rrel_typesrc_node_iddst_node_id (E_R, relational) - - It is keyed by the stable string node_id (NOT the Python-internal u128 atom id), so a - Rust ingest adapter (`hg_fiber::ingest_bundle`) rebuilds the SAME two-edge-class graph on - the hellgraph substrate, minting its own atom ids. This is the parity contract: - fiber_projection is the reference oracle, hg_fiber is the real engine, this bundle is what - they must agree on. Deterministic (sorted) so it is a stable golden vector. Structural - only in v0 — anchors/labels/claims layer in once the Rust value-write path is bound. + Anode_idanchor_ref (page anchor — provenance-of-location) + Knode_idclaim_varvalueegrade (claim atom — for the fiber-product verdict) + + Keyed by the stable string node_id (NOT the Python-internal u128 atom id), so the Rust + ingest adapter (`hg_fiber::ingest_bundle`) rebuilds the SAME graph on the hellgraph + substrate, minting its own atom ids. The parity contract: fiber_projection is the reference + oracle, hg_fiber is the real engine, this bundle is what they must agree on. Deterministic + (sorted) so it is a stable golden vector. + + N/C/R are structure and live in the real hellgraph graph. A/K are fiber-retrieval DOMAIN + data (page anchors, ownership claims) — hellgraph's ValuePayload models only Field/Proof, + not arbitrary strings/scalars, so on the Rust side these ride an `hg_fiber` sidecar next to + the store (they are not core graph facts). That sidecar is what lets the fiber-product + verdict + double grounding run on the substrate, not just in Python. """ rev = {atom_id: node_id for (_tenant, node_id), atom_id in g.id_map.items()} nodes = [f"N\t{rev[a]}\t{g.nodes[a].type_name}" for a in sorted(g.nodes, key=lambda a: rev[a])] - cont, rel = [], [] + cont, rel, anchors, claims = [], [], [], [] for l in g.containment_links(): p = next(t for (r, t, _o) in l.members if r == "parent") c = next(t for (r, t, _o) in l.members if r == "child") @@ -321,4 +332,11 @@ def to_bundle(g: ProjectedGraph) -> str: s = next(t for (r, t, _o) in l.members if r == "src") d = next(t for (r, t, _o) in l.members if r == "dst") rel.append(f"R\t{l.type_name}\t{rev[s]}\t{rev[d]}") - return "\n".join(nodes + sorted(cont) + sorted(rel)) + "\n" + for v in g.values: + node_id = rev[v.subject_atom] + if v.key == "anchor": + anchors.append(f"A\t{node_id}\t{v.payload}") + elif v.key.startswith(CLAIM_PREFIX): + var = v.key[len(CLAIM_PREFIX):] + claims.append(f"K\t{node_id}\t{var}\t{v.payload['value']}\t{v.payload['egrade']}") + return "\n".join(nodes + sorted(cont) + sorted(rel) + sorted(anchors) + sorted(claims)) + "\n" diff --git a/tools/tests/fixtures/fiber_ownership.bundle b/tools/tests/fixtures/fiber_ownership.bundle index 06f49d5..19e4db9 100644 --- a/tools/tests/fixtures/fiber_ownership.bundle +++ b/tools/tests/fixtures/fiber_ownership.bundle @@ -9,3 +9,7 @@ C filing-A/s4.2 entity/parentco C filing-B/root filing-B/s2.1 C filing-B/s2.1 entity/subco R gleif-L2:isDirectParentOf entity/parentco entity/subco +A entity/parentco filing-A#p87§4.2 +A entity/subco filing-B#p14§2.1 +K entity/parentco owns_pct:parentco|subco 100 verified +K entity/subco owns_pct:parentco|subco 100 verified diff --git a/tools/tests/fixtures/fiber_ownership_dag.json b/tools/tests/fixtures/fiber_ownership_dag.json index f62dbf4..882c970 100644 --- a/tools/tests/fixtures/fiber_ownership_dag.json +++ b/tools/tests/fixtures/fiber_ownership_dag.json @@ -4,82 +4,147 @@ "nodes": [ { "graph_node": { - "node_id": "filing-A/root", "tenant_id": "acme-tenant", "node_kind": "document", + "node_id": "filing-A/root", + "tenant_id": "acme-tenant", + "node_kind": "document", "display_name": "ParentCo Inc. Form 10-K (FY2025)", "distribution_class": "public_derived", - "created_at": "2026-02-14T00:00:00Z", "updated_at": "2026-02-14T00:00:00Z" + "created_at": "2026-02-14T00:00:00Z", + "updated_at": "2026-02-14T00:00:00Z" }, "confidentiality_class": "public" }, { "graph_node": { - "node_id": "filing-A/s4.2", "tenant_id": "acme-tenant", "node_kind": "clause", + "node_id": "filing-A/s4.2", + "tenant_id": "acme-tenant", + "node_kind": "clause", "display_name": "Item 4.2 — Subsidiaries and Controlled Entities", "distribution_class": "public_derived", - "created_at": "2026-02-14T00:00:00Z", "updated_at": "2026-02-14T00:00:00Z" + "created_at": "2026-02-14T00:00:00Z", + "updated_at": "2026-02-14T00:00:00Z" }, "confidentiality_class": "firm_approved" }, { "graph_node": { - "node_id": "entity/parentco", "tenant_id": "acme-tenant", "node_kind": "organization", + "node_id": "entity/parentco", + "tenant_id": "acme-tenant", + "node_kind": "organization", "display_name": "ParentCo Inc.", - "aliases": ["ParentCo", "ParentCo Incorporated"], - "attributes": { "lei": "5493001KJTIIGC8Y1R12", "jurisdiction": "US-DE" }, + "aliases": [ + "ParentCo", + "ParentCo Incorporated" + ], + "attributes": { + "lei": "5493001KJTIIGC8Y1R12", + "jurisdiction": "US-DE", + "claim:owns_pct:parentco|subco": { + "value": 100, + "egrade": "verified" + } + }, "distribution_class": "public_derived", - "created_at": "2026-02-14T00:00:00Z", "updated_at": "2026-02-14T00:00:00Z" + "created_at": "2026-02-14T00:00:00Z", + "updated_at": "2026-02-14T00:00:00Z" }, "confidentiality_class": "firm_approved", "evidence": { - "evidence_id": "ev-parentco-42", "tenant_id": "acme-tenant", - "source_ref": "filing-A", "anchor_ref": "filing-A#p87§4.2", - "observed_at": "2026-02-14T00:00:00Z", "ingested_at": "2026-07-04T00:00:00Z", - "extractor_ref": "tree-adapter/sec-10k@v0", "confidence": 0.98, - "distribution_class": "public_derived", "receipt_ref": "rcpt-ev-parentco-42" + "evidence_id": "ev-parentco-42", + "tenant_id": "acme-tenant", + "source_ref": "filing-A", + "anchor_ref": "filing-A#p87§4.2", + "observed_at": "2026-02-14T00:00:00Z", + "ingested_at": "2026-07-04T00:00:00Z", + "extractor_ref": "tree-adapter/sec-10k@v0", + "confidence": 0.98, + "distribution_class": "public_derived", + "receipt_ref": "rcpt-ev-parentco-42" } }, { "graph_node": { - "node_id": "filing-B/root", "tenant_id": "acme-tenant", "node_kind": "document", + "node_id": "filing-B/root", + "tenant_id": "acme-tenant", + "node_kind": "document", "display_name": "SubCo LLC Form 10-K (FY2025)", "distribution_class": "internal_private", - "created_at": "2026-03-01T00:00:00Z", "updated_at": "2026-03-01T00:00:00Z" + "created_at": "2026-03-01T00:00:00Z", + "updated_at": "2026-03-01T00:00:00Z" }, "confidentiality_class": "client_confidential" }, { "graph_node": { - "node_id": "filing-B/s2.1", "tenant_id": "acme-tenant", "node_kind": "clause", + "node_id": "filing-B/s2.1", + "tenant_id": "acme-tenant", + "node_kind": "clause", "display_name": "Item 2.1 — Ownership and Capital Structure", "distribution_class": "internal_private", - "created_at": "2026-03-01T00:00:00Z", "updated_at": "2026-03-01T00:00:00Z" + "created_at": "2026-03-01T00:00:00Z", + "updated_at": "2026-03-01T00:00:00Z" }, "confidentiality_class": "client_confidential" }, { "graph_node": { - "node_id": "entity/subco", "tenant_id": "acme-tenant", "node_kind": "organization", + "node_id": "entity/subco", + "tenant_id": "acme-tenant", + "node_kind": "organization", "display_name": "SubCo LLC", - "attributes": { "lei": "213800WSGIIZCXF1P572", "jurisdiction": "US-DE" }, + "attributes": { + "lei": "213800WSGIIZCXF1P572", + "jurisdiction": "US-DE", + "claim:owns_pct:parentco|subco": { + "value": 100, + "egrade": "verified" + } + }, "distribution_class": "internal_private", - "created_at": "2026-03-01T00:00:00Z", "updated_at": "2026-03-01T00:00:00Z" + "created_at": "2026-03-01T00:00:00Z", + "updated_at": "2026-03-01T00:00:00Z" }, "confidentiality_class": "matter_restricted", "evidence": { - "evidence_id": "ev-subco-21", "tenant_id": "acme-tenant", - "source_ref": "filing-B", "anchor_ref": "filing-B#p14§2.1", - "observed_at": "2026-03-01T00:00:00Z", "ingested_at": "2026-07-04T00:00:00Z", - "extractor_ref": "tree-adapter/sec-10k@v0", "confidence": 0.95, - "distribution_class": "internal_private", "receipt_ref": "rcpt-ev-subco-21" + "evidence_id": "ev-subco-21", + "tenant_id": "acme-tenant", + "source_ref": "filing-B", + "anchor_ref": "filing-B#p14§2.1", + "observed_at": "2026-03-01T00:00:00Z", + "ingested_at": "2026-07-04T00:00:00Z", + "extractor_ref": "tree-adapter/sec-10k@v0", + "confidence": 0.95, + "distribution_class": "internal_private", + "receipt_ref": "rcpt-ev-subco-21" } } ], "edges": [ - { "class": "containment", "parent": "filing-A/root", "child": "filing-A/s4.2" }, - { "class": "containment", "parent": "filing-A/s4.2", "child": "entity/parentco" }, - { "class": "containment", "parent": "filing-B/root", "child": "filing-B/s2.1" }, - { "class": "containment", "parent": "filing-B/s2.1", "child": "entity/subco" }, - { "class": "relational", "type_name": "gleif-L2:isDirectParentOf", - "src": "entity/parentco", "dst": "entity/subco" } + { + "class": "containment", + "parent": "filing-A/root", + "child": "filing-A/s4.2" + }, + { + "class": "containment", + "parent": "filing-A/s4.2", + "child": "entity/parentco" + }, + { + "class": "containment", + "parent": "filing-B/root", + "child": "filing-B/s2.1" + }, + { + "class": "containment", + "parent": "filing-B/s2.1", + "child": "entity/subco" + }, + { + "class": "relational", + "type_name": "gleif-L2:isDirectParentOf", + "src": "entity/parentco", + "dst": "entity/subco" + } ] } diff --git a/tools/tests/test_fiber_projection.py b/tools/tests/test_fiber_projection.py index 0ee3d26..290fdb1 100644 --- a/tools/tests/test_fiber_projection.py +++ b/tools/tests/test_fiber_projection.py @@ -143,8 +143,13 @@ def test_edge_referencing_unknown_node_rejected(): def test_to_bundle_matches_golden_parity_vector(): # The bundle is the contract the Rust hg_fiber ingest must reproduce byte-for-byte. g = fp.project(_load()) + b = fp.to_bundle(g) with open(os.path.join(_HERE, "fixtures", "fiber_ownership.bundle"), encoding="utf-8") as fh: - assert fp.to_bundle(g) == fh.read() - # keyed by node_id (not the internal u128 atom id) so a different engine can rebuild H. - assert "N\tentity/parentco\torganization" in fp.to_bundle(g) - assert "R\tgleif-L2:isDirectParentOf\tentity/parentco\tentity/subco" in fp.to_bundle(g) + assert b == fh.read() + # structure — keyed by node_id (not the internal u128 atom id) so a different engine rebuilds H. + assert "N\tentity/parentco\torganization" in b + assert "R\tgleif-L2:isDirectParentOf\tentity/parentco\tentity/subco" in b + # domain data for double-grounding on the substrate: anchors (A) + claims (K). + assert "A\tentity/parentco\tfiling-A#p87§4.2" in b + assert "K\tentity/parentco\towns_pct:parentco|subco\t100\tverified" in b + assert "K\tentity/subco\towns_pct:parentco|subco\t100\tverified" in b