Skip to content

Deploy v8 SNAPSHOT to Clojars - #122

Merged
theronic merged 31 commits into
v8.0.0-SNAPSHOTfrom
main
Aug 17, 2026
Merged

Deploy v8 SNAPSHOT to Clojars#122
theronic merged 31 commits into
v8.0.0-SNAPSHOTfrom
main

Conversation

@theronic

Copy link
Copy Markdown
Owner

No description provided.

Petrus Theron and others added 30 commits August 14, 2026 22:53
…cs into main

Every implemented change is archived under openspec/changes/archive with
today's date; only adopt-stable-discovery-enumeration stays active. Delta
specs were folded chronologically so MODIFIED/REMOVED targets existed when
applied. The three v7-era Datomic-only cache designs (add-intelligent,
add-consistency-aware, harden-v7-4) were archived without folding: their
whole premise was replaced by the v8 chain and one never merged (PR #80).

Hand merges where the mechanical fold was refused: modular-backend-workspace
'Shared backend contract' and 'Upgrade documentation' (upgrade renamed the
DataScript scenario to cover Datahike; publish's text is kept and the three
still-true upgrade scenarios are retained), 'Graph-independent coherence
adapter contract' and backend-native-revision-consistency 'Backend
capability honesty' (remove-unknown replaces the exact-current-only and
unknown-authority scenarios).

Requirements contradicted by already-archived newer changes are pruned from
main (causal tokens/anchors/journal, explicit coherence authority and proof
modes, content-proof cache validity, graph-order retraction bookkeeping,
single-flight result join, the reverted derived-digest cache key) and the
stale six-function SPI requirement is replaced by the shipped adapter
operation contract. Requirements contradicted by the still-active
stable-discovery change are declared as REMOVED deltas on that change so its
archive folds correctly. The three archived changes with residual open tasks
carry a note pointing at where each residue is tracked.
…ease-assurance gate

Sweep of definitions with no reference anywhere in src, tests, formal
bridges, build scripts or the demo repositories (clj-kondo analysis plus
grep), verified by the CI-equivalent battery on a fresh JVM (621 tests,
25,976 assertions, 0 failures) and the DataScript CLJS suite (193 tests,
7,311 assertions):

- eacl.engine.v8: the inert acyclic observation vars (*acyclic-route?* was
  never bound, so *acyclic-work-stats*/add-acyclic-work! never ran),
  *inactive-recursive-cycle-guards*, *count-stats* (bound, never read), the
  retired recursive-cursor-version/recursive-order-abi, find-relation-def,
  schema-version-stamp, and permission-schema-components with its Kosaraju
  helpers; the Datomic client and eacl.relay drop the :lookup-eid and
  :recursive-logical cursor kinds nothing has minted since the routing.
- eacl.datomic.impl.indexed / impl / db: dead façade wrappers, the
  *count-stats* bindings, resets of cache keys that no longer exist,
  relation-populated?, relationship-relation-id, affected-relation-ids.
- eacl.subproblem-cache: metric writers of the retired tiers and the two
  lookup helpers that were declared closure roots with no callers (roots
  list updated, ledger regenerated).
- eacl.formal.production-kernel: dead acyclic/merge bridge functions and
  nine unused generated-class imports; the reflection mutant detector no
  longer looks for the two type hints those functions carried.
- eacl.spicedb.parser REPL helpers and their pprint require;
  eacl.engine.relationships/after-cursor?; DataScript/Datahike max-entid,
  find-relation-def, build-schema-catalog, avet-range, calc-set-deltas
  aliases, schema-change-attrs; the empty eacl.impl.spicedb stub; unused
  requires and destructured bindings flagged by clj-kondo.
- formal/stable-discovery: the source_refinement_bridge bound to
  eacl.engine.v8 vars removed by the 9.2 excision and made verify-fast.sh
  fail with a NullPointerException after its Dafny/TLC stages; the gate's
  escape-hatch scan used rg and silently skipped when rg was absent. The
  bridge is removed, the scan uses grep, and the gate is green again (506
  obligations, all mutants killed, 6 s).

engine/all-permission-nodes stays: it is the only dispatch site of the
required :all-permission-nodes adapter operation and the dispatch closure
ledger requires one.
…nt contract

Rewrites, against the source, the statements that presented the interim v8
acyclic/merge/indexed engines, their cursors and options as current:

- README: stable first-discovery order instead of Datomic-eid/"acyclic vs
  recursive" order, result-edge cursors instead of per-path frontiers,
  reducer-exhausting counts instead of 16,384-eid frontier pages, eacl_z4_
  token examples (eacl_z3_ is rejected), the Datomic-only page-token TTL
  qualified, cache layers as they exist.
- AGENTS.md: the dev/restart-backend! sequence (no such namespace), the
  CI-equivalent test battery form, the module bench paths, the
  source-closure regeneration rule; .rules link to clojure-rules.md.
- docs/stable-discovery-engine.md: the public envelopes and
  :eacl.pagination/* keys instead of the standalone eacl_sd1. token and
  :eacl.page/* keys; failure semantics and topology qualification now say
  which physical components the routed path installs (only the cut-point).
- docs/cache.md and docs/v8-subproblem-cache.md: the live tiers (answer,
  identity projection, sealed plan, latest checkpoint, visited page)
  instead of relationship projections and completed denotations.
- docs/formal-verification.md, formal/README.md, trusted-boundary.md,
  production-decision-inventory.md, temporal-model.md,
  final-assurance-audit.md, integration-spike.md,
  formal-verification-corrections.md: the generated kernel is authoritative
  for four decisions and the CLJC stable engine for enumeration; retired
  Dafny leaves labelled as such pending the task 9.2 cut; stale ledger
  counts replaced by the ledger itself; complete-logical-page and
  cloudafrica/eacl references fixed; the decision inventory names the
  sealed-plan/reducer/page decisions.
- docs/release-notes-v8.0.md: six-function SPI, :coherence-authority,
  denotation reuse, per-relation generation vector, cursor payload v10,
  route-specific order, routing/merge dispatch and scan waves corrected.
- Adapter-boundary, modules-and-upgrade, module READMEs, PORTING.md:
  populated-relation checks, cursor frontier identity, SCC/fixed-point
  wording removed; safe_retraction.clj listed.
- formal/stable-discovery/README.md: quarantine list of files that no
  longer exist, task-status sentences, bridge count.
- docs/adr and docs/plans gain READMEs marking them historical; the stray
  "007 ... copy.md" is renamed, ADR 008/009 headers fixed, the 2026-08-07
  exploration prompt moved from adr/ to plans/; the v6-vs-v8 benchmark and
  the v7 arrow bug-fix notes labelled historical (and the benchmark's
  :live-results option corrected to :remember-answers); docs/index.md
  links the engine doc and the reports.
Bugs, formal-versus-implementation verdicts, specification and backend
discrepancies, dead-code inventory, and optimizations found while auditing
core/ at ac3cbac: the exhaustion-target cap on exact counts, point checks
and bare :last (reproduced), the standalone stable-page checkpoint key that
omits the basis, the unwired section-7 physical components, the Datomic
expand-permission-tree codec bypass, Datahike temporal-snapshot identity,
the noHistory stamp on the exact path, the broken release-assurance gate,
error-shape drift, and the optimizations (schema-generation plan cache,
leaf existence probes for can?, dependency-equal checkpoint keys, backward
page replay, count materialization) with the soundness argument for each.
Records what this branch archived, pruned, corrected and removed.
…s, codec-aware Datomic adapter, temporal Datahike identity, exact fallback on unreadable stamps

Each fix carries a regression test; the CI-equivalent battery on a fresh
JVM (627 tests, 26,009 assertions, 0 failures), the DataScript CLJS suite
(193 tests, 7,311 assertions) and the stable-discovery release-assurance
gate (506 obligations) are green.

- Exhaustive runs are unbounded. `stable-reducer/exhaustion-target` is
  positive infinity and is the target of every exhaustive route (exact
  counts, anchored point checks, bare :last windows), so a run ends only at
  an empty stack or a typed :max-admissions/:max-values failure — never at
  the retired 1,000,000-result cap that silently truncated counts and
  point checks once the public limits authorized more work
  (physical_route_test/exhaustive-runs-are-unbounded-test drives 1,000,001
  results through both directions).
- The standalone stable-page API keys checkpoints on the whole execution
  binding, basis included, so a checkpoint recorded at one basis is never
  resumed by a token minted at another
  (stable_page_test/checkpoint-identity-includes-the-basis-test).
- The Datomic client passes :object-eid-fn to its adapter, so
  :object-id->internal resolves external ids through the client's
  :object-id->lookup-ref/:object-id->ident codec; expand-permission-tree
  was the one operation handing the adapter an external id and it resolved
  a hardwired [:eacl/id id] instead — an absent topology for codecs whose
  ids differ from :eacl/id (config_test/expand-permission-tree-uses-the-client-id-codec-test).
- The Datahike adapter reads configuration through Datahike's IDB/-config
  protocol, so temporal (AsOfDB) snapshots report their origin's store
  identity, attribute representation, writer and history capabilities
  instead of nil (the exact fallback previously minted a different snapshot
  identity and lost :fully-consistent); the published store identity is
  bounded to {:backend :id} so jdbc/s3 connection configuration never
  reaches snapshot ids, cache bases or cursor digests; :read-failed no
  longer counts as absence
  (consistency_v3_test/temporal-fallback-adapter-keeps-its-source-identity-test).
- The Datomic exact-cursor fallback no longer depends on reading the
  :db/noHistory relation stamp through d/as-of: when the generated decision
  reports divergence purely because the exact snapshot's dependency proof
  was unreadable while its native revision and execution identity are the
  cursor's own, the continuation is :exact by identity; readable stamps
  that differ still diverge and another revision or source is never
  rescued (consistency_v3_test/exact-fallback-tolerates-unreadable-historical-stamps-test).
- The reducer's schedule admits each work-id once per successor batch and
  skips nil items, refining StableReducer.Admit literally
  (stable_reducer_test/schedule-admits-each-work-id-once-per-batch-test);
  the replay ledger dissocs keys at zero; the answer-cache semantic keys
  carry the public order ABI; the missing-relation write error is typed
  :eacl/unknown-relation-or-permission on all three backends and
  :eacl.basis/selection-failure carries :type.

The report records each item's status; the section-7 physical wiring, the
unstamped-Datomic write guard, and the DataScript/Datahike create
serialization and delete batching are recorded as design decisions rather
than repaired.
…onize the error shapes

Relationship writes validate their schema names before any endpoint
resolves, with the read side's typed taxonomy
(eacl.schema.errors/validate-relationship-write!, called by both clients):
unknown definition -> :eacl/unknown-definition; unknown relation, or a
subject type the relation does not declare -> :eacl/unknown-relation-or-permission
(the latter with :reason :subject-type-not-declared). The backends' own
"Missing Relation" throws carry the same category as a fallback.

Schema writes: reference-validation failures are
:eacl.schema/invalid-reference (with the :errors vector), unsupported
features are :eacl.schema/unsupported-feature, and every already-typed
parser error also carries :eacl/error. validate-schema-references now
rejects a relation whose subject type is not a defined definition (SpiceDB
rejects `relation reader: nobody`) whenever the schema carries its
definition list, i.e. on every write-schema!; Datomic's private copy of the
validator is replaced by the shared one.

Page requests: out-of-range :first/:last are
:eacl.pagination/invalid-page-size (with :size/:max); both directions, both
bounds, a bound without its direction, :cursor/:limit and list keys on a
count are :eacl.pagination/invalid-page-request; a nil bound keeps
:eacl.pagination/invalid-cursor.

Harmonized additively across backends: :eacl/unknown-object,
:eacl/relationship-conflict, :eacl.schema/concurrent-write (Datomic also
reports :expected-generation/:actual-generation/:backend-error) and
:eacl.schema/relation-in-use carry :eacl/error equal to :type; the Datomic
client's freshness errors use :reason :freshness-timeout/:head-behind/
:sync-failed and carry :requested-order-hint/:observed-order-hint beside
their :requested-t/:observed-t.

Also: the eacl-datomic release config pins com.datomic/peer 1.0.7705 like
the module (the published POM declared 1.0.7622); the README's
write-relationships! update shape, per-backend delete-object! semantics and
SpiceDB-differences section are corrected; tx-schema-version-guard
documents the supported unstamped v7-compatibility regime (a fail-closed
variant was tried and reverted: schema_basis_test deliberately writes on an
unstamped database).

Tests: schema_error_contract_test/relationship-writes-share-the-schema-taxonomy-test
and schema-and-page-request-errors-are-typed-test. Battery 629 tests /
26,047 assertions / 0 failures; CLJS 193 / 7,311 / 0.
Section 7 of adopt-stable-discovery-enumeration delivered the three-outcome
read classification, retry, service-edge admission and topology capability
record as library components; the routed public path installed only the
execution cut-point (audit item 2.3). This installs them:

- Every routed adapter read (point checks, lookups, counts) runs through
  classified-fetch-fn + retrying-fetch-fn (eacl.engine.v8/stable-fetch-fn):
  a foreign adapter failure is classified :retryable and retried up to three
  times for the same descriptor under the request's original absolute
  deadline, then surfaces as :eacl.scan/failure with :classification and
  :cause-class and the original exception as its cause. Typed EACL errors
  (contract violations, limits, deadlines, cancellation) pass through the
  boundary unwrapped and unretried (physical/typed-eacl-error?), so the
  pinned public error contracts are unchanged. Attempts are reported as
  :adapter-attempts in the traversal work stats.
- A :service-admission client option ({:max-concurrent :max-replays
  :max-replays-per-key}, validated by physical/normalize-service-admission)
  installs the bulkhead around routed enumerations (slots held for the full
  synchronous call chain, :eacl.service/admission-rejected) and the replay
  ledger, keyed by continuation identity, around checkpoint-miss replays,
  backward runs and last windows (:eacl.service/replay-rejected). Both
  clients bind engine/*service-admission* per request; an omitted option
  installs no bulkhead. The ledger dissocs keys at zero.
- Both clients derive the adapter's closed topology capability record from
  its declared execution profile plus the engine's read boundary
  (physical/adapter-topology-capabilities) and fail closed at construction
  with :eacl.topology/unqualified when it does not certify stable discovery
  (physical/require-qualified-topology!); the three bundled adapters declare
  the strict sequential profile and qualify.

Tests: physical_route_test routed-reads-are-classified-and-retried-test,
service-admission-bounds-routed-enumerations-test,
topology-qualification-test; config_test service-admission-option-test.
Docs: stable-discovery-engine.md and v8-backend-modules-and-upgrade.md.
Battery 633 tests / 26,087 assertions / 0 failures; CLJS 193 / 7,311 / 0;
formal gate 506 obligations.
Two writers that both planned a :create of the same relationship against
the same pre-write value both committed on DataScript and Datahike (the
second add was a redundant datom) and both callers saw success, where the
Datomic client and SpiceDB report :eacl/relationship-conflict to the loser
(audit report 2026-08-15, section 2.19).

tx-update-relationship now keeps the plan-time conflict for an
already-present relationship and otherwise emits a transaction function,
create-relationship-at-commit, that re-checks both endpoint values against
the transaction-time database and returns the relationship adds or throws
the typed conflict. DataScript (CLJ and CLJS) always runs it; Datahike runs
it only under its default in-process writer (eacl.datahike.db/direct-writer?,
now shared with the backend adapter) because a remote writer cannot
transport a function value. Datahike reports a failing transaction function
wrapped, so the client's transaction wrapper recovers the typed error from
the cause chain (eacl.datahike.core/typed-transaction-error).

Deterministic interleaving tests plan two creates against one pre-write
value and commit both, on DataScript and on Datahike in both attribute
representations. OpenSpec change serialize-create-conflicts added and
archived into converged-relationship-storage; README, module READMEs and
the audit report updated; AGENTS.md notes that cljs.main/-main calls
shutdown-agents, so the CLJS build runs last on a shared nREPL.
Wire the physical execution layer (classified/retried reads, service-edge bulkhead, topology qualification) onto the routed engine
…p' into agent/serialize-create-conflicts

# Conflicts:
#	formal/verification/public-source-closure.json
Serialize DataScript/Datahike :create conflicts at commit time
Stable-engine audit: report, dead-code sweep, gate repair, doc corrections, OpenSpec archive fold
Add cooperative cancellation to EACL reads
Adopt stable-discovery enumeration: sealed plans, width-one reducer, stable cursors
[v8.0] Add cache, recursive schema, backend modules, and verified engine
Permission-tree expansion accepts a relation as its root and then reads that
relation's relationships, but no permission path names the relation, so its
dependency closure was empty. An empty closure resolves to a complete proof
with dependency stamp 0, which the managed cross-snapshot tier treats as equal
at every later snapshot: once expansion became cacheable, a relation-rooted
tree survived the relationship writes it reports. The closure now includes the
relation definitions of any node with no permission paths, on both the Datomic
and shared orchestration paths.

Also from the same review:

- Weigh nested answers by retained collection entries. Permission trees carry
  their payload in :intermediate/:leaf nodes rather than a :data vector, so
  every tree was admitted at the 512-byte floor and the weight budget stopped
  bounding what the answer tiers retain. `some?` guards the page branch
  because ClojureScript and Clojure disagree on `(counted? nil)`.
- Require adapter determinism to MINT a snapshot-exact identity, not merely to
  read one. A client whose adapter readers refuse was still seeding the tier
  with entries none of its exact requests could consult.
- Stop republishing to the snapshot-exact tier on every exact-current hit. The
  answer is offered when computed or promoted; republishing re-ran the answer
  validator and weight function on the hot path and recorded a publication
  race against the entry it had just found.
- Bypass the snapshot-exact tier when an adapter cannot mint a canonical
  identity, instead of failing the request. A cache that cannot key an answer
  is a caching limit, and nil is the documented outcome for such views.
- Report the selected snapshot as an exact hit's cache basis rather than
  copying the entry's, which may have been retained from an answer computed at
  an earlier basis.
- Cancel the Datomic sync future when a causal-floor or authoritative-head
  wait stops waiting, as targeted exact catch-up already does. Both keep their
  established failure taxonomy.
- Refuse exact-snapshot consistency for d/filter, d/since and d/history views,
  which report their origin's database id and basis and would otherwise mint
  the same snapshot identity as the plain value while answering a different
  question.
- Await an unobserved revision in Datahike exact selection under the caller's
  bound, so a lagging reader reports bounded lag rather than missing history.
- Bound cursor reconstruction by the request's remaining execution time, as
  every other selection path already is.
- Consume the selected context's :snapshot-exact? instead of recomputing it,
  and drop the :completed-cache? field that had become a constant.
- Refresh the change's delta specs against the main specs they edited, so the
  change can archive.
Datahike has no `d/sync` (replikativ/datahike#958), so the only ways to wait
for a revision the local value has not observed are unbounded polling or a
fixed N-second timeout, and neither distinguishes a revision that is merely
late from one that will never arrive. Listeners are not an option either: they
fire only for transactions made through the connection that registered them.

Exact selection therefore reports a revision above the local head as
unavailable immediately, restoring the guard this branch had replaced with a
bounded await, and leaves the deadline for the caller to spend. Retained
history at or below the local head still resolves without waiting, and
`:select-at-least` keeps its await, which is the operation whose contract is
to wait.

The local head is also more authoritative than it first appears: Datahike
caches connections per store config in-process, so two `connect` calls with
one config return the same object and a direct writer cannot lag itself.

The regression test now asserts the unavailable result and that selection
returns without spending the caller's timeout.
Fix cache correctness and bounds found reviewing exact-snapshot reuse
Cursors no longer expire, `at-exact-snapshot` now calls `(d/sync T)` if Peer is behind and exact queries now leverage Exact Answer cache
@theronic
theronic merged commit 6aee747 into v8.0.0-SNAPSHOT Aug 17, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant