What
ref/serialization.md states the invariant that the flat JSON-LD bundle and the holon "are two forms of one Dataset and name the same things the same way". It is violated in two places, by the same mechanism.
Concepts. Expanded to absolute IRIs, from the committed coffee-roaster sample:
library concept
flat https://robosystems.ai/taxonomy/rs-gaap/v1/Assets
holon https://robosystems.ai/taxonomy/rs-gaap/v1/Assets ✓ same node
tenant concept
flat https://robosystems.ai/concept/driftline:InventoryRawMaterials
holon https://robosystems.ai/taxonomy/driftline/InventoryRawMaterials ✗ two nodes
Five concepts in the SaaS demo, eight in the coffee one. The flat form's IRI also carries a colon inside a path segment, which no consumer will read as a QName.
Structures. The same report's equity statement, from the two forms:
flat structureName / prefLabel = "rs-gaap — Statement of Changes in Equity — Roll Forward (Total)"
holon structureName / prefLabel = "0004 - Statement - Statement of Changes in Equity"
The holon's writer sets both to the composed sort key, so the form name — Classified, Multi-step, Indirect, Roll Forward — is absent from the holon entirely. For a ledger report that is not decoration: it says which of several presentations of the same accounts this is. It is recoverable only from the role-URI slug (…/Equity-rollforward). There is no rs:roleDefinition or documentation term beside it. This is finding 3 of specs/xbrlkit/three-renderings-disagree.md, folded in here.
Why
rdf/jsonld.py::_concept_uri resolves a prefix through a static _PREFIX_NS map and falls through when it misses:
def _concept_uri(qname: str) -> URIRef:
if ":" not in qname:
return URIRef(f"https://robosystems.ai/concept/{qname}")
prefix, local = qname.split(":", 1)
ns = _PREFIX_NS.get(prefix)
if ns is None:
return URIRef(f"https://robosystems.ai/concept/{qname}") # ← every tenant
return URIRef(str(ns) + local)
A tenant namespace is https://robosystems.ai/taxonomy/<tenant>/, one per tenant and minted with it, so a static table can never list them — the rule has to be general. The holon path already is: xbrlkit's writer mints …/taxonomy/<tenant>/<local> and declares the prefix in the document's @context.
This is the third instance of one defect class. The same static-table assumption produced the label loss fixed in #1391 (Element.name unread for tenant concepts) and the bare-local-name display fixed in report-components#54 (qname() could not compact a tenant namespace). Both of those were renderer- or label-side; this one is identity.
The structure half has a different mechanism but the same shape: one writer composes a name the other doesn't, and neither carries the field the other has.
Scope - Affected
Scope - Out of Scope
- The XBRL 2.1 package's
link:definition (finding 2 of the same spec). No reader: the flavor is write-only in the platform, nothing calls classify_network on a package, and the demos' only reader is Arelle checking structure. Stays parked in that spec with its own trigger.
- report-components' display compaction — fixed in 0.6.0, a different layer.
Acceptance Criteria
Notes
The concept half was found by the invariant gate built for #1393 — written to catch content drift, it surfaced a live defect on its first run.
Both halves want the same five-set sample regeneration, which is the expensive part (~2.5× the 10,372/10,552-line diff that two demos cost in #1395). That is the reason they are one issue and not two.
Worth deciding at the same time whether @context in the flat form should declare the tenant prefix. The holon does, which is why its driftline:X round-trips; without it the flat form would carry a long absolute IRI where the holon carries a compact one — same node, different ergonomics.
Related Issues
What
ref/serialization.mdstates the invariant that the flat JSON-LD bundle and the holon "are two forms of one Dataset and name the same things the same way". It is violated in two places, by the same mechanism.Concepts. Expanded to absolute IRIs, from the committed coffee-roaster sample:
Five concepts in the SaaS demo, eight in the coffee one. The flat form's IRI also carries a colon inside a path segment, which no consumer will read as a QName.
Structures. The same report's equity statement, from the two forms:
The holon's writer sets both to the composed sort key, so the form name — Classified, Multi-step, Indirect, Roll Forward — is absent from the holon entirely. For a ledger report that is not decoration: it says which of several presentations of the same accounts this is. It is recoverable only from the role-URI slug (
…/Equity-rollforward). There is nors:roleDefinitionor documentation term beside it. This is finding 3 ofspecs/xbrlkit/three-renderings-disagree.md, folded in here.Why
rdf/jsonld.py::_concept_uriresolves a prefix through a static_PREFIX_NSmap and falls through when it misses:A tenant namespace is
https://robosystems.ai/taxonomy/<tenant>/, one per tenant and minted with it, so a static table can never list them — the rule has to be general. The holon path already is: xbrlkit's writer mints…/taxonomy/<tenant>/<local>and declares the prefix in the document's@context.This is the third instance of one defect class. The same static-table assumption produced the label loss fixed in #1391 (
Element.nameunread for tenant concepts) and the bare-local-name display fixed in report-components#54 (qname()could not compact a tenant namespace). Both of those were renderer- or label-side; this one is identity.The structure half has a different mechanism but the same shape: one writer composes a name the other doesn't, and neither carries the field the other has.
Scope - Affected
robosystems/operations/serialization/rdf/jsonld.py—_concept_uri, and whether the emitted@contextshould declare the tenant prefix the way the holon's doesstructureName/skos:prefLabelfrom the producer's own name, the composed definition under its own term, and the reader restoring both (this also removes thers:structureOrderdrift on round-trip: the reader takesstructureNameas the definition and the numeric prefix is gone)examples/*/sample_output/— one full five-set regeneration covers both halvestests/operations/serialization/test_sample_artifact_invariants.py—test_both_forms_name_the_same_conceptsisxfail(strict=True)on the two demos that author concepts; a structure-name invariant belongs beside itScope - Out of Scope
link:definition(finding 2 of the same spec). No reader: the flavor is write-only in the platform, nothing callsclassify_networkon a package, and the demos' only reader is Arelle checking structure. Stays parked in that spec with its own trigger.Acceptance Criteria
test_both_forms_name_the_same_conceptspasses with thexfailmarker removed, and a structure-name invariant is added and passes.Notes
The concept half was found by the invariant gate built for #1393 — written to catch content drift, it surfaced a live defect on its first run.
Both halves want the same five-set sample regeneration, which is the expensive part (~2.5× the 10,372/10,552-line diff that two demos cost in #1395). That is the reason they are one issue and not two.
Worth deciding at the same time whether
@contextin the flat form should declare the tenant prefix. The holon does, which is why itsdriftline:Xround-trips; without it the flat form would carry a long absolute IRI where the holon carries a compact one — same node, different ergonomics.Related Issues
specs/xbrlkit/three-renderings-disagree.md(vault)