Change Log

All notable changes to this project will be documented in this file.

[Unreleased]

Changed

  • Adopted specl 1.0 from PyPI (pixi.toml), replacing a git pin that resolved to 0.2.0. This crosses two graph contracts and two item-IRI relocations, so docs/SPEC.md was migrated to contract 2:

    • spec_base is now declared: https://zwelz3.github.io/holonic/spec.ttl#. Every item has a permanent, dereferenceable IRI — R1.1 resolves to …/spec.ttl#R1.1, and the graph is published to that path by the docs CI job. The .ttl extension is load-bearing: GitHub Pages serves it as text/turtle, which an extensionless path would not be.

    • User-story triad renamed asA/soThat → role/benefit. role and owner are now references, so a # Personas section (P1–P7) and an # Agents section (AG1) were added and the annotations point at identifiers rather than repeating a spelling.

    • RDF vocabulary terms moved from constrains: to the new governs:. constrains: keeps its declared range of specl:Component; a requirement governing an RDF class and one constraining a Python class are different claims that the single key had conflated.

    • Design Considerations and Comments carry DN1–DN7 and C1–C5 identifiers. All twelve bullets were previously dropped in silence — three of them without even a warning — so none of that content reached the graph.

    • Every requirement gained an implementation: annotation, which drives specl’s new Progress score (84%, distinct from the 92% maturity score: one is how much is built, the other how completely it is written).

    • Removed six verifiedBy: none annotations (R1.4, R5.2, R5.3, R5.4, R9.2, R9.44). none was not a null — it minted a real spec:test-none a specl:Test node, so six unverified requirements were passing the verification check by pointing at a fake artifact. They now warn honestly.

  • Spec validation runs in CI for the first time (.github/workflows/ci.yml), gating on SHACL Violations. Warnings become blocking automatically when the spec declares status: production — the threshold lives in the spec, not the workflow.

  • The maturity badge is published rather than committed. static/spec-badge.svg is deleted; the badge is rendered into docs/source/_extra/badges/ and served from GitHub Pages. A committed badge is derived data that has to be pushed back, which rejects the next push and leaves the committed copy drifting from the real score.

  • scripts/spec_compliance_check.py now detects implementation drift — it fails if a requirement’s implementation: annotation disagrees with what the checker actually observes, which is what keeps the Progress score from decaying into decoration. Its report header also reads the spec version instead of a hardcoded 0.4.2.

Added

  • Untargeted-type check in membrane validation (#50, R3.7, resolves SPEC OQ11). SHACL passes a node whose type no boundary shape targets, so a portal injecting the wrong type left the membrane INTACT under fail_on_breach. Validation now reports such nodes in MembraneResult.untargeted (UntargetedNode), at Info by default, and traverse(fail_on_breach=True) treats the nodes it injected as violations. A holon’s boundary can exempt a class with cga:permitsType or set the severity with cga:untargetedTypeSeverity, both new ontology terms. The two xfail tests for OQ11 now pass. A holon whose portals inject node types its shapes do not target needs shapes or cga:permitsType declarations before using fail_on_breach (D-0.9.0-2).

  • Verification for six previously untested requirements — R1.4, R5.2, R5.3, R5.4, R9.2 and R9.44 now have tests and are implementation: verified. 26 new cases across test_ontology.py, test_backend.py, test_portal.py and test_membrane.py, covering: no library write reaching the default graph; traversal recording prov:wasDerivedFrom while never emitting cga:derivedFrom; the HolonSplit/HolonMerge PROV subclassing; the graph-metadata vocabulary; and ShapeViolation field population.

  • test_spec_records_creation_date guards dct:created in both SPEC.md and SPEC.ttl. The value was silently lost in the specl migration once already, because specl 0.2.0 had been supplying it from the clock and 1.0 requires it declared. It now lives in a <!--specl --> block and is a constant — nothing regenerates it.

  • rdflib deprecation noise silenced (pyproject.toml filterwarnings). rdflib 7.6 renamed Dataset.default_context and Dataset.contexts but its own SPARQL engine still calls the old names, so every query and update the suite issues emitted two or three DeprecationWarnings from inside rdflib — about 10,000 per run, burying anything worth reading. Filtered by message rather than by category, so holonic’s own deprecation warnings still surface. Nothing fixable downstream; drop the filter once rdflib stops calling its own deprecated properties.

  • docs/ROADMAP.md — the human-readable view of the roadmap, moved out of the README. SPEC.md remains the machine-checked source (R9.* requirements, OQ1–OQ11 open questions); the README’s ## Roadmap section duplicated it by hand and had already drifted, still citing OQ1–OQ10.

  • docs/spec-history.ttl — spec maturity recorded as specl:MaturityAssessment activities, seeded with a 0.9.0 baseline (maturity 99%, progress 86%). Written at release cadence by spec-history, deliberately not in CI: the file is append-only and must be committed to persist, so a CI job would have to push it back. Scores before this point are not comparable — they were produced under graph contract 1 against different shapes.

  • test_ontology_utilization.py — an accounting audit for the CGA ontology. Every one of the 118 declared terms is either live or listed in a registry with a stated reason; a term that is neither fails the build, as does a registry entry for a term that has since become live or been deleted. Current split: 74 live (63%), 44 declarative, 0 unaccounted.

    Liveness is established three ways because no one way suffices. Static source matching alone reports false deaths for interpolated IRIs — membrane health is emitted as f"urn:holonic:ontology:{health.value.capitalize()}", so cga:Intact appears in no source file and is entirely live — so the audit also exercises a representative holarchy and collects what actually reaches the graph, and counts terms referenced by cga-shapes.ttl, which do work when a user writes them even though the library never does.

Fixed

  • fail_on_breach no longer fails open on an unrecognized SHACL severity (#30). _parse_shacl_report matched severities by suffix and dropped any result it could not classify, so a shape declaring a custom severity left the membrane INTACT and traverse(..., fail_on_breach=True) injected the breaching data. Severities are now compared with sh:Violation, sh:Warning and sh:Info exactly, and a result with any other severity, or none, counts as a violation. sh:Info results, previously discarded, are kept in the new MembraneResult.infos and never lower health. validate_membrane and dry_run share one health rule (R3.6).

  • MembraneResult.conforms agrees between validate_membrane and dry_run. dry_run passed allow_infos=True to pyshacl and validate_membrane did not, so a report holding only sh:Info results gave conforms=True from one and False from the other. Both now run SHACL through run_shacl, and conforms is false only for a result above sh:Info (R3.6).

  • Sixteen malformed specl:Component identifiers in docs/SPEC.ttl. specl splits a constrains: value on every comma, and seven requirements listed symbols as path (a, b), which produced components such as holonic/client.py (freshness and is_stale). The lists are now written path (a), path (b), and test_spec_constrains_items_have_no_comma_inside_parentheses fails on the old pattern.

  • Six CGA terms carried no skos:definition, rdfs:label or rdfs:comment — MembraneHealth, interiorTripleCount, lastIngestion, splitRationale, mergeRationale and splitTarget, each sitting beside documented siblings. Now documented; a term nobody but its author can read is dead in a second sense. Guarded by test_every_term_is_documented.

Fixed

  • docs/SPEC.md reports zero SHACL warnings (was 11), maturity 99%, Requirement subscore 85/85. Open issues gained accountability metadata: OQ6 a recommendation and owner (and lost a duplicated status: line), OQ10 and OQ11 an owner. OQ4 moved to resolved — its lazy-mode gate was usage evidence that never arrived, and the absence of usage was itself the determining criterion.

  • scripts/spec_compliance_check.py no longer treats a compound check id such as R9.4-predicates as its own requirement, which had reported spurious implementation drift.

Known issues

  • Four parser warnings remain, all one upstream limitation: specl 1.0 cannot express a urn: vocabulary namespace, so the CGA terms under governs: emit as literals instead of IRIs. Reported upstream. Until it is resolved, CI cannot adopt specl-translate --fail-on-warning (available as the spec-gate task). Re-namespacing the ontology to work around this is explicitly not planned.

  • OQ10 remains open, capping the OpenIssue subscore at 10/11. It is a genuine unresolved design question, not a missing annotation. OQ11 was resolved by R3.7.

[0.8.0] - 2026-08-12

Security- and correctness-hardening release driven by the v0.7.1 holonic audit. This release contains breaking changes; see docs/MIGRATION.md.

Security

  • Turtle/SPARQL injection in provenance writes (S1). record_traversal and record_validation (including the no-op path) now validate every IRI argument and escape the label literal before templating, closing an injection vector where a crafted IRI could append arbitrary triples.

  • Type-term injection (S2). holon_type / portal_type are now checked against a strict PNAME shape or validated as a full IRI. A ':'-containing value is no longer spliced raw into the Turtle body; malformed values raise ValueError.

  • _escape_construct backslash blindness (S4). Backslashes are escaped before quotes so a trailing backslash can no longer break out of a triple-quoted CONSTRUCT literal.

  • Portal CONSTRUCT fail-open (S5). A portal is now scoped to its source holon (projections, else interior) and never silently widened to the whole dataset. Whole-dataset traversal requires an explicit opt-in. See Breaking below.

  • Sealed-portal bypass (S6). The seal check is now a fail-closed exact ASK { ?g { <portal> a cga:SealedPortal } }, so a dual-typed portal can no longer slip past the previous nondeterministic SELECT ... LIMIT 1.

  • Second-order store-derived IRI injection (S3). The read path no longer splices result-row IRIs back into fresh query text. The ~30 ?holon / ?from_holon / ?graph sites in client.py and the ScopeResolver BFS walk in scope.py now pass IRIs as validated rdflib.URIRef bindings through a single _bind_iri chokepoint; the write-only metadata refresh path (SPARQL UPDATE, no initBindings) validates every graph/holon IRI at entry. An IRI that reached the store via raw ingestion (bypassing the validating add_* methods) is now rejected with ValueError or n3()-escaped rather than becoming a delayed injection primitive.

  • since injection in collect_audit_trail (S7 — not an audit finding; found while remediating S1–S6). The audit-trail query is assembled by string surgery, and limit / offset were already int()-cast, but the caller’s since string was interpolated raw into the FILTER. A value carrying a quote closed the typed literal and appended attacker-chosen patterns to a query the caller believed was a timestamp filter. since is now validated against a strict xsd:dateTime shape (anchored \A/\Z, so a trailing newline cannot smuggle a second line) and emitted through rdflib’s n3(), the same escaping the **bindings contract uses.

Breaking

  • collect_audit_trail(since=...) raises ValueError for a value that is not a well-formed xsd:dateTime, including since="". Previously a malformed value reached the SPARQL engine, where an ill-typed comparison silently matched nothing — a filter that returned an empty trail rather than an error. Callers passing a datetime should pass dt.isoformat().

  • traverse_portal() / traverse() no longer run a portal’s CONSTRUCT against the entire dataset when the source holon has no projections. They scope to the source’s interior instead, and raise ValueError when the source has neither projections nor interiors. Pass unscoped_portals_allowed=True (or declare cga:sourceLayer cga:DatasetRole on the portal) to restore the legacy behaviour. Scoping narrows which graphs are visible, not how the CONSTRUCT is evaluated: the in-scope layers are still presented as named graphs, so a portal written as WHERE { GRAPH ?g { ... } } — the idiom every portal used when CONSTRUCTs ran against the whole dataset — keeps working, with ?g binding to in-scope graph IRIs only. Portal queries need no rewrite.

  • add_holon(holon_type=...) / add_portal(portal_type=...) raise ValueError on a type term that is neither a valid PNAME nor a valid IRI.

  • ProjectionPipeline execution methods dropped their backend parameter (A3). ProjectionStep.apply, ProjectionPipeline.apply, apply_to_graph, and apply_to_lpg no longer accept backend=. The parameter existed only to redirect CONSTRUCT steps to backend.construct(), which ran them against the whole dataset and caused the scope-leak/chaining bug fixed above. Materialize the source graph first, then call apply (as HolonicDataset.apply_pipeline already does). See MIGRATION.md.

  • Backend **bindings value contract (A1/A2). query/construct/ask now bind values by explicit rdflib term type: a value that is already an rdflib Node binds verbatim, and any other Python value binds as a typed Literal. IRIs must therefore be passed as rdflib.URIRef (of any scheme). This replaces the 0.7.x heuristic that promoted only urn:-prefixed strings to IRIs and silently mis-bound http:// strings — and FusekiBackend, which previously discarded bindings entirely, now honors them safely.

Fixed

  • Scoped portal traversal no longer flattens named graphs. The first cut of the S5 scoping fix merged the in-scope layers into a single rdflib.Graph, which made any portal CONSTRUCT containing GRAPH ?g { ... } unevaluable — rdflib raises “You performed a query operation requiring a dataset (i.e. ConjunctiveGraph), but operating currently on a single graph.” That is the shape every portal written before 0.8.0 uses, since CONSTRUCTs then ran against the whole dataset, so the break was broad and the error message pointed nowhere near the cause. The scope is now rebuilt as an rdflib.Dataset holding each in-scope layer under its own IRI (default_union=True, matching RdflibBackend), so both idioms work and ?g still binds only to in-scope graphs. Scope graphs are collected through _safe_layer_graph, so an unmaterialized layer in the scope no longer aborts the traversal on Fuseki. Note the remaining asymmetry: run_projection merges a holon’s interiors into one unnamed graph before running pipeline CONSTRUCT steps, so a GRAPH clause is not available there. That is longstanding and intentional — transform steps operate on a flat graph and intermediate results have no named-graph identity.

  • yFiles visualizations work on yfiles-jupyter-graphs 2.x. 2.0 renamed hierarchic_layout to hierarchical_layout; the viz widgets called the 1.x name unconditionally, so every hierarchic render — the default for all four widget families — raised AttributeError. Layout dispatch now lives in holonic.viz._layout.apply_layout, which resolves whichever spelling the installed version provides, so 1.x and 2.x are both supported. holonic’s own "hierarchic" token is unchanged ("hierarchical" is accepted as an alias), and an unknown token now raises ValueError instead of silently falling through to the default layout.

  • An unmaterialized layer graph no longer aborts validation for the whole holarchy. A layer can be registered (cga:hasInterior / cga:hasBoundary) before it holds any triples; Fuseki’s Graph Store Protocol answers 404 for such a graph, which raised out of validate_membrane and therefore out of validate_all, which walks every holon in one loop. The membrane-collection sites now route through _safe_layer_graph, which treats absence as “contributes no triples” — equivalent for a union — while letting every other backend error propagate, so an unreachable store can never be mistaken for an empty one. When an absent boundary is the reason a holon has no shapes, the MembraneResult.report_text names the graphs instead of reading identically to “this holon defines no constraints”.

  • RdflibBackend caches parsed SPARQL for reused template strings (prepareQuery LRU), removing the per-call reparse cost on the parameterized read path (audit P1).

  • Fail-closed traversal is now verified (E1). traverse(fail_on_breach=True) has passing tests that drive a genuine SHACL Violation (an injected instance of the boundary’s sh:targetClass that violates a sh:minCount) and assert both MembraneBreachError and a byte-identical target interior. The two sh:targetClass-with-no-instances cases remain documented xfails (SPEC OQ11).

  • Breach rollback no longer poisons the no-op cache (E2). The cga:lastProjectionHash is now written only after validation passes, so a rolled-back breach leaves no stored hash; a retry after fixing the boundary re-injects instead of reporting a phantom no-op. The hash is written in a single DELETE/INSERT WHERE (replacing the prior read + delete + parse).

  • Breach rollback preserves concurrent writes (C2). Rollback now removes exactly the triples the call injected (via targeted DELETE DATA) instead of replacing the whole interior graph, so a triple written to the same interior by another writer is no longer clobbered. (Blank-node-bearing projections still assume exclusive access — see docs/MIGRATION.md.)

  • FusekiBackend works inside a running event loop (C1). Operations now run on a dedicated worker-thread loop, so calls from a Jupyter kernel or an async web handler no longer raise RuntimeError: Cannot run the event loop while another loop is running.

  • FusekiClient.__aenter__ no longer orphans a live session by unconditionally overwriting self._session; it now guards like open().

  • Projection pipeline CONSTRUCT steps no longer leak the whole dataset (A3). A ProjectionPipeline CONSTRUCT step now always runs against the graph it is handed (the previous step’s output), instead of, when a backend was present, running against the entire backend dataset. This fixes two bugs at once: apply_pipeline() no longer folds triples from other holons into a single holon’s projection, and multi-step pipelines now chain correctly (step n sees step n-1’s output). See Breaking, below, for the signature change this required.

Changed

  • FusekiBackend reuses one client/session for its lifetime (P5) instead of opening and tearing down an aiohttp.ClientSession (and TCP/TLS connection) per call. Call close() — or use the backend as a context manager (with FusekiBackend(...) as be:) — to release the pooled session and worker loop deterministically; a weakref.finalize handler does so best-effort at garbage-collection time.

  • holarchy_summary() reads health from persisted records by default (P4). The health distribution is now derived from each holon’s most-recent persisted ValidationRecord in a single aggregate query instead of re-running pyshacl on every holon. Holons that have never been validated no longer contribute to the distribution. Pass live_health=True for the old re-validate-every-holon behaviour. See docs/MIGRATION.md.

Performance

  • holarchy_summary() no longer makes ~4+N round-trips (P4). Counts and roots come from one list_holons_summary() pass plus an aggregate portal COUNT; health from one LATEST_HEALTH query; staleness from the P7 aggregate. Per-holon memberOf probes and per-holon validate_membrane calls are gone.

  • iter_holons() runs 2 queries instead of 1+4N (P3). Layer graphs for a page of holons are fetched in one LIST_HOLON_LAYERS query and grouped in Python rather than four per-holon layer queries each. Return type is unchanged (HolonInfo).

  • stale_holons() uses one aggregate query instead of ~5N (P7). A single MAX(?timestamp)-per-holon query (LATEST_TRAVERSALS) determines staleness; materialization then rides the 2-query iter_holons().

  • Graph metadata refresh issues one UPDATE instead of two (P2). The eager per-write path now runs a single DELETE/INSERT/WHERE per graph (and per holon rollup) rather than a separate clear-then-insert, halving the update round-trips and closing the window between them. batch() remains the recommended wrapper for bulk ingestion.

  • The CGA ontology + shapes are parsed once per process (P6). The bundled Turtle sources are parsed into a module-level cached rdflib.Graph and their triples copied into each new dataset’s backend; a persistent backend that already holds the ontology graph skips the upload (graph_exists guard).

Added

  • FusekiBackend.close() and context-manager support (__enter__/__exit__).

  • HolonicError exception base (audit A1). A new top-level holonic.HolonicError is the common base for every exception the library raises. MembraneBreachError, SealedPortalError, FusekiError, and TransformNotFoundError now derive from it, so except HolonicError catches the whole family. Each also keeps the builtin base it already subclassed (SealedPortalError→ValueError, FusekiError→RuntimeError, TransformNotFoundError→KeyError), so existing handlers are unaffected — see MIGRATION.md.

  • holonic.GraphNotFoundError — a backend-agnostic “this named graph is not in the store” error, exported from holonic and holonic.backends.store. AbstractHolonicStore.get_graph still says backends SHOULD return an empty graph for a missing IRI; a backend that cannot (Fuseki: GSP answers 404 for both absent and empty) now MUST raise this type. FusekiClient raises FusekiGraphNotFound, which subclasses both it and FusekiError, so pre-0.8.0 except FusekiError handlers are unaffected and callers can detect absence by type rather than by matching "not found" / "404" in a message.

  • ProjectionPipeline.to_spec(iri, description=None) (audit A3). Converts a code-assembled pipeline into a declarative, registry-persistable ProjectionPipelineSpec — the canonical pipeline model — so a pipeline can be registered via register_pipeline() and executed through run_projection(). Transform steps are translated to their registered names; an unregistered inline transform callable raises ValueError. New helper holonic.plugins.name_for_transform() backs the callable→name lookup.

Internal

  • HolonicDataset.traverse() decomposed into focused helpers (_resolve_target_interior, _projection_hash, _inject_projection, _rollback_injection, _store_projection_hash, _record_traversal_outcome), cutting its cyclomatic complexity (audit CQ2).

  • project_to_lpg() decomposed into _find_list_heads, _find_blank_parents, and _build_nodes_and_edges (with per-triple _project_triple), and its six keyword knobs bundled into an internal ProjectionOptions dataclass — cutting the function’s cyclomatic complexity from ~48 (audit CQ3). Behavior is unchanged; the existing projection tests cover it.

  • Projection-pipeline registry and execution extracted from the HolonicDataset god class into a new holonic._pipelines.PipelineManager delegate (audit AR1/CQ1), following the existing MetadataRefresher/ScopeResolver pattern. The public methods (register_pipeline, register_pipeline_ttl, attach_pipeline, list_pipelines, get_pipeline, run_projection) are unchanged thin wrappers; the private helpers _pipeline_to_ttl, _step_from_node, _read_pipeline_steps_ordered, and _record_projection_activity moved off HolonicDataset onto the manager. Behavior is unchanged; the plugin/pipeline tests cover it.

  • Console-oriented read projections extracted from HolonicDataset into a new holonic._console.ConsoleReads delegate (audit AR1/CQ1, “core → console layering”): list_holons_summary, get_holon_detail, holon_interior_classes, holon_neighborhood, list_portals, and get_portal now carry their logic on the delegate, with unchanged thin wrappers on HolonicDataset. Behavior is unchanged; the console/browser tests cover it.

  • HolonicDataset god-class decomposition completed (audit AR1/CQ1). The four remaining clusters were extracted onto delegates following the same pattern as PipelineManager/ConsoleReads: holonic._membrane.MembraneValidator (validate_membrane, validate_all), holonic._provenance.ProvenanceLog (recording plus the audit/derivation/rollback reads over those records), holonic._portals.PortalRegistry (portal declaration, mutation, and discovery), and holonic._traversal.TraversalEngine (portal crossing: layer scoping, CONSTRUCT execution, projection injection/rollback, outcome recording). client.py drops from 3862 to 2842 lines and HolonicDataset from 89 methods to 81 — every public method retained as an unchanged thin wrapper, so the facade is API-identical. Verified against the full suite at 568 passed / 9 skipped / 2 xfailed, unchanged from before the extraction.

    Two deliberate asymmetries. The traversal privates moved without facade wrappers, because each was called only from inside that cluster — so the extraction removes them from the facade rather than merely relocating them; the one in-tree test pinning _scoped_dataset (the S5 scoping property) was repointed at the delegate. And _safe_layer_graph, _register_layer, and _maybe_refresh deliberately stayed on HolonicDataset: holon writes, portals, traversal, validation, provenance, and projections all call them, so they are shared infrastructure, not any one cluster’s property. Pushing them into a delegate would have forced the other five to reach through it.

CI / tooling

  • mypy is now part of lint-check (ratcheted per-module as code is cleaned).

  • aiohttp is available in the dev environment; the Fuseki backend tests no longer skip unconditionally.

  • Added a dockerized Apache Jena Fuseki integration suite (pixi run test-integration, opt-in --integration marker).

  • The notebook harness now executes every example in a real IPython kernel (nbclient) instead of a substring heuristic that produced false greens.

  • ipykernel / nbclient / nbformat are declared test dependencies. The notebook harness needs a python3 kernelspec, which used to arrive transitively (jupyterlab in the dev environment, nbconvert in deps-lint). The py311 / py313 CI environments pull neither, so every notebook failed with NoSuchKernel before executing a cell. They are now declared in feature.deps-test and in the test extra, next to the task that needs them.

  • Release gate (packaging). The “Verify package” CI step now runs twine check dist/* and, on a refs/tags/v* build, asserts the git tag equals holonic.__version__ (failing the job on a mismatch) — the check that would have caught the 0.7.1 artifact drift. The PyPI publish step gained skip-existing: true so a re-run of a partially-published tag no longer hard-fails on immutable files.

  • sync_notebooks_to_jlite.py pins widget distributions to the syncing environment. ipywidgets resolves a widget’s frontend labextension by name and semver, and jupyter lite build bakes that labextension into the site from this environment — so a browser that resolves a different version off PyPI renders an empty output area instead of raising, which is close to undebuggable from the reader’s side. Pins are rewritten on every sync (a re-pin overwrites rather than stacks), and a distribution missing from the environment is left alone with a warning, since there is then no bundled frontend to match.

Documentation

  • Core project docs are no longer dropped from the site (audit “toctree drops core docs”). SPEC, DECISIONS, MIGRATION, and CHANGELOG are now first-class Sphinx pages via include-shims (docs/source/{spec,decisions,migration,changelog}.md); index.md’s “Project” toctree and its inline links point at them instead of the unresolvable ../SPEC/../DECISIONS/../MIGRATION/../../CHANGELOG paths that silently omitted them. A new test_docs_structure.py guard fails if any index.md toctree entry stops resolving to a real document. (fail_on_warning is intentionally left false: rendering these large docs surfaces a pre-existing content-formatting warning class that is out of scope for this release; the toctree regression is guarded by the test instead of by -W.)

  • Dangling verifiedBy: traceability repaired (audit “spec-verifiedby dangling”). 39 of the requirement→test citations in docs/SPEC.md (and the generated docs/SPEC.ttl) named test node IDs that no longer existed after tests were reorganized into classes; each is now repointed to the current node ID, or marked verifiedBy: none where no test genuinely covers the requirement (6 such gaps: R1.4, R5.2, R5.3, R5.4, R9.2, R9.44). A new test_docs_structure.py guard statically resolves every node-form citation, so a future rename that orphans a link fails CI.

  • Notebook 11 no longer dies on the first threaded cell under JupyterLite. Pyodide compiles CPython without pthreads, so Thread.start() raises RuntimeError: can't start new thread and ThreadPoolExecutor fails the same way on first submit — which took out two of the three dispatch patterns the notebook exists to demonstrate. Both now branch on sys.platform == "emscripten": the event-queue dispatcher drops its worker and drains cooperatively via a new drain() (wait() still works on both platforms), and the asyncio adapter drops its executor and runs the traversal inline inside the coroutine. Agent IRIs are unchanged, so the provenance comparison and pattern-comparison table further down stay accurate in the browser. Native Python takes the threaded path exactly as before.

  • Notebook 13’s viz install actually installs something under JupyterLite. %pip install "holonic[viz]" was a no-op in the browser: the injected per-notebook cell has already installed a plain holonic, and micropip treats an already-present distribution as satisfying the requirement — extras included — so yfiles_jupyter_graphs never landed and the failure surfaced as a ModuleNotFoundError several cells later. The cell now names the packages explicitly, sidestepping extras resolution, and imports the widget package immediately so a failed install is reported where it happened. The retry is no longer silenced or wrapped in a second except.

  • Notebook 13 explains a holonic/yFiles version skew instead of raising AttributeError. yFiles 2.0 renamed hierarchic_layout to hierarchical_layout; releases before 0.8.0 call the 1.x name directly, and JupyterLite installs holonic from PyPI, so a reader gets that broken pairing until 0.8.0 ships. The install cell now detects the combination — absence of GraphWidget.hierarchic_layout together with absence of the holonic.viz._layout shim — and raises with both versions and the two ways out. The check is a capability probe rather than a version comparison, because the working tree carries the shim while __version__ still reads 0.7.1 until the release bump.

Dependencies

  • Declared the effective rdflib>=7,<8 ceiling explicitly in pyproject.toml and pixi.toml rather than inheriting it transitively from pyshacl; the library binds directly against rdflib 7 APIs (Dataset default_union, initBindings term semantics).

  • Capped yfiles-jupyter-graphs below 3 in pyproject.toml and pixi.toml. The previous open-ended >=1.7 is what broke 0.7.1 in the field: yFiles 2.0.0 landed on 2026-08-10, ~2.5 months after that release, renaming hierarchic_layout to hierarchical_layout, so any clean pip install holonic[viz] from then on resolved an incompatible major and raised AttributeError on the default layout. holonic.viz._layout resolves either spelling, so both majors work now — the ceiling is there to stop the next major from repeating it.

[0.7.1] - 2026-05-25 (bugfix)

  • Fix jlite by adding explicit holonic install for each .ipynb.

  • Remove unused pydantic import.

[0.7.0] - 2026-05-11

Upstream consumer integration and API surface expansion. Driven by friction points documented in docs/UPSTREAM-RECOMMENDATIONS.md from a downstream FastAPI/React console application.

Added

  • classify_sparql(query) utility that returns the SPARQL form ('select', 'ask', 'construct', 'describe', 'update'). Strips comments and string literals before matching. Eliminates the need for consumers to reimplement SPARQL classification.

  • validate_iri(iri) public entry point to the library’s IRI validation. Raises ValueError for unsafe characters.

  • get_activity(activity_iri) direct lookup of a single provenance activity by IRI. Returns TraversalRecord, ValidationRecord, or None. Eliminates the O(all) scan through collect_audit_trail().

  • holarchy_summary(max_age=, recent_limit=) aggregated dashboard snapshot returning HolarchySummary with holon count, portal count, root count, health distribution, staleness count, and recent activities. Reduces 4+ SPARQL round-trips to a single method call.

  • on_traversal(callback) / on_validation(callback) notification hooks. Callbacks fire synchronously after each traverse() or validate_membrane(). Eliminates polling for same-process event detection.

  • ShapeViolation dataclass with structured fields: shape_iri, focus_node, path, value, message, severity. Replaces opaque text-only violation reporting.

  • MembraneResult.shape_violations field populated from the SHACL report graph with full ShapeViolation objects.

  • HolarchySummary dataclass for dashboard integration.

Changed

  • collect_audit_trail() now accepts pagination parameters: limit, offset, since (ISO-8601 timestamp), and kind ('traversal' or 'validation'). Filtering is pushed to the SPARQL engine via ORDER BY DESC(?timestamp) LIMIT/OFFSET and FILTER(?timestamp > ...). Backward compatible; calling with no arguments returns the full trail as before.

Upstream recommendations disposition

#

Recommendation

Decision

1

Async backend

Protocol definition deferred

2

Paginated audit trail

Shipped

3

Activity lookup

Shipped

4

SPARQL classification

Shipped

5

IRI validation (public)

Shipped

6

Derivation chain as graph

Declined (list[str] is correct)

7

Holarchy graph

Deferred

8

Dashboard summary

Shipped

9

find_path efficiency

Deferred

10

Change notification hooks

Shipped

11

Serialization / Pydantic

Declined (to_dict is stable API)

12

Membrane violation detail

Shipped

[0.6.0] - 2026-05-06

Comprehensive audit remediation. Addresses 38 of 40 gap tests from the verified feature audit. Two remaining tests are marked xfail pending resolution of SHACL target-class semantics (OQ11 in SPEC).

Breaking

  • Portal CONSTRUCT queries now scope to projections by default. When the source holon has registered projection graphs and the portal does not declare an explicit cga:sourceLayer, the CONSTRUCT query runs against the projection graphs rather than the full dataset. This prevents PII and sensitive data in raw interiors from leaking through unscoped CONSTRUCT queries. Portals that need full-dataset access can declare cga:sourceLayer cga:InteriorRole explicitly.

Added

  • MembraneResult.is_healthy convenience property. Returns True when health == MembraneHealth.INTACT.

  • Portal type in data model. portal_type: str | None field on PortalInfo, PortalSummary, PortalDetail. All discovery SPARQL queries updated to SELECT the portal’s RDF subtype.

  • SealedPortalError exception (subclass of ValueError). traverse_portal() raises it when the portal is a cga:SealedPortal, regardless of whether a CONSTRUCT query exists.

  • validate_all() batch membrane validation returning dict[str, MembraneResult].

  • update_portal(iri, construct_query=, label=, portal_type=) in-place portal update preserving the IRI and metadata.

  • traverse(fail_on_breach=True) fail-closed traversal. When validation returns COMPROMISED, injected triples are rolled back and MembraneBreachError is raised.

  • traverse_path(source, target) multi-hop traversal along the shortest portal path. Records provenance per hop.

  • dry_run(source, target) simulates a traversal without mutating state. Returns projected triples and a what-if membrane result.

  • compose(holon_iris, layers=) unions interior graphs across multiple holons into one queryable rdflib.Graph.

  • last_traversal(holon_iri) returns the most recent TraversalRecord targeting a holon.

  • derivation_chain(holon_iri) walks prov:wasDerivedFrom backward to return upstream holon IRIs.

  • rollback_traversal(activity_iri) undoes a traversal by re-running the portal’s CONSTRUCT and removing the projected triples from the target interior.

  • Incremental traversal. traverse() computes a SHA-256 hash of the projected graph and compares against a stored hash (cga:lastProjectionHash). Identical projections are skipped as no-ops. Active only when agent_iri is provided.

  • Staleness tracking. freshness(holon_iri) returns a timedelta since the last traversal. is_stale(holon_iri, max_age=) returns a boolean. stale_holons(max_age=) returns all stale holons.

  • Source layer scoping. Portals can declare cga:sourceLayer cga:ProjectionRole or cga:InteriorRole to control which graphs the CONSTRUCT executes against. When no sourceLayer is declared, the library defaults to projection scope if projections exist.

  • SPEC requirements R9.23 through R9.37 covering all audit findings. OQ11 captures the SHACL target-class gap as a critical open design decision.

Audit Remediation (third-party + follow-up)

  • C1: Turtle injection fixed. _escape_ttl() now applied to all user-supplied labels in add_holon(), add_portal(), update_portal().

  • C2: get_graph() returns a copy. RdflibBackend.get_graph() now creates a detached copy with namespace bindings instead of returning a live reference to the dataset graph.

  • S4: IRI validation at API boundaries. _validate_iri() rejects empty strings and characters unsafe in Turtle/SPARQL contexts. Called at the top of all public add_* methods.

  • M1: SHACL report parsing uses structured graph. Replaces fragile text scanning with sh:ValidationResult queries against the pyshacl report graph.

  • M2: Snapshot-based rollback. traverse(fail_on_breach=True) snapshots the target interior before injection and restores it on breach. Eliminates the risk of removing pre-existing triples that overlapped with the projection.

  • M3: ds.batch() context manager. Suppresses metadata refresh during the block and fires one consolidated refresh on exit. Nests safely. Restores mode on exception without refreshing.

  • M4: Concurrency semantics documented in the HolonicStore protocol docstring.

  • O3: DEBUG logging added to add_holon, add_portal, traverse_portal, traverse, validate_membrane.

  • O4: pydantic removed from hard dependencies. Zero imports in the codebase; no longer installed as a required dependency.

  • S3 (partial): Dead _get_or_create_loop removed from FusekiBackend. Connection pooling deferred to 0.7.

Deferred to 0.7

  • S1: Decompose client.py (~3,460 lines) into delegate modules.

  • S2: Migrate to parameterized SPARQL queries via initBindings.

  • S3 (remaining): FusekiBackend connection pooling.

Fixed

  • traverse() now registers injected interiors. Previously, traverse_portal() wrote triples but did not call _register_layer(), causing validate_membrane() to validate against an empty graph (vacuous INTACT false positive).

  • get_holon() rewritten to O(1). Uses a direct filtered SPARQL query (5 queries total) instead of scanning all holons via iter_holons().

[0.5.0] - 2026-04-23

Breaking cleanup release. Removes the deprecated aliases that have been warned about since 0.4.0, adds generator-based iteration for large holarchies, and reorganizes the example notebooks into logical sections with a landing page.

Breaking

  • GraphBackend alias removed. Use HolonicStore (or AbstractHolonicStore as a base class). The alias was deprecated in 0.4.0 and warned through all of 0.4.x.

  • registry_graph constructor kwarg removed. Use registry_iri.

  • ds.registry_graph property removed. Use ds.registry_iri.

  • HOLONIC_SILENCE_DEPRECATION env var is no longer needed. There are no remaining deprecation warnings to silence.

Added

  • add_holon(holon_type=...) kwarg for asserting a functional subtype (e.g. "cga:AgentHolon", "cga:DataHolon") at creation time without raw SPARQL UPDATE. The holon always carries a cga:Holon; the kwarg adds a second rdf:type assertion.

  • Generator-based iteration with pagination. New methods yield results lazily so large holarchies avoid full materialization:

    • iter_holons(limit=, offset=) — yields HolonInfo one at a time

    • iter_portals_from(iri, limit=, offset=) — yields PortalInfo

    • iter_portals_to(iri, limit=, offset=) — yields PortalInfo The existing list_holons() and find_portals_from/to() methods now delegate to the generators and also accept limit/offset. Pagination threads through to SPARQL LIMIT/OFFSET.

  • bulk_load(holons=, portals=) for batch construction. Accepts lists of holon and portal specs (dicts matching the add_holon() and add_portal() parameter names), suppresses per-write metadata refresh during the batch, and fires one consolidated refresh at the end. Significantly faster than N individual add_holon() + add_portal() calls for initial holarchy construction.

  • list_named_graphs() is already a mandatory protocol method on HolonicStore and implemented in both shipped backends. No change needed; R9.17 was already complete.

  • Projection method documentation clarified. project_holon() and run_projection() are distinct operations, not aliases: project_holon is ad-hoc LPG structural collapse with no provenance; run_projection is governed pipeline execution with full PROV-O activity recording. Block comment in client.py explains the three projection methods and their purposes.

  • Notebook reorganization. All 13 notebooks renumbered into logical sections (Foundations 01-03, Governance & Integration 04-06, Operations 07-09, Advanced 10-13). Landing page (00_start_here.ipynb) rebuilt with sectioned tables and hyperlinks. All cross-references and titles corrected.

  • py.typed marker added. Downstream consumers using mypy or pyright will now get type-checking support from holonic’s annotations.

  • __repr__ on HolonicDataset. repr(ds) now shows HolonicDataset(backend=RdflibBackend, holons=3, registry='...').

  • to_dict() on all dataclasses. Every model and console-model dataclass (20 classes) inherits _DictMixin providing to_dict() for JSON-ready serialization. Enum values are converted to their string value automatically.

  • export() and export_graph(). Serialize the entire dataset to TriG/N-Quads or a single named graph to Turtle/JSON-LD/N-Triples. Closes the build-to-persist gap: ds.export() is the complement to building a holarchy via add_holon/add_portal.

  • summary() portal display improved. Shows Label (source -> target) when a label exists, plain source -> target when it doesn’t. Eliminates the previous redundant display.

  • Developer landing page. notebooks/00_index.ipynb provides a sectioned guide to all 13 example notebooks with descriptions, learning objectives, and a quick-reference code block. Separate from the JupyterLite landing page (00_start_here.ipynb); the sync script skips 00_-prefixed notebooks so each directory keeps its own landing page.

Fixed

  • Latent bug in COLLECT_HOLONS query. Used cga:classification (nonexistent) instead of cga:dataClassification. Fixed in 0.4.3 but included here for completeness.

[0.4.3] - 2026-04-22

Ontology enrichment and holon subtype shapes. One breaking change: cga:dataClassification is now an ObjectProperty ranging over cga:ClassificationLevel individuals rather than a DatatypeProperty accepting free-text strings. See docs/MIGRATION.md for the migration path.

Breaking

  • cga:dataClassification changed from DatatypeProperty to ObjectProperty. Range is now cga:ClassificationLevel. Existing triples using string literals ("CUI", "SECRET") must be rewritten as IRI references (cga:Internal, cga:Confidential). The shipped individuals are cga:Public, cga:Internal, cga:PII, cga:Confidential, cga:Restricted) plus government tiers (cga:CUI, cga:Secret, cga:TopSecret). Downstream consumers may add domain-specific individuals. SHACL shapes now validate with sh:class cga:ClassificationLevel instead of sh:datatype xsd:string.

Added

  • New SHACL shapes for holon subtypes:

    • cga:AgentHolonShape — Info severity: agent holons should have interior (state/memory), boundary (output constraints), and context (execution history) layers populated.

    • cga:AggregateHolonShape — Warning severity via SPARQL: flags aggregate holons that have interior data but no traversal provenance linking that data to a prov:Activity. Catches holons misclassified as aggregates.

  • notebooks/06_holon_subtypes.ipynb — new notebook exercising AgentHolon, AggregateHolon, and ClassificationLevel shapes. Shows well-formed vs. incomplete agent holons, correct vs. suspect aggregates, and the new typed classification values.

  • OQ10 added to SPEC — upper-ontology alignment (BFO/CCO, gist) captured as a deferred open question with the alignment-not-import recommendation.

Fixed

  • Latent bug in COLLECT_HOLONS query. The query used cga:classification (nonexistent property) instead of cga:dataClassification. Classification values were silently never returned by the console model. Fixed.

[0.4.2] - 2026-04-18

Structural lifecycle completion. The library now exposes clean deletion for holons and portals, completing the CRUD surface that add_holon / add_portal started. Portal creation gains extensibility: construct_query is optional, the portal’s RDF type is customizable, and arbitrary Turtle can be appended via extra_ttl. Portal discovery is relaxed to recognize any portal subtype (previously hardcoded to cga:TransformPortal).

Added

  • HolonicDataset.remove_holon(iri) (R9.20) — cascading removal of a holon and all its state. Cleans up the registry entry, all four layer graphs, graph-typing triples, graph-level metadata records, the per-holon rollup, and every portal where the holon is source or target. Child holons that referenced the removed holon via cga:memberOf are orphaned (their memberOf triple is removed) but preserved. Provenance activities are preserved because provenance is immutable history. Idempotent — returns False for a non-existent IRI rather than raising.

  • HolonicDataset.remove_portal(portal_iri) (R9.21) — targeted portal removal. Deletes all triples with the portal as subject from every graph containing them (typically the source boundary graph and the registry mirror). The boundary graph itself is preserved; sibling portals and SHACL shapes in the same graph are unaffected. Idempotent.

  • add_portal() gains three new parameters (R9.22): construct_query is now optional (default None); the new portal_type kwarg (default "cga:TransformPortal") lets callers create cga:IconPortal, cga:SealedPortal, or downstream subclasses; the new extra_ttl kwarg accepts additional Turtle triples for predicates carried by downstream portal subclasses. All existing positional-argument calls continue to work unchanged.

  • cga:IconPortal class added to cga.ttl. Previously referenced in documentation but not declared in the ontology. An IconPortal represents a purely referential relationship between two holons — traversal returns an empty projection because no transformation is defined. Used when a relationship should be visible for discovery or display without implying a data flow.

  • Sharpened portal subclass semantics in the ontology. The cga:TransformPortal, cga:IconPortal, and cga:SealedPortal skos:definition blocks now document the per-subtype cga:constructQuery expectations explicitly: TransformPortal requires one, IconPortal and SealedPortal must not carry one.

  • New SHACL shapes: cga:IconPortalShape and cga:SealedPortalShape enforce the cga:constructQuery maxCount 0 constraint at Warning severity. A TransformPortal silently reclassified to SealedPortal for temporary blocking retains its query definition but surfaces a warning, which is the intended behavior — the warning signals intent to reclassify back.

  • Ontology enrichment — holistic review and improvement of cga.ttl and cga-shapes.ttl:

    • Ontology version bumped to 0.4.2 with dcterms:modified, rdfs:seeAlso (docs site, PROV-O, SHACL), and a skos:note about the standalone/alignment strategy (OQ10).

    • All 68 properties now carry skos:definition (29 were bare).

    • All four LayerRole individuals expanded from one-line to multi-line definitions covering what belongs in each layer, what the library writes automatically, and what downstream consumers add.

    • New cga:ClassificationLevel class with five individuals (Public, CUI, PII, Secret, TopSecret).

    • cga:PortalType individuals (UnidirectionalPortal, BidirectionalPortal) now have definitions and a note that directionality is orthogonal to subtype, populated by downstream consumers.

    • cga:Holon class gains a skos:note documenting the dcterms:conformsTo governance pattern.

    • New shapes: cga:AlignmentHolonShape (Warning: should have label and interior), cga:HolonStewardshipShape (Info: nudge for stewardship on all holon subtypes), cga:ProjectionPipelineSpecShape (Warning: should have label and steps), cga:ProjectionPipelineStepShape (Warning: step with neither transformName nor constructQuery does nothing).

    • Shapes file version bumped to 0.4.2.

  • notebooks/04_governed_boundaries.ipynb — new notebook demonstrating governed boundary contracts. Exercises cga:AlignmentHolon, cga:DataDomain, cga:dataSteward, cga:dataClassification, cga:usesAlignment, and PortalClassificationShape. Builds a two-department holarchy with CCO and Schema.org ontologies, a governed portal with an alignment holon justification, and queries the full governance chain in a single SPARQL query.

  • Notebooks renumbered 04→05 through 11→12 to accommodate the new governed-boundaries notebook at position 04. All cross-references within notebooks updated. JupyterLite content and the landing notebook reflect the new numbering.

  • src/holonic/test/test_lifecycle.py — 19 new tests covering the three lifecycle surfaces plus metadata-eager behavior during cascade.

  • src/holonic/test/test_ontology.py — 5 new tests verifying the IconPortal class is declared and the three portal subtype shapes enforce their expected semantics (TransformPortal without query → violation; IconPortal/SealedPortal with query → warning; IconPortal/SealedPortal without query → conforms).

Changed

  • Portal discovery queries relaxed. FIND_PORTALS_FROM/TO/DIRECT in holonic/sparql.py no longer filter on a cga:TransformPortal. The cga:sourceHolon + cga:targetHolon predicate pair uniquely identifies a portal regardless of its specific subtype, and relaxing the filter means cga:SealedPortal, cga:IconPortal, and downstream subclasses created via the new portal_type kwarg are discoverable through the same public API.

  • Portal discovery deduplicated. FIND_PORTALS_FROM/TO/DIRECT now use SELECT DISTINCT. The pre-0.4.2 queries matched the portal triples in every graph they appeared, which produced one duplicate row per additional graph (typically returning each portal twice — once from the boundary graph, once from the registry mirror). This was a latent bug; existing test assertions used >= 1 rather than == 1 and did not catch it.

  • add_portal() now fires _maybe_refresh(graph_iri) after writing the portal triples, matching the invariant the other add_* methods maintain. Previously add_portal silently skipped metadata refresh — a latent bug surfaced while adding the lifecycle methods.

SPEC updates

  • R9.20, R9.21, R9.22 added under “Shipped in 0.4.2” with full annotations (priority, constrains, acceptance, verifiedBy). Spec maturity remains at 100% (63/63 clean, 0 violations, 0 warnings).

  • Compliance check script extended with three new checks for R9.20–R9.22. Script now runs 64 checks total: 56 pass, 0 fail, 8 manual (SHOULD items deferred per their recommendation text).

Notes for downstream

No migration work is required for 0.4.2. All existing API signatures continue to work unchanged; the three lifecycle additions and the add_portal() new kwargs are additive. The portal discovery relaxation affects what find_portals_* returns when non-TransformPortal subtypes exist in the registry — these were previously invisible to the public API despite being declared in the ontology. If a downstream consumer was relying on the hardcoded filter as a way to exclude non-TransformPortal subtypes from results, they can re-add the filter at the consumer level.


[0.4.1] - 2026-04-18

Documentation, release hygiene, a new dispatch-patterns notebook, restored JupyterLite deployment, and a new visualization example. No library behavior changes.

Added

  • docs/source/dom-comparison.md — a dedicated documentation page mapping holonic concepts onto the W3C DOM for readers already familiar with that model. Covers the concept-mapping table, how holons might eventually be dispatched, and where the DOM analogy diverges. Expanded treatment of synchronous APIs over asynchronous substrates.

  • OQ9 in docs/SPEC.md — captures DOM-style event propagation as a candidate coordination model, with status deferred and recommendation to verify strain against real use cases before committing to implementation.

  • notebooks/11_dispatch_patterns.ipynb — demonstrates three dispatch patterns against the synchronous HolonicDataset API: direct synchronous, event-queue with a single worker, and asyncio over a single-worker ThreadPoolExecutor. Ties directly to the DOM comparison documentation. Includes a correctness note about rdflib’s SPARQL parser not being thread-safe.

  • notebooks/13_visualization.ipynb — restored yFiles-based visualization example. Uses HolarchyViz, HolonViz, SPARQLExplorer, and ProvenanceViz from the existing holonic.viz module. Begins with %pip install "holonic[viz]" to pull the optional yfiles-jupyter-graphs dependency.

  • JupyterLite deployment restored. jupyterlite/content/ now carries all example notebooks plus a new 00_start_here.ipynb landing notebook that introduces the in-browser environment and its constraints. The build_jl pixi task outputs to docs/source/_extra/jupyterlite/ and Sphinx’s html_extra_path copies it verbatim into the output root at /jupyterlite/.

  • .readthedocs.yaml — new config that runs jupyter lite build as a pre-build step, so the hosted docs at ReadTheDocs include the in-browser lab at /jupyterlite/.

  • “Try in browser” section in docs/source/index.md linking to the hosted JupyterLite build.

  • scripts/sync_notebooks_to_jlite.py — keeps jupyterlite/content/ in sync with the canonical notebooks/ directory. Invoked via pixi run sync_notebooks_to_jlite.

  • scripts/spec_compliance_check.py — comprehensive release-gate audit that exercises each SPEC requirement against actual library behavior (not just specl annotation coverage). Invoked via pixi run -e spec spec-compliance. Reports 53 pass, 0 fail, 8 manual (SHOULD items deferred per their recommendation text) for 0.4.1.

  • ProjectionPipeline.apply_to_graph() — alias for apply() that matches the terminal-method naming used in SPEC R7.3 alongside apply_to_lpg(). Existing callers using apply() keep working unchanged.

  • PyPI badge in the README (shields.io/pypi/v/holonic).

  • Note about conda-forge in the README install section — package is available on PyPI now; conda-forge support is in progress.

Changed

  • Spec maturity badge location moved from build/spec-badge.svg to static/spec-badge.svg so the generated artifact is committed alongside other static assets rather than living in the (gitignored) build directory. README link updated.

  • Sphinx release in docs/source/conf.py corrected from 0.2.0 to 0.4.1. This was a stale config value driving an incorrect version label in the ReadTheDocs header.

  • Version bumped to 0.4.1 in pyproject.toml and src/holonic/__init__.py.

  • jupyterlite/requirements.txt rewritten; old file pinned holonic>=0.1,<0.2 which was long stale.

  • SPEC R7.2 wording aligned with actual implementation parameter names (collapse_types, collapse_literals, resolve_blanks, resolve_lists). No code change; purely a documentation accuracy fix surfaced by the new compliance check.

Notes for downstream

No migration work is required for 0.4.1. Behavior, protocols, and APIs are unchanged from 0.4.0. ProjectionPipeline.apply() remains fully supported; apply_to_graph() is a new alias, not a replacement. Upgrading is a drop-in version bump.


[0.4.0] - 2026-04-16

Breaking release. The GraphBackend protocol is renamed HolonicStore; the old name remains as a deprecated alias through all of 0.4.x. FusekiBackend requires dataset as a keyword argument. See docs/MIGRATION.md for a full checklist.

Added

  • HolonicStore protocol (holonic.backends.store) — replaces GraphBackend as the canonical backend interface. Identical mandatory surface.

  • AbstractHolonicStore ABC — recommended base class for new backends. Marks mandatory methods as @abstractmethod; provides a placeholder for optional-method defaults that will grow over 0.4.x.

  • Optional native dispatch hook: refresh_graph_metadata( graph_iri, registry_iri). Backends that implement this method are called directly by MetadataRefresher.refresh_graph; backends without it get the generic Python fallback. Dispatch is duck-typed via hasattr — no registration required.

  • registry_iri parameter on HolonicDataset. Replaces registry_graph; the old name remains as a deprecated keyword-only alias.

  • docs/MIGRATION.md — per-release migration guide. 0.3.x → 0.4.0 is the first entry.

  • 13 new tests in test_deprecation_and_dispatch.py covering alias warnings, suppression via environment variable, parameter-rename compatibility, FusekiBackend constructor enforcement, and native-dispatch hook behavior.

Changed (breaking)

  • FusekiBackend constructor: dataset is now keyword-only. FusekiBackend("http://...", "ds") raises TypeError. Use FusekiBackend("http://...", dataset="ds").

  • First-party backends inherit AbstractHolonicStore. RdflibBackend and FusekiBackend now extend the ABC. Client code that tests isinstance(backend, HolonicStore) still works; code that tests isinstance(backend, GraphBackend) via the deprecated alias also still works.

Deprecated

  • GraphBackend importable from holonic, holonic.backends, and holonic.backends.protocol. Emits DeprecationWarning on first use per module per Python session. Scheduled for removal in 0.5.0.

  • registry_graph kwarg on HolonicDataset. Emits DeprecationWarning when used. Scheduled for removal in 0.5.0. Reading ds.registry_graph as an attribute is silent and remains supported through 0.4.x.

Deprecation suppression

Set HOLONIC_SILENCE_DEPRECATION=1 in the environment to suppress all 0.4.0 deprecation warnings. Intended for CI usage until downstream migration is complete.

Internal

  • New file: src/holonic/backends/store.py.

  • src/holonic/backends/protocol.py converted to deprecation shim using __getattr__.

  • src/holonic/backends/__init__.py rewritten to export new canonical names with a __getattr__-based GraphBackend alias.

  • Package-level src/holonic/__init__.py gains a __getattr__ shim for the same purpose.

  • src/holonic/client.py: internal attribute renamed self.registry_graph → self.registry_iri. All 14 internal call sites updated.

  • src/holonic/_metadata.py: refresh_graph now discovers and dispatches to native refresh_graph_metadata when present.

  • First-party backends inherit AbstractHolonicStore.

  • New docs: docs/MIGRATION.md, docs/DECISIONS.md § 0.4.0.

  • Specl moved out of the default dev pixi environment. A new dedicated spec environment pulls specl from git only when the spec pipeline is invoked (pixi run -e spec ...). Keeps day-to-day work from depending on a git pull of specl. See CONTRIBUTING.md § “Specl (optional, for spec work)”.

  • SPEC.md fully annotated for 100% maturity. Every requirement carries priority, constrains, acceptance, and verifiedBy sub-bullets. User stories carry asA, soThat, and acceptance. Open questions reformatted as OQ1–OQ7 with owner, recommendation, and status sub-bullets. Requires specl 0.2.0+ for the Phase 1 sub-bullet parser. Score: 60/60 requirements clean, 0 violations, 0 warnings. Badge rendered at build/spec-badge.svg and embedded in the README.

Limits (known, documented)

  • The optional protocol surface is currently one method (refresh_graph_metadata). Scope walking, bulk load, and pipeline execution native hooks are planned additively for 0.4.x releases as evidence of native-backend benefit emerges.

  • The deprecation-warning system uses module-level boolean flags. Multi-process deployments may emit the warning once per process rather than once per session cluster-wide. Acceptable for dev ergonomics; not a concern for production.

[0.3.5] - 2026-04-16

Added

  • Projection plugin system. Projection pipelines are RDF-modeled specs in the registry; Python transforms are discovered via importlib.metadata entry points (holonic.projections group). Pipelines reference transforms by registered name. See docs/DECISIONS.md § 0.3.5.

  • holonic.plugins module with:

    • @projection_transform(name) decorator for first-party transform registration.

    • get_registered_transforms() — merges first-party and entry-point transforms; first-party wins on name collision.

    • resolve_transform(name) — lookup by registered name.

    • TransformNotFoundError — raised at registration time when a pipeline references an unknown transform.

    • transform_version(name) — returns "<pkg-name>==<version>" for provenance recording.

    • host_metadata() — returns dict of hostname, platform, Python version, holonic version.

  • First-party transforms registered via entry points: strip_blank_nodes, localize_predicates, collapse_reification. Declared in pyproject.toml under [project.entry-points."holonic.projections"]. Third-party packages register the same way.

  • CGA ontology section 9 (Projection Pipeline Vocabulary) — adds cga:ProjectionPipelineSpec class, cga:ProjectionPipelineStep class, cga:hasPipeline (Holon → Spec), cga:hasStep (Spec → rdf:List), plus cga:stepName, cga:transformName, cga:transformVersion, cga:runHost, cga:runPlatform, cga:runPythonVersion, cga:runHolonicVersion. cga:constructQuery is reused from the portal vocabulary.

  • New HolonicDataset methods:

    • register_pipeline(spec) — validates transform names, writes spec to registry, returns the spec IRI.

    • register_pipeline_ttl(ttl) — raw-Turtle escape hatch for advanced use.

    • attach_pipeline(holon_iri, spec_iri) — declares holon has access to a pipeline.

    • list_pipelines(holon_iri) — returns ProjectionPipelineSummary objects for attached pipelines.

    • get_pipeline(spec_iri) — returns full ProjectionPipelineSpec with step order preserved via rdf:List walk.

    • run_projection(holon_iri, spec_iri, *, store_as=None, agent_iri=None) — merges interior graphs, executes steps, optionally stores result as a projection layer, records a prov:Activity in the holon’s context graph.

  • New dataclasses in console_model:

    • ProjectionPipelineSpec — top-level pipeline with iri, name, steps, description.

    • ProjectionPipelineStep — one step with name, transform_name, construct_query.

    • ProjectionPipelineSummary — lightweight listing record.

  • Four new SPARQL templates: LIST_PIPELINES_FOR_HOLON_TEMPLATE, READ_PIPELINE_DETAIL_TEMPLATE, PIPELINE_STEPS_TEMPLATE, WALK_PIPELINE_LIST_TEMPLATE.

  • Provenance recording for projection runs. Activities carry prov:used, prov:generated, prov:startedAtTime, prov:endedAtTime, optional prov:wasAssociatedWith, cga:transformVersion per transform used, and four cga:run* host-metadata fields.

  • run_projection honors metadata_updates per D-0.3.3-5. When store_as is set and mode is eager, the projection graph’s metadata refreshes automatically. Same contract as other library-mediated writes.

  • 20 new tests in test_plugins.py covering registration, validation, attachment, listing, detail readback, step-ordering preservation, execution with and without store_as, provenance recording, CONSTRUCT-only steps, the Turtle escape hatch, and multi-pipeline-per-holon scenarios.

Design decisions

  • docs/DECISIONS.md § 0.3.5 documents the 10 architectural decisions for this release: RDF-modeled pipelines + entry-point transforms (hybrid), rdf:List for step ordering, dataclass naming (ProjectionPipelineSpec / ProjectionPipelineStep), dogfooded entry-point registration for first-party transforms, loose provenance with host metadata, metadata-refresh consistency, narrow scope with console integration hooks, separate holonic.plugins module, transform signature conventions, naming.

Internal

  • Files touched: __init__.py (version, exports), client.py (pipeline methods + helpers), console_model.py (three new dataclasses), sparql.py (four templates), projections.py (first-party transforms registered via decorator), ontology/cga.ttl (section 9), pyproject.toml (version, entry points), docs/DECISIONS.md.

  • New files: plugins.py, test/test_plugins.py.

Limits (known, documented)

  • Pipeline steps do not carry per-step arguments. Transforms are invoked with their defaults only. For parameterized transforms (e.g. filter_by_class(class_iri)), use the construct_query step form or wait for argument support in a later release.

  • transform_version() returns None in source-only environments where holonic is not installed as a distribution. In normal pip install / pip install -e . environments it resolves the package and version correctly.

  • Pipelines with construct_query steps execute the CONSTRUCT against the current in-memory intermediate graph via rdflib.Graph.query().graph. For multi-step CONSTRUCT pipelines, each step sees the previous step’s output, not the original merged interior.

  • Pre-0.3.5 projections registered via project_holon(store_as=...) or apply_pipeline(store_as=...) remain in place but have no associated cga:ProjectionPipelineSpec. A migration to synthesize minimal specs for historical projections is deferred to a later release (SPEC R9.11).

  • Console integration is intentionally deferred. The list_pipelines and get_pipeline methods return console-shaped summary/detail records ready for consumption; the actual UI work lives in the holonic-console repo.

[0.3.4] - 2026-04-16

Added

  • Ontological graph categories. Named graphs carry RDF types in the registry via a flat + role pattern: <graph> a cga:HolonicGraph ; cga:graphRole cga:InteriorRole (or BoundaryRole, ProjectionRole, ContextRole, RegistryRole). Type-based discovery (SELECT ?g WHERE { ?g a cga:HolonicGraph ; cga:graphRole cga:InteriorRole }) works without SPARQL entailment. Rationale: docs/DECISIONS.md § D-0.3.4-1.

  • CGA ontology section 8 (Graph Type Vocabulary) — adds cga:HolonicGraph umbrella class, cga:graphRole property, cga:RegistryRole individual. cga:LayerGraph rdfs:subClassOf cga:HolonicGraph makes every existing LayerGraph discoverable via the new vocabulary.

  • Eager typing on registration. add_interior, add_boundary, add_projection, add_context now write the typing triples alongside the layer binding. Zero API change; one extra triple per registration.

  • Registry self-typing. The registry graph itself is typed as cga:HolonicGraph with cga:graphRole cga:RegistryRole on the first metadata refresh.

  • holonic.scope module. Scoped discovery across the holarchy. Public classes: ResolvePredicate (protocol), HasClassInInterior, CustomSPARQL, ResolveMatch, ScopeResolver.

  • HolonicDataset.resolve(predicate, from_holon, *, max_depth=3, order="network", limit=50) — BFS walk through the holarchy returning ResolveMatch records. Three topologies: "network" (outbound + inbound portals, default), "reverse-network" (inbound only), "containment" (cga:memberOf chain). max_depth clamps to [0, 100]; limit clamps to [1, 10000]. Strict BFS with alphabetical tiebreaking for determinism.

  • Two predicate classes. HasClassInInterior(class_iri) uses the 0.3.3 class inventory when present, falls back to a direct interior query when not. CustomSPARQL(ask_template) is the escape hatch: templates use {holon_iri} and {registry_iri} placeholders substituted via str.replace so normal SPARQL braces do not need escaping.

  • Migration CLI: holonic-migrate-registry. Backfills graph typing for layer graphs registered before 0.3.4. Dry-run by default; --apply writes. Idempotent. Added as a [project.scripts] entry point; also callable as python -m holonic.cli.migrate_registry.

  • Seven new SPARQL templates: TYPE_GRAPH_TEMPLATE, QUERY_GRAPH_TYPE_TEMPLATE, LIST_UNTYPED_LAYER_GRAPHS_TEMPLATE, WALK_OUTBOUND_PORTAL_NEIGHBORS_TEMPLATE, WALK_INBOUND_PORTAL_NEIGHBORS_TEMPLATE, WALK_MEMBER_OF_NEIGHBORS_TEMPLATE, ASK_HAS_CLASS_IN_INTERIOR_TEMPLATE.

  • 27 new tests: test_typed_graphs.py (ontology, eager typing, registry self-typing, migration CLI) and test_scope.py (predicate matching, BFS ordering, depth/limit clamps, three order modes, CustomSPARQL, standalone ScopeResolver).

Design decisions

  • docs/DECISIONS.md § 0.3.4 documents the 10 architectural decisions: flat + role (not subclass hierarchy) for graph categories, eager typing + migration CLI for backfill, narrow predicate API (two classes), strict BFS (no weighting), holonic.scope as a public module, typing in the existing registry graph, registry self-typing, health aggregates deferred, no protocol extension, naming.

Internal

  • Files touched: __init__.py, client.py (_register_layer + __init__ + resolve), _metadata.py (_ensure_registry_typed), sparql.py, ontology/cga.ttl, pyproject.toml, docs/SPEC.md, docs/DECISIONS.md.

  • New files: scope.py, cli/__init__.py, cli/migrate_registry.py, test/test_typed_graphs.py, test/test_scope.py.

  • Version bumped in __init__.py and pyproject.toml.

Limits (known, documented)

  • ASK_HAS_CLASS_IN_INTERIOR_TEMPLATE has a fallback branch that queries the interior graph directly if the class inventory is missing (because metadata_updates="off" and no explicit refresh). This keeps correctness at the cost of slower ASK evaluations for unregistered callers.

  • Additional predicate classes (HasPortalProducing, HasShapeFor, LabelMatches) are not yet shipped. Use CustomSPARQL for these cases. Upgrade path will be additive if evidence warrants first-class support.

  • The GraphBackend protocol was NOT extended. Scope resolution is a pure Python implementation against the existing backend surface. Protocol split with optional mixin is held for 0.4.0.

[0.3.3] - 2026-04-16

Added

  • Graph-level metadata in the registry. Per-graph triple counts, last-modified timestamps, and class inventories are now materialized as triples in the registry graph (urn:holarchy:registry by default, configurable via HolonicDataset(registry_graph=...)).

  • HolonicDataset.refresh_metadata(holon_iri) — recompute and persist metadata for all layer graphs of a holon plus the per-holon rollup. Use after out-of-band backend writes.

  • HolonicDataset.refresh_all_metadata() — same, for every holon in the registry. Returns holon count.

  • HolonicDataset.get_graph_metadata(graph_iri) — read the currently-materialized GraphMetadata for a graph. Returns None if nothing has been written (either metadata_updates="off" with no explicit refresh, or the graph is unknown to the registry).

  • HolonicDataset(metadata_updates=...) parameter — controls automatic refresh policy. "eager" (default) runs a refresh after every library-mediated write to a layer graph. "off" suppresses all automatic refreshes; callers use refresh_metadata() explicitly. Lazy mode is deliberately not included; see docs/DECISIONS.md § D-0.3.3-2.

  • console_model.GraphMetadata — dataclass with fields iri, triple_count, last_modified, refreshed_at, class_inventory, graph_role. Re-exported from the top-level holonic package.

  • HolonDetail extended non-breakingly — new optional fields layer_metadata: dict[str, GraphMetadata] and holon_last_modified: str | None. get_holon_detail() populates them when registry metadata is present.

  • CGA ontology section 7 (Graph-Level Metadata) — adds cga:ClassInstanceCount class, per-graph properties (cga:tripleCount, cga:lastModified, cga:refreshedAt), inventory predicates (cga:inGraph, cga:class, cga:count), and the rollup cga:holonLastModified.

  • Eight new SPARQL templates in holonic.sparql: COUNT_GRAPH_TRIPLES_TEMPLATE, COUNT_GRAPH_TYPES_TEMPLATE, CLEAR_GRAPH_METADATA_TEMPLATE, CLEAR_HOLON_METADATA_TEMPLATE, READ_GRAPH_METADATA_TEMPLATE, READ_GRAPH_CLASS_INVENTORY_TEMPLATE, LIST_HOLON_LAYER_GRAPHS_TEMPLATE, LIST_HOLON_INTERIOR_GRAPHS_TEMPLATE.

  • Internal holonic._metadata.MetadataRefresher — unit-testable class that owns all graph-metadata computation. Uses stable inventory record IRIs (<graph>/inventory/<slug>-<8-hex-hash>) for idempotent clear + insert writes. Backend-agnostic; relies only on the GraphBackend protocol.

  • 20 new tests in test_metadata.py covering: eager/off modes, refresh idempotence, multi-type inventory, inventory replacement on re-refresh, per-holon rollup across multiple interior graphs, custom registry graph, and reconciliation after out-of-band writes.

Design decisions

  • New docs/DECISIONS.md file captures the 10 architectural decisions made for 0.3.3 (registry-as-single-graph, two-mode update policy, reified class-inventory records, per-graph + per-holon metadata duality, write-trigger list, public API shape, deferred health aggregation, timestamp conventions, internal refresher pattern, naming). Append-only going forward.

Internal

  • Files touched: __init__.py, client.py, console_model.py, sparql.py, ontology/cga.ttl. New file: _metadata.py, test/test_metadata.py, docs/DECISIONS.md.

  • Version bumped in __init__.py and pyproject.toml.

Limits (known, documented)

  • Direct backend.put_graph() / backend.update() calls bypass the eager refresh hook. Use refresh_metadata() to reconcile.

  • metadata_updates="lazy" is not yet offered. Deferred pending evidence of a concrete need.

  • Aggregate membrane health is not yet materialized in the registry. Deferred to 0.3.4 alongside scope resolution (see DECISIONS.md § D-0.3.3-7).

[0.3.1] - 2026-04-13

Added

  • holonic.console_model module with operator-tool dataclasses: HolonSummary, HolonDetail, ClassInstanceCount, NeighborhoodNode, NeighborhoodEdge, NeighborhoodGraph (graphology-compatible via to_graphology()), PortalSummary, PortalDetail. Exported from the top-level holonic package.

  • HolonicDataset.list_holons_summary() — single-query lightweight holon listing, no per-holon layer fan-out. The existing list_holons() is unchanged and remains the rich-HolonInfo path.

  • HolonicDataset.get_holon_detail(iri) — full descriptor including layer graph IRIs and an interior triple count.

  • HolonicDataset.holon_interior_classes(iri) — (class_iri, count) pairs across all interior graphs.

  • HolonicDataset.holon_neighborhood(iri, depth=1) — portal-bounded BFS subgraph shaped for direct serialization to graphology JSON.

  • HolonicDataset.list_portals() and get_portal(iri) — flat portal browsing, separate from the source/target-keyed find_portals_* family.

  • HolonicDataset.portal_traversal_history(iri, limit=50) — recent PROV-O activities scoped to one portal. Limit is clamped to 10,000 to bound runaway queries. Note: the current RECORD_TRAVERSAL template does not record the portal IRI as a structured triple, so history is filtered by (source, target) pair. Correct in the common case of one portal per ordered pair; if multiple portals share endpoints, results are pooled.

  • FusekiBackend(extra_headers={...}) and the underlying FusekiClient(extra_headers={...}) — sets default headers merged into every outbound request. Per-call headers take precedence. Lets external orchestrators (e.g. holonic-console) pass bearer tokens, mTLS-handshake hints, or tenant identifiers through.

  • New SPARQL templates in holonic.sparql: COLLECT_HOLONS, COUNT_INTERIOR_CLASSES_TEMPLATE, COUNT_INTERIOR_TRIPLES_TEMPLATE, PORTAL_TRAVERSAL_HISTORY_TEMPLATE.

Internal

  • Files touched: client.py, sparql.py, __init__.py, backends/_fuseki_client.py, backends/fuseki_backend.py, plus the new console_model.py. CRLF line endings on the touched source files normalized to LF; remaining files in the repo are untouched.

[0.3.0] - 2026-04-07

Added

  • Holon visualization example

  • Auto-docs

Changed

  • Extensions to the OWL and SHACL ontologies

Fixed

  • RDF-native holon depth computation and tree structure