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, sodocs/SPEC.mdwas migrated to contract 2:spec_baseis now declared:https://zwelz3.github.io/holonic/spec.ttl#. Every item has a permanent, dereferenceable IRI —R1.1resolves to…/spec.ttl#R1.1, and the graph is published to that path by thedocsCI job. The.ttlextension is load-bearing: GitHub Pages serves it astext/turtle, which an extensionless path would not be.User-story triad renamed
asA/soThat→role/benefit.roleandownerare now references, so a# Personassection (P1–P7) and an# Agentssection (AG1) were added and the annotations point at identifiers rather than repeating a spelling.RDF vocabulary terms moved from
constrains:to the newgoverns:.constrains:keeps its declared range ofspecl: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–DN7andC1–C5identifiers. 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: noneannotations (R1.4, R5.2, R5.3, R5.4, R9.2, R9.44).nonewas not a null — it minted a realspec:test-none a specl:Testnode, 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 declaresstatus: production— the threshold lives in the spec, not the workflow.The maturity badge is published rather than committed.
static/spec-badge.svgis deleted; the badge is rendered intodocs/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.pynow detects implementation drift — it fails if a requirement’simplementation: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 hardcoded0.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 inMembraneResult.untargeted(UntargetedNode), at Info by default, andtraverse(fail_on_breach=True)treats the nodes it injected as violations. A holon’s boundary can exempt a class withcga:permitsTypeor set the severity withcga:untargetedTypeSeverity, both new ontology terms. The twoxfailtests for OQ11 now pass. A holon whose portals inject node types its shapes do not target needs shapes orcga:permitsTypedeclarations before usingfail_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 acrosstest_ontology.py,test_backend.py,test_portal.pyandtest_membrane.py, covering: no library write reaching the default graph; traversal recordingprov:wasDerivedFromwhile never emittingcga:derivedFrom; theHolonSplit/HolonMergePROV subclassing; the graph-metadata vocabulary; andShapeViolationfield population.test_spec_records_creation_dateguardsdct:createdin bothSPEC.mdandSPEC.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.tomlfilterwarnings). rdflib 7.6 renamedDataset.default_contextandDataset.contextsbut its own SPARQL engine still calls the old names, so every query and update the suite issues emitted two or threeDeprecationWarnings 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.mdremains the machine-checked source (R9.*requirements,OQ1–OQ11open questions); the README’s## Roadmapsection duplicated it by hand and had already drifted, still citingOQ1–OQ10.docs/spec-history.ttl— spec maturity recorded asspecl:MaturityAssessmentactivities, seeded with a 0.9.0 baseline (maturity 99%, progress 86%). Written at release cadence byspec-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()}", socga:Intactappears 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 bycga-shapes.ttl, which do work when a user writes them even though the library never does.
Fixed
fail_on_breachno longer fails open on an unrecognized SHACL severity (#30)._parse_shacl_reportmatched severities by suffix and dropped any result it could not classify, so a shape declaring a custom severity left the membrane INTACT andtraverse(..., fail_on_breach=True)injected the breaching data. Severities are now compared withsh:Violation,sh:Warningandsh:Infoexactly, and a result with any other severity, or none, counts as a violation.sh:Inforesults, previously discarded, are kept in the newMembraneResult.infosand never lower health.validate_membraneanddry_runshare one health rule (R3.6).MembraneResult.conformsagrees betweenvalidate_membraneanddry_run.dry_runpassedallow_infos=Trueto pyshacl andvalidate_membranedid not, so a report holding onlysh:Inforesults gaveconforms=Truefrom one andFalsefrom the other. Both now run SHACL throughrun_shacl, andconformsis false only for a result abovesh:Info(R3.6).Sixteen malformed
specl:Componentidentifiers indocs/SPEC.ttl. specl splits aconstrains:value on every comma, and seven requirements listed symbols aspath (a, b), which produced components such asholonic/client.py (freshnessandis_stale). The lists are now writtenpath (a), path (b), andtest_spec_constrains_items_have_no_comma_inside_parenthesesfails on the old pattern.Six CGA terms carried no
skos:definition,rdfs:labelorrdfs:comment—MembraneHealth,interiorTripleCount,lastIngestion,splitRationale,mergeRationaleandsplitTarget, each sitting beside documented siblings. Now documented; a term nobody but its author can read is dead in a second sense. Guarded bytest_every_term_is_documented.
Fixed
docs/SPEC.mdreports zero SHACL warnings (was 11), maturity 99%, Requirement subscore 85/85. Open issues gained accountability metadata: OQ6 a recommendation and owner (and lost a duplicatedstatus:line), OQ10 and OQ11 an owner. OQ4 moved toresolved— its lazy-mode gate was usage evidence that never arrived, and the absence of usage was itself the determining criterion.scripts/spec_compliance_check.pyno longer treats a compound check id such asR9.4-predicatesas 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 undergoverns:emit as literals instead of IRIs. Reported upstream. Until it is resolved, CI cannot adoptspecl-translate --fail-on-warning(available as thespec-gatetask). 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_traversalandrecord_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_typeare 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 raiseValueError._escape_constructbackslash 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 nondeterministicSELECT ... 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/?graphsites inclient.pyand theScopeResolverBFS walk inscope.pynow pass IRIs as validatedrdflib.URIRefbindings through a single_bind_irichokepoint; the write-only metadata refresh path (SPARQL UPDATE, noinitBindings) validates every graph/holon IRI at entry. An IRI that reached the store via raw ingestion (bypassing the validatingadd_*methods) is now rejected withValueErrororn3()-escaped rather than becoming a delayed injection primitive.sinceinjection incollect_audit_trail(S7 — not an audit finding; found while remediating S1–S6). The audit-trail query is assembled by string surgery, andlimit/offsetwere alreadyint()-cast, but the caller’ssincestring was interpolated raw into theFILTER. A value carrying a quote closed the typed literal and appended attacker-chosen patterns to a query the caller believed was a timestamp filter.sinceis now validated against a strictxsd:dateTimeshape (anchored\A/\Z, so a trailing newline cannot smuggle a second line) and emitted through rdflib’sn3(), the same escaping the**bindingscontract uses.
Breaking
collect_audit_trail(since=...)raisesValueErrorfor a value that is not a well-formedxsd:dateTime, includingsince="". 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 adatetimeshould passdt.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 raiseValueErrorwhen the source has neither projections nor interiors. Passunscoped_portals_allowed=True(or declarecga:sourceLayer cga:DatasetRoleon 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 asWHERE { GRAPH ?g { ... } }— the idiom every portal used when CONSTRUCTs ran against the whole dataset — keeps working, with?gbinding to in-scope graph IRIs only. Portal queries need no rewrite.add_holon(holon_type=...)/add_portal(portal_type=...)raiseValueErroron a type term that is neither a valid PNAME nor a valid IRI.ProjectionPipelineexecution methods dropped theirbackendparameter (A3).ProjectionStep.apply,ProjectionPipeline.apply,apply_to_graph, andapply_to_lpgno longer acceptbackend=. The parameter existed only to redirect CONSTRUCT steps tobackend.construct(), which ran them against the whole dataset and caused the scope-leak/chaining bug fixed above. Materialize the source graph first, then callapply(asHolonicDataset.apply_pipelinealready does). See MIGRATION.md.Backend
**bindingsvalue contract (A1/A2).query/construct/asknow bind values by explicit rdflib term type: a value that is already an rdflibNodebinds verbatim, and any other Python value binds as a typedLiteral. IRIs must therefore be passed asrdflib.URIRef(of any scheme). This replaces the 0.7.x heuristic that promoted onlyurn:-prefixed strings to IRIs and silently mis-boundhttp://strings — andFusekiBackend, 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 containingGRAPH ?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 anrdflib.Datasetholding each in-scope layer under its own IRI (default_union=True, matchingRdflibBackend), so both idioms work and?gstill 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_projectionmerges a holon’s interiors into one unnamed graph before running pipeline CONSTRUCT steps, so aGRAPHclause 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-graphs2.x. 2.0 renamedhierarchic_layouttohierarchical_layout; the viz widgets called the 1.x name unconditionally, so every hierarchic render — the default for all four widget families — raisedAttributeError. Layout dispatch now lives inholonic.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 raisesValueErrorinstead 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 ofvalidate_membraneand therefore out ofvalidate_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, theMembraneResult.report_textnames the graphs instead of reading identically to “this holon defines no constraints”.RdflibBackendcaches parsed SPARQL for reused template strings (prepareQueryLRU), 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 SHACLViolation(an injected instance of the boundary’ssh:targetClassthat violates ash:minCount) and assert bothMembraneBreachErrorand a byte-identical target interior. The twosh:targetClass-with-no-instances cases remain documentedxfails (SPEC OQ11).Breach rollback no longer poisons the no-op cache (E2). The
cga:lastProjectionHashis 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 singleDELETE/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 — seedocs/MIGRATION.md.)FusekiBackendworks 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 raiseRuntimeError: Cannot run the event loop while another loop is running.FusekiClient.__aenter__no longer orphans a live session by unconditionally overwritingself._session; it now guards likeopen().Projection pipeline CONSTRUCT steps no longer leak the whole dataset (A3). A
ProjectionPipelineCONSTRUCT 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
FusekiBackendreuses one client/session for its lifetime (P5) instead of opening and tearing down anaiohttp.ClientSession(and TCP/TLS connection) per call. Callclose()— or use the backend as a context manager (with FusekiBackend(...) as be:) — to release the pooled session and worker loop deterministically; aweakref.finalizehandler 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 persistedValidationRecordin a single aggregate query instead of re-running pyshacl on every holon. Holons that have never been validated no longer contribute to the distribution. Passlive_health=Truefor the old re-validate-every-holon behaviour. Seedocs/MIGRATION.md.
Performance
holarchy_summary()no longer makes ~4+N round-trips (P4). Counts and roots come from onelist_holons_summary()pass plus an aggregate portalCOUNT; health from oneLATEST_HEALTHquery; staleness from the P7 aggregate. Per-holonmemberOfprobes and per-holonvalidate_membranecalls are gone.iter_holons()runs 2 queries instead of 1+4N (P3). Layer graphs for a page of holons are fetched in oneLIST_HOLON_LAYERSquery 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 singleMAX(?timestamp)-per-holon query (LATEST_TRAVERSALS) determines staleness; materialization then rides the 2-queryiter_holons().Graph metadata refresh issues one UPDATE instead of two (P2). The eager per-write path now runs a single
DELETE/INSERT/WHEREper 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.Graphand their triples copied into each new dataset’s backend; a persistent backend that already holds the ontology graph skips the upload (graph_existsguard).
Added
FusekiBackend.close()and context-manager support (__enter__/__exit__).HolonicErrorexception base (audit A1). A new top-levelholonic.HolonicErroris the common base for every exception the library raises.MembraneBreachError,SealedPortalError,FusekiError, andTransformNotFoundErrornow derive from it, soexcept HolonicErrorcatches 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 fromholonicandholonic.backends.store.AbstractHolonicStore.get_graphstill 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.FusekiClientraisesFusekiGraphNotFound, which subclasses both it andFusekiError, so pre-0.8.0except FusekiErrorhandlers 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-persistableProjectionPipelineSpec— the canonical pipeline model — so a pipeline can be registered viaregister_pipeline()and executed throughrun_projection(). Transform steps are translated to their registered names; an unregistered inline transform callable raisesValueError. New helperholonic.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 internalProjectionOptionsdataclass — 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
HolonicDatasetgod class into a newholonic._pipelines.PipelineManagerdelegate (audit AR1/CQ1), following the existingMetadataRefresher/ScopeResolverpattern. 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_activitymoved offHolonicDatasetonto the manager. Behavior is unchanged; the plugin/pipeline tests cover it.Console-oriented read projections extracted from
HolonicDatasetinto a newholonic._console.ConsoleReadsdelegate (audit AR1/CQ1, “core → console layering”):list_holons_summary,get_holon_detail,holon_interior_classes,holon_neighborhood,list_portals, andget_portalnow carry their logic on the delegate, with unchanged thin wrappers onHolonicDataset. Behavior is unchanged; the console/browser tests cover it.HolonicDatasetgod-class decomposition completed (audit AR1/CQ1). The four remaining clusters were extracted onto delegates following the same pattern asPipelineManager/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), andholonic._traversal.TraversalEngine(portal crossing: layer scoping, CONSTRUCT execution, projection injection/rollback, outcome recording).client.pydrops from 3862 to 2842 lines andHolonicDatasetfrom 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_refreshdeliberately stayed onHolonicDataset: 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).aiohttpis 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--integrationmarker).The notebook harness now executes every example in a real IPython kernel (nbclient) instead of a substring heuristic that produced false greens.
ipykernel/nbclient/nbformatare declared test dependencies. The notebook harness needs apython3kernelspec, which used to arrive transitively (jupyterlab in thedevenvironment, nbconvert indeps-lint). Thepy311/py313CI environments pull neither, so every notebook failed withNoSuchKernelbefore executing a cell. They are now declared infeature.deps-testand in thetestextra, next to the task that needs them.Release gate (packaging). The “Verify package” CI step now runs
twine check dist/*and, on arefs/tags/v*build, asserts the git tag equalsholonic.__version__(failing the job on a mismatch) — the check that would have caught the 0.7.1 artifact drift. The PyPI publish step gainedskip-existing: trueso a re-run of a partially-published tag no longer hard-fails on immutable files.sync_notebooks_to_jlite.pypins widget distributions to the syncing environment. ipywidgets resolves a widget’s frontend labextension by name and semver, andjupyter lite buildbakes 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, andCHANGELOGare 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/../../CHANGELOGpaths that silently omitted them. A newtest_docs_structure.pyguard fails if anyindex.mdtoctree entry stops resolving to a real document. (fail_on_warningis intentionally leftfalse: 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 indocs/SPEC.md(and the generateddocs/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 markedverifiedBy: nonewhere no test genuinely covers the requirement (6 such gaps: R1.4, R5.2, R5.3, R5.4, R9.2, R9.44). A newtest_docs_structure.pyguard 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()raisesRuntimeError: can't start new threadandThreadPoolExecutorfails the same way on firstsubmit— which took out two of the three dispatch patterns the notebook exists to demonstrate. Both now branch onsys.platform == "emscripten": the event-queue dispatcher drops its worker and drains cooperatively via a newdrain()(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 plainholonic, and micropip treats an already-present distribution as satisfying the requirement — extras included — soyfiles_jupyter_graphsnever landed and the failure surfaced as aModuleNotFoundErrorseveral 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 secondexcept.Notebook 13 explains a holonic/yFiles version skew instead of raising
AttributeError. yFiles 2.0 renamedhierarchic_layouttohierarchical_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 ofGraphWidget.hierarchic_layouttogether with absence of theholonic.viz._layoutshim — 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 reads0.7.1until the release bump.
Dependencies
Declared the effective
rdflib>=7,<8ceiling explicitly inpyproject.tomlandpixi.tomlrather than inheriting it transitively frompyshacl; the library binds directly against rdflib 7 APIs (Datasetdefault_union,initBindingsterm semantics).Capped
yfiles-jupyter-graphsbelow 3 inpyproject.tomlandpixi.toml. The previous open-ended>=1.7is what broke 0.7.1 in the field: yFiles 2.0.0 landed on 2026-08-10, ~2.5 months after that release, renaminghierarchic_layouttohierarchical_layout, so any cleanpip install holonic[viz]from then on resolved an incompatible major and raisedAttributeErroron the default layout.holonic.viz._layoutresolves 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
pydanticimport.
[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. RaisesValueErrorfor unsafe characters.get_activity(activity_iri)direct lookup of a single provenance activity by IRI. ReturnsTraversalRecord,ValidationRecord, or None. Eliminates the O(all) scan throughcollect_audit_trail().holarchy_summary(max_age=, recent_limit=)aggregated dashboard snapshot returningHolarchySummarywith 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 eachtraverse()orvalidate_membrane(). Eliminates polling for same-process event detection.ShapeViolationdataclass with structured fields:shape_iri,focus_node,path,value,message,severity. Replaces opaque text-only violation reporting.MembraneResult.shape_violationsfield populated from the SHACL report graph with fullShapeViolationobjects.HolarchySummarydataclass for dashboard integration.
Changed
collect_audit_trail()now accepts pagination parameters:limit,offset,since(ISO-8601 timestamp), andkind('traversal'or'validation'). Filtering is pushed to the SPARQL engine viaORDER BY DESC(?timestamp) LIMIT/OFFSETandFILTER(?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 declarecga:sourceLayer cga:InteriorRoleexplicitly.
Added
MembraneResult.is_healthyconvenience property. ReturnsTruewhenhealth == MembraneHealth.INTACT.Portal type in data model.
portal_type: str | Nonefield onPortalInfo,PortalSummary,PortalDetail. All discovery SPARQL queries updated to SELECT the portal’s RDF subtype.SealedPortalErrorexception (subclass ofValueError).traverse_portal()raises it when the portal is acga:SealedPortal, regardless of whether a CONSTRUCT query exists.validate_all()batch membrane validation returningdict[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 andMembraneBreachErroris 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 queryablerdflib.Graph.last_traversal(holon_iri)returns the most recentTraversalRecordtargeting a holon.derivation_chain(holon_iri)walksprov:wasDerivedFrombackward 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 whenagent_iriis provided.Staleness tracking.
freshness(holon_iri)returns atimedeltasince 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:ProjectionRoleorcga:InteriorRoleto control which graphs the CONSTRUCT executes against. When nosourceLayeris 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 inadd_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 publicadd_*methods.M1: SHACL report parsing uses structured graph. Replaces fragile text scanning with
sh:ValidationResultqueries 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
HolonicStoreprotocol 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_loopremoved 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(), causingvalidate_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 viaiter_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
GraphBackendalias removed. UseHolonicStore(orAbstractHolonicStoreas a base class). The alias was deprecated in 0.4.0 and warned through all of 0.4.x.registry_graphconstructor kwarg removed. Useregistry_iri.ds.registry_graphproperty removed. Useds.registry_iri.HOLONIC_SILENCE_DEPRECATIONenv 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 carriesa cga:Holon; the kwarg adds a secondrdf:typeassertion.Generator-based iteration with pagination. New methods yield results lazily so large holarchies avoid full materialization:
iter_holons(limit=, offset=)— yieldsHolonInfoone at a timeiter_portals_from(iri, limit=, offset=)— yieldsPortalInfoiter_portals_to(iri, limit=, offset=)— yieldsPortalInfoThe existinglist_holons()andfind_portals_from/to()methods now delegate to the generators and also acceptlimit/offset. Pagination threads through to SPARQL LIMIT/OFFSET.
bulk_load(holons=, portals=)for batch construction. Accepts lists of holon and portal specs (dicts matching theadd_holon()andadd_portal()parameter names), suppresses per-write metadata refresh during the batch, and fires one consolidated refresh at the end. Significantly faster than N individualadd_holon()+add_portal()calls for initial holarchy construction.list_named_graphs()is already a mandatory protocol method onHolonicStoreand implemented in both shipped backends. No change needed; R9.17 was already complete.Projection method documentation clarified.
project_holon()andrun_projection()are distinct operations, not aliases:project_holonis ad-hoc LPG structural collapse with no provenance;run_projectionis governed pipeline execution with full PROV-O activity recording. Block comment inclient.pyexplains 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.typedmarker added. Downstream consumers using mypy or pyright will now get type-checking support from holonic’s annotations.__repr__onHolonicDataset.repr(ds)now showsHolonicDataset(backend=RdflibBackend, holons=3, registry='...').to_dict()on all dataclasses. Every model and console-model dataclass (20 classes) inherits_DictMixinprovidingto_dict()for JSON-ready serialization. Enum values are converted to their string value automatically.export()andexport_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 viaadd_holon/add_portal.summary()portal display improved. ShowsLabel (source -> target)when a label exists, plainsource -> targetwhen it doesn’t. Eliminates the previous redundant display.Developer landing page.
notebooks/00_index.ipynbprovides 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 skips00_-prefixed notebooks so each directory keeps its own landing page.
Fixed
Latent bug in
COLLECT_HOLONSquery. Usedcga:classification(nonexistent) instead ofcga: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:dataClassificationchanged from DatatypeProperty to ObjectProperty. Range is nowcga:ClassificationLevel. Existing triples using string literals ("CUI","SECRET") must be rewritten as IRI references (cga:Internal,cga:Confidential). The shipped individuals arecga: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 withsh:class cga:ClassificationLevelinstead ofsh: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 aprov: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_HOLONSquery. The query usedcga:classification(nonexistent property) instead ofcga: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 viacga:memberOfare orphaned (theirmemberOftriple is removed) but preserved. Provenance activities are preserved because provenance is immutable history. Idempotent — returnsFalsefor 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_queryis now optional (defaultNone); the newportal_typekwarg (default"cga:TransformPortal") lets callers createcga:IconPortal,cga:SealedPortal, or downstream subclasses; the newextra_ttlkwarg accepts additional Turtle triples for predicates carried by downstream portal subclasses. All existing positional-argument calls continue to work unchanged.cga:IconPortalclass added tocga.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, andcga:SealedPortalskos:definition blocks now document the per-subtypecga:constructQueryexpectations explicitly: TransformPortal requires one, IconPortal and SealedPortal must not carry one.New SHACL shapes:
cga:IconPortalShapeandcga:SealedPortalShapeenforce thecga:constructQuery maxCount 0constraint 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.ttlandcga-shapes.ttl:Ontology version bumped to 0.4.2 with
dcterms:modified,rdfs:seeAlso(docs site, PROV-O, SHACL), and askos:noteabout the standalone/alignment strategy (OQ10).All 68 properties now carry
skos:definition(29 were bare).All four
LayerRoleindividuals 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:ClassificationLevelclass with five individuals (Public,CUI,PII,Secret,TopSecret).cga:PortalTypeindividuals (UnidirectionalPortal,BidirectionalPortal) now have definitions and a note that directionality is orthogonal to subtype, populated by downstream consumers.cga:Holonclass gains askos:notedocumenting thedcterms:conformsTogovernance 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. Exercisescga:AlignmentHolon,cga:DataDomain,cga:dataSteward,cga:dataClassification,cga:usesAlignment, andPortalClassificationShape. 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/DIRECTinholonic/sparql.pyno longer filter ona cga:TransformPortal. Thecga:sourceHolon+cga:targetHolonpredicate pair uniquely identifies a portal regardless of its specific subtype, and relaxing the filter meanscga:SealedPortal,cga:IconPortal, and downstream subclasses created via the newportal_typekwarg are discoverable through the same public API.Portal discovery deduplicated.
FIND_PORTALS_FROM/TO/DIRECTnow useSELECT 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>= 1rather than== 1and did not catch it.add_portal()now fires_maybe_refresh(graph_iri)after writing the portal triples, matching the invariant the otheradd_*methods maintain. Previouslyadd_portalsilently 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 statusdeferredand 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-workerThreadPoolExecutor. 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. UsesHolarchyViz,HolonViz,SPARQLExplorer, andProvenanceVizfrom the existingholonic.vizmodule. Begins with%pip install "holonic[viz]"to pull the optionalyfiles-jupyter-graphsdependency.JupyterLite deployment restored.
jupyterlite/content/now carries all example notebooks plus a new00_start_here.ipynblanding notebook that introduces the in-browser environment and its constraints. Thebuild_jlpixi task outputs todocs/source/_extra/jupyterlite/and Sphinx’shtml_extra_pathcopies it verbatim into the output root at/jupyterlite/..readthedocs.yaml— new config that runsjupyter lite buildas 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.mdlinking to the hosted JupyterLite build.scripts/sync_notebooks_to_jlite.py— keepsjupyterlite/content/in sync with the canonicalnotebooks/directory. Invoked viapixi 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 viapixi 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 forapply()that matches the terminal-method naming used in SPEC R7.3 alongsideapply_to_lpg(). Existing callers usingapply()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-forgesupport is in progress.
Changed
Spec maturity badge location moved from
build/spec-badge.svgtostatic/spec-badge.svgso the generated artifact is committed alongside other static assets rather than living in the (gitignored) build directory. README link updated.Sphinx
releaseindocs/source/conf.pycorrected from0.2.0to0.4.1. This was a stale config value driving an incorrect version label in the ReadTheDocs header.Version bumped to
0.4.1inpyproject.tomlandsrc/holonic/__init__.py.jupyterlite/requirements.txtrewritten; old file pinnedholonic>=0.1,<0.2which 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
HolonicStoreprotocol (holonic.backends.store) — replacesGraphBackendas the canonical backend interface. Identical mandatory surface.AbstractHolonicStoreABC — 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 byMetadataRefresher.refresh_graph; backends without it get the generic Python fallback. Dispatch is duck-typed viahasattr— no registration required.registry_iriparameter onHolonicDataset. Replacesregistry_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.pycovering alias warnings, suppression via environment variable, parameter-rename compatibility,FusekiBackendconstructor enforcement, and native-dispatch hook behavior.
Changed (breaking)
FusekiBackendconstructor:datasetis now keyword-only.FusekiBackend("http://...", "ds")raisesTypeError. UseFusekiBackend("http://...", dataset="ds").First-party backends inherit
AbstractHolonicStore.RdflibBackendandFusekiBackendnow extend the ABC. Client code that testsisinstance(backend, HolonicStore)still works; code that testsisinstance(backend, GraphBackend)via the deprecated alias also still works.
Deprecated
GraphBackendimportable fromholonic,holonic.backends, andholonic.backends.protocol. EmitsDeprecationWarningon first use per module per Python session. Scheduled for removal in 0.5.0.registry_graphkwarg onHolonicDataset. EmitsDeprecationWarningwhen used. Scheduled for removal in 0.5.0. Readingds.registry_graphas 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.pyconverted to deprecation shim using__getattr__.src/holonic/backends/__init__.pyrewritten to export new canonical names with a__getattr__-basedGraphBackendalias.Package-level
src/holonic/__init__.pygains a__getattr__shim for the same purpose.src/holonic/client.py: internal attribute renamedself.registry_graph→self.registry_iri. All 14 internal call sites updated.src/holonic/_metadata.py:refresh_graphnow discovers and dispatches to nativerefresh_graph_metadatawhen present.First-party backends inherit
AbstractHolonicStore.New docs:
docs/MIGRATION.md,docs/DECISIONS.md§ 0.4.0.Specl moved out of the default
devpixi environment. A new dedicatedspecenvironment 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. SeeCONTRIBUTING.md§ “Specl (optional, for spec work)”.SPEC.md fully annotated for 100% maturity. Every requirement carries
priority,constrains,acceptance, andverifiedBysub-bullets. User stories carryasA,soThat, andacceptance. Open questions reformatted asOQ1–OQ7withowner,recommendation, andstatussub-bullets. Requires specl 0.2.0+ for the Phase 1 sub-bullet parser. Score: 60/60 requirements clean, 0 violations, 0 warnings. Badge rendered atbuild/spec-badge.svgand 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.metadataentry points (holonic.projectionsgroup). Pipelines reference transforms by registered name. Seedocs/DECISIONS.md§ 0.3.5.holonic.pluginsmodule 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 inpyproject.tomlunder[project.entry-points."holonic.projections"]. Third-party packages register the same way.CGA ontology section 9 (Projection Pipeline Vocabulary) — adds
cga:ProjectionPipelineSpecclass,cga:ProjectionPipelineStepclass,cga:hasPipeline(Holon → Spec),cga:hasStep(Spec → rdf:List), pluscga:stepName,cga:transformName,cga:transformVersion,cga:runHost,cga:runPlatform,cga:runPythonVersion,cga:runHolonicVersion.cga:constructQueryis reused from the portal vocabulary.New
HolonicDatasetmethods: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)— returnsProjectionPipelineSummaryobjects for attached pipelines.get_pipeline(spec_iri)— returns fullProjectionPipelineSpecwith step order preserved viardf:Listwalk.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 aprov:Activityin the holon’s context graph.
New dataclasses in
console_model:ProjectionPipelineSpec— top-level pipeline withiri,name,steps,description.ProjectionPipelineStep— one step withname,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, optionalprov:wasAssociatedWith,cga:transformVersionper transform used, and fourcga:run*host-metadata fields.run_projectionhonorsmetadata_updatesper D-0.3.3-5. Whenstore_asis set and mode iseager, the projection graph’s metadata refreshes automatically. Same contract as other library-mediated writes.20 new tests in
test_plugins.pycovering registration, validation, attachment, listing, detail readback, step-ordering preservation, execution with and withoutstore_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:Listfor 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, separateholonic.pluginsmodule, 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 theconstruct_querystep form or wait for argument support in a later release.transform_version()returnsNonein source-only environments whereholonicis not installed as a distribution. In normalpip install/pip install -e .environments it resolves the package and version correctly.Pipelines with
construct_querysteps execute the CONSTRUCT against the current in-memory intermediate graph viardflib.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=...)orapply_pipeline(store_as=...)remain in place but have no associatedcga: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_pipelinesandget_pipelinemethods return console-shaped summary/detail records ready for consumption; the actual UI work lives in theholonic-consolerepo.
[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(orBoundaryRole,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:HolonicGraphumbrella class,cga:graphRoleproperty,cga:RegistryRoleindividual.cga:LayerGraph rdfs:subClassOf cga:HolonicGraphmakes every existing LayerGraph discoverable via the new vocabulary.Eager typing on registration.
add_interior,add_boundary,add_projection,add_contextnow 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:HolonicGraphwithcga:graphRole cga:RegistryRoleon the first metadata refresh.holonic.scopemodule. 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 returningResolveMatchrecords. Three topologies:"network"(outbound + inbound portals, default),"reverse-network"(inbound only),"containment"(cga:memberOfchain).max_depthclamps to[0, 100];limitclamps 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 viastr.replaceso 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;--applywrites. Idempotent. Added as a[project.scripts]entry point; also callable aspython -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) andtest_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.scopeas 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__.pyandpyproject.toml.
Limits (known, documented)
ASK_HAS_CLASS_IN_INTERIOR_TEMPLATEhas a fallback branch that queries the interior graph directly if the class inventory is missing (becausemetadata_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. UseCustomSPARQLfor these cases. Upgrade path will be additive if evidence warrants first-class support.The
GraphBackendprotocol 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:registryby default, configurable viaHolonicDataset(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-materializedGraphMetadatafor a graph. ReturnsNoneif nothing has been written (eithermetadata_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 userefresh_metadata()explicitly. Lazy mode is deliberately not included; seedocs/DECISIONS.md§ D-0.3.3-2.console_model.GraphMetadata— dataclass with fieldsiri,triple_count,last_modified,refreshed_at,class_inventory,graph_role. Re-exported from the top-levelholonicpackage.HolonDetailextended non-breakingly — new optional fieldslayer_metadata: dict[str, GraphMetadata]andholon_last_modified: str | None.get_holon_detail()populates them when registry metadata is present.CGA ontology section 7 (Graph-Level Metadata) — adds
cga:ClassInstanceCountclass, per-graph properties (cga:tripleCount,cga:lastModified,cga:refreshedAt), inventory predicates (cga:inGraph,cga:class,cga:count), and the rollupcga: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 theGraphBackendprotocol.20 new tests in
test_metadata.pycovering: 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.mdfile 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__.pyandpyproject.toml.
Limits (known, documented)
Direct
backend.put_graph()/backend.update()calls bypass the eager refresh hook. Userefresh_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_modelmodule with operator-tool dataclasses:HolonSummary,HolonDetail,ClassInstanceCount,NeighborhoodNode,NeighborhoodEdge,NeighborhoodGraph(graphology-compatible viato_graphology()),PortalSummary,PortalDetail. Exported from the top-levelholonicpackage.HolonicDataset.list_holons_summary()— single-query lightweight holon listing, no per-holon layer fan-out. The existinglist_holons()is unchanged and remains the rich-HolonInfopath.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()andget_portal(iri)— flat portal browsing, separate from the source/target-keyedfind_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 currentRECORD_TRAVERSALtemplate 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 underlyingFusekiClient(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 newconsole_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