Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions tools/fiber_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:<var>` (§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")
Expand Down Expand Up @@ -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:

N<TAB>node_id<TAB>node_kind
N<TAB>node_id<TAB>node_kind (atom)
C<TAB>parent_node_id<TAB>child_node_id (E^⊑, containment)
R<TAB>rel_type<TAB>src_node_id<TAB>dst_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.
A<TAB>node_id<TAB>anchor_ref (page anchor — provenance-of-location)
K<TAB>node_id<TAB>claim_var<TAB>value<TAB>egrade (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")
Expand All @@ -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"
4 changes: 4 additions & 0 deletions tools/tests/fixtures/fiber_ownership.bundle
Original file line number Diff line number Diff line change
Expand Up @@ -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
127 changes: 96 additions & 31 deletions tools/tests/fixtures/fiber_ownership_dag.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
13 changes: 9 additions & 4 deletions tools/tests/test_fiber_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading