extensionless ‘…/spec#’ would not be served as Turtle and the identifier

would resolve to nothing. spec-publish stages the graph to that path.

spec_base: https://zwelz3.github.io/holonic/spec.ttl# version: 0.9.0 status: prototype authors: [zwelz3]

Intent

Provide a Python library that implements Cagel’s four-graph holon model on top of RDF named graphs, enabling distributed knowledge systems to expose genuine inside/outside distinction, governed traversal, and event-level provenance without inventing new query languages or wire protocols. The library is a client-side coordination layer in front of any quad-aware graph store (rdflib, Apache Jena Fuseki, Oxigraph, GraphDB) — not a graph database itself.

Purpose

Make it practical to build holarchies for digital engineering, enterprise knowledge graphs, and agentic memory systems. A holon is a named IRI whose four layers (interior, boundary, projection, context) live as named graphs in the quad store; portals are RDF entities carrying SPARQL CONSTRUCT queries; membranes are SHACL shapes. All state lives in the graph. Python is a thin convenience layer over SPARQL and PROV-O.

Requirements

R1 Four-Graph Model

  • R1.1 Each holon MUST have an IRI that threads through four named-graph layers: interior, boundary, projection, and context.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset

    • governs: cga:Holon

    • acceptance: Given a fresh HolonicDataset, when add_holon(iri, label) is called, then the holon’s IRI is registered as cga:Holon and its four layer graphs resolve via cga:hasInterior, cga:hasBoundary, cga:hasProjection, and cga:hasContext.

    • verifiedBy: src/holonic/test/test_deprecation_and_dispatch.py::test_add_holon_without_holon_type

  • R1.2 A holon MAY have multiple named graphs in any one layer role (e.g. urn:holon:x/interior/radar and urn:holon:x/interior/fusion); operations that read a layer MUST treat the set as a union.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset

    • governs: cga:LayerRole

    • acceptance: Given a holon with two interior graphs added with different graph_iri values, when a SPARQL union query ranges over GRAPH ?g patterns, then triples from both graphs match.

    • verifiedBy: src/holonic/test/test_client_projections.py::TestProjectHolon::test_multiple_interiors_are_merged

  • R1.3 Layer membership MUST be declared via cga:hasInterior, cga:hasBoundary, cga:hasProjection, cga:hasContext. Layer graph IRIs MUST be discoverable by SPARQL from the holon IRI; discovery by IRI suffix convention is forbidden as a correctness mechanism.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset, cga.ttl

    • acceptance: Given a holon with all four layers populated, when the registry is queried with SPARQL following cga:hasInterior et al., then every layer graph IRI is returned without relying on string patterns.

    • verifiedBy: src/holonic/test/test_holon.py::TestAllLayers::test_all_four_layers_on_one_holon

  • R1.4 The library MUST NOT flatten named graphs to a single default graph at any layer in the stack. Every triple belongs to a named graph.

    • priority: MUST

    • implementation: verified

    • verifiedBy: src/holonic/test/test_backend.py::TestNoDefaultGraphFlattening::test_full_lifecycle_writes_nothing_to_the_default_graph

    • constrains: HolonicDataset, HolonicStore

    • acceptance: Given any library-mediated write, when inspecting the backend’s default graph, then no triples produced by the library appear there.

R2 Store Protocol

  • R2.1 A HolonicStore protocol MUST provide quad-aware named-graph CRUD (graph_exists, get_graph, put_graph, post_graph, delete_graph, parse_into, list_named_graphs) and SPARQL dispatch (query, construct, ask, update).

    • priority: MUST

    • implementation: verified

    • constrains: HolonicStore, RdflibBackend, FusekiBackend

    • acceptance: Given an RdflibBackend instance, when isinstance checked against HolonicStore, then the check passes; every listed method exists and accepts the declared signature.

    • verifiedBy: src/holonic/test/test_backend.py::TestProtocolConformance::test_rdflib_backend_implements_protocol

  • R2.2 An in-memory backend (RdflibBackend) MUST be shipped so the library is usable with no running server.

    • priority: MUST

    • implementation: verified

    • constrains: RdflibBackend

    • acceptance: Given only the base pip install holonic, when HolonicDataset() is called without arguments, then a working instance is returned with RdflibBackend bound as the default.

    • verifiedBy: src/holonic/test/test_backend.py::TestRdflibBackend

  • R2.3 An HTTP backend (FusekiBackend) MUST be shipped for Apache Jena Fuseki via SPARQL 1.1 and the W3C Graph Store Protocol.

    • priority: MUST

    • implementation: verified

    • constrains: FusekiBackend

    • acceptance: Given a running Fuseki server, when FusekiBackend(url, dataset=ds) is instantiated and used with HolonicDataset, then CRUD and SPARQL operations round-trip through HTTP against the server.

    • verifiedBy: src/holonic/test/test_fuseki_integration.py::TestFusekiRoundTrip

  • R2.4 FusekiBackend MUST accept extra_headers at construction so external orchestrators can pass bearer tokens, mTLS hints, or tenant identifiers through to Fuseki.

    • priority: MUST

    • implementation: verified

    • constrains: FusekiBackend, _fuseki_client

    • acceptance: Given FusekiBackend(url, dataset=ds, extra_headers={“Authorization”: “Bearer x”}), when any HTTP request is issued, then the header is present on the outbound request.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestFusekiBackendExtraHeaders::test_fuseki_client_stores_extra_headers

  • R2.5 The protocol MUST remain synchronous. Async consumers wrap calls in their own thread-pool bridge; an async variant, if added, MUST be a separate protocol and not replace the sync one.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicStore

    • acceptance: Given the HolonicStore protocol definition, when method signatures are inspected, then none are declared with async def and no awaitables are returned.

    • verifiedBy: src/holonic/backends/store.py (structural, enforced by type system)

  • R2.6 Backend method return types MUST use rdflib.Graph for CONSTRUCT and graph-fetch operations to preserve rdflib’s serialization, iteration, and SPARQL evaluation surface.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicStore, RdflibBackend, FusekiBackend

    • acceptance: Given store.get_graph() or store.construct() is called, when the return value is inspected, then it is an rdflib.Graph instance supporting serialize(), iteration, and further SPARQL queries.

    • verifiedBy: src/holonic/test/test_client_projections.py::TestRawSparqlPassthrough::test_construct_returns_graph

R3 CGA Ontology and Membrane Validation

  • R3.1 The package MUST ship the Context-Graph Architecture (CGA) ontology (cga.ttl) and SHACL shapes (cga-shapes.ttl) as package data.

    • priority: MUST

    • implementation: verified

    • constrains: cga.ttl, cga-shapes.ttl, package-data declaration

    • acceptance: Given pip install holonic, when the package is introspected, then both TTL files are present as resources under holonic.ontology and parse cleanly with rdflib.

    • verifiedBy: src/holonic/test/test_ontology.py::TestOntologyFilesExist

  • R3.2 The ontology MUST define cga:Holon and its functional subclasses (cga:DataHolon, cga:AlignmentHolon, cga:AgentHolon, cga:GovernanceHolon, cga:AggregateHolon, cga:IndexHolon), cga:Portal and its subclasses (cga:TransformPortal, cga:IconPortal, cga:SealedPortal), cga:LayerGraph, and cga:LayerRole. Portal subtype semantics MUST be enforced by SHACL shapes in cga-shapes.ttl: cga:TransformPortal requires exactly one cga:constructQuery; cga:IconPortal and cga:SealedPortal must not carry cga:constructQuery (carrying one is semantically incoherent because an IconPortal is purely referential and a SealedPortal blocks traversal).

    • priority: MUST

    • implementation: verified

    • constrains: cga.ttl

    • acceptance: Given cga.ttl, when SPARQL queries every declared class, then all listed class IRIs are returned with their subclass relationships intact.

    • verifiedBy: src/holonic/test/test_ontology.py::TestOntologyAutoLoaded

  • R3.3 The ontology MUST use RDFS plus minimal OWL (class hierarchy, domain/range). The library MUST NOT depend on an OWL reasoner for correctness of any method.

    • priority: MUST

    • implementation: verified

    • constrains: cga.ttl, HolonicDataset

    • acceptance: Given an rdflib-backed HolonicDataset, when the entailment extra is not installed, then every library method produces correct results without a reasoner.

    • verifiedBy: src/holonic/test/ (entire suite runs without owlrl installed)

  • R3.4 validate_membrane(holon_iri) MUST run pyshacl against the union of the holon’s interior graphs using the union of its boundary graphs and MUST return a MembraneResult with one of Intact / Weakened / Compromised.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.validate_membrane, MembraneResult

    • acceptance: Given a holon whose interior violates a boundary shape, when validate_membrane is called, then result.health is MembraneHealth.Compromised and result.violations lists the specific SHACL failures.

    • verifiedBy: src/holonic/test/test_membrane.py::TestMembraneValidation::test_compromised_membrane

  • R3.5 Membrane health MUST be recordable in the context graph as cga:membraneHealth on a prov:Activity.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.record_validation, cga.ttl

    • acceptance: Given a governed traversal with validate=True, when the activity is read from the context graph, then cga:membraneHealth is present and matches the MembraneResult.health value.

    • verifiedBy: src/holonic/test/test_audit.py::TestCollectAuditTrail::test_validation_recorded_in_trail

  • R3.6 Membrane validation MUST fail closed on SHACL result severity. A result whose sh:resultSeverity is sh:Violation, or is absent, or is any term other than sh:Warning and sh:Info, MUST be counted as a violation; sh:Warning results MUST lower health to Weakened; sh:Info results MUST be reported in MembraneResult.infos and MUST NOT lower health. MembraneResult.conforms MUST be false exactly when the SHACL report holds a result above sh:Info. validate_membrane() and dry_run() MUST apply the same rule.

    • priority: MUST

    • implementation: verified

    • constrains: holonic.client._parse_shacl_report, holonic.client._health_from_report, MembraneResult

    • acceptance: Given a boundary shape that declares a custom severity and an interior that violates it, when traverse() is called with fail_on_breach=True, then MembraneBreachError is raised.

    • verifiedBy: src/holonic/test/test_membrane.py::TestSeverityFailsClosed

    • verifiedBy: src/holonic/test/test_membrane.py::TestConformsAgreesAcrossCallers

  • R3.7 Membrane validation MUST report each typed interior node that no boundary shape targets as an UntargetedNode in MembraneResult.untargeted. Targeting follows SHACL Core (sh:targetClass with rdfs:subClassOf* in the data graph, implicit class targets, sh:targetNode, sh:targetSubjectsOf, sh:targetObjectsOf); a boundary with a SPARQL-based target skips the check. A class named by cga:permitsType in the holon’s boundary, and its subclasses, MUST be exempt. The severity MUST be the holon’s cga:untargetedTypeSeverity when declared, resolved as R3.6 resolves SHACL severities; otherwise Info for validate_membrane() and dry_run(), and Violation for the nodes traverse(fail_on_breach=True) injects.

    • priority: MUST

    • implementation: verified

    • constrains: holonic._membrane.find_untargeted, holonic._membrane.untargeted_policy, UntargetedNode, MembraneResult

    • acceptance: Given a portal that injects a node whose type no boundary shape targets, when traverse() is called with fail_on_breach=True, then MembraneBreachError is raised and the target interior is unchanged.

    • verifiedBy: src/holonic/test/test_membrane.py::TestUntargetedTypes

R4 Portal and Traversal Semantics

  • R4.1 Portals MUST be first-class RDF entities stored in boundary graphs with cga:sourceHolon, cga:targetHolon, and (for cga:TransformPortal) cga:constructQuery.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.add_portal

    • governs: cga:Portal, cga:TransformPortal

    • acceptance: Given add_portal(iri, source, target, construct_query, label), when the registry is queried, then the portal is found with cga:sourceHolon, cga:targetHolon, and cga:constructQuery as RDF triples.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestGetPortal::test_returns_construct_query

  • R4.2 Portal discovery MUST be SPARQL-driven (find_portals_from, find_portals_to, find_portal, find_path). Python-side iteration over cached portal objects is forbidden.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset discovery methods, sparql.py templates

    • acceptance: Given portals in the registry, when find_portals_from(iri) is called, then results come from a SPARQL SELECT executed against the store, not from a Python cache.

    • verifiedBy: src/holonic/test/test_portal.py::TestPortalDiscovery::test_find_portals_from

  • R4.3 traverse_portal(portal_iri) MUST execute the portal’s CONSTRUCT query against the quad store and MAY inject the result into a target named graph.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.traverse_portal

    • acceptance: Given a registered portal and inject_into=target_graph, when traverse_portal is called, then the CONSTRUCT’s triples appear in the target graph and the CONSTRUCT result Graph is returned.

    • verifiedBy: src/holonic/test/test_portal.py::TestPortalTraversal::test_traverse_portal_injects_into_target

  • R4.4 traverse(source, target, validate=True) MUST compose discovery, traversal, membrane validation, and provenance recording as one governed operation. Validation failure MUST NOT inject data into the target interior.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.traverse

    • acceptance: Given a boundary shape that rejects the source data, when traverse(source, target, validate=True) is called, then the target interior contains no new triples and the returned membrane result is Compromised.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestFailClosedTraversal::test_fail_on_breach_leaves_interior_byte_identical

  • R4.5 Multi-hop path finding (find_path) MUST discover a portal route from source to target. The current implementation fetches the portal adjacency list via Q.ALL_PORTALS and runs Python BFS. Deferred to 0.8.0: migrate to a SPARQL property-path query or cache the adjacency list across calls.

    • priority: MUST

    • implementation: in-progress

    • constrains: HolonicDataset.find_path, sparql.py

    • acceptance: Given a chain of three holons linked by portals, when find_path(source, target) is called, then a list of PortalInfo objects representing the path is returned.

    • verifiedBy: src/holonic/test/test_portal.py::TestPathFinding::test_multi_hop_path

R5 Provenance

  • R5.1 Every governed traversal and membrane validation MUST emit a prov:Activity into the target holon’s context graph, including prov:wasAssociatedWith, prov:used, prov:generated, and prov:startedAtTime.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.traverse, HolonicDataset.record_traversal, HolonicDataset.record_validation

    • acceptance: Given traverse(source, target, agent_iri=”urn:agent:x”), when the context graph is queried, then a prov:Activity exists with all four listed predicates populated.

    • verifiedBy: src/holonic/test/test_audit.py::TestCollectAuditTrail::test_traverse_then_collect

  • R5.2 Graph-to-graph derivation produced by a traversal (target interior derived from source interior) MUST use prov:wasDerivedFrom and MUST be distinct from cga:derivedFrom.

    • priority: MUST

    • implementation: verified

    • verifiedBy: src/holonic/test/test_portal.py::TestTraversalDerivationProvenance::test_traversal_does_not_assert_structural_dependency

    • constrains: HolonicDataset.traverse, cga.ttl

    • acceptance: Given a completed traversal, when the context graph is queried, then the target interior carries prov:wasDerivedFrom pointing to the source interior AND no cga:derivedFrom triple was created as a side effect.

  • R5.3 cga:derivedFrom MUST be reserved for persistent holon-to-holon structural dependency independent of any activity. The two properties coexist; neither replaces the other.

    • priority: MUST

    • implementation: verified

    • verifiedBy: src/holonic/test/test_ontology.py::TestDerivationVocabulary::test_cga_derived_from_is_declared_holon_to_holon

    • verifiedBy: src/holonic/test/test_ontology.py::TestDerivationVocabulary::test_cga_derived_from_is_not_conflated_with_prov

    • constrains: cga.ttl

    • acceptance: Given cga.ttl, when SPARQL queries cga:derivedFrom, then it is declared an owl:ObjectProperty scoped Holon→Holon AND carries no rdfs:subPropertyOf or owl:equivalentProperty axiom relating it to prov:wasDerivedFrom.

  • R5.4 HolonSplit and HolonMerge MUST be modeled as prov:Activity subclasses with prov:used (source holon) and prov:generated (resulting holons).

    • priority: MUST

    • implementation: verified

    • verifiedBy: src/holonic/test/test_ontology.py::TestSplitMergeActivities::test_instance_carries_used_and_generated

    • constrains: cga.ttl

    • acceptance: Given a HolonSplit instance, when SPARQL queries its rdf:type chain, then prov:Activity appears in the class hierarchy and both prov:used and prov:generated are declared.

R6 Console Model

  • R6.1 The holonic.console_model module MUST expose dataclasses tuned for JSON serialization over HTTP: HolonSummary, HolonDetail, ClassInstanceCount, NeighborhoodNode, NeighborhoodEdge, NeighborhoodGraph, PortalSummary, PortalDetail.

    • priority: MUST

    • implementation: verified

    • constrains: console_model.py

    • acceptance: Given each named dataclass, when round-tripped through dataclasses.asdict + json.dumps + json.loads, then no information is lost and no rdflib types leak.

    • verifiedBy: src/holonic/test/test_deprecation_and_dispatch.py::test_holon_summary_to_dict

  • R6.2 NeighborhoodGraph.to_graphology() MUST return a dict matching graphology’s JSON shape so sigma.js can consume it without further transformation.

    • priority: MUST

    • implementation: verified

    • constrains: NeighborhoodGraph.to_graphology

    • acceptance: Given a NeighborhoodGraph with nodes and edges, when to_graphology() is called, then the returned dict has top-level ‘nodes’ and ‘edges’ arrays with the key/attributes shape graphology expects.

    • verifiedBy: src/holonic/test/test_console_model.py::TestNeighborhoodGraphologyShape::test_empty_graph

  • R6.3 HolonicDataset.list_holons_summary() MUST return lightweight summaries via a single SPARQL query with no per-holon fan-out.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.list_holons_summary, sparql.py

    • acceptance: Given N holons in the registry, when list_holons_summary() is called, then exactly one SELECT query is issued against the store and N HolonSummary objects are returned.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestListHolonsSummary::test_returns_summaries

  • R6.4 HolonicDataset.get_holon_detail(iri) MUST return full layer IRIs plus interior triple count.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.get_holon_detail, HolonDetail

    • acceptance: Given a holon with two interior graphs, when get_holon_detail(iri) is called, then the returned HolonDetail lists both interior graph IRIs and interior_triple_count matches the actual triple sum.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestGetHolonDetail::test_returns_layer_iris

  • R6.5 HolonicDataset.holon_interior_classes(iri) MUST return (class_iri, count) pairs computed by SPARQL, not by Python iteration over triples.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.holon_interior_classes, sparql.py

    • acceptance: Given an interior with three distinct rdf:type values, when holon_interior_classes is called, then three ClassInstanceCount pairs come back from a single GROUP BY SPARQL query.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestHolonInteriorClasses::test_counts_distinct_subjects_per_class

  • R6.6 HolonicDataset.holon_neighborhood(iri, depth=1) MUST return a BFS subgraph bounded by portal topology, with depth clamped to a reasonable maximum to bound runaway traversals.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.holon_neighborhood

    • acceptance: Given depth=1000, when holon_neighborhood(iri, depth=1000) is called, then the library clamps to its internal cap (not 1000) and returns within expected time bounds.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestHolonNeighborhood::test_depth_one_includes_portal_neighbors

  • R6.7 HolonicDataset.portal_traversal_history(iri, limit=50) MUST return recent prov:Activity records scoped to the portal, with limit clamped to 10,000.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.portal_traversal_history

    • acceptance: Given limit=50000, when portal_traversal_history is called, then at most 10000 records come back regardless of how many activities exist.

    • verifiedBy: src/holonic/test/test_console_methods.py::TestPortalTraversalHistory::test_records_appear

R7 Projections

  • R7.1 Projection operations MUST support two modes: CONSTRUCT (result stays as RDF, storable in the holarchy) and Pythonic (result exits RDF into LPG-shaped dicts for visualization consumers).

    • priority: MUST

    • implementation: verified

    • constrains: projections.py

    • acceptance: Given a source graph, when build_construct() returns a CONSTRUCT string AND project_to_lpg() returns a ProjectedGraph dict, then both modes are available and produce the documented shapes.

    • verifiedBy: src/holonic/test/test_projections.py::TestProjectToLPG::test_basic_projection

  • R7.2 project_to_lpg(graph, ...) MUST support four independent boolean flags controlling the core simplifications: collapse_types, collapse_literals, resolve_blanks, and resolve_lists. Each flag governs a distinct transformation (types→node annotations, literals→node attributes, blank nodes inlined as nested attributes, RDF lists resolved to Python lists) and each MUST be toggleable independently of the others.

    • priority: MUST

    • implementation: verified

    • constrains: project_to_lpg

    • acceptance: Given a graph with all four RDF features, when project_to_lpg is called with each flag independently toggled, then each feature is transformed only when its corresponding flag is true.

    • verifiedBy: src/holonic/test/test_projections.py::TestProjectToLPG::test_type_collapse

  • R7.3 ProjectionPipeline MUST compose named steps as a sequence of CONSTRUCT queries and Python transforms, with .apply_to_lpg() and .apply_to_graph() terminal methods.

    • priority: MUST

    • implementation: verified

    • constrains: ProjectionPipeline

    • acceptance: Given a pipeline of two CONSTRUCT steps and one transform, when apply_to_graph() is called, then steps run in the declared order and the result reflects all three transformations.

    • verifiedBy: src/holonic/test/test_projections.py::TestProjectionPipeline::test_chained_steps

  • R7.4 project_holon(iri, store_as=...) MUST merge all interior graphs, apply the pipeline, and optionally write the result back to a named graph.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.project_holon

    • acceptance: Given a holon with two interiors, when project_holon(iri, store_as=”urn:out”) is called, then the output graph contains triples derived from both interiors and is registered as a cga:hasProjection layer.

    • verifiedBy: src/holonic/test/test_client_projections.py::TestProjectHolon::test_store_as_writes_named_graph_and_registers_layer

  • R7.5 project_holarchy() MUST project the topology (holons as nodes, portals and cga:memberOf as edges) into an LPG.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.project_holarchy

    • acceptance: Given a holarchy with three holons and two portals and one memberOf relation, when project_holarchy() is called, then the resulting ProjectedGraph has three nodes and three edges.

    • verifiedBy: src/holonic/test/test_client_projections.py::TestProjectHolarchy::test_holarchy_member_of_edges_present

R8 Testing and Distribution

  • R8.1 The default pytest suite MUST pass against RdflibBackend with no external services.

    • priority: MUST

    • implementation: verified

    • constrains: src/holonic/test/

    • acceptance: Given a clean environment with base install only, when pytest src/holonic/test/ is run, then every test passes or is skipped with a declared marker like @pytest.mark.fuseki.

    • verifiedBy: src/holonic/test/ (full suite, 385 passing)

  • R8.2 Fuseki integration tests, if any, MUST be marked with @pytest.mark.fuseki and skipped when Fuseki is unavailable.

    • priority: MUST

    • implementation: in-progress

    • constrains: test_console_methods.py, pyproject.toml pytest config

    • acceptance: Given no Fuseki server, when pytest runs, then Fuseki-tagged tests are skipped rather than errored.

    • verifiedBy: pyproject.toml markers + src/holonic/test/test_console_methods.py usage of importorskip

  • R8.3 Test fixtures MUST use Turtle loaded via backend.parse_into() rather than programmatic rdflib graph construction.

    • priority: MUST

    • implementation: in-progress

    • constrains: src/holonic/test/, STYLE_GUIDE.md

    • acceptance: Given the test suite, when grepped for Graph().add((URIRef, URIRef, …)) patterns, then no production test uses that programmatic style for fixture data; all fixtures are Turtle.

    • verifiedBy: src/holonic/test/ (convention enforced by STYLE_GUIDE.md)

  • R8.4 The package MUST publish to PyPI under holonic and be installable with pip install holonic.

    • priority: MUST

    • implementation: verified

    • constrains: pyproject.toml, release workflow

    • acceptance: Given a clean virtualenv, when pip install holonic is run, then the package installs from PyPI and python -c "import holonic" succeeds.

    • verifiedBy: .github/workflows/release.yml (manual verification at tag time)

  • R8.5 Optional extras (dev, docs, entailment, fuseki, lint, notebooks, test, viz) MUST keep the base install under five seconds on a typical network.

    • priority: SHOULD

    • implementation: verified

    • constrains: pyproject.toml optional-dependencies

    • acceptance: Given a clean virtualenv, when pip install holonic is timed, then installation completes in under five seconds on a 50 Mbps connection.

    • verifiedBy: manual timing at release rehearsal

R9 Next Evolution

Shipped in 0.3.3

  • R9.1 The registry SHOULD carry graph-level operational metadata per layer graph: triple count, last-modified timestamp, and materialized class inventory. HolonicDataset.refresh_metadata(), refresh_all_metadata(), and get_graph_metadata() provide the read/write surface. Shipped with metadata_updates="eager"|"off" policy.

    • priority: SHOULD

    • implementation: verified

    • constrains: HolonicDataset, MetadataRefresher, cga.ttl

    • acceptance: Given a holon with one interior graph, when add_interior runs in eager mode, then get_graph_metadata returns a GraphMetadata with triple_count equal to the parsed count and last_modified within the last second.

    • verifiedBy: src/holonic/test/test_metadata.py::test_eager_mode_refreshes_on_add_interior

  • R9.2 The CGA ontology MUST declare graph-level metadata vocabulary (cga:ClassInstanceCount class; cga:tripleCount, cga:lastModified, cga:refreshedAt, cga:inGraph, cga:class, cga:count, cga:holonLastModified properties).

    • priority: MUST

    • implementation: verified

    • verifiedBy: src/holonic/test/test_ontology.py::TestGraphMetadataVocabulary::test_metadata_property_declared_with_range

    • constrains: cga.ttl section 7

    • acceptance: Given cga.ttl, when SPARQL queries the listed IRIs, then all are declared with appropriate rdfs:domain and rdfs:range.

Shipped in 0.3.4

  • R9.3 The CGA ontology MUST declare graph-category vocabulary so named graphs carry RDF types discoverable via SPARQL. Implemented as the flat+role pattern: cga:HolonicGraph class plus cga:graphRole object property ranged over cga:LayerRole individuals. New individual cga:RegistryRole covers the cross-cutting registry graph. See docs/DECISIONS.md § D-0.3.4-1.

    • priority: MUST

    • implementation: verified

    • constrains: cga.ttl section 8

    • acceptance: Given add_interior(“urn:h”), when the registry is queried, then the interior graph is declared as cga:HolonicGraph with cga:graphRole cga:InteriorRole.

    • verifiedBy: src/holonic/test/test_typed_graphs.py::test_add_interior_types_graph

  • R9.4 The library MUST add a resolve(predicate, from_holon, max_depth, order, limit) method implementing decreasing-priority scope resolution across the holarchy. HolonicDataset.resolve() in holonic.scope. Two predicate classes shipped: HasClassInInterior (uses 0.3.3 class inventory) and CustomSPARQL (escape hatch). Three ordering modes: "network" (default, outbound+inbound portals), "reverse-network", "containment". Strict BFS topology.

    • priority: MUST

    • implementation: verified

    • constrains: holonic.scope, HolonicDataset.resolve

    • acceptance: Given a linear chain of four holons with a target class in the last holon, when resolve(HasClassInInterior(cls), from_holon=first, max_depth=3) is called, then the last holon is returned with distance=3.

    • verifiedBy: src/holonic/test/test_scope.py::test_resolve_walks_outbound_portals

  • R9.5 The library MUST ship a migration CLI to backfill graph-type declarations for pre-0.3.4 deployments. holonic-migrate-registry entry point, idempotent, dry-run by default.

    • priority: MUST

    • implementation: verified

    • constrains: holonic.cli.migrate_registry

    • acceptance: Given a pre-0.3.4 registry without cga:HolonicGraph type declarations, when holonic-migrate-registry --apply is run, then every layer graph gains its type; a second invocation is a no-op.

    • verifiedBy: src/holonic/test/test_typed_graphs.py::test_migration_is_idempotent

Shipped in 0.3.5

  • R9.6 The library MUST ship a projection plugin system. Projection pipelines are RDF-modeled specs (cga:ProjectionPipelineSpec) in the registry graph, carrying an rdf:List of cga:ProjectionPipelineStep entries. Python transforms are discovered via the holonic.projections entry-point group (both first-party and third-party). Pipelines reference transforms by registered name; registration validates name resolution.

    • priority: MUST

    • implementation: verified

    • constrains: holonic.plugins, HolonicDataset.register_pipeline, cga.ttl section 9

    • acceptance: Given a ProjectionPipelineSpec with one step referencing transform “strip_blank_nodes”, when register_pipeline(spec) is called, then the spec is written to the registry as cga:ProjectionPipelineSpec and the transform is resolved via the entry-point registry.

    • verifiedBy: src/holonic/test/test_plugins.py::test_register_pipeline_validates_transform_names

  • R9.7 The library MUST ship HolonicDataset.register_pipeline(), attach_pipeline(), list_pipelines(), get_pipeline(), and run_projection(). Running a pipeline emits a prov:Activity in the target holon’s context graph with loose version tracking (cga:transformVersion) and host metadata (cga:runHost, cga:runPlatform, cga:runPythonVersion, cga:runHolonicVersion). Running with store_as refreshes metadata on the output graph per D-0.3.3-5.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset, holonic.plugins.host_metadata

    • acceptance: Given a registered pipeline attached to a holon, when run_projection(holon, spec, store_as=out) is called, then a prov:Activity appears in the holon’s context graph with all four cga:run* host metadata predicates populated.

    • verifiedBy: src/holonic/test/test_plugins.py::test_run_projection_records_provenance

Shipped in 0.4.0

  • R9.8 The backend protocol MUST split into a mandatory core and an optional mixin. Implemented as HolonicStore (Protocol, mandatory) + AbstractHolonicStore (ABC, recommended base with hook points for optional methods). Backends declaring only the Protocol surface continue to work via the library’s Python-fallback helpers. Optional native methods are dispatched via hasattr. The initial optional surface is a single method: refresh_graph_metadata(graph_iri, registry_iri).

    • priority: MUST

    • implementation: verified

    • constrains: holonic.backends.store, MetadataRefresher

    • acceptance: Given a store with a refresh_graph_metadata method, when MetadataRefresher.refresh_graph runs, then the native method is invoked; given a store without it, the Python fallback runs instead.

    • verifiedBy: src/holonic/test/test_deprecation_and_dispatch.py::test_metadata_refresher_dispatches_to_native

  • R9.9 The library MUST preserve GraphBackend as a deprecated alias through the 0.4.x series and provide a migration guide (docs/MIGRATION.md). Deprecation warnings are suppressible via HOLONIC_SILENCE_DEPRECATION=1. Superseded by R9.18 in 0.5.0: the alias was removed as planned.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/init.py, holonic/backends/init.py, holonic/backends/protocol.py

    • acceptance: Given holonic 0.5.0, when code imports GraphBackend, then an AttributeError is raised (alias removed per R9.18).

    • verifiedBy: src/holonic/test/test_deprecation_and_dispatch.py::test_graphbackend_removed_from_holonic

  • R9.10 The HolonicDataset constructor parameter previously named registry_graph MUST be renamed to registry_iri for consistency with internal usage. The old name was a deprecated alias through 0.4.x and was removed in 0.5.0 per R9.18.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.init

    • acceptance: Given holonic 0.5.0, when code passes registry_graph= to HolonicDataset, then a TypeError is raised (kwarg removed per R9.18). registry_iri= is the canonical parameter.

    • verifiedBy: src/holonic/test/test_deprecation_and_dispatch.py::test_registry_graph_kwarg_removed

Remaining (gated on evidence)

  • R9.11 The library SHOULD ship a holonic.generators module formalizing the existing examples/ holarchy generators (company, research lab, random).

    • priority: SHOULD

    • implementation: verified

    • constrains: holonic.generators (new module)

    • acceptance: Given from holonic.generators import company_holarchy, when the generator is invoked with a seed, then a deterministic holarchy with documented structure is produced against any HolonicStore.

    • verifiedBy: (pending implementation)

  • R9.12 The library SHOULD offer a metadata_updates="lazy" mode (dirty tracking with flush_metadata()) if evidence emerges that eager and off modes leave a practical gap.

    • priority: SHOULD

    • implementation: in-progress

    • constrains: HolonicDataset, MetadataRefresher

    • acceptance: Given HolonicDataset(metadata_updates=”lazy”), when multiple writes occur followed by flush_metadata(), then metadata reflects the final state without per-write overhead.

    • verifiedBy: (pending implementation, gated on evidence from downstream usage)

  • R9.13 The registry SHOULD optionally aggregate per-holon membrane health for fast dashboards and scope-resolution health predicates. Semantics and invalidation rules to be decided before implementation.

    • priority: SHOULD

    • implementation: in-progress

    • constrains: cga.ttl, HolonicDataset, registry schema

    • acceptance: Given a holarchy with recent membrane validations, when the registry is queried for aggregated health, then per-holon rollup values are returned without recomputation from scratch.

    • verifiedBy: (pending implementation; design in open question OQ4)

  • R9.14 The scope resolver SHOULD gain additional predicate classes (HasPortalProducing, HasShapeFor, LabelMatches) if evidence emerges that CustomSPARQL is being used as a workaround for missing first-class predicates.

    • priority: SHOULD

    • implementation: in-progress

    • constrains: holonic.scope

    • acceptance: Given usage data showing CustomSPARQL patterns for portal/shape/label matching, when the named predicate classes ship, then the CustomSPARQL workarounds become first-class.

    • verifiedBy: (pending implementation, gated on usage evidence)

  • R9.15 The library SHOULD ship a migration pass that synthesizes minimal cga:ProjectionPipelineSpec resources for projection graphs registered before 0.3.5, so historical projections get consistent provenance.

    • priority: SHOULD

    • implementation: in-progress

    • constrains: holonic.cli.migrate_projections (new CLI)

    • acceptance: Given a pre-0.3.5 registry with projection layer graphs but no pipeline specs, when the migration CLI is run, then a minimal ProjectionPipelineSpec is synthesized for each projection and attached via cga:hasPipeline.

    • verifiedBy: (pending implementation)

  • R9.16 Pipeline steps SHOULD support per-step arguments (e.g. filter_by_class(class_iri)) via a cga:stepArguments JSON literal or structured argument record.

    • priority: SHOULD

    • implementation: in-progress

    • constrains: ProjectionPipelineStep, cga.ttl

    • acceptance: Given a pipeline step with transform_name=”filter_by_class” and arguments={“class_iri”: “urn:ex:Widget”}, when run_projection executes, then the transform receives the arguments as keyword args.

    • verifiedBy: (pending implementation)

  • R9.17 The optional protocol surface SHOULD grow additively during 0.4.x with native hooks for scope walking, bulk load, and pipeline execution as backend-specific optimization opportunities arise.

    • priority: SHOULD

    • implementation: in-progress

    • constrains: holonic.backends.store, AbstractHolonicStore

    • acceptance: Given a backend implementing an optional method like walk_neighbors_native, when the corresponding library helper dispatches, then hasattr-based detection invokes the native method without regressing duck-typed backends.

    • verifiedBy: (pending per-method implementation, following the refresh_graph_metadata precedent from R9.8)

  • R9.18 In 0.5.0, the GraphBackend alias MUST be removed along with the registry_graph kwarg and ds.registry_graph property.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/init.py, holonic/backends/init.py, holonic/backends/protocol.py, HolonicDataset

    • acceptance: Given holonic 0.5.0 installed, when code imports GraphBackend or passes registry_graph=, then an ImportError or TypeError is raised respectively.

    • verifiedBy: src/holonic/test/test_deprecation_and_dispatch.py::test_graphbackend_removed_from_holonic, test_graphbackend_removed_from_backends_protocol, test_registry_graph_kwarg_removed, test_registry_graph_property_removed

Shipped in 0.4.1

  • R9.19 The library MUST ship a JupyterLite static build of the example notebooks for in-browser exploration without local Python installation. Build integrates with the existing notebooks/ directory via scripts/sync_notebooks_to_jlite.py; output lands in docs/source/_extra/jupyterlite/ so Sphinx’s html_extra_path serves it at /jupyterlite/ alongside the documentation. Notebook 13 (yFiles visualization) is excluded from in-browser execution because yFiles requires a Jupyter server extension that Pyodide cannot provide; the landing notebook 00_start_here.ipynb documents this constraint.

    • priority: MUST

    • implementation: verified

    • constrains: jupyterlite/, scripts/sync_notebooks_to_jlite.py, .readthedocs.yaml, pixi.toml (tasks-jlite), docs/source/_extra/jupyterlite/

    • acceptance: Given the hosted documentation, when a reader opens the docs and follows the “Try in browser” link, then the JupyterLite lab loads and notebooks 01-12 execute against a Pyodide kernel without local Python installation.

    • verifiedBy: docs/source/index.md “Try in browser” section + .readthedocs.yaml pre_build hooks + manual verification during release rehearsal

Shipped in 0.4.2

  • R9.20 The library MUST provide HolonicDataset.remove_holon(iri) completing the CRUD lifecycle started by add_holon(). Cleans up the holon’s registry entry, all four layer graphs (interior/boundary/projection/context), graph-typing triples, graph-level metadata records, the per-holon rollup, and cascades to remove_portal() for every portal where the holon is source or target. Child holons that reference the removed holon via cga:memberOf are orphaned (their memberOf triple is removed) but not themselves deleted. Idempotent — returns False for a non-existent IRI rather than raising. Provenance activities referencing the removed holon are preserved because provenance is immutable history.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.remove_holon, holonic/client.py

    • acceptance: Given a holon with interior, boundary, and context layer graphs, plus a portal from it and a portal to it, plus a child holon referencing it as cga:memberOf, when remove_holon(iri) is called, then the holon is gone from list_holons(), all four layer graphs are deleted via backend.graph_exists()=False, both portals are gone from find_portals_from/find_portals_to, and the child remains in list_holons() but without its memberOf triple.

    • verifiedBy: src/holonic/test/test_lifecycle.py::TestRemoveHolon

  • R9.21 The library MUST provide HolonicDataset.remove_portal(portal_iri) completing the CRUD lifecycle started by add_portal(). Removes all triples with portal_iri as subject from every graph containing them (typically the source holon’s boundary graph and the registry mirror). The boundary graph itself is preserved — sibling portals and SHACL shapes in the same graph are unaffected. Idempotent — returns False for a non-existent portal IRI rather than raising. Metadata refresh fires per affected graph when metadata_updates="eager".

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.remove_portal, holonic/client.py

    • acceptance: Given a source holon with two portals declared in the same boundary graph plus a SHACL shape, when remove_portal(portal_a) is called, then portal_a is absent from find_portals_from(source), portal_b is still discoverable, and the SHACL shape is still present in the boundary graph.

    • verifiedBy: src/holonic/test/test_lifecycle.py::TestRemovePortal

  • R9.22 The add_portal() method MUST support creation of all portal subtypes declared in the CGA ontology (cga:TransformPortal, cga:IconPortal, cga:SealedPortal) as well as downstream subclasses via three additive parameters: construct_query is optional (default None); portal_type is a customizable RDF type (default "cga:TransformPortal"); extra_ttl accepts additional Turtle triples for predicates carried by downstream portal subclasses. All existing positional calls continue to work unchanged. Portal discovery (find_portals_from/to/direct) MUST match any portal subtype — the pre-0.4.2 hardcoded filter on cga:TransformPortal is relaxed because the cga:sourceHolon + cga:targetHolon predicate pair uniquely identifies a portal regardless of its specific subtype. Discovery queries use SELECT DISTINCT to deduplicate results across the boundary graph and registry mirror. The CGA ontology ships cga:IconPortal (previously undeclared) and SHACL shapes (cga:IconPortalShape, cga:SealedPortalShape) that warn when these subtypes carry cga:constructQuery; cga:TransformPortalShape continues to require exactly one cga:constructQuery.

    • priority: MUST

    • implementation: verified

    • constrains: HolonicDataset.add_portal, holonic/client.py, holonic/sparql.py (FIND_PORTALS_FROM), holonic/sparql.py (FIND_PORTALS_TO), holonic/sparql.py (FIND_PORTAL_DIRECT), holonic/ontology/cga.ttl (cga:IconPortal), holonic/ontology/cga-shapes.ttl (cga:IconPortalShape), holonic/ontology/cga-shapes.ttl (cga:SealedPortalShape)

    • acceptance: Given a portal created with portal_type="cga:SealedPortal" and no construct_query, when find_portals_from(source) is called, then exactly one PortalInfo is returned with construct_query=None; and given a portal created with extra_ttl carrying a downstream predicate, when the boundary graph is queried by SPARQL for that predicate, then the extra triples are present; and given a TransformPortal without a constructQuery or a SealedPortal/IconPortal carrying one, when the registry is validated against cga-shapes.ttl, then the corresponding shape reports a violation (for TransformPortal) or warning (for SealedPortal and IconPortal).

    • verifiedBy: src/holonic/test/test_lifecycle.py::TestAddPortalExtensibility and src/holonic/test/test_ontology.py::TestPortalSubtypeShapeSemantics

Shipped in 0.6.0

  • R9.23 Portal discovery queries MUST return the portal’s RDF type (excluding base cga:Portal) as a portal_type field on PortalInfo, PortalSummary, and PortalDetail. All five discovery SPARQL queries include OPTIONAL { ?portal a ?portalType . FILTER(?portalType != cga:Portal) }.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/model.py (PortalInfo), holonic/console_model.py (PortalSummary), holonic/console_model.py (PortalDetail), holonic/sparql.py, holonic/client.py

    • acceptance: Given a TransformPortal, when find_portals_from() is called, then portal.portal_type contains "urn:holonic:ontology:TransformPortal". Given a SealedPortal, when get_portal() is called, then detail.portal_type contains "SealedPortal".

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestPortalTypeInDataModel

  • R9.24 traverse_portal() MUST check the portal’s RDF type before executing the CONSTRUCT query. If the portal is cga:SealedPortal, it MUST raise SealedPortalError (a ValueError subclass) regardless of whether the portal carries a constructQuery.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (traverse_portal), holonic/model.py (SealedPortalError)

    • acceptance: Given a SealedPortal with or without a CONSTRUCT query, when traverse_portal() or traverse() is called, then SealedPortalError is raised with a message identifying the portal.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestSealedPortalEnforcement

  • R9.25 traverse() MUST register the target interior graph as cga:hasInterior after injection. It MUST look up existing registered interiors first and inject into the first one found; if none exist, it falls back to the {target_iri}/interior convention name. This ensures validate_membrane() can see the injected data.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (traverse)

    • acceptance: Given a target holon with no pre-existing interior, when traverse() injects triples, then GET_HOLON_INTERIORS returns the injected graph and validate_membrane() validates against the injected data, not an empty graph.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestTraverseInteriorRegistration

  • R9.26 get_holon() MUST use a direct filtered SPARQL query (O(1) in holarchy size) rather than scanning all holons via iter_holons().

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (get_holon)

    • acceptance: Given 20 holons, when get_holon() is called, then the method source does not reference iter_holons.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestGetHolonEfficiency

  • R9.27 validate_all() MUST validate membranes for all holons and return dict[str, MembraneResult].

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (validate_all)

    • acceptance: Given 2 holons, when validate_all() is called, then a dict with 2 entries is returned, each value a MembraneResult.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestValidateAll

  • R9.28 traverse() MUST accept fail_on_breach: bool = False. When True and validation returns COMPROMISED, it MUST roll back injected triples and raise MembraneBreachError. Rollback restores a pre-injection snapshot of the target interior.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (traverse)

    • acceptance: Given a traversal that produces COMPROMISED validation, when fail_on_breach=True, then MembraneBreachError is raised and the target interior is unchanged. Injected nodes of a type no shape targets compromise the membrane (R3.7).

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestFailClosedTraversal

  • R9.29 update_portal() MUST update portal properties in-place without requiring remove+add. Uses per-graph SPARQL DELETE for old values and Turtle parse for new values.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (update_portal)

    • acceptance: Given a portal with a CONSTRUCT query, when update_portal(construct_query=new_query) is called, then get_portal() returns the new query.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestPortalUpdate

  • R9.30 traverse_path() MUST execute multi-hop traversals by calling find_path() then looping traverse(). Raises ValueError if no path exists. Records provenance per hop when agent_iri is provided.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (traverse_path)

    • acceptance: Given a 3-holon chain A->B->C, when traverse_path("A", "C") is called, then 2 hops are executed and provenance records exist for each.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestTraversePath

  • R9.31 dry_run() MUST simulate a traversal without mutating state. Runs the CONSTRUCT, merges with existing interiors in memory, validates against boundary shapes, and returns (projected_graph, membrane_result).

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (dry_run)

    • acceptance: Given a source and target with data, when dry_run() is called, then projected triples are returned, a membrane result is returned, and the target interior is empty.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestPortalDryRun

  • R9.32 last_traversal(holon_iri) MUST return the most recent TraversalRecord targeting a holon. derivation_chain(holon_iri) MUST walk prov:wasDerivedFrom to return upstream holon IRIs.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (last_traversal), holonic/client.py (derivation_chain)

    • acceptance: Given traversals A->B->C, when derivation_chain("C") is called, then ["B", "A"] is returned. When last_traversal("C") is called, then a TraversalRecord with target_iri="C" is returned.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestPerHolonProvenanceHelpers

  • R9.33 compose(holon_iris) MUST union interior graphs across multiple holons into a single rdflib.Graph. Accepts optional layers parameter for specifying which layer types to include.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (compose)

    • acceptance: Given holons A and B with different vocabularies, when compose(["A", "B"]) is called, then the returned graph contains triples from both holons’ interiors.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestCompositionViews

  • R9.34 rollback_traversal(activity_iri) MUST undo a traversal by re-running the portal’s CONSTRUCT and removing the projected triples from the target interior. Returns the number of triples removed.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (rollback_traversal)

    • acceptance: Given a recorded traversal, when rollback_traversal(activity_iri) is called, then the target interior is empty.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestRollback

  • R9.35 traverse_portal() MUST support source layer scoping. When a portal declares cga:sourceLayer cga:ProjectionRole, the CONSTRUCT runs against projection graphs only. When no sourceLayer is declared but the source holon has projection graphs, the CONSTRUCT defaults to projection scope. Portals can force full-dataset access via cga:sourceLayer cga:InteriorRole.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (traverse_portal)

    • acceptance: Given a source holon with PII in the interior and a sanitized projection, when a portal without explicit sourceLayer is traversed, then the CONSTRUCT sees only projection data and PII does not leak.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestPortalConstructScope

  • R9.36 traverse() MUST compute a SHA-256 hash of the projected graph and compare against a stored hash (cga:lastProjectionHash) in the target’s context graph. When hashes match, injection is skipped and the traversal is recorded as a no-op. Hash tracking is active only when agent_iri is provided.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (traverse)

    • acceptance: Given two identical traversals, when the second is executed with agent_iri, then the audit trail contains a record with “no-op” in its label.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestIncrementalTraversal

  • R9.37 freshness(holon_iri) MUST return a timedelta since the most recent traversal. is_stale(holon_iri, max_age) MUST return True when freshness exceeds the threshold. stale_holons(max_age) MUST return all holons exceeding the threshold.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (freshness), holonic/client.py (is_stale), holonic/client.py (stale_holons)

    • acceptance: Given a traversal with agent_iri, when freshness() is called, then a timedelta is returned.

    • verifiedBy: src/holonic/test/test_verified_gaps.py::TestStalenessTracking

Shipped in 0.7.1

  • R9.38 collect_audit_trail() MUST accept optional limit, offset, since, and kind parameters for SPARQL-level pagination. Filtering is pushed to the SPARQL engine via ORDER BY DESC(?timestamp) LIMIT/OFFSET and FILTER(?timestamp > ...). Calling with no arguments returns the full trail (backward compatible).

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (collect_audit_trail)

    • acceptance: Given 100 traversals, when collect_audit_trail(limit=5, offset=10) is called, then exactly 5 records are returned starting from the 11th most recent.

    • verifiedBy: src/holonic/test/test_audit.py::TestCollectAuditTrail

  • R9.39 get_activity(activity_iri) MUST return a TraversalRecord or ValidationRecord for a single provenance activity, or None if not found. Single SPARQL query per attempt.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (get_activity)

    • acceptance: Given a recorded traversal activity, when get_activity(iri) is called, then a TraversalRecord with matching fields is returned.

    • verifiedBy: (verified via collect_audit_trail round-trip)

  • R9.40 classify_sparql(query) MUST return the SPARQL form ('select', 'ask', 'construct', 'describe', 'update') by stripping comments and string literals before keyword matching. Raises ValueError for unclassifiable queries.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (classify_sparql)

    • acceptance: Given "PREFIX ex: <urn:ex:> SELECT ?s WHERE { ?s ?p ?o }", when classify_sparql() is called, then 'select' is returned.

    • verifiedBy: (utility function; exercised by downstream consumers)

  • R9.41 validate_iri(iri) MUST be a public entry point to the library’s IRI validation. Raises ValueError for empty strings or characters unsafe in Turtle/SPARQL contexts.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (validate_iri), holonic/client.py (_validate_iri)

    • acceptance: Given validate_iri("<unsafe>"), when called, then ValueError is raised.

    • verifiedBy: src/holonic/test/test_audit_remediation.py::TestIRIValidation

  • R9.42 holarchy_summary(max_age=, recent_limit=) MUST return a HolarchySummary dataclass aggregating holon count, portal count, root count, health distribution, staleness count, and recent activities.

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (holarchy_summary), holonic/console_model.py (HolarchySummary)

    • acceptance: Given a holarchy with 3 holons and 2 portals, when holarchy_summary() is called, then summary.holon_count == 3 and summary.portal_count == 2.

    • verifiedBy: (integration method; exercised by downstream console)

  • R9.43 on_traversal(callback) and on_validation(callback) MUST register callbacks that fire synchronously after each traverse() or validate_membrane(). Callbacks receive the relevant arguments (source, target, projected, membrane_result for traversal; holon_iri, result for validation).

    • priority: MUST

    • implementation: verified

    • constrains: holonic/client.py (on_traversal), holonic/client.py (on_validation), holonic/client.py (traverse), holonic/client.py (validate_membrane)

    • acceptance: Given a registered callback, when traverse() completes, then the callback has been called exactly once with the traversal arguments.

    • verifiedBy: (callback mechanism; exercised by downstream console EventBroker)

  • R9.44 MembraneResult.shape_violations MUST carry a list[ShapeViolation] populated from the pyshacl report graph. ShapeViolation has fields: shape_iri, focus_node, path, value, message, severity.

    • priority: MUST

    • implementation: verified

    • verifiedBy: src/holonic/test/test_membrane.py::TestShapeViolationDetail::test_focus_node_identifies_the_offending_node

    • constrains: holonic/model.py (ShapeViolation), holonic/model.py (MembraneResult), holonic/client.py (_parse_shacl_report)

    • acceptance: Given a membrane violation, when validate_membrane() returns, then result.shape_violations[0].focus_node identifies the offending node.

Deferred to 0.8.0+

User Stories

  • US1 As a knowledge engineer, I create a holon, add interior data from multiple sources into named sub-graphs, declare boundary shapes, and validate the membrane — all from a single HolonicDataset instance against in-memory rdflib.

    • role: P1

    • benefit: I can enforce structural contracts on holon interior data without standing up external infrastructure

    • acceptance: Given HolonicDataset() with an in-memory backend, when I create a holon with two interiors and a boundary shape and call validate_membrane, then I receive a MembraneResult with health and violation details.

  • US2 As a systems integrator, I register a portal between two holons with a CONSTRUCT query that translates one vocabulary to another, traverse it, and see the target interior populated only when the membrane validates.

    • role: P2

    • benefit: vocabulary translation between holons is governed rather than ad-hoc

    • acceptance: Given a source holon with CCO-shaped data and a target holon with Schema.org boundary shapes, when I register a CONSTRUCT portal and call traverse(source, target, validate=True), then Schema.org triples appear in the target only if validation passes.

  • US3 As an operator-tool builder, I use list_holons_summary(), holon_neighborhood(iri, depth=2), and to_graphology() to render a sigma.js graph of the holarchy in a web console without any custom SPARQL.

    • role: P3

    • benefit: I can ship a working web console without writing SPARQL in my frontend code

    • acceptance: Given a populated holarchy, when I call list_holons_summary() and holon_neighborhood(iri, depth=2).to_graphology(), then the returned dicts deserialize to JSON suitable for direct sigma.js consumption.

  • US4 As a platform engineer, I deploy against Fuseki with bearer-token auth by passing extra_headers={"Authorization": "Bearer ..."} to FusekiBackend and the library transparently authenticates every SPARQL request.

    • role: P4

    • benefit: secrets-based authentication flows through to the SPARQL endpoint without library modifications

    • acceptance: Given FusekiBackend(url, dataset=ds, extra_headers={“Authorization”: “Bearer x”}), when any SPARQL request is issued, then the Authorization header reaches the server unchanged.

  • US5 As an agent author, I use portal traversal as an agentic memory write path: each agent turn runs through a portal with SHACL shape validation, and every turn produces a PROV-O record linking agent, input, output, and activity time.

    • role: P5

    • benefit: agent memory writes are validated and auditable end-to-end

    • acceptance: Given an agent iterating through a governed portal with validate=True, when a turn completes, then the context graph contains a prov:Activity with prov:wasAssociatedWith the agent, prov:used the input, prov:generated the output, and prov:startedAtTime the turn timestamp.

  • US6 As a data steward, I query the context graph across all holons in a holarchy and receive an audit trail of traversals with membrane health, agent attribution, and timestamps.

    • role: P6

    • benefit: I can produce a complete activity record for compliance review

    • acceptance: Given a holarchy with traversal history, when I run a SPARQL query over the union of context graphs, then I receive activity, agent, input, output, timestamp, and membrane health for every governed operation.

  • US7 As a spec author, I translate this SPEC to Turtle via specl-translate, validate it against the shipped SHACL shapes, and publish a maturity badge in the README.

    • role: P7

    • benefit: spec quality is machine-measurable and visible to external consumers

    • acceptance: Given docs/SPEC.md, when specl-translate and specl-validate score are run, then a maturity percentage and SVG badge artifact are produced and the spec’s structural integrity is asserted by SHACL.

Personas

Who the user stories are about. Declared once and referenced by identifier so that two stories about the same person share a node rather than a spelling — role: takes P3, never “operator-tool builder”, because minting from the surface string would make “operator-tool builder” and “Operator-Tool Builder” two personas with nothing reporting it.

  • P1 Knowledge engineer

    • prefLabel: knowledge engineer

    • altLabel: ontologist

  • P2 Systems integrator

    • prefLabel: systems integrator

  • P3 Operator-tool builder

    • prefLabel: operator-tool builder

    • altLabel: console developer

  • P4 Platform engineer

    • prefLabel: platform engineer

  • P5 Agent author

    • prefLabel: agent author

    • altLabel: agent developer

  • P6 Data steward

    • prefLabel: data steward

  • P7 Spec author

    • prefLabel: spec author

Agents

Who is accountable for an item. owner: takes an identifier for the same reason role: does: held as a literal it was a spelling repeated across items and joinable only by string equality.

  • AG1 Zach Welz

    • prefLabel: zwelz3

Design Considerations

  • DN1 The dataset is the holarchy. Python methods are convenience over SPARQL, not a separate object model. Dataclasses returned from queries are ephemeral views, not persistent state.

  • DN2 Named graphs are hypergraphs. Recognizing this subsumes the RDF-vs-LPG distinction and makes the four-graph model a natural consequence of standard RDF 1.1 semantics.

  • DN3 SPARQL is the primary control surface. Python handles only what SPARQL cannot express (Dijkstra path ranking, template rendering, pipeline orchestration).

  • DN4 Minimal dependencies. The hard deps are rdflib and pyshacl. Optional extras add jupyter, aiohttp (Fuseki), owlrl (entailment), and visualization widgets. pydantic was removed in 0.6.0; the library uses plain dataclasses with _DictMixin.to_dict() for serialization.

  • DN5 PROV-O for activity-level provenance. Graph-to-graph derivation via prov:wasDerivedFrom. Holon-to-holon structural dependency via cga:derivedFrom. These are distinct concepts and both exist.

  • DN6 SHACL shapes, not OWL axioms, for constraint checking. Reasoner-free by design; entailment is optional via the owlrl extra.

  • DN7 Nested Turtle notation everywhere the library emits RDF. Predicates grouped with ;, object lists with ,. No verbose explicit-triple form in examples, fixtures, or docs.

Comments

  • C1 The four layers (interior, boundary, projection, context) are explicit in the ontology as cga:LayerRole individuals (cga:InteriorRole, cga:BoundaryRole, cga:ProjectionRole, cga:ContextRole) and as bridging properties (cga:hasInterior etc. as sub-properties of cga:hasLayer). The model accommodates multiple graphs per role.

  • C2 The registry graph (urn:holarchy:registry) in R9 proposals refers to a cross-cutting catalog graph for holarchy-level declarations. It is distinct from per-holon layer graphs and is declared at the holarchy level, not the holon level.

  • C3 holonic-console is the primary downstream consumer driving the 0.3.1 API additions. Its stage 2 work (sigma.js neighborhood view, portal browser, provenance feed) is the functional proof for R6.

  • C4 specl provides the SHACL validation of this SPEC itself. The spec is dogfood: the library under development is specified using a tool whose ontology was co-designed with it.

  • C5 Cagle’s “The Graph as State Machine” (The Inference Engineer, April 2026) frames a holonic graph as a graph-level state machine where the four layers map to Scene / Boundary / Event / Projection and sh:SPARQLRule is the transition function driven by a clock-triggered tick. The library today implements the four layers structurally but operates event-triggered (portal traversals invoked by callers) rather than clock-triggered. Whether to add a tick primitive is captured as OQ8.

Open Questions and Gaps (flag for follow-up)

  • OQ1 Registry-graph typing vs holon-graph-as-holon. If typed graphs (R9.1) are adopted, should the registry graph itself be typed as cga:RegistryGraph, and should it be modeled as the interior of a RegistryHolon? Bootstrapping implications unresolved.

    • owner: AG1

    • recommendation: Type the registry graph explicitly as cga:RegistryRole (shipped in 0.3.4); defer the registry-as-holon question until after R9.1 ships.

    • status: resolved

  • OQ2 Subclass vs flat+roles for graph categories. cga:InteriorGraph rdfs:subClassOf cga:HolonicGraph versus ?g a cga:HolonicGraph ; cga:graphRole cga:InteriorRole. Subclass is terser in queries; flat+roles supports multi-role graphs cleanly (e.g. a graph that is both an interior and a provenance aggregate).

    • owner: AG1

    • recommendation: Adopt flat+roles because the dual-role case is real (see cga:AggregateHolon); cost is one extra triple pattern per query. Shipped in 0.3.4 via D-0.3.4-1.

    • status: resolved

  • OQ3 Scope resolution ordering configurability. The proposed order (self → portal neighbors → siblings → parent → wider) serves network-proximity queries. Containment-first ordering serves governance queries. Reverse portal distance serves debugging queries.

    • owner: AG1

    • recommendation: Default to network-proximity; accept an order= parameter on resolve() taking a strategy enum. Shipped in 0.3.4 with three modes: network, reverse-network, containment.

    • status: resolved

  • OQ4 Class-inventory staleness strategy. Materialized inventory in the registry needs a refresh policy: synchronous (slow writes), asynchronous (fast writes, stale reads), on-demand (explicit refresh), or TTL-based (configurable staleness).

    • owner: AG1

    • recommendation: Per-HolonicDataset strategy at construction time. Shipped in 0.3.3 with eager and off modes; lazy mode deferred to R9.12 gated on usage evidence. Resolved: that evidence never arrived, and the absence of usage was itself the determining criterion, so the gate answered the question rather than deferring it. Eager and off remain the shipped modes; reopen only if a consumer produces the usage R9.12 was waiting on.

    • status: resolved

  • OQ5 Graph-level metadata write amplification. If every portal traversal updates cga:interiorTripleCount and last-modified in the registry, write amplification dominates in high-throughput pipelines.

    • owner: AG1

    • recommendation: Make metadata updates opt-in via a constructor kwarg. Shipped in 0.3.3 as metadata_updates=”eager”|”off” on HolonicDataset.

    • status: resolved

  • OQ6 Protocol rename (GraphBackend to HolonicStore). Resolved in 0.4.0 via R9.8, R9.9. Deprecated alias removed in 0.5.0 via R9.18.

    • owner: AG1

    • recommendation: Renamed in 0.4.0 via R9.8 and R9.9; the deprecated GraphBackend alias was removed in 0.5.0 via R9.18. No further action.

    • status: resolved

  • OQ7 Federation semantics across multiple registries are out of scope for 0.3.x. Open question preserved because the typed-graph design (R9.1) should not close the door on registry-to-registry declarations like cga:federatesWith.

    • owner: AG1

    • recommendation: Defer to 0.5.0 design session; ensure R9.3’s typed-graph vocabulary leaves room for a future federation property without schema migration.

    • status: deferred

  • OQ8 Graph-level tick semantics and transition rules. Cagle’s “The Graph as State Machine” (The Inference Engineer, April 2026) frames a holonic graph as a graph-level state machine in the Game-of-Life sense: Scene Graph (interior state), Boundary Graph (SHACL rules including sh:SPARQLRule transition functions), Event Graph (PROV-O transition history), and Projection Graph (external observation). The library implements all four layers structurally but does not implement a tick — portal traversals are event-triggered rather than clock-triggered, and SHACL is used for validation rather than for $this-based transformation rules. A future HolonicDataset.tick() primitive could fire SHACL transition rules across the whole holarchy, either accumulating PROV-O activities into the Event Graph or mutating the Scene Graph directly via SPARQL UPDATE. Whether to add this is a distinct architectural question from anything currently in the roadmap; adopting it would extend the library from a read-heavy, portal-triggered coordination layer into a living-system model with continuous dynamics.

    • owner: AG1

    • recommendation: Wait for Part 2 of the Cagle series (Active Inference / Free Energy Principle) to land before committing to a tick API. Evaluate whether a tick is the right primitive or whether something finer-grained (an active-inference step, an energy-minimization iteration) is more apt. If pursued at all, target a holonic.contrib experimental module first to test the pattern without committing the core ontology. No release slot assigned; this may or may not be implemented.

    • status: deferred

  • OQ9 DOM-style event propagation as a coordination model. Cagle proposes (LinkedIn thread, April 2026) that plural-orchestrator coordination can be resolved by treating the holarchy as a structure over which events propagate in the W3C Document Object Model’s capture/target/bubble pattern. Under this framing: the containing holon makes no expectations on child interiors (opacity is first-class); external events arrive at a holon and are either consumed, delegated to children via portals, or ignored after propagation completes; “eventually ignored” is a legitimate outcome rather than a failure mode. This is distinct from OQ8’s tick proposal — event propagation is structural (who receives an event depends on the containment topology) rather than clock-triggered. The library today implements something DOM-adjacent via portal traversal and multi-hop path finding, but does not expose explicit event-dispatch semantics. Open questions surface immediately when mapping DOM to holons: DOM events are synchronous and the DOM is a strict tree, whereas a federated holarchy is asynchronous and a containment graph may have multiple portal paths between two holons. Whether to adopt the DOM framing as library architecture, as a mental model for explaining existing capabilities, or neither, remains open.

    • owner: AG1

    • recommendation: Before any implementation, verify whether the existing portal-traversal API strains under real use cases that DOM-style event dispatch would address more naturally. If strain emerges, the design question becomes: extend existing portal semantics with explicit event-phase hooks (capture/target/bubble), add a dedicated dispatch_event API parallel to traverse, or adopt the DOM model as framing while keeping the current API unchanged. The documentation at docs/source/dom-comparison.md establishes the mental-model mapping without committing to implementation. No release slot assigned; this may or may not be implemented.

    • status: deferred

  • OQ10 Upper-ontology alignment (BFO/CCO, gist). The CGA ontology’s structural core (Holon, Portal, LayerGraph) is genuinely novel and does not reduce cleanly to existing upper-ontology categories. A holon is not a BFO Continuant, not an Occurrent, not a gist Content — it is a structural container with an inside/outside distinction, layer-scoped named graphs, and a membrane. Forcing it into an upper-ontology branch would misrepresent what it is. The governance vocabulary (DataDomain, BusinessProcess, ExternalSystem, Capability) maps more naturally to established categories (CCO Agent, CCO InformationBearingEntity, gist Organization). The recommended approach is alignment-not-import: ship optional alignment modules (cga-bfo-alignment.ttl, cga-gist-alignment.ttl) that downstream consumers load when interoperability with a specific upper ontology is required. The core ontology stays standalone, reasoner-free, and philosophically uncommitted. BFO/CCO alignment supports consumers in regulated industries (ISO 21838-2); gist alignment supports commercial enterprise consumers. Both can be developed in parallel without affecting the core.

    • owner: AG1

    • status: open

    • recommendation: Develop alignment modules as separate Turtle files under src/holonic/ontology/. Use skos:broadMatch / skos:closeMatch in the core ontology for lightweight cross-referencing without owl:imports. Document the alignment rationale in docs/source/ontology.md. Gated on a concrete downstream consumer requesting BFO or gist compatibility.

  • OQ11 SHACL target-class validation gap for fail-on-breach. Standard SHACL sh:targetClass validates only instances of the target class; when no instances exist, validation reports conformant. This means fail_on_breach=True cannot detect the case where a portal injects data of the WRONG type (e.g. bad:Wrong instead of good:Required). The membrane reports INTACT because there are no good:Required instances to validate against. The fail_on_breach mechanism (R9.28) is correctly implemented for cases where SHACL does report violations. Closing this gap requires one of: (a) non-standard SHACL interpretation where boundary shapes assert minimum cardinality on target-class instances, not just properties; (b) a pre-validation check that verifies at least one instance of each sh:targetClass exists in the interior after injection; (c) custom SPARQL-based validation in addition to SHACL. Two tests in test_verified_gaps.py are marked xfail pending resolution. This is a critical design decision because it affects whether fail_on_breach provides meaningful protection against schema-mismatch errors, not just constraint violations within the correct schema.

    • owner: AG1

    • status: resolved

    • recommendation: Resolved in 0.9.0 by R3.7 (D-0.9.0-2) with a variant of approach (c) rather than (b). Option (b) fails a valid interior that has no instances of some class yet. Instead, membrane validation reports each typed node that no shape targets; a fail-closed traversal treats the nodes it injected as violations unless the holon permits their type, and the two tests in test_verified_gaps.py pass without xfail.

Appendix A: Suggested Namespaces

@prefix cga:     <urn:holonic:ontology:> .
@prefix prov:    <http://www.w3.org/ns/prov#> .
@prefix sh:      <http://www.w3.org/ns/shacl#> .
@prefix owl:     <http://www.w3.org/2002/07/owl#> .
@prefix rdfs:    <http://www.w3.org/2000/01/rdf-schema#> .
@prefix dcterms: <http://purl.org/dc/terms/> .
@prefix schema:  <https://schema.org/> .
@prefix skos:    <http://www.w3.org/2004/02/skos/core#> .

Appendix B: Module Map (0.9.0)

src/holonic/
├── __init__.py            re-exports HolonicDataset, models, ontology loaders
├── client.py              HolonicDataset — the primary API surface
├── model.py               HolonInfo, PortalInfo, MembraneResult, ShapeViolation, AuditTrail, SurfaceReport
├── console_model.py       HolonSummary, HolonDetail, NeighborhoodGraph, PortalSummary, ...
├── sparql.py              SPARQL templates as named string constants
├── projections.py         CONSTRUCT_STRIP_TYPES, project_to_lpg, ProjectionPipeline
├── backends/
│   ├── store.py           HolonicStore protocol + AbstractHolonicStore ABC
│   ├── rdflib_backend.py  in-memory rdflib.Dataset backend
│   ├── fuseki_backend.py  Apache Jena Fuseki HTTP backend
│   └── _fuseki_client.py  low-level HTTP helpers
├── ontology/
│   ├── cga.ttl            CGA ontology (holons, portals, layers, governance)
│   └── cga-shapes.ttl     SHACL shapes (membrane validation scaffolding)
├── viz/
│   ├── graph_builder.py   build visualization-ready graph structures
│   ├── formatters.py      text/table formatters
│   ├── provenance.py      provenance visualization helpers
│   ├── styles.py          color/shape palettes
│   └── widgets.py         ipywidgets composite displays
└── test/                  pytest suite (all run against RdflibBackend)