All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
sparq-zk / sparq-zk-compose (opt-in
dual-leaf):DualLeafV2routes all six value lanes, and the host builds every lane's value-lane sub-proof inputs from typed operands.dual_leaf_v2::is_hookable_datatypeandencode_literalnow routexsd:boolean,xsd:dateTimeandxsd:dateas well asxsd:integer,xsd:decimalandxsd:double, so a graph committed underDualLeafV2carries a real value hook, under that datatype's own lane constant, for every literal on them — which is what a value-lane FILTER member needs the committed leaf to be. Two deliberate consequences. The three newly routed lanes inherit the fail-closed ingest rule, so any lexical outside their §13.2 value domain (non-canonical spellings such as"1"^^xsd:booleanor a24:00:00instant, and also XSD-canonical forms the domain excludes: an untimezoned or non-Zxsd:dateTime/xsd:date, or more than three fraction digits) now fails the WHOLE commitment instead of taking the degenerate string lane. And a V2 commitment over a graph that CONTAINS a literal on one of those three lanes changes; every leaf on the three already-routed lanes, every degenerate string-lane leaf, every IRI and blank-node leaf, and the whole-graph commitment over a graph of only those are BYTE-IDENTICAL, pinned by a fixed vector captured before the change.DualLeafV1's routing is unchanged, so an already-issued V1 graph is unaffected. Beside this,build::build_filter_value_dl_{int, f64, decimal}(&Literal, FilterOp, bound: &Literal)complete the V2 builder set next to the existing_bool/_datetime/_dateentries, over ONE shared assembly: both operands go through the lane's owndual_leaf_v2encoder, the COMMITTED literal's lane constant is carried, and the disclosed verdict is COMPUTED by that member's host verdict mirror (build::integer_verdict,build::double_bits_verdict, the sharedbuild::signed_epoch_verdict) rather than taken from the caller. The decimal lane rescales the FILTER constant to the committed literal's scale EXACTLY — never rounded — refusing an inexact or overflowing rescale withBuildValueDlV2Error::DecimalScaleNotRepresentable, through the same two functionsvalue_pinpins with.BuiltFilterValueDlNumeric::lane_prooffeeds the checked typed prove entry, so theProver.tomlis rendered from validated typed fields and never from caller TOML. Every lane's builder output satisfiesvalue_pin::check_value_lane_pinfor the matching typed FILTER, and changing any single pinned field of it — member, lane constant, op, bound, verdict — is refused with that field's ownPinError. The value-lane verifier gates are UNCHANGED and still CLOSED:verifier::prefilter_manifest_structurerefuses every value-lane sub-proof at stage 0 withCheckError::ValueLaneSubProofNotAccepted, the stage-2 operand match,filter_edge_trueandencode_pattern_slotare untouched, and a test asserts per lane that a manifest assembled entirely by these builders is still refused. Host-side Rust only; no.nrsource changed. Research grade, NOT externally audited (sq-qhy4); the value lanes carry the #769-accepted INV-VL downgrade (CR-G8). No soundness or privacy claim. -
sparq-zk-compose: verifier-side authenticated commitment method. The verifier no longer has to assume which commitment method a committed graph was made under, and it does not read one from the manifest either — it RECOVERS it from the issuer signature.
method_authbuilds one candidate signed message perCommitmentMethod::recognised()method from values the verifier already established (the commitment, the issuer-attested salt, the issuer-bound status reference, and the attested holder digest when the attestation is holder-bound) plus the method-UNBOUND message, andclassifyreports which one the presented signature verifies over:AuthenticatedMethod::Bound(m)orAuthenticatedMethod::Unbound.MethodPolicyis the relying party's decision over the answer, carried on the external trust anchor asKeySet::with_method_policy(thewith_hidden_issuer_depthprecedent, so no public verifier signature changes):MethodPolicy::current()is the default,require_method_bound()refuses an attestation that binds no method (CheckError::MethodNotAuthenticated), andadmit_legacy_dual_leaf_v1()is the written-down opt-in for an attested superseded method (refused otherwise withCheckError::MethodNotAdmitted); more than one method's message verifying isCheckError::AmbiguousMethod. For a HIDDEN-ONLY commitment the method is selected by the hidden entry's declaredmessage, which must EQUAL one of the candidates — a value matching none of them is refused withCheckError::HiddenIssuerMessageMismatch, and the policy applies to the method the hidden-issuer proof is then required to bind. Every entry over a hidden-only commitment is checked, and they must all select the same candidate, so the message check does not depend on the order the entries are listed in. New PRESENTATION message shapes and signers insparq-zk, each under its own domain tag:sig::presentation_message_with_status_and_scheme(ZKSIG_M5, for a bearer attestation) andsig::commitment_message_with_holder_and_scheme(ZKSIG_M4, the one shape binding both a holder key and a method), signed withSecretKey::{sign_presentation_with_status_and_scheme, sign_commitment_with_holder_and_scheme}. A REGISTRY entry's ownZKSIG_M3signature is deliberately NOT a presentation attestation — recording a commitment in an issuer's registry is a separate statement from authorising its presentation, so presenting one as a clear attestation reportsCheckError::InvalidIssuerSignature. Back-compatible with ONE qualification, about the declaredmessageof hidden-issuer entries: a hidden-issuer entry's declaredmessagewas previously not read at all, and over a hidden-only commitment it is now required to equal a verifier-built candidate (for every entry over that commitment), so a hidden-only manifest whose declared message is empty, not decodable as a field element, or unmatched is now refused withCheckError::HiddenIssuerMessageMismatch; a refusal of a malformed hidden-only manifest may also be reported earlier, in the pre-filter, than it was before. The hidden-issuer stage likewise requires EVERY entry's declaredmessage, including one over a commitment that also has a clear attestation, to equal the message its proof is checked against, and that check may now be reported ahead of an entry's depth or proof refusal. Otherwise every manifest that verified before still verifies under the default policy — an attestation over the method-unboundZKSIG_C3/ZKSIG_C4message authenticates asUnbound, which the default policy accepts, and a bad signature still reportsCheckError::InvalidIssuerSignature. The newly accepted manifests are only those carrying an issuer signature over a method-bound PRESENTATION message (ZKSIG_M5/ZKSIG_M4). Not externally audited (sq-qhy4); no soundness or privacy property is claimed. -
sparq-zk:
verify::fragment_typed_filtersre-derives a query's FILTER obligations keeping each constant as the typedoxrdf::Literalthe query spells (verify::TypedQueryFilter { variable, op, constant }), for a relying party that needs the constant's datatype and lexical form rather than au64. It shares one private walker withfragment_filters, whose behaviour and error strings are unchanged (a regression test asserts the messages verbatim), and widens the datatype axis only: the admitted comparison shape is still?var op <literal>after theconst op ?varflip, with everything else fail-closed. Alsocommit::CommitmentMethod::recognised(), the enumeration counterpart offrom_scheme_iri. -
sparq-zk-compose:
value_pinholds the relying party's pinned value-lane inputs for the dual-leaf lanes (OFF-by-defaultdual-leaffeature): given an authenticatedDualLeafV2method and a typed query FILTER,expected_value_lane/check_value_lane_pinstate what a value-lane sub-proof's member, lane constant (for the decimal lane, the scale it names), op, bound and verdict would have to be, each equality failing closed with its ownPinError;scan_methodrequires every commitment a scan draws from to have authenticated the same method, andcheck_lane_routedrefuses a lane the whole-graph commitment builder does not route. The value-lane verifier gates stay CLOSED: the unconditional stage-0CheckError::ValueLaneSubProofNotAcceptedrefusal is unchanged and still first, the stage-2 binding-edge operand match andfilter_edge_trueare untouched, and a manifest every one of whose pins holds is still refused (a test asserts exactly that). Computing a pin is not enforcing one; the gate-opening change is separate. Research-grade, not externally audited (sq-qhy4); the value lanes carry the #769-accepted INV-VL downgrade (CR-G8). -
sparq-zk:
dual_leaf_v2is a second dual-leaf commitment method (CommitmentMethod::DualLeafV2, scheme IRIhttps://sparq.dev/ns/zk#poseidon2-dualleaf-v2) behind the OFF-by-defaultdual-leaffeature. It keeps V1's leaf shapes and re-derives the constants: lane constants in BLAKE3's key-derivation mode over the length-framed(datatype IRI, lane params)pair, the degenerate fold left in the plain mode, and avalue_none()re-derived until it falls outside the reachable value-handle band, whichis_in_handle_banddecides and a test asserts — the checkable invariantresearch/zk-field-native-encoding.md§14.1 asks for, discharging that reserved-tag requirement on the HOST side. The in-circuit half landed with the component-witnessed members in the entry below: thezk/dual-leaf-v2Noir library mirrors the sentinel and its band check, andcompose_core::dual_leaf_v2_leafapplies it at thescan_v2dl_*/join_eq_v2dl_*witnessed component sites, which noCircuitIdnames. On the value-lane FILTER members a sentinel is still refused only because theiru64range decomposition cannot represent it. V1's constants, vectors and public API are unchanged and V1 stays recognised and verifiable, but it is no longer production-selectable. The §14.1LANG_NONEseparation is asserted on the host side only (see the hardening entry below). Both versions carry the same #769-accepted INV-VL downgrade; not externally audited (CR-G8 / sq-qhy4). -
sparq-zk-compose:
filter_value_dl_boolis theDualLeafV2xsd:booleanlane's dedicated member —filter_value_dl_int's public layout field-for-field, plus in-circuit constraints that the witnessedVALUE_HOOKand the public bound lie in{0, 1}, so a handle the integer member'su64range decomposition admits is refused by the relation. Wired throughCircuitId::FilterValueDlBool,ProofInputs::FilterValueDlBool,manifest::boolean_lane_const_v2,build::build_filter_value_dl_bool,toml::filter_value_dl_bool_prover_toml, the verifier's public-input reconstruction and the dispatch matrix. TheDualLeafV1boolean lane keeps sharing the integer member, unchanged. The boolean, dateTime and date lanes have V2 builders; the integer, decimal and double lanes have V2 host encoders and V2 renderers but no V2buildhelper yet. No soundness or privacy claim. -
sparq-zk-compose (opt-in
successful-results): METHOD-BOUND successful-result issuance, added beside the retained contract rather than replacing it. Two new members,result_v5_k1_n16_p3_r4_f0_d10andresult_v5_k2_n16_p3_r4_f0_d10, compile a newcompose_core::result_m5::result_check_m5relation that folds the METHOD-BOUND presentation messagesig::presentation_message_with_status_and_scheme(root, salt, status_ref, scheme_iri_to_field(CommitmentMethod::StringCanonicalV1.scheme_iri()))(ZKSIG_M5), so the issuer attests the string-canonical commitment METHOD and not merely the commitment. The scheme digest is a compile-time constant of the relation, so the public ABI is the version-1 f=0 one unchanged and the verifier's public-input reconstruction does not move.prepare_resultselects these members by default at one or two credentials with no private predicate and status depth 10, and refuses any other bucket by name instead of silently issuing a method-unbound proof.compose_core::resultandcompose_core::result_publicare byte-for-byte as they were, so every shipped version-1 to version-4 member keeps its compiled circuit, ABI and verification-key bytes and the fixed-foundation rebuild check passes unchanged.crates/sparq-zk-compose/tests/result_scheme_pin.rspins the new relation as the legacy one with only the signed message substituted, with a control test that the substitution does change it. Gate-count baselines and the constraint-count pack cover the two members. Research grade, NOT externally audited (sq-qhy4). No soundness or privacy claim.
-
sparq-zk-compose: the verifier now selects each sub-proof's circuit member — and therefore its canonical verification key — from the AUTHENTICATED commitment method of the commitments that sub-proof is bound to, instead of from the manifest's declared
CircuitIdalone. The method is derived per sub-proof: aScan(and anextended-fragmentPathReach) resolves every commitment in its owncommitmentsvector, aJoinEqresolves the ENDPOINT commitments of every declaredJoinEdgethat covers it (sub_proofs[edge.scan_a].commitments[edge.graph_a]andsub_proofs[edge.scan_b].commitments[edge.graph_b]) rather than its own prover-declaredcommit_a/commit_b, and aFilter*member inherits the method of the source scan of every validated binding edge that binds it; all of them must agree. The declared id is resolved through the(commitment-method x circuit)dispatch matrix (dispatch::resolve_circuit, opt-in behinddual-leaf) and only the resolved member is used. Three new fail-closed refusals:CheckError::MemberNotLegalForMethod(the member is not legal for that method),CheckError::SubProofMethodUnresolved(no single method could be derived — differing methods across bound commitments, a mixture of attested and unattested ones, an unparseable bound commitment, or a sub-proof with no derivable method whose member is not legal under every admissible method),CheckError::SubProofCommitmentNotAuthenticated(a bound commitment is ABSENT from the authenticated map, so nothing was authenticated for it at all),CheckError::AuxiliaryMemberIdInSubProof(a manifest sub-proof declares an id that belongs to one of the auxiliary bb roles below) andCheckError::MemberNotForFixedRole. Absence from the authenticated map is NOT read asUnbound: it is refused by name. A sub-proof all of whose bound commitments authenticated no method has no method to resolve against, so its declared member is NOT echoed back either — it is held to the same method-independence rule as a sub-proof bound to no commitment at all (legal under every admissible method, whereMethodPolicy::widest_admissible()names that set), which refuses a value-lane member and admits a string-lane one. Under the defaultMethodPolicyan unbound attestation is refused upstream, so that case is reachable only under the written-downadmit_unbound()downgrade. A sub-proof bound to a commitment covered only by a hidden-issuer proof defers its resolution until afterbind_hidden_issuer_attestations, so a candidate method is never read as authenticated. The five auxiliary bb gates (hidden and fully-hidden revocation, hidden issuer, holder PoK, holder set) are method-independent by construction — their member is fixed by the relying party's own policy or registry and declared nowhere in the manifest — and each now selects it through a named per-role fixed-member path that refuses any id outside its own role. This adds refusals and no acceptance. Behaviour change: a manifest whose sub-proof binds commitments that authenticated different methods, or a mixture of attested and unattested ones, used to be accepted and is now refused. For aScanthat is narrower than it sounds:CheckError::ScanPatternMethodsMixedalready refused commitments whose methods disagree on the pattern-constant ENCODER, so what is newly refused there is the pairing of a method-UNBOUND commitment with aStringCanonicalV1one, which share one encoder. AFilter*and aJoinEqhad no such check. An edge-less FILTER over a member legal under every admissible method still verifies. Scope: the member resolution and all of its refusals compile only with thedual-leaffeature, which is OFF by default, because they are decided by thedual-leafdispatch matrix; the stage 2g′ endpoint gate andCheckError::JoinProofNotBoundToAnEdgebelow are unconditional and apply in every feature state. Not externally audited (sq-qhy4); no soundness or privacy property is claimed as achieved. -
sparq-zk-compose: the hidden-join gate's ENDPOINT half now runs in BOTH stage-1 regimes.
bind_joinsenforces two separable things — the endpoint resolution plus commitment-matching (each declaredJoinEdge's join proof must carry publiccommit_a/commit_bbyte-equal to the two referenced scans'commitments[graph_*]), which reads only the manifest, and the slot binding, which re-derives the query's patterns. The first is now its own gate (bind_join_endpoints, stage 2g′) and is no longer deferred with the second in theextended-fragmentregime; only the slot binding and the N-way chain check remain deferred there (sq-1zf94). It is also what theJoinEqmember resolution above reads its endpoints from. Behaviour change (extended-fragmentonly): a declared hiddenJoinEdgewhose join proof names commitments other than its endpoint scans' is now refused (CheckError::JoinCommitmentMismatch) instead of standing as a prover-declared linkage, and a hidden join one of whose endpoints is a bounded-path (PathReach) sub-proof rather than aScanis refused fail-closed (CheckError::JoinEdgeKindMismatch). The flat regime's verdicts are unchanged — it already ran the whole gate. In BOTH regimes and in EVERY feature state, aJoinEqsub-proof that noJoinEdgenames is now refused (CheckError::JoinProofNotBoundToAnEdge): itscommit_a/commit_bwere free of the manifest's declared linkage, and no later gate read them. Adds refusals and no acceptance. -
sparq-lws-core (notifications): subscription expiry is decided against a durable high-water time stored in the index, so a restart with the clock stepped back does not revive an expired subscription. On the first start after upgrading, and after a restore that drops index-only records, every subscription with an
expiresis removed and its subscriber must subscribe again; subscriptions without one are kept. Notifications now start only over a store that can raise that time atomically (the in-memory and embedded indexes);LwsState::newfails over any other. While the time cannot be stored, notification endpoints answer503and nothing is delivered. A stored time ahead of the clock stands (subscriptions expiring before it stay expired); to recover from a clock that jumped forward, correct the clock and start once withSOLID_SERVER_LWS_RESET_SUBSCRIPTION_CLOCK=1(LwsConfig::reset_subscription_clock), which, while the state is built, removes every subscription with anexpiresand then stores the clock. AnLwsStatenow holds a lease per store handle (shared by every clone of its store and by anyTrackedwrapper of one) while it lives: a secondLwsStatebuilt over a clone or a wrapper of a running one's store is refused, so a state that replaces another must be built after the old one is dropped. A store opened again separately over the same backend has a lease of its own.Store::reset_markertakes that lease's guard.Tracked::newtakes its inner store's lease and no longer accepts one from the caller, and the store's generic mutators refuse an IRI shaped like the stored time, which changes only throughraise_markerandreset_marker. -
sparq-reason (N3 backward chaining): a variable substitution is cheaper to COPY, with no change to any name or answer. The
name -> termmap a premise evaluation and a backward proof carry now holds the variable's name as a reference-counted string rather than an owned one, so copying a substitution — which the backward path does once per candidate rule site and once per solution, the join path once per candidate fact, and a formula multiset match once per pairing it tries — no longer allocates a fresh name for every entry it carries. That mattered most where the substitution is largest: a backward step's renamed variables stay in the substitution its whole subtree inherits, so the copies grew with the depth of the proof in allocation count, not merely in bytes. Alongside it, the unification of a backward goal against a rule conclusion decides the cases it can decide — the two sides already equal, or either one an unbound variable — on BORROWED terms, where it used to resolve and deep-copy both sides every time; only the member-wise unification of two compounds still owns its two sides, and only the term actually bound is copied. On the FORWARD side, a premise atom's variable names are now minted at most once for the whole atom rather than once per candidate fact, so matching a join atom against its candidates no longer allocates and frees a name per bound variable per row; a name is minted on the first row that actually binds its position, so a position no candidate ever binds costs nothing at all.The hasher is unchanged and the amount of hashing is unchanged. A key is still hashed as the variable name's bytes under SipHash, which is what keeps a crafted document from degrading the map, and a lookup still computes one hash of the same bytes and lands in the same bucket — one lookup per lookup, as before; the saving is in what a copy costs, not in what a lookup costs. No output depends on the substitution's iteration order: every reader of a whole binding, including the proof-mode tie-break, sorts it or asks a question of every entry. Every
__bw<n>_name, derived fact, proof,why()string andsparq-provIRI is byte-identical, pinned against output captured from the parent commit, and the W3C N3 inference conformance report is unchanged. No run that previously answered is refused, and no refusal changes. (#7080) -
sparq-zk-compose / zk (sq-j506, #7121; Noir library #7103): the
DualLeafV2component-witnessed scan and hidden-join circuit members, and a method-aware pattern-constant encoder. Two NEW Noir members,scan_v2dl_k1_n16_r4andjoin_eq_v2dl_na16_nb16, recompute each graph's per-slot term encodings from witnessed V2 leaf components throughcompose_core::dual_leaf_v2_leaf::recompute_term— applying the reserved-sentinel band check and the reserved language-tag check at the witnessed component sites — and then run the UNCHANGEDscan_check/join_eq_checkbodies. The shippedscan_*/join_eq_*members and the relation sources are byte-unchanged, so existing verification keys and all issued material stay valid; each new member's public-input vector is its shipped counterpart's, and the two are told apart by their verification keys.verifier::encode_pattern_slotnow folds a query pattern constant with the encoder the scan's AUTHENTICATED commitment method selects, with no fallback, andbind_scan_pattern_methodsadds two named refusals —CheckError::ScanPatternMethodsMixed(aK > 1scan whose commitments select different encoders: one pattern constant cannot match two encodings) andCheckError::ScanPatternMethodNotEncodable.verifier::bind_fragment_scanstakes the authenticated-method map as a second argument. Gate order: the attestation gate that authenticates the methods runs AHEAD of the stage-1 query-binding gates, andbind_scan_pattern_methodsimmediately after it, so every comparison site reads an authenticated map rather than comparing string-canonically whatever the attested method was; in theextended-fragmentregimeverify_fragment_manifestruns it beforebind_fragment_scans, which is that regime's only scan-slot binding. The gate SET is unchanged and every gate still runs, so nothing that was refused becomes acceptable; what changes is which refusal is reported first for a manifest that fails both an attestation check and a structural check, and that the pattern gates now refuse strictly more. The value-lane and V2-scan verifier gates stay CLOSED: nothing becomes acceptable, and every change here only adds refusals. BEHAVIOUR CHANGE in a DEFAULT build: a scan whose authenticated method isDualLeafV1orDualLeafV2is now refused when thedual-leaffeature is off, because the host dual-leaf encoders are behind that feature and the verifier must not re-check such a scan's pattern constants string-canonically. Such a manifest used to be accepted. Research-grade, not externally audited (sq-qhy4). -
sparq-zk-compose (BREAKING for
successful-resultsusers): successful-result issuance and verification are METHOD-BOUND by default. A method-UNBOUND (ZKSIG_C3) credential, which attests no commitment method at all, is refused with a named error, and the newResultPolicy::admit_unbound_legacyfield (a public struct field, so constructingResultPolicyrequires it) is the written-down opt-in that acts on already-issued unbound material and issues through the retained legacy members. It has the same shape and meaning asMethodPolicy::admit_unbound: under it the commitment method rests on trusted-issuer honesty rather than an attestation the circuit recomputes. The signed (result_v3_*) and public-pattern (result_v4_*) families have no method-bound member yet, so they need that opt-in. The requirement is ONE boundness check on the SELECTED member, not one arm per contract family, so a family added later cannot inherit the legacy behaviour by omission. Each selected credential is now witnessed under the message the SELECTED member's relation folds, and an incoherent pairing — a method-bound attestation through a legacy member, or the reverse — is refused by name rather than witnessed into a proof that could not succeed. Legacy proofs stay verifiable; nothing previously refused is admitted. -
sparq-zk-compose (BREAKING for a relying party on default settings):
MethodPolicy::current()— the default policy onverifier::KeySet— now REFUSES a method-UNBOUND commitment attestation (CheckError::MethodNotAuthenticated) instead of accepting one. A commitment whose method is not issuer-attested has no authenticated leaf shape, so there is nothing for a later gate to pin a lane constant or a pattern-slot encoding against; refusing it by default is the prerequisite that makes an authenticated method something a gate could rely on (sq-j506). A manifest whose attestation signs only theZKSIG_C3/ZKSIG_C4message verified before and is now refused: re-issue over the method-boundZKSIG_M5/ZKSIG_M4shapes, or state the downgrade withKeySet::with_method_policy(MethodPolicy::current().admit_unbound()). The newMethodPolicy::admit_unbound()is that written-down opt-in, andrequire_method_bound()takes the requirement back again; the unbound dial andadmit_legacy_dual_leaf_v1()are independent, so neither widens the other. It opens no gate: the Stage-0ValueLaneSubProofNotAcceptedvalue-lane refusals are untouched and stay closed. Research-grade, NOT externally audited (sq-qhy4) — no soundness or privacy property is claimed as achieved. -
sparq-zk-compose (opt-in
successful-results): the successful-result relations now authenticate every credential slot over the METHOD-BOUND presentation messagesig::presentation_message_with_status_and_scheme(ZKSIG_M5) instead of the method-UNBOUNDcommitment_message_with_status(ZKSIG_C3), with the folded scheme digest fixed to the STRING-CANONICAL method. The relations proveh3(Enc(s), Enc(p), Enc(o))leaves over the string-canonical term encoding and nothing else, so the digest is a compile-time constant in the circuit (compose_core::result::SCHEME_STRING_CANONICAL_V1, withhashes::h5for the arity the message needs) rather than a public input — which makes the commitment method part of the relation instead of a prover choice. The host refuses per cause: an attested method that is not string-canonical, and a method-unbound attestation, which attests no method at all. There is no opt-in for the unbound case here, because the circuit side has no unbound message to recompute. AResultCredentialmust therefore carry aZKSIG_M5signature; an already-issuedZKSIG_C3one is refused.crates/sparq-zk-compose/tests/result_scheme_pin.rspins the message the circuit recomputes from its own Noir literals to the one the host signs, so the two sides cannot drift apart silently. Everyresult_v1_*/result_v2_*/result_v3_*/result_v4_*member routes through the two shared library functions, so no member source changes; thezk/composesource hashes and verification-key/gate evidence are regenerated by the dedicated-hardware measurement run. NOT externally audited (sq-qhy4). -
sparq-reason (N3 backward chaining): the per-goal cost of naming a backward renaming by its proof-tree fingerprint is reduced, with no change to any name or answer. A goal's canonical identity key is built into one pre-sized buffer with the length prefix written straight into it, instead of a fresh string per term and a fresh string per length; the walk that resolves a goal's variables to the end of their chains now BORROWS the terms it did not have to rebuild, and keeps its occurs-check stack by reference; a candidate rule application folds its node key once rather than three times, and renders its
__bw<n>_prefix without allocating.log:conclusionon a nested formula with no<=rule no longer registers that formula's scope salt: a purely forward nested closure can mint no name, so it no longer spends the run's backward identity allowance nor grows the run's tables, and the salt's value is unchanged either way. Every__bw<n>_name, derived fact, proof,why()string andsparq-provIRI is byte-identical, and the W3C N3 inference conformance report is unchanged.This does change which runs are refused, in the direction of refusing fewer. Nested closures accumulate their salts run-wide against
max_join_rows × max_roundswhile each closure gets a fresh join/round allowance of its own, so a document with no<=rule anywhere but a deep CHAIN of nestedlog:conclusionclosures could be refused with thebackward proof-node limit exceededcut for backward naming it can never perform. Such a document now answers under limits that previously refused it. A document that DOES have a backward rule and also nests forward-only closures can still be refused under tight limits, but the cut it is refused with may change frombackward proof-node limit exceededto a forward-closure limit. Wherever both the old and the new behaviour answer, the answers are identical; no run that previously answered is now refused. -
sparq-zk-compose: hardening — the salt a HIDDEN-ONLY commitment's issuer-signed message is recomputed from is now resolved by AGREEMENT over every hidden-issuer entry declared over that commitment, instead of from whichever entry the prover listed first. All such entries must carry a salt, every one must decode to a field element, and all must decode to the SAME one (the hex spelling may differ, since agreement is compared on the decoded element). If any is absent or malformed, or two disagree, the salt does not resolve and the commitment is refused as unreferenced — the same fail-closed outcome a single missing salt already produced. The previous first-entry-wins resolution made that verdict depend on entry order (
[no salt, salt]refused while[salt, no salt]proceeded, and a second, disagreeing salt was never read). Underverify_manifestthe new rule refuses strictly more: the clear-attestation-preferred path is unchanged, and no manifest that verified before is newly refused unless it carried a salt-less, malformed-salt or disagreeing hidden-issuer entry over the same commitment. The structural prefilter alone is not a verifier: there, an unresolved salt now skips the salt-reuse check, and the full verifier refuses the commitment as unreferenced instead. -
sparq-zk: doc correction —
RegistryEntry::issued_with_statusdocumented its signed message as the method-unboundcommitment_message_with_status(ZKSIG_C3); it signs the method-boundcommitment_message_with_status_and_scheme(ZKSIG_M3), which is what the code already did and whatverify_commitment_signature_with_statusrecomputes. No behaviour change. -
sparq-zk: hardening — the dual-leaf encoders now assert the §14.1
LANG_NONEseparation on the host side instead of assuming it.dual_leaf::degenerate_value_componentanddual_leaf_v2::degenerate_value_componentreturnResult<Fr, DualLeafError>and refuse a language tag whose language constant equals the reservedLANG_NONEsentinel with the newDualLeafError::ReservedLanguageConstant, so such a tag fails the literal, term, triple and whole-graph encoders rather than being encoded;checked_lang_const(in both modules) is the gate. A language constant is a 248-bit truncated BLAKE3 digest andLANG_NONEis the field element1, so only a fixed-target preimage reaches the refusal. No committed value, vector or leaf changes. The in-circuit half of the assert landed with the component-witnessedscan_v2dl_*/join_eq_v2dl_*members, which noCircuitIdnames and so no verify path selects (research/zk-field-native-encoding.md§14.1, §14.3). -
sparq-reason (N3): rule existentials are now skolemized in a deterministic order, so for the same parsed input, and as long as the binding values and firing sets are themselves stable, their labels (
__sk<n>_<label>), and the statement keys andsparq-proventity IRIs of facts carrying them, no longer depend on hash-set traversal order or hasher width (native vswasm32). Bindings that still carry traversal order (for example alog:collectAllInlist) can still change those keys (#6844). Existing labels are renumbered, so these keys and IRIs differ from those minted by earlier versions; regenerate stored ones.why()proof trees can still vary with traversal order. Ranking each round's existential firings adds a per-firing cost to rules with existential conclusions, since reduced (#6843). (#6749) -
sparq-reason (N3): the per-round ranking that makes existential skolem labels deterministic (#6749) now orders a round's firing keys by a packed prefix of the keys rather than by comparing whole keys, and a binding whose firing already minted its existentials no longer allocates its key. The labels, the statement keys and the proof steps are unchanged, byte for byte; only the cost of arriving at them is lower. Measured against the pre-#6749 path on an existential-heavy closure, a wide-firing-key closure and a proof-mode run. (#6843)
-
sparq-zk:
CommitmentMethod::DualLeafV1is no longer production-selectable (is_production_selectable()isfalsefor it), andsecprop::production_method_iris()yields the V2 scheme IRI in its place. V1 stays recognised, decodable and verifiable for already-issued graphs; the newCommitmentMethod::checked_for_issuancerefuses it fail-closed when a NEW entry selects a method. Breaking — registry:RegistryEntry::schemeis no longer a public field (read it throughRegistryEntry::scheme()); the method is set only at construction, byRegistryEntry::new_already_issued(doc, c, salt, method)(records any recognised method, for decoding and already-issued entries),RegistryEntry::new_for_issuance(…)and the signedRegistryEntry::{issued, issued_with_status}(…, method, …, sk), which returnResult<_, MethodNotSelectable>and refuse a non-selectable method.with_methodis removed with no replacement builder, so a recorded method cannot be rewritten after construction. The issuer signature now covers the recorded method, for every method includingstring-canonical: issuance signs the newsig::commitment_message_with_scheme/commitment_message_with_status_and_scheme, which bindsig::scheme_iri_to_field(scheme), andverify_commitment_signature{,_with_status}recompute that message from the recorded scheme, so an entry whosezk:schemewas changed after signing fails verification and an unrecognised scheme fails closed. Breaking — behaviour change: a registry entry is never verified over the method-independentsig::commitment_message_with_salt/commitment_message_with_statusmessage, so a registry signature made before this change does not verify. No such entry shipped:sparq-zkis unpublished, and no released artifact calls the registry issuance code. The manifest attestation path (verifier's issuer-signature check) is a separate message and is unchanged. Retiring V1 in a deployed registry stays the operator's choice: an existing entry keeps its method. These host entry points take acommit::MethodUse(NewIssuanceis the default and is refused for a V1 method or constant;LegacyAlreadyIssuedis the written-down opt-in for re-deriving or re-verifying an already-issued commitment, and is a CALLER ASSERTION — nothing reads a signature, entry or timestamp to confirm it):dual_leaf::{commit_triples_dual, commit_graph_dual}(newDualCommitError::MethodNotSelectable),dispatch::resolve_circuit_for_use(newDispatchError::MethodNotSelectableForIssuance) andsparq-zk-compose'sdriver::CircuitProver::prove_value_lane(below).dispatch::resolve_circuititself is unchanged — it is the verification view, so an already-issued graph keeps resolving. Per-term encoders (dual_leaf::encode_literal,dual_leaf_boolean::encode_boolean,dual_leaf_datetime::{encode_datetime, encode_date}, …) are deliberately NOT gated: they produce components, not a commitment, a recorded entry, a renderedProver.tomlor a proof. -
sparq-zk-compose: Breaking — V1 value-lane helpers move behind
legacy_v1. Everything that produces aDualLeafV1value-lane constant is reachable only by naming the newlegacy_v1module:legacy_v1::{boolean_datatype_const, datetime_datatype_const, date_datatype_const}(moved frommanifest),legacy_v1::{build_filter_value_dl_boolean, build_filter_value_dl_datetime, build_filter_value_dl_date}(returninglegacy_v1::BuildValueDlV1Error::{Encode, Pairing}) and the V1 rendererslegacy_v1::filter_value_dl{,_boolean,_f64,_decimal,_datetime}_prover_toml, each of which admits V1 constants only (DispatchError::NotALegacyV1LaneConst). The ordinary names produce V2 only:build::{build_filter_value_dl_datetime, build_filter_value_dl_date}now encode the V2 dateTime/date lanes (returningDualLeafError), and every ordinary value-lanetoml::filter_value_dl*_prover_tomldrops itsMethodUseargument, returnsResult<String, DispatchError>and refuses a V1 constant. Verification-side V1 support is unchanged: rawbbverification of a V1 proof,dispatch::resolve_circuit, the verifier's id derivation and theLegacyAlreadyIssuedpairing view all still accept V1 (the manifest pre-filter refuses every value-lane sub-proof, V1 or V2, as before). -
sparq-zk-compose: Breaking — one typed checked prove entry.
driver::CircuitProver::prove_value_lane(method, use_, challenge, &value_lane::ValueLaneProof, out_dir, tag)replacesprove_in_for_method(removed, withDriverError::ProverTomlLaneMismatchanddriver::top_level_datatype_const). It takes no circuit id and no TOML: the newvalue_lanemodule'sValueLaneProof(one typed variant per lane, built withValueLaneProof::from_inputs(&ProofInputs, LaneWitness)or a builder'slane_proof()) carries the public inputs and private witnesses, the member is derived from the lane byValueLane::member(throughValueLaneProof::circuit_id), the tablevalue_lane::derive_value_lane_memberand the verifier's id derivation also read, and theProver.tomlis rendered inside the entry byValueLaneProof::checked_prover_tomlafter the(method, member, use)dispatch, the canonical-FieldHexcheck over every written field (challenge and private witnesses included), the lane↔member allow-list and the method/constant version agreement (DispatchError::MethodLaneVersionMismatch). A refusal precedes anynargo/bbsubprocess. The rawCircuitProver::{prove, prove_in, gen_witness*}entries stay ungated and are documented as such. Every value-lane renderer, ordinary or legacy, refuses a non-canonicalFieldHexin any field it writes, using the same decoder as the dispatch check, so the rendered text is always the flat form. -
sparq-zk-compose: each value-lane
datatype_constis now bound to its own member by an AFFIRMATIVE allow-list decided on DECODED field elements.dispatch::check_lane_const_for_member(&CircuitId, &FieldHex, MethodUse)andcheck_lane_member_pairing{,_for_use}accept a constant on a member iff a shipped lane derives it for that member — V1 integer and V1 boolean onfilter_value_dl_int, V1/V2 double onfilter_value_dl_f64, V1/V2 decimal at any encodable scale (fd ∈ 1..=19) onfilter_value_dl_decimal, V1/V2 dateTime and date onfilter_value_dl_datetime, V2 integer onfilter_value_dl_int, and the V2 boolean constant onfilter_value_dl_booland nothing else — with a V1 constant accepted only underMethodUse::LegacyAlreadyIssued. A constant no lane derives is refused on every member (DispatchError::UnknownLaneConst), a known constant on another member isLaneMemberMismatch, a V1 constant for new issuance isLegacyLaneConstForIssuance, and a text that is not the canonical rendering of the element it decodes to isNonCanonicalFieldHex(field_from_hex_stralso accepts uppercase,0x-less, short and at-or-above-modulus spellings, which a text comparison would let through). TheProofInputs-levelcheck_lane_member_pairing{,_for_use}checksoperand_encanddatatype_constfor canonical spelling, and every value-lane renderer and the checked prove entry check everyFieldHexthey write (public inputs, challenge and private witnesses) with the same decoder. The same pairing function runs at the value-lane builders, every value-lane renderer,CircuitProver::prove_value_laneand stage 0 ofverifier::prefilter_manifest_structure(CheckError::LaneMemberMismatch). TheProofInputs-level check additionally requires the VARIANT and the declaredidto name the same member (DispatchError::ValueLaneVariantIdMismatch), with the member derived from the variant byvalue_lane::derive_value_lane_member, the function the verifier's id derivation shares; a typedValueLaneProofcarries no separate id, so the checked prove entry cannot be handed a crossed pair. The rawCircuitProver::{prove, prove_in, gen_witness*}entries stay method- and lane-agnostic, so a caller rendering its ownProver.tomlis outside this boundary.datatype_constis a public input no member pins, which is why the decision lives here.check_lane_member_pairing_for_usealone does not cross-check the method version against the constant version underMethodUse::LegacyAlreadyIssued;prove_value_lanedoes (aDualLeafV1method needs a V1 constant and aDualLeafV2method a V2 one; the research-onlyValueOnlyV1, reachable only underLegacyAlreadyIssued, has no dual-leaf version and accepts either). This is a host boundary plus one structural pre-filter refusal: the value-lane verifier gates stay closed until the verifier authenticates the method and pins member, lane, scale and bound itself (sq-j506). -
sparq-zk-compose:
verifier::prefilter_manifest_structurerefuses EVERY value-lane sub-proof (CheckError::ValueLaneSubProofNotAccepted). The refusal is unconditional on the sub-proof being a value-lane member and inspects no edge; it is sound to refuse them wholesale becausefilter_edge_truedischarges a query FILTER only through afilter_intedge, so no value-lane member can discharge an obligation in this verifier (that wiring is sq-j506). Behaviour change: such a manifest was previously ACCEPTED, with the sub-proof reconstructed and verified while discharging nothing. When sq-j506 wires the value-lane query binding, this refusal is what it replaces with a real edge check. -
sparq-reason:
MaterializedN3Graph::with_limitsbuilds the incremental N3 graph under aClosureLimits(newuses the default). The counting path now sizes each builtin's argument list and result before building it and charges a round and fact allowance; past a limit the graph rebuilds through the batch engine, which refuses with"n3: forward closure limit exceeded …", so the graph fails closed instead of growing a term every round. (#6937) -
sparq-reason:
ClosureLimits::max_join_rowsis an opt-in allowance for the transient work of one round of an N3 forward closure: the binding rows its premise joins produce and the times its rules fire, counted together. The default is unbounded, so every closure accepted before is still accepted; set a limit to opt in. The text engine, thecompiled-rulesevaluator and the incremental counting engine ofMaterializedN3Graphcharge each before it is stored; a round past a set allowance returns"n3: forward closure limit exceeded … max_join_rows …"instead of building rows that multiply with each unconstrained premise pattern.ClosureLimitsis#[non_exhaustive], so the new field is additive. Thecompiled-rulesevaluator's first round of each stratum is now naive, as the text engine's is, instead of seeding each join position with the whole store. (#6951) -
sparq-reason (N3): a forward rule whose premise declares its own universal (
{ @forAll :x. :x :p :o } => { … }, W3Csimple_forAll_premise.n3) now fires where the store proves the quantified premise, instead of being refused. A premise universal binds only a universal of the store: a document-level@forAllone, or inside alog:conclusion/log:supportsrun that formula's own (never an enclosing formula's). Distinct premise universals need distinct store universals, and no other variable of the firing may mention one, at any depth, or bind a value holding a blank node (an existential whose witness may depend on it). A ground fact never proves it. Supported where the universal is a subject or object (or in a list or triple term there) of a stored-predicate atom, in a document with no backward rules; every other placement (or a premise that also holds a blank constant, as a rule inside a closed formula can), and an N3 query document, is still refused with the same error. Thecompiled-rulesengine compiles such a rule as one that never fires (its store is ground), andMaterializedN3Graphevaluates it in fallback mode. (#6882) -
sparq-core:
net::is_forbidden_ipis the one outbound-address classifier behind the SSRF guards of SERVICE, sparq-fedclient and the LWS federation client, which re-export it under the names they had; its answers are unchanged (#6927).Graph::is_directory_backed()reports whether a graph or any named graph under it is opened from a directory; it is alwaysfalsewithout themmapfeature (#6944). -
sparq-solid: an ODRL permission attached to a pod document now grants per request through
sparq_policy::decide, for a target that exists and has a full static decision: Read onodrl:read(#6941); Write when one policy permitsodrl:modifyandodrl:delete, and Append onodrl:appendor wherever Write is granted (#6942), each from a rule that names the action, so anodrl:usepermission still grants Read only. Write and Append are never granted on a container or a referenced group document, and apply only in an update made ofINSERT DATA,DELETE DATAandDELETE/INSERT … WHERE: attached policies grant data changes, not graph management (CLEAR,DROP,CREATE,LOAD,COPY,MOVE), andPodStore::decide_removal(used by the MCPresource_deletetool) ignores them. No permission grants a create or Control, and a prohibition or an explicit deny still vetoes. -
Security-property vocabulary: two ordered dimensions,
secx:AdversaryModel(Malicious ⊐ Covert ⊐ SemiHonest) andsecx:CorruptionThreshold(DishonestMajority ⊐ HonestMajority ⊐ SuperHonestMajority), with ODRL leftOperandssecx:requiresAdversaryModel/secx:requiresCorruptionThresholdand their order chains in the admissibility ruleset. sparq-mpc ships a per-protocol annotation graph behind the default-offsecprop-annotationsfeature. The change is additive, but the serialisedsparq-secprop-vocabregistry, the ODRL profile Turtle and the admissibility ruleset text gain the new facts, so anything that pins those outputs byte for byte will see them (#6977). -
sparq-reason (N3): matching a quoted-formula pattern against a quoted-formula value now has a step budget for its backtracking over statement pairings. A match that never backtracks is not limited; past the budget the formula does not match and the run records a cut (
formula unification exhausted its step budget), so a negation or aggregation in that run is refused. (#6953) -
sparq-engine / sparq-fedclient: SERVICE and federation requests no longer use proxy environment variables (
HTTP_PROXY,HTTPS_PROXY,ALL_PROXY) and have no proxy support, so the address the egress resolver vets is always the address that is dialled. -
sparq-reason (N3): a run already refused by a
ClosureLimitslimit no longer reports a negation or aggregation evaluated after the refusal ("refused a negation or aggregation … over an incomplete set"); it fails with the refusal itself, which names the limit (for example… over max_term_bytes (…)). Both are refusals: only the error text changes. (#6951) -
sparq-reason (N3): formula-level
@forAlldeclarations keep their own scope, including nested and sibling quoted formulas. Naive, compiled and incremental engines reject@forAllin a rule's own premise or a scoped body pattern with a specific error; alog:forAllIn/log:collectAllInclause or alog:includes/log:notIncludespattern bound through a variable is checked when it is evaluated and fails the run the same way. Templates and other quoted values keep their own universals at any nesting depth. A document parsed bylog:parsedAsN3/log:semanticsdeclares its top-level@forAllin the resulting formula, the same term as the inline{ @forAll … }. Quoted formula terms retain identity matching, conclusion universals keep their own scope, and closure-introduced universals are restored in the result formula. Document-level@forAllis unchanged. Exact writers place each formula-level declaration inside its formula. (#6851) -
Breaking (sparq-reason,
compiled-rulesfeature):n3::compiled::evalandBoundRuleSet::evalnow returnResult<Vec<[Id; 3]>, String>and run under the defaultClosureLimits; newn3::compiled::eval_with_limitsandBoundRuleSet::eval_with_limitstake aClosureLimits. A compiled closure past its round, fact or term-size allowance returnsErr("n3: forward closure limit exceeded …")instead of running until memory runs out. (#6934) -
Breaking (sparq-reason):
ClosureLimitshas a new field,max_term_bytes(default 16 MiB), and is now#[non_exhaustive], so it can no longer be built with a struct literal outside the crate. Start from the default and set fields:let mut l = ClosureLimits::default(); l.max_term_bytes = 1 << 20;. A forward N3 closure whose rules would build a term past the limit (a string or list that doubles each round) now returnsErr("n3: forward closure limit exceeded …")instead of aborting on a failed allocation. (#6833) -
Breaking (sparq-fedclient): pushed-down
VALUEScells,ORDER BYkeys and filter conjuncts are typed and checked instead of inserted as text.render_values_blocktakes&[Vec<Term>]and returnsResult<String, RenderError>, rendering each cell with the strict term renderer;RenderErrorhas a newValuesvariant;push_grouptakes&[OrderKey](OrderKey::asc("x"),OrderKey::desc("x")) and returnsNonefor an invalid key. A filter conjunct is pushed only when its text parses as exactly one SPARQL expression over its declared variables, with noEXISTSand no\u/\Uescape; otherwise it is evaluated locally. A group with any local conjunct projects all its variables and is sent without itsLIMIT, so local evaluation sees every row it needs. (#6949) -
sparq-reason (N3): the blank nodes of a document parsed at run time (
log:parsedAsN3,log:semantics) are standardized apart as__rp{k}_{key}_<label>, where the key is derived from the document's text and base with SHA-256. Before, every such document's_:awas one node, shared with the rules document's_:a. The label is the same on every build, in a materialization and inwhy(), and for aMaterializedN3Graph's whole life; re-reasoning exported output is a fixed point. A text that spells a graph's caller prefix (__bs{k}_) is no longer refused. A run or graph keeps at most 100,000 distinct runtime documents; one more fails the run. Documents with no runtime parse evaluate as before. (#6867) -
sparq-solid: a multi-operation update that includes a
DELETE/INSERT … WHEREwith aGRAPH ?vartarget now runs, all-or-nothing, on a store with no directory behind it: each operation is authorized against what it writes, and one unwritable target refuses the whole request. A directory-backed store still refuses such a request, and its error names the reason.CLEAR NAMED/DROP NAMEDnow also needs Write on graphs that earlier operations of the same request wrote or created. (#6944) -
SERVICE: both results readers build terms with one shared literal and triple-term builder. An SRX triple term missing its subject or predicate reports
triple term without subject/predicate, as SRJ does, and an invalid one is printed withDisplay. A<binding>whose<variable>is declared only after the results have started is ignored. (#6905) -
Dependencies: sparq-fedclient drops its unused direct
serdedependency and serde_json'sraw_valuefeature (#6911); sparq-engine-service dropsrustc-hash(#6917).
-
sparq-lws-core (LWS notifications, type index, records): notifications follow the subscriber's current access. A WebSocket notification the subscriber may no longer read is not sent and decides no close; an access timer (every 30 seconds,
ACCESS_RECHECK) closes the socket (1008) once read access is gone; a check that cannot be made closes it1011(on the timer) or holds the notification back and asks a subscriber that may read to reconcile (1011) at the next tick. The receive token gates connecting only: an open socket is not closed when it expires, and an expired token never connects again in the process. A webhook subscription decides access before asking whether its topic exists, and a topic being created is decided with nothing read of it. The type index decides read access before anything else is asked of a resource, passes over one the caller may not read as one not there (failing only when a grant names the caller itself), and charges linkset-record names nothing against its budget. A record removal waits for its fence at most as long as for a lock in the request, and in the background for longer each attempt up to the store's call timeout (STORE_CALL_TIMEOUT); it keeps the container held until a removal left to settle in the background is settled, and answers507when the store refuses it for want of room; a subscription being cancelled hears nothing until it is back in force; a subscription create decides its subscriber's due subscriptions before counting. An LDP write refuses a dot segment a URL normaliser could resolve away (%-decoded, or between backslashes) with400; what is stored under one can still be read and deleted. -
sparq-reason (N3): with a
{ … }formula (or{}) as the scope, alog:includes/log:notIncludes/log:supportspattern given through a variable DIRECTLY bound to a stored formula that holds no variable at any depth is matched as that formula, as stored (its blank nodes are its existentials), the same as the formula written in the rule; its statements are charged to the round'smax_join_rowsallowance. It was matched as the empty pattern, solog:includesheld whatever the formula held andlog:notIncludesnever fired. Every other value (a formula holding a variable, a non-formula value, an unbound variable) and every bound pattern with the store as the scope still match as the empty pattern (#6943). Each containment matches its pattern's blank nodes afresh: a witness found by onelog:includesno longer constrains the next one in the premise. (refs #6936, #6943) -
sparq-reason (N3): three more N3 outputs no longer depend on hash-set traversal order, so the same input gives byte-identical labels, proofs and
term_key/statement_keyson every run and every target. Alog:conclusion/log:supportsclosure's DERIVED rows are sorted into a canonical term order instead of the nested closure's fact-set order (a formula's row order is part of its term identity). The rdf:first/rest cell labels minted when a list value is interned (_l<k>_<n>) are numbered by interning every reachable list value up front in that same order, instead of from a counter bumped as the rows arrived. And where a round derives ONE fact several ways — a rule whose conclusion is symmetric or has several triples firing under distinct bindings, two different rules, or the transitivity fast path walking several intermediate paths — the proof step records the derivation with the least premise vector rather than whichever one arrived first, sowhy()names the same supporting facts every run. (refs #6844) -
sparq-reason (N3): the backward chainer's freshened variable names (
__bw<n>_) now address the DERIVATION they standardize apart, instead of counting applications. A rule conclusion can bind a goal variable to a quoted formula that still holds that application's renamed variables, and that value reaches a derived fact — so the name was part of the fact's term identity (itsterm_key/statement_keys, itswhy()string, itssparq-provIRI) while the counter's value depended on how many applications had run before it, and so on a fact set's traversal order and the hasher's width (native vswasm32). A name is now the FINGERPRINT of the derivation: a 64-bit fold of the evaluation scope, the node's place in the proof tree (the premise-atom index, then the(rule, conclusion)site index, plus a scoped operator's clause position) and the goal being proved — folded step by step, each step from the one above it, so neither the name nor the work of building it grows with the depth of the proof — rendered in the__bw<n>_form the readers already accept, so no writer, key or refusal changes shape, and a name's length no longer grows with the depth of the proof. Two derivations that fold alike are refused — the run fails rather than let them share one set of renamed variables, and the table that detects it belongs to the whole top-level run, so a collision between a data closure, a query's own evaluation and a nestedlog:conclusionclosure is caught too. Every part a name is built from is registered the same way, the goal included: it enters the name as an INTERNED id, not as a bare fold, so two different goals cannot merge by folding alike, and it is identified from its FULLY RESOLVED form — every variable walked to the end of its binding chain, through lists, triple terms AND quoted formulas — so two goals that differ only past one binding (a repeated query variable aliasing two renamed ones), or only inside a quoted formula whose variable the caller bound and alog:includespremise reads, stay two goals instead of merging and losing a row. Two branches that reach the SAME resolved goal do share one renaming: their proofs' formula values are then one term rather than two α-variants, so a query projecting only the proved value can return one row where the counter returned two — the same collapse that makes the #6757 programs saturate. A branch the conclusion projects still gets its own row. Sibling nested closures at one nesting depth are also told apart, by the nested document's canonical key — itsterm_keys folded in explicitu64arithmetic, NOT the pointer-width-dependent hash the import-cycle guard uses, so a nested closure's names agree between native andwasm32as well. A run can now be refused for this: the nodes and identities a run registers are charged toClosureLimits::max_join_rows×ClosureLimits::max_rounds— the per-round row allowance times the rounds the run may take, since the tables accumulate over every round — and the entry that would be one too many fails the run with a cut; a(rule, conclusion)site is registered only once its conclusion unifies with the goal, so the sites a search rejects cost nothing, and the DEFAULTmax_join_rowsis the unbounded sentinel, so a default-limits run is never refused for it, and an evaluation with no<=rule in it registers nothing at all, since no name it could mint would ever rename a variable. The resolution that identifies a goal leaves a variable it is already inside the expansion of as it is — a formula bound to?xmay name?xitself, and that occurrence is the formula's own — so a self-naming formula resolves to a finite term instead of being refused; a goal the resolution does refuse (its depth or its term size) now FAILS the run with that reason, where it used to come back as an empty proof that read as "not provable"; and a site's fingerprint is checked against the run's table before it renames anything, since a fold already held by another key renames the conclusion into names the inherited bindings hold, which fails unification and would otherwise skip the check entirely. Three consequences: a fact carrying a renamed variable has a different identity than before, so regenerate storedsparq-provIRIs for such facts; proofs of different goals keep the distinct values they had, including values inside a quoted formula, which substitution does not reach; and a program whose only round-to-round growth was that counter (the #6757 shape: a backward rule concluding a formula with a variable, a forward rule copying it out) now SATURATES instead of being refused at the closure limit, because re-proving one goal mints one name. A program that diverges by growing its terms is still refused. (#6844) -
sparq-reason:
MaterializedN3Graph::new/with_limitsnow ignore abase_factsentry with a variable outside a formula, asinsertalready does, instead of adding it to the base. -
sparq-reason (N3): a rule whose existential conclusion contains a quoted formula with variables (e.g.
{ ?x :p :v } => { _:e :q { ?x :r :v } }) now fires once per distinct binding of those variables. Before, it could fire only once and drop the other bindings' conclusions, so such closures can now be larger and may reach closure limits sooner. (#6749) -
sparq-reason: deleting a fact that the rules document itself asserts from a
MaterializedN3Graphnow sticks when the graph re-runs the batch engine (fallback mode), andwhy()no longer cites such a deleted fact as a premise. The re-run took the document's facts from its source text as well as from the graph's base. -
sparq-policy:
containsanddetect_conflictsnow prove containment and certain overlap throughodrl:and/odrl:or/odrl:xonecompound constraints where the runtime semantics decide it, instead of always answeringUnknown/Possible; they still never reportContainsorCertaina compound cannot prove (#6985). -
sparq-solid: blank nodes in ACL/ACR documents are skolemized on the document's IRI instead of its position, so writing or deleting one document no longer renumbers another document's skolem IRIs and clears the authorization cache (#6960).
-
sparq-reason (N3):
ClosureLimits::max_term_bytesnow also covers numeric builtin results, forward and reverse, which are sized from their value before the literal is rendered (#6929), and the parse behindlog:parsedAsN3andlog:semantics, which is charged as it builds terms, so a short source whose prefixes or copied terms expand is refused before the expanded formula is built (#6894). The document's own parse stays uncharged. -
sparq-engine: a
SELECTwhose deadline passes or that is cancelled while its result terms are materialised returns the budget error instead of a complete, late result. This coversquery,query_with_budget, theWHEREstep ofCONSTRUCT/DESCRIBEandEXPLAIN ANALYZE. (#6946) -
SERVICE (results XML): elements and attributes are matched by namespace as well as local name, so an element in another namespace or with an undeclared prefix is ignored and can no longer stand in for a result, binding or term, or cut a literal short (#6932);
its:dirsets a literal's direction only whenitsis declared as the ITS namespace (#6935); triple terms nested more than 61 deep are refused, the depth SRJ admits (#6916). -
sparq-zk:
ZkDataset::query_tracedrefuses a query with aFROMorFROM NAMEDclause, with the verifier's reason, instead of returning an empty trace. (#6950) -
sparq-solid: a graph named by a bare
scheme://(for example<https://>) is no longer seeded as the container of every pod root, so its.acl/.acrno longer take part in their inheritance. (#6939) -
LWS: extension link relations compare case-insensitively wherever they are stored, matched or searched, as RFC 8288 requires (#6899); the storage owner is notified of a Delete whose prior state cannot be read, and every other non-open subscriber is still refused (#6912); a listing has no
Last-Modifiedwhen the container's or any member's time is unknown, a member of unknown time lists nomodified, and a linkset'sselfentry omitsmodifiedinstead of dating it the epoch (#6919, #6924); while a journaled change is being put back, requests and subscription topic checks are decided on the resource as it was before the change, so a caller only the change would admit gets403at once instead of503(#6925).
0.1.6 - 2026-10-13
First release with the sparq-rdf PyPI wheels (the v0.1.5 wheel builds failed; npm 0.1.5
packages were published). Publication is not implied by this entry; verify the tagged run
and every registry and release artifact.
The entries for #6869 and #6897 were added after the v0.1.6 release commit (66bebdb3).
Both changes are in that commit, but its copy of this file does not list them.
-
sparq-lws-core: the type index and
QUERYtype search, paged, listed in the storage description. Experimental. (#6650) -
sparq-lws-core: the token endpoint accepts OpenID Connect ID tokens, with DPoP proofs for bound ones. (#6721)
-
LWS authentication suites as packages:
sparq-lws-authndefines the suite interface;sparq-lws-authn-samladds SAML 2.0 subject tokens,sparq-lws-authn-openidholds the OpenID Connect suite andsparq-lws-authn-ssithe did:key and controlled identifier suites. A deployment chooses which suites the token endpoint offers. Experimental, not published to crates.io. (#6819, #6820, #6821) -
CI: hosted previews of the site, specs and papers for pull requests that change them. (#6818)
-
Research prototypes (proof-methods/, not part of the shipped runtime): a Noir opening written with field-constraint rotations and additions (#6815), and tamper checks for both Noir openings plus a gate-count script (#6816).
-
sparq-e2ee-ng:
PublicGrant::check_epochenforces a grant's signedmax_epochceiling. (#6853) -
Research prototypes (zk/, not part of the shipped runtime): one variable-length BLAKE3 opening for the 64-bit integer result circuits (#6829).
- LWS token endpoint: a SAML 2.0 subject token is refused unless the assertion's
NameIDis an http(s) URL. Every subject token's expiry is checked again just before an access token is issued, still with the clock-skew allowance, so a token that expires while its documents are fetched is refused. (#6819, #6821) - sparq-trust (status lists):
decode_encoded_listnow rejects au(base64url) body whose length is ≡ 1 mod 4 even when the leftover bits are zero (for exampleugAAAA); such a body has no byte-string meaning, and every other non-canonical form was already refused. A list published by a conforming encoder is unaffected. (#6873) - Breaking (sparq-fedclient): pushed-down SPARQL renders every pattern position strictly as
one SPARQL term.
lower_leafnow returnsResult<SubQuery, RenderError>, the interpreters reportInterpError::Render, andpush_groupreturnsNonefor a pattern holding a variable name that is not a SPARQLVARNAME, an IRI with a characterIRIREFforbids, or a"-prefixed literal that is not exactly one SPARQL literal. Pre-rendered literals are re-serialised with canonical escaping. (#6877) - Breaking (sparq-e2ee-ng):
encode_secretreturnsZeroizing<Vec<u8>>, built in one buffer that is wiped on drop (#6862).Commit::decoderefuses a parent set that is not sorted and duplicate-free, andCommit::encodewrites it that way (#6845). - Breaking (sparq-lws-core):
parse_linksreturns an iterator ofLinkvalues parsed on demand, so the header caps bound the parsing work (#6839). The notification sender takes aPreparedbatch;Notifier::dropped()counts only notifications that were owed, and subscriptions past the delivery bound are counted byNotifier::unprepared()(#6841). - sparq-reason: deleting from a materialized graph with an RDFS meta schema runs an incremental DRed delete instead of rebuilding the closure. (#6842)
- sparq-solid: a rules-free
DELETE/INSERT … WHEREupdate commits without forking the store. (#6830) - SERVICE and sparq-fedclient share one SPARQL results decoder and SRJ driver, one SSRF resolver, and one pattern and N-Triples term renderer; sparq-fedclient parses federation results with the engine's streaming SRJ reader. (#6840, #6872, #6879, #6884, #6895, #6896, #6898)
- sparq-core: shared hex, percent, JSON-string, civil-date, base58btc and base64url codecs replace per-crate copies. (#6850, #6864, #6866, #6868, #6873)
- sparq-shacl: one JS report renderer replaces the two wasm copies, with public
to_js_jsonandmessage_texthelpers on the report. (#6856) - CI: zk-native-composition also runs when
supply-chain/config.tomlchanges (#6861); the LWS.NET suite's own 57 test definitions run as a ratchet (#6831).
- Release pipeline: release files are staged in an empty
release-assets/directory. v0.1.5 published the repository'sassets/logo.svgas an extra asset with no provenance; every other v0.1.5 asset is covered by the isolated-builder bundles. (#6812) - Publish pipeline: the
sparq-rdfwheel build no longer passes--releasewith--profile, which current maturin rejects, and the npm provenance checks wait for the registry to serve the new version. (#6817) - sparq-solid / sparq-prov (date stamps): epoch seconds before 0000-03-01 now format to the
correct proleptic-Gregorian date. The previous conversion was one day late there (and could
emit 29 February in a non-leap year); the only dates it already got right were
29 February of years divisible by 400. For such instants a trust-graph grant's
auth:notAfterand its deterministic grant IRI change (for example-62167219200now formats as0000-01-01, not0000-01-02), and years -1 to -999 are now written with four digits after the sign (-0001, not-001), as XSD requires. Instants from 0000-03-01 onwards are unchanged; signature inputs and PROV IRIs are unaffected. (#6864) - SSRF: the egress classifier shared by SERVICE, sparq-fedclient and the LWS federation client refuses every non-global address range. (#6852, #6870)
- LWS: SAML subject confirmations are alternatives, as SAML Core defines them: one satisfied bearer confirmation confirms the assertion (#6827); the server checks authorization before revealing set-aside or lock state (#6848); a store write that times out is treated as having an unknown outcome (#6860); the server binary forbids unsafe code (#6876).
- sparq-mpc: federated plans fail closed when a participant has no privacy policy. (#6825)
- JavaScript: the remaining RDF and SPARQL text builders validate interpolated terms against the grammar. (#6826)
- sparq-reason: the N3 nesting limit fits a 2 MiB thread stack, so deeply nested input is refused instead of overflowing the stack. (#6835)
- sparq-reason: base-graph blank nodes stay distinct from blank labels the N3 parser allocates. (#6779)
- sparq-reason: the N3 evaluator bounds its stack use on untrusted input. Under the default policy, terms built past 128 levels are dropped as cuts, nested closures and proof steps share one stack allowance per run, and a run with cuts refuses a later negation or aggregation instead of reading missing facts. (#6869)
- sparq-fedclient: the ASK reachability probe keeps an endpoint's existing query string
and replaces only its
queryparameter. (#6897) - LWS: a type-search filter too long to page is refused as too complex (422). (#6889)
- sparq-shacl:
LATERALblocks are covered by the SHACL-SPARQL pre-binding checks and theVALUESpush-down. (#6846) - JSON-LD conformance: the oracles compare arrays inside
@jsonliterals in order. (#6857) - CI: the scheduled and full-sweep runs are green again (#6824, #6837), and the nightly visual sweep builds its inputs first and pushes re-minted baselines to a review branch (#6858, #6887, #6890, #6892, #6893).
0.1.5 - 2026-10-10
Fix-forward release after the v0.1.4 release workflow failed before creating a GitHub Release (no v0.1.4 assets, npm or PyPI packages were published). Publication is not implied by this entry; verify the tagged run and every registry and release artifact.
- Release pipeline: the Windows matrix rows write their archive, binary and GUI bundle
digests in text mode (
<hex> <name>). Git Bash's binary-mode*<name>record failed the archive-digest collector, which stopped the v0.1.4 run before provenance and the GitHub Release. (#6717) - sparq-policy / sparq-solid (ODRL): hardened ODRL policy evaluation and the ODRL authorization bridge. Some policies accepted by v0.1.4 are now rejected at parse time. (#6734, #6737)
- sparq-solid: a SPARQL Update's WHERE clause is now evaluated against the session's read
view, the same as a query, and graphs named in
USING/USING NAMEDneed Read access. (#6763) - sparq-solid: a SPARQL Update request with more than one operation can no longer include a
DELETE/INSERT … WHEREwith a variableGRAPHtarget; send that operation on its own. (#6763) - sparq-server / sparq-lws-core (ODRL): rules that target an
odrl:AssetCollectionnow apply to the collection'sodrl:partOfmembers on the server's ODRL lane and the LWS ODRL gate. (#6773) - sparq-solid: if rebuilding the authorization view fails after an accepted
.acl/.acrchange, the store denies every request until a rebuild succeeds, instead of continuing to enforce the previous rules. (#6742) - sparq-reason:
@forAlluniversals are named by their full IRI, so@forAll a:x, b:xare two variables andreason_n3_pass_allno longer prints internal__ua_names. Exact writers refuse a document that would not re-read the same way. (#6701) - sparq-reason: a forward N3 closure that never reaches a fixpoint stops with an error once
it exceeds the configurable
ClosureLimitsround or fact count. The limits count rounds and facts, not bytes, so rules that grow a term each round can still use unbounded memory. (#6769) - sparq-reason:
math:comparisons promote numeric types as XPath does. (#6772) - sparq-reason: the RDFS closure reaches its fixpoint when schema properties have schemas
of their own, including with
why()and incremental updates. (#6736) - sparq-core / sparq-engine:
xsd:dateTimeStampwithout a timezone is ill-formed (#6725), and XSD 1.1 year zero is accepted (#6747). - sparq-engine: integers and decimals beyond
i128order by value (#6697),ROUNDkeeps a negative zero (#6703), CONSTRUCT and DESCRIBE go throughPreparedQueryand honourBASE(#6677), and the query budget bounds CONSTRUCT / DESCRIBE N-Triples output (#6714). - sparq-canon: tied RDFC-1.0 N-degree hashes break ties independently of blank-node labels (#6645), and RDF 1.2 permutation enumeration is lazy and budgeted (#6702).
- sparq-shacl: date and dateTime comparisons use the shared exact temporal comparator. (#6733)
- sparq-server: CSV, TSV and XML SELECT results are streamed instead of rendered whole. (#6708)
- sparq-lws-core: durable intents restore changes that a process stop interrupted. (#6767)
- sparq-client: the shared N-Triples writer validates term parts. (#6716)
- @sparq-org/sparq:
@sparq-org/sparq/wasm/*resolves through the packageexportsmap, and the wasm copy step runs on any platform Node runs on. (#6739) - sparq-forms: update terms that can't be written safely are rejected. (#6707)
- Desktop GUI: Graph view refuses UPDATE (#6704), full-text search terms and SHACL report strings are escaped (#6741), and the ODRL preview shows the policy's own per-request verdict (#6760).
- Site and wasm: horizontally scrolling tables and code blocks are keyboard-focusable (#6777), and the parser size optimisation applies again after the spargebra rename (#6782).
- Dependencies: Rust dependency updates in the root and side lockfiles (#6694), and an
npm override for
basic-ftp(#6751).
- sparq-solid:
PodStore::attach_odrl_policykeeps an ODRL policy on the store and evaluates its prohibitions for the requesting session on every read, query, decision and update path. (#6759) - sparq-lws-core:
SOLID_SERVER_PROTOCOL=lwsserves the LWS 1.0 storage surface: storage description, paged containers, data resources with preconditions and byte ranges,POST/PUT/DELETEand linksets. Experimental. (#6728) It also accepts JSON Patch on data resources (#6805) and linksets (#6806), and serves access grants and access requests (#6730) and webhook notifications (#6731). - Research prototypes (zk/ and proof-methods/, not part of the shipped runtime): the
stacked ZK and credential-query work (#6647), a revealed-signature mode for the zkVM
eddsa-rdfc-2022 relation (#6787), the
eddsa-sha256-merkle-2026Merkle-root cryptosuite and spec (#6789), a disclosed-credential re-evaluation method (#6788), a QuickSilver prover for the zk/compose circuits (#6790), an AWS Nitro Enclaves attestation method (#6794), a paper measurement harness and executor evidence records (#6791, #6793), and a hand-written ACIR prototype for the integer literal opening (#6803). - sparq-proved-evaluator (research): the V5 host rejects DESCRIBE and dataset clauses
(
FROM/FROM NAMED). (#6792) sparq-vectorscan open canonical little-endian.spqvstores on big-endian hosts by validating the file and byte-swapping its dense f32 region into aligned owned storage;.spqvwriters and the.spqgreader/writer remain little-endian-host only.
-
Breaking (sparq-reason): an N3 document whose rules cycle through negation (
log:notIncludes,log:collectAllIn,log:forAllInover a predicate the same document derives) now returns an error naming the negated predicate, at every N3 entry point. Acyclic documents are evaluated stratum by stratum. The*_with_cyclesvariants opt in to the previous acceptance. (#6705) -
Breaking (sparq-engine, JSON-LD compaction):
graph_to_jsonld_compact, the CLI'sjsonld-compactand the wasm serializer now produce JSON-LD 1.1 compaction output. The RDF is unchanged, but a single top-level node has no@graphwrapper, nodes come out in@idorder, and@id/@typemaps and reverse properties follow the spec. (#6674) -
Breaking (sparq-engine, JSON-LD framing):
graph_to_jsonld_framed,write_jsonld_framedand the wasm framed serializer now use the JSON-LD 1.1 framing algorithm. Frames match over the merged graph, so matches no longer carry named-graph wrappers,@linkbehaves as@once, and a frame that matches nothing returns{}. (#6727) -
Breaking (sparq-core): the native N-Triples / N-Quads parser follows the W3C grammar. IRIs must be absolute, may not contain the excluded characters and allow only
\u/\Uescapes, so input it used to accept may now fail to parse. (#6723) -
Breaking (sparq-core): the native N-Triples / N-Quads parser requires well-formed BCP47 language tags and an exact
ltr/rtlbase direction, matching the serial parser. (#6740) -
Breaking (sparq-mcp):
container_listleaves out members the session cannot read, the stdio transport refuses requests larger thanServerConfig::max_request_bytes, and a newquery_timeout_mssetting bounds query time. (#6700) -
Workspace, desktop, and public npm package versions move to 0.1.5, with every path-dependency requirement and lockfile record following.
-
Breaking (@sparq-org/solid-server): the optional
fastifypeer dependency is now^5.12.5(was^4.28.0 || ^5.0.0). Fastify 4.x is end of life and has no release that fixes GHSA-p68q-wchp-6fh7 (authentication bypass via malformed URLs); fastify releases before 5.12.5 carry further request-validation advisories. -
Breaking (sparq-vc):
sign,sign_graph,verifyandverify_graphvalidate proof options before any signing or verification work:verificationMethodmust be an absolute IRI,proofPurposea supported VC v2 term or an absolute IRI, andcreatedan XSD 1.1dateTime. Theauthentication,capabilityDelegation,capabilityInvocationandkeyAgreementpurposes now hash as their VC v2 context IRIs (sec:…Method), and absolute-IRI purposes hash verbatim. Proofs signed with those purposes by earlier releases no longer verify and must be re-signed; there is no fallback.assertionMethodproofs are unaffected. -
FROMgraph merges standardize source blank nodes apart, including a singleFROM. A blank node in that merged default graph no longer aliases its source node accessed withGRAPH, even whenFROM NAMEDnames the same graph. Protocol dataset overrides and LWS union-default expansion use this behavior; blank-node labels in results may change. -
sparq_engine::QueryBudgetadds temporal/numeric capacity controls and an optional EBV dialect selector. Exhaustive struct literals must supply the new fields or use..Default::default(); defaults retain ordinary native behavior. Additive detailed evaluation APIs distinguish row/byte capacity, numeric/temporal capacity, deadline, cancellation and execution failure. -
Query wrappers retain
VERSIONannouncements through algebra rewrites. Unknown or incompatible labels reject during query preparation; proof profiles explicitly require REC 2013 semantics, and UPDATE remains REC-only.
0.1.4 - 2026-09-26
[GPT-5] Recovery candidate after the immutable v0.1.3 release workflow failed before creating a GitHub Release. Publication is not implied by this entry; verify the tagged run and every registry and release artifact before using the install paths.
- The release job reads the pinned SLSA generator's actual
provenance-nameoutput for both signed bundles and rejects missing or unexpected names before downloading. The v0.1.3 workflow requested an absent output, downloaded unrelated artifacts, and failed while generating checksums over their directories.
- Workspace, desktop, and public npm package versions move to 0.1.4. Root lockfile records follow the workspace with external dependency versions unchanged; the standalone bench, fuzz, and desktop lockfiles are regenerated by cargo against their current manifests.
- The crates.io publish set is the focused core of 12 crates plus the
sparq-spargebraparser fork (see docs/release.md §4); every other workspace crate ispublish = false. The three emptysparq-wrapper-*stub crates are removed. - The v0.1.3 tag and already-published container artifacts remain intact. This candidate rebuilds and attests its own bytes under a new immutable source tag.
0.1.3 - 2026-09-21
[GPT-6] New complete-release candidate after the immutable v0.1.2 attempt. Publication is not implied by this entry; use the release runbook and require the cadence guard and hosted release verification to pass.
- [GPT-6] Already-expired or cancelled SELECT-JSON requests refuse before scanning, emitting output, or waiting for unrelated Rayon pool work.
- GUI installer staging uses Bash 3.2-compatible glob options on macOS. The shared staging step is exercised by hermetic fixtures for every desktop platform; empty output still fails and the full release alias/provenance contracts remain enforced.
- Desktop package and installer metadata now match the release version, instead of retaining the old 0.1.0 value underneath versioned asset filenames.
- Workspace packages, Python's derived version, all three public npm packages and the desktop application target 0.1.3. Lockfiles preserve external dependencies; the private LWS crate and tooling retain their independent versions.
- Release preparation and installation documentation now distinguish the incomplete v0.1.2 attempt from this new candidate. The existing v0.1.2 tag, containers and provenance remain intact; they are not rebuilt or renamed as v0.1.3 artifacts.
- A maintainer-authorized cadence exception admits only the v0.1.3 recovery with pinned predecessor/tag evidence and definitive crates.io absence. All other versions retain the 24-hour minimum, and indeterminate reads still refuse.
0.1.2 - 2026-09-20
[GPT-6] Incomplete GitHub Release. Both container publication lanes completed, but run 35540471918 failed on macOS GUI staging and the alias completeness gate correctly blocked the GitHub Release. The immutable tag and published containers remain unchanged. The fix and next complete-release candidate belong to v0.1.3.
[GPT-6] Recovery release prepared from the changes after the immutable v0.1.1
source tag. Publication is not implied by this version entry; verify the release
artifacts and registries using the release runbook.
- Experimental, default-off
sparq-core/overlay-deleted-projectionscaches deletion counts by index permutation. Its retained-memory and cold-read tradeoffs are documented in the core README andbench/overlay-count; no universal speedup is claimed. - The experimental private
sparq-lws-coreserver resolvesacl:agentGroupmembership from same-pod group documents. Missing, malformed, off-origin, and non-member groups grant nothing; resolution performs no outbound fetch.
- Bind joins reuse contiguous groups only after validating sorted input; capped seed blocks can reuse bounded right-hand scans when no query budget is armed.
- Core updates preserve the numeric memo when the dictionary is unchanged.
- Workspace packages and all three public npm packages target
0.1.2, including@sparq-org/eyereasoner-compat(previously0.1.0). The private native LWS crate retains its independent version. PyPIsparq-rdfderives the workspace version. - CI now verifies completed nightly work and reporter identity, reserves heavy runner capacity, and bounds benchmark dashboard publication. Site and GUI dependency patches update Next.js and sharp.
- Nested queries restore the outer budget, cancellation handle, accounting, and sticky error on return or unwind; child budgets remain independent.
- Predicate statistics serialize in stable predicate-ID order without changing the on-disk format. UPDATE differential tests preserve RDF term identity.
- Scheduler queue-occupancy tests park both workers and release them safely on assertion panic, preventing the feature-matrix test hang.
- Release SLSA permissions permit the reusable provenance workflow. The Cargo bootstrap graph excludes the path-only introspection test dependency, while retaining versioned dev-dependencies in dependency-first publish validation.
- Release and package workflows require matching tag, build commit, and manifest versions. Automatic release-plz version calculation is contained until the first crates.io bootstrap; the manual version PR's tag handoff remains active.
0.1.1 - 2026-08-31
[GPT-6] Incomplete bootstrap, not a complete release. The immutable source tag
points to 1a63aa7c638bd80da55f1811d5fb97e8d014f631. The features below describe
that source snapshot, not successful registry or binary publication. As verified on
2026-09-12, its release workflow failed before starting jobs; no GitHub Release,
crates.io packages, or PyPI sparq-rdf distribution were published. npm
@sparq-org/sparq@0.1.1 and @sparq-org/solid-server@0.1.1 exist without
dist.attestations and cannot be republished to add provenance.
The earlier v0.1.0 tag was also incomplete. Both tags remain fixed for auditability;
post-tag fixes and complete-release recovery belong to 0.1.2, not this snapshot.
APIs remain experimental and unstable.
Crates.io build caveat: crates.io builds resolve upstream spargebra 0.4.6 — the vendored SPARQL-parser conformance fixes (vendor/spargebra/SPARQ-PATCHES.md) apply only to git builds until the upstream PRs land.
-
Storage (
sparq-core) — dictionary-encoded triple store with six sorted permutation indexes (Hexastore/RDF-3X lineage); a 3-permutation compact-index mode (~half the index memory) for memory-constrained targets; parallel index construction. -
Out-of-core mode — build the indexes on disk and query them memory-mapped: a 100M-triple external build at a few GB peak RAM, then fast open with ~0 committed heap (OS page cache only), at query speeds matching the in-memory engine (figures:
research/BENCHMARKS.md). -
Parsing — N-Triples, Turtle, N-Quads, TriG (via
oxttl); parallel and streaming, with transparent.gz/.bz2/.zstdecompression. Real-Wikidata ingestion builds all six permutations out-of-core on a fanless laptop (rate:research/wikidata-ingestion-benchmark.md). -
SPARQL query (
sparq-engine) — SELECT/ASK; Basic Graph Patterns, FILTER, OPTIONAL, UNION, MINUS, VALUES, BIND, aggregation + GROUP BY, ORDER BY, DISTINCT, LIMIT/OFFSET; sort-merge, hash, and worst-case-optimal joins with greedy cardinality-based planning; parallel scan / filter / sort / aggregation / serialization. -
Inference (
sparq-reason, opt-in) — RDFS, OWL-RL, and a Notation3 subset with saturate-then-sweep single-pass materialization (a large speedup over a naive fixpoint; figures inresearch/BENCHMARKS.md/ the crate README) and union-findowl:sameAs. -
CLI (
sparq-cli) — load/query/bench subcommands, on-disk index build +query-mmap,reason, a per-subsystem parallel-scaling harness, and hardware-tiered release binaries (per-ISA prefetch tuning selected per silicon family). -
HTTP server (
sparq-server) — W3C SPARQL 1.1 Protocol query endpoint (GET/POST) and Graph Store HTTP Protocol read side; JSON / XML / CSV / TSV results with content negotiation; Docker image (distroless,ghcr.io/sparq-org/sparq-server). -
WebAssembly (
sparq-wasm, unpublished) — the core engine compiled for the browser with a minimal bundle (no threads, compact index); ships via npm later, not crates.io. -
Python full-text search (
sparq-py×sparq-text) — theTextIndexlifecycle follow-up recorded incrates/sparq-py/TODO.mdis implemented on the wrapper:Graph.text_search(query, any=False, limit=None)returns BM25-ranked(Term, score)hits over the default graph's string literals (AND of tokens by default,any=Truefor OR,*-suffix prefix tokens), andGraph.query_textruns SELECTs with thetext:magic predicates (text:matches/text:matchesAny/text:score) throughsparq_text::query_text. The index is built lazily on first use, cached on theGraph, and invalidated on every mutating swap (update/reason/reason_n3_with— the wrapper's mutation paths rebuild the graph, so lazy rebuild, notapply_delta, is the policy that matches);build_text_index()builds eagerly (returns the indexed-literal count) anddrop_text_index()releases the cache. Opt-in stays consistent with native (full-text is engaged by depending onsparq-text; the new edge is on the leafsparq-pycrate only, so engine/CLI/wasm stay text-free;sparq-textitself is unchanged). 12 new pytest cases (38 → 50). -
Concurrent-serving Wave A2 + A3 (
sparq-serve) — the single sequenced writer with group-commit over the A1 generation ring (research/concurrent-serving.md§6.5). One writer thread owns the sole right to publish generations; updates arriving within a group-commit window (default 3 ms / 256 updates) apply to one working copy and publish as ONE generation (one epoch tick), with per-update atomicity — a failed update is reported to its submitter and skipped, never poisoning its batch (re-fork-and-replay recovery for partial mutation). Opt-inCommitGranularity::CommuteGroupsplits a window into maximal runs of provably-commuting pure-data updates via conservative graph-level footprint analysis (Footprint::of_sparql; same-named-graph opposite-polarity or unanalyzable = barrier) and publishes one conflict-free generation per run — committed result is byte-identical to serial (FIFO application either way).GraphApplieris the productionApplyUpdates(structuralGraph::fork+ delta-overlay apply + threshold compaction). A3:Writer::submit's returned generation number doubles as a single-node read-your-writesshard_seqtoken;GenerationRing::read_your_writes(token)pins the current generation iff it already reflects that commit (number >= token),Noneotherwise (a lagging replica must wait/redirect). Readers never block —current()stays lock-free. Tested: group-commit + FIFO + failed-update isolation, commutativity-batch differential correctness (CommuteGroup == Window == serial, + 200-op fuzz), 50k-triple bulk-update atomicity (a concurrent sampler sees 0-or-all, never a partial prefix; never stalls), retention/pinning under sustained writes, and read-your-writes token semantics. Benched inbench/serve/writer_spike(group-commit ~7.8× writer throughput at batch 16 vs 1; readers within ~1.3× of idle p99 under load; the too-large-batch regression recorded honestly).sparq-servestays out of thesparq-wasmdependency graph (server-side only). -
Concurrent-serving Wave B — read-side scheduler (
sparq-serve) — the cost-aware queryScheduler(research/concurrent-serving.md§6.2; goal #4 requirements 3 + 4: cheap-query prioritisation + no head-of-line blocking). A bounded thread pool with two lanes split by a crude cost estimate (Wierman & Nuyens: SRPT tolerates crude size errors): cheap jobs are prioritised (FIFO) and always keep a reserved worker, while heavy jobs run under a bounded concurrency cap (defaultcores/2, clamped<= workers-1so a cheap worker is always free). The reserved-cheap-capacity invariant is the structural no-head-of-line guarantee — an expensive-query flood can never block a cheap query, because at least one worker is always available the instant a cheap job arrives. Within the heavy lane, ordering is Umbra-style SRPT-approx + unbounded aging (litreview-C:p0 = 10^4start, cost lowers priority, wait raises it), so the most-postponed heavy job eventually becomes the max-priority pick — starvation-free. Readers still pin a generation on entry inside the submitted closure (A1 snapshot consistency), so scheduling reorders when/where work runs but never what it computes: results are byte-identical to unscheduled execution. Library-first: sync, runtime-agnostic (std::thread+ condvars, no HTTP/async types); the estimator and the work are both injected, so the scheduler never reaches into the engine itself. Tested (empirical-honesty suite): differential result-equality vs direct execution (closure + real-engine COUNT queries over the ring), bounded heavy concurrency, head-of-line cheap-p99 containment under a concurrent expensive query (open-loop, coordinated-omission-safe arrival), starvation-freedom under a sustained cheap flood, SRPT-orders-cheaper-first + aging-overtakes-a-cheaper-fresh-heavy (deterministic), no-regression on an all-cheap workload vs a plain pool, panic-isolation, and shutdown shed-don't-hang. Honest scope: true preemption is out (the engine has no re-entrant suspend/resume); the cap + reserved capacity deliver the no-HoL property without it. Stays out ofsparq-wasm's graph. -
Python bindings parity wave (
sparq-py) — the PythonGraphcatches up with the engine/reasoner surface: nativeask()(engine early-exit instead of the SELECT-count rewrite; SELECT still accepted), newconstruct()/describe()(lists of(s, p, o)Termtuples; DESCRIBE is concise-bounded-description), named-graph parity (the stale "named graphs unsupported by update / dropped by reason" caveats removed: updates ride engine update v2, andreason()/reason_n3_with()now carryGraph.namedacross the rebuild), newinconsistencies()(OWL 2 RL clash report fromsparq_reason::inconsistencies), and newreason_n3_with(rules)(caller-supplied N3 rules over an already-loaded graph, composed exactly likeMaterializedN3Graph's fallback: default graph rendered as N-Triples under the rules document, run throughreason_n3; on error the graph is unchanged). 18 new pytest cases (20 → 38); README/TODO refreshed. Verified non-gap recorded honestly: full-texttext:predicates do NOT work through plainGraph.query—sparq-textis a deliberately standalone opt-in crate, so exposing it needs aTextIndexlifecycle on the wrapper (left as a documented follow-up incrates/sparq-py/TODO.md, not wired in quietly). -
JS/wasm parity wave (
@sparq-org/sparq+sparq-wasm+sparq-core) — six recorded parity gaps closed; wasm bundle 1,593,075 → 1,643,103 B (+50,028 B, +3.14%, all from three new wasm capabilities, measured per feature; JS-only features byte-identical):- ASK from JS (0 B):
queryBoolean()now uses the engine's native ASK (boolean JSON form, first-solution early exit) instead of the SELECT-rewrite + count path;queryJson()serves ASK too. Plus a test pinning full-text-style matching via plain SPARQL string functions (and thatREGEXstays compiled out of wasm by design — the engine's non-defaultregexcargo feature). - Named graphs (+16,906 B): wasm
Store.loadDataset(N-Quads/TriG named graphs preserved); JSoptions.datasetonfromString/fromQuads;GRAPH/FROM/FROM NAMEDqueries and full-dataset SPARQL Update (already compiled in) now reachable;match()/countQuads()graph-aware (wildcard spans named graphs via aUNION { GRAPH ?g … }). - Streaming results (+4,575 B): wasm
Store.queryChunkscursor over the engine's chunked SPARQL-JSON serialiser (~64 KiB, row-boundary splits, concatenation byte-identical toquery()); JSqueryBindingsStream()generator (for…ofandfor await…of) backed by a chunk-boundary-agnostic incremental row parser (SparqlJsonRowsParser),queryJsonChunks()for raw forwarding. - Incremental updates from JS (+28,547 B:
applyDelta+14,895,updateInPlace+13,652): new core APIGraph::apply_delta_nquads(N-Quads batch, deletes-first, routed per graph, named graphs auto-created, bnodes by label); wasmStore.applyDelta+Store.updateInPlace(engineupdate_in_place); JSapplyDelta/addQuads/removeQuads, andupdate()now applies in place — O(batch), no index rebuild. - Compressed ingest in the browser (0 B, JS-only):
fromCompressed()/decompress()— zstd via pure-JSfzstd(dynamically imported; decodes the multi-frame streamsCompressedSinkemits — re-verified), gzip vianode:zlib(multi-member) /DecompressionStream(single-member, browser caveat documented per the D4 §3 matrix).fzstdis the package's single runtime dependency now. - Dictionary-fetch protocol client (0 B, JS-only):
SparqDictionaryClientimplements D4 §4 —Sparq-Dictionary/Sparq-Dictionary-Currentnegotiation, content-addressed (truncated-id, prefix-verified) dictionary caching with background warm-up fromGET /dictionary/{dict-id}, pluggable dict-capable decoder hook (fzstd has no dictionary API), RFC 8878 frame-header dictID parsing. - JS test suite 16 → 42 (
node --test), +1 nativesparq-coreunit test.
- ASK from JS (0 B):
-
Opt-in inference proof trees (
sparq-reason, non-defaultexplainfeature) —why(triple)onMaterializedGraph/MaterializedOwlGraph/MaterializedN3Graphreturns aProofTreefor any closure triple: which rule fired (W3C spec rule ids —rdfs2..11,cax-sco,prp-trp,scm-*— or documented engine rulesinv-dom/inv-rng/sym-dif,n3-rule-<i>) from which premises, recursively to asserted facts. The shape is flat and ZK-witness-friendly (premises-before-conclusion node list, root last, shared sub-proofs deduplicated, deterministic, terms as self-contained strings — a planned ZKP module can consume it as a derivation witness); JSON + indented-text renderings; depth/size caps (ExplainOpts). Proofs are a witness, not all witnesses, and always reflect the CURRENT base: after a delta, retracted supports are never served (an alternative support is found, or the triple no longer explains). Design: derivations are reconstructed from the counting engines' closed TBox + deterministic emission functions (plus raw TBox edge maps and base reverse indexes kept under the feature) rather than stored per derivation. OWL fallback mode (recursive/ equality features) is documented as unexplained; N3why()re-runs the batch engine with proof recording per call. Id-level batch N3 proofs bridge viaexplain::n3_proof_tree. The feature is cfg'd out entirely by default: zero hot-path cost, zero wasm impact (sparq-reason is not in the wasm graph; bundle byte-identical). When ON: batchmaterialize_*is unchanged (untouched code); incremental handles pay a bounded constant for the base reverse indexes on initial build, and small deltas stay ms-scale and far faster than re-materialization (measured cost in the crate README); N3 unchanged (no extra maintenance state). Design lever if that ever matters: build the indexes lazily on firstwhy(). -
Opt-in full-text search over literals (
sparq-text, new crate) — a small, owned BM25 inverted index over a graph's string literals (UAX #29 tokenizer + Unicode lowercasing viaunicode-segmentation; deliberately no tantivy) where the dictionary term id IS the document id, so hits join back to triples through the ordinary permutation indexes.TextIndex::buildscans the dict once (rayon-sharded underparallel);apply_deltamirrorsGraph::apply_deltabatches incrementally (pinned equal to a rebuild by a differential test). Query surface: AND (search), OR (search_any),*-suffix prefix tokens (autocomplete), BM25 scores — and, behind the default-onenginefeature, thetext:magic predicates (http://sparq.dev/text#):?lit text:matches "q"/text:matchesAny/text:score ?s, rewritten at the spargebra-algebra level into inlineVALUESover the hits and executed through the engine's existingPreparedQuery: From<spargebra::Query>seam — zero engine changes, zero wasm impact (bundle byte-checked; the crate sits outside every default dependency graph, likesparq-geo/sparq-vectors). Build / index-size / query-latency figures on 1M synthetic literals:crates/sparq-text/README.md(run itsbench_textexample). -
Opt-in time-travel queries (
sparq-serve,sparq-server) — a retained generation IS a queryable snapshot. Library:RingConfig::time_travel(TimeTravelConfig { max_generations, max_age }, defaultNone) extends ring retention beyond the concurrency bound K (K stays a floor — time travel extends, never shrinks);GenerationRing::at(n)pins a retained generation,Generation::published_at(stamped by the injectableRingConfig::clock) +GenerationRing::as_of(t)resolve "as of T" to a generation, honestNoneonce history ages out. Server (non-defaulttime-travelcargo feature):?generation=Non/sparql(URL or url-encoded body) serves the store as of that generation; every/sparqlresponse carries aSparq-Generationheader (update 204s carry the generation containing the update — the read-your-writes token, the horizontal-scaling ADR's shard_seq concept); aged-out →410 Gone, never-published/unparsable/pinned-update →400; retention via--time-travel-generations[16] /--time-travel-max-age[off]. Feature off: the parameter handling is compiled out (tests cover both compilations). Memory cost recorded honestly: each retained generation is a FULLGraphuntil the structural-fork follow-up lands (delta-chain/OSTRICH-style retention is the named follow-up; the API needs no change for it). Wasm bundle unchanged. -
GeoSPARQL function completion (
sparq-geo) — thegeof:registry grows from 12 to 35 functions:geof:getSRID(xsd:anyURI), the generic DE-9IMgeof:relate, the full Egenhofer (geof:eh*, 8) and RCC8 (geof:rcc8*, 8) relation families (GeoSPARQL 1.0 Req 25/26 matrix patterns), the set operationsgeof:intersection/union/difference/symDifference(polygonal operands via geo'sBooleanOps; other operand types are a clear per-row expression error), andgeof:buffer(unlocked by the geo 0.30→0.33 bump; metric radii buffer in a local equirectangular metre frame). All exercised through real SPARQL. -
GeoIndex: antimeridian, named graphs, incremental updates (
sparq-geo) — ball queries crossing ±180° split into two longitude windows (merged, deduped; brute-force-verified near the seam);GeoIndex::buildscans every named graph and entries record their origin graph +geo:asWKTnode; newGeoIndex::apply_deltamirrors aGraph::apply_deltabatch into the R-tree incrementally (rstar insert/remove, O(batch·log n) — no rebuild), includinggeo:hasGeometryownership re-keying. -
CRS reprojection (
sparq-geo, opt-inreprojectfeature) — pure-Rust proj4 (proj4rs; no C dependency) behind a curated EPSG table (27700 — verified against the Ordnance Survey worked example to ~1e-6°, 3857, 2154, 25832/25833, UTM 326xx/327xx):reproject::to_crs84brings projected literals into the geographic machinery (metric distance, GeoIndex). -
HDT quality-of-life (
sparq-hdt) — GZipped containers (.hdt.gz) are detected by magic bytes (not file names) and decompressed on the fly in every entry point; newheader()/header_reader()expose the HDT header (dataset metadata triples — VoID statistics, provenance) as a queryable sparqGraph. -
CLI HDT ingestion (
sparq-cli, opt-inhdtfeature) —--features hdtwires sparq-hdt into the loader dispatch: format argumenthdtor a.hdt/.hdt.gzextension loads throughsparq_hdt::load; without the feature the CLI exits with a rebuild hint. Off by default (MSRV 1.87 vs the CLI's 1.85 floor + decode-stack weight; rationale incrates/sparq-cli/Cargo.toml). -
Recorded engine/core API seams (audit of the opt-in crates' TODOs) — the cross-crate seams the "modify no existing crate" waves recorded, now implemented:
sparq-core:Graph::load_str_with_base/parse_to_triples_with_base(base-IRI resolution for Turtle/TriG — removes sparq-shacl's oxttl+from_partsworkaround);Dict::iter()(the official dense-1-based-id term iterator sparq-introspect leaned on by hand);Graph::iter_ids/iter_ids_sorted(col)(borrowing canonical-id triple iterator, overlay-merged, zero alloc per row — sparq-shacl's term scans, sparq-sim's distinct subject/object enumeration); regression test pinningGraph::saveon a SPARSE numeric cache (the panic sparq-py recorded was already fixed bydense_numerics; now pinned).sparq-engine:PreparedQuery— parse-once / execute-many entry points (query/ask/count/query_json/construct/describe*_prepared(_with_budget)twins; the string APIs are now thin wrappers), the seam sparq-rsp recorded for per-window evaluation without re-parsing; and#[derive(Debug)]onQueryResult(workspace TODO, recorded by sparq-nlq).sparq-enginecs-plannerfeature (opt-in, non-default) +sparq-introspectcharacteristic_set_ids(): characteristic-set star-join cardinality estimation (Neumann & Moerkotte) injected viacs::CsTable/with_cs_table; the greedy planner scores star candidates by the conditional CS expansion and usesΣ_{C⊇Q} count(C)as the subject-variable ndv (join order only — results identical; zero code in the wasm/default builds). Gate (q-error through the real planner recurrence vs true cardinalities, synthetic correlated stars): PredStat median 1.83 / gmean 7.02 / max 1283 vs CS 1.00 / 1.00 / 1.00.
-
Incremental inference maintenance (
sparq-reason) — counting-based incremental closure maintenance under base inserts AND deletes (deletion costs the same as insertion: exact derivation counts, no overdelete/rederive pass), extending T18's RDFSMaterializedGraphto two new opt-in, zero-wasm-impact siblings:MaterializedOwlGraph(OWL 2 RL): the monotone assertional rules (prp-spo1, cax-sco, prp-dom/rng, prp-inv1/2, prp-symp, prp-eqp1/2, cax-eqc1/2) maintained via a property-orientation closure mirroring the batchPropExpand;prp-trpvia a dedicated exact transitive layer (per-property effective-edge multisets, closure recomputed on touch — cost proportional to the transitive subgraph — and diffed into the counts). TBox mutations / scm-* and the recursive/equality features (sameAs, Functional/InverseFunctional, chains, restrictions, cardinality, hasKey, oneOf, intersection/union) take documented re-materialization fallbacks (OwlModetelemetry +full_rebuilds()).MaterializedN3Graph(Notation3): a counting fast path for rule sets that ANALYSIS proves monotone with input-stratified negation — ground IRI predicates, a verified-parity builtin whitelist (log:uri, log:equalTo/notEqualTo, string:concatenation/scrape/encodeForUri),?UNSCOPED log:notIncludesonly over predicates no rule derives (guard deltas rebuild), recursion via recursive-SCC layers; everything else falls back to the batch engine per mutation. Fallback cases include: out-of-whitelist data reached at evaluation (sticky); baselog:implies-family triples (rules-as-data); and rules whose premise has NO plain join atom (an empty{}premise or a builtin/guard-only premise), since counting seeds emissions only from plain premise atoms and would otherwise drop their conclusions ([OPUS-4.8] reviews 1868/1884). The sparq-solid WAC rules and ACP strata a/b qualify; acp-c does not (variable conclusion predicate) — asserted by test. Differential property tests hold every counting-maintained profile equal to its from-scratch batch closure after every randomized edit batch (RDFS/OWL/N3); rule shapes outside the maintained profile (above) take the always-correct batch fallback rather than being maintained. Benchmarks (bench/inference/incremental-bench.md, olympics scale + a 1k-doc WAC pod): 1-triple deltas maintain in microseconds vs ~1s re-materialization (~10⁵–10⁶x); 10k-triple deltas 16–500x; live WAC ACL edits 11–162 ms vs the 0.84 s engine re-run, with the incremental initial build itself beating the batch engine (anchored premise evaluation). Conformance and the batch paths are untouched.
-
Severity-aware conformance + conformance memo (
sparq-shacl) —ValidationReport::conforms_violations_only()(CI-style gating: warnings and infos report but don't break conformance) andresults_with_severity(iri); the spec'ssh:conformsis untouched and the API stays infallible. Inside the validator,conforms()now memoises(focus, shape) → boolwith a cycle-soundness rule that leaves the recursion guard unchanged (a result is cached only when no guard re-entry escaped below its frame). W3C core suite unchanged at 98/98; pinned by new cyclic-shape and severity tests. -
Neighbor-sparse fallback for
most_similar(sparq-sim) — entities whose signature elements all point at degree-1 neighbors (every event names exactly one sport) generated NO candidates in v1.SimConfig::profile_fallback(default on) fills starved slots with predicate-profile (role) matches, ranked below every exactly-scored result; predicate blocks are scanned most-selective-first with amax(4k, 64)stop. Re-measured on the real olympics eval: Sport precision@10 0.000 (0/0) → 1.000 (400/400), City 0.500 (2/4) → 0.995 (398/400), full 2400/2400 candidate coverage, ~0.3 ms mean latency cost. The per-element IDF hub budget was investigated and is measured-and-rejected (the eval is saturated). -
Streaming store builds, weighted RRF, bytes-backed open (
sparq-vectors) —StreamingWriterremoves the build-phase RAM ceiling (vectors append straight to the.spqvdata section, the id→slot index spills to a sidecar; output byte-identical to the in-RAM builder — no format change; duplicates reported at finalize),fuse_rrf_weighted(Elasticsearch-style per-list weights, unit weights ≡fuse_rrf), andVectorStore::open_from_bytes(filesystem-less environments; validation identical toopen, all read paths shared). No wasm feature wired (deliberate — the crate stays out of the wasm graph); DiskANN-style persistent ANN, quantization and big-endian remain deferred with rationale insparq-vectors/TODO.md. -
Overlay window evaluation, t0, CONSTRUCT/ASK (
sparq-rsp) — closed windows are no longer rebuilt from scratch: the newEvalModeselects the R2R materialisation strategy —PersistentDict(one dictionary per continuous query, terms interned once at push time, per-window graphs from already-interned ids; the new default — wins every benchmark scenario, 1.2–5.3× over the v1 rebuild, biggest on sliding windows),Delta(one live graph + set-semanticapply_deltaper slide with churn-driven compaction; measured slower everywhere, kept opt-in), andRebuild(the v1 baseline, still right for unbounded-vocabulary streams). Plus: RSP-QL window origint0(WindowSpec::with_t0), and continuous CONSTRUCT (ContinuousConstruct— stream-to-stream transformation with exact set-diff ISTREAM/DSTREAM) and ASK (ContinuousAsk) query forms. All three modes are observationally identical (pinned by tests); README throughput table re-measured per mode. Zero wasm-bundle impact (opt-in crate).
-
[OPUS-5] N3 incremental maintenance: the base↔layer ownership transfer no longer re-materializes (
sparq-reason,sq-6tykl.6) — a fact that is both asserted and derivable by a recursive-SCC layer is charged to the base while asserted, so mutating its base copy hands ownership between the base and the layer without changing the closure. The sign-homogeneous delta round could not express that hand-off and recovered with a full (non-sticky) re-materialization. The affected layer's own local fixpoint — recomputed in that round — now decides the hand-off directly: the layer step runs before the counted-rule step, a fact the layer re-derives is put straight back and dropped from the round's delta, and no derivation count is disturbed. The hand-off is settled whenever the affected layer is recomputed, which makes it lazy in the assert direction: retracting the base copy enters the round and normalizes ownership immediately, whereas asserting a fact the closure already holds contributes nothing to the round, so the layer keeps its copy alongside the new base one until a later delta recomputes that layer. The interim double ownership is observationally inert — while the base copy exists the layer's entry can never suppress a seed, and nothing consults it without the base guard.full_rebuilds()no longer increments for these deltas. Behaviour-neutral for the closure (the from-scratch differential oracle intests/incremental_n3_prop.rsis unchanged and green); the TBox and guard-predicate full-rebuild fallbacks are untouched and remain documented. -
[SONNET-4.6] N3 rule-existential blank labels now use a source-fresh numbered namespace, so closure output may mint labels such as
_:__sk0_1_einstead of the previous_:__sk1_eshape. -
[GPT-5.6] Breaking (
@sparq-org/sparq) —SparqStore.queryBindings(sparql, context?)now returnsPromise<ResultStream<Bindings>>instead ofBindings[]; await it and consumedata/end/errorevents (or usequery()for synchronous materialisation). Unsupportedcontext.sourcesoverrides, including an empty array, now reject instead of being ignored. -
Pipelined streaming N-Triples ingest (
sparq-core) —Graph::load_reader_parallelnow fills its 32 MiB block from repeated shortread()s and overlaps decompression with parallel parse (producer thread → bounded channel → rayon parse → dict merge), matching thebuild_external_ntriples_parallelpipeline. Streaming ingest of a gzip/zstd N-Triples file is now far faster than the pre-fix per-read()flush and matches or beats decompress-then-parse (figures:research/custom-parsers-baseline.md).load_reader_parallelrequiresR: Read + Send. -
Inference fixpoint linearization + delta-driven evaluation (
sparq-reason, fixpoint-opt thread) — the chain-transitivity derivation storms are gone, and closure-only callers stop paying for proof bookkeeping:- OWL-RL
prp-trpis evaluated as the LINEAR ruleR(x,y), GEN(y,z) ⊢ R(x,z)over generator edges (edges not derived by prp-trp itself;TC(GEN) = TC(R)), with the per-round schema rebuild andprp-fp/prp-ifpmoved into the delta sweep — owl-benchowl-transitive(2k-edge chain, ~2M-pair closure): 54 s → 0.25 s. - The N3 forward chainer detects transitivity-shaped rules
(
{?x P ?y. ?y P ?z} => {?x P ?z}) and runs the same linearization, bypassing the generic binding machinery — anc500 (500-link:ancestorchain): 52 s → 0.18 s engine-internal (closure byte-identical, 125,250). run_closuretakes aStepMode: premise materialization and proof-step interning are skipped when the caller discards them (reason_n3, the CLI closure path; conformance keeps conclusions only) — grid30 closure 1.94 s → 1.02 s (−47%), dt100k −18% (engine-internal min-of-5, identical closures). Inference conformance unchanged (1637 pass / 0 fail / 17 documented divergences); wasm artifact size unchanged; refreshed EYE head-to-head inbench/inference/eye-comparison.md.
- OWL-RL
-
sparq-geodepends on geo 0.33 (was 0.30; clean API compatibility): brings theBuffertrait that makesgeof:bufferimplementable. -
GeoIndex::entries()returns an iterator (was a slice): entry slots are tombstoned/reused by incremental deletes. -
OWL 2 RL rule completion (
sparq-reason) — the remaining OWL 2 RL/RDF rules (Profiles §4.3):cls-oo(oneOf member typing),cls-maxqc1/2(qualified-cardinality-0 clashes ininconsistencies()), the schema-level restriction-subsumption familyscm-hv/scm-svf1/scm-svf2/scm-avf1/scm-avf2(indexed/guarded joins grouped by onProperty resp. filler — never the naive quadratic restriction×restriction pass), and the premise-freecls-thing/cls-nothing1axioms (occurrence-guarded). The rule-coverage table inresearch/inference-completeness-audit.mdis now fully implemented or explicitly by-design (prp-ap, eq-ref, dt-, reflexive scm- — each argued). Conformance scores and closure throughput unchanged; zero wasm-bundle impact. -
EXPLAIN / EXPLAIN ANALYZE (
sparq-engine,sparq-server, T22) — query-plan introspection: engineexplain()renders the algebra tree plus a planning-only dry run of the BGP planner (greedy GOO join order with cardinality estimates, per-step join strategy — merge/hash/bind/worst-case-optimal — and pushed-down filters) by replaying the executor's own planning helpers;explain_analyze()(SELECT/ASK) executes under a thread-local per-operator trace reporting output rows + wall time per operator (zero-cost when off: one flag read per operator entry). Served on/sparqlvia?explain=/explain=analyzeorAccept: text/x-sparq-explain. -
Prometheus metrics (
sparq-server, T22) — hand-rolled text exposition atGET /metrics(no new dependencies): request counts by endpoint/status, a/sparqllatency histogram, active-subscription and graph-triple gauges, an update counter. -
SPARQL CONSTRUCT / DESCRIBE (
sparq-engine,sparq-server) — RDF-graph query results: spec-conformant CONSTRUCT template instantiation (unbound/illegal slots skipped, fresh blank nodes per solution, set-deduplicated) and DESCRIBE as concise bounded description; engine APIconstruct/describe/construct_ntriples(+_with_budgetvariants); served over HTTP withapplication/n-triples/text/turtlecontent negotiation under the existing query budget. W3C construct evaluation suites pass 9/9 run (1 skip:FROM). -
Streamed SELECT results (
sparq-server) — JSON SELECT bodies are streamed from an ordered chunk sequence (enginequery_json_chunks_with_budget; concatenation byte-identical, sameContent-Length) instead of one giant string: peak server RSS for a 1M-row SELECT (202 MB body) drops from ~750 MB to ~405–570 MB.
-
Persisted per-predicate stats never loaded on
open(sparq-core) —load_pred_statsread the predicate id as 8 bytes whilesave_pred_statswrites a 4-byteId, mis-framing every record: the load always failed andopen()silently fell back to recomputing the stats, which paged the ENTIRE POS+PSO permutations into RAM (the dominant out-of-core open cost the persisted file exists to avoid). Measured on the 10M-triple synthetic dir: open 0.52 s → 0.019 s, RSS after open 236 MB → 2.3 MB (research/memory-tiering.md). Regression-tested (pred_stats_load_is_some_and_exact). -
N3 incremental maintenance: base↔layer ownership transfer (
sparq-reason) — deleting a fact that is both asserted and derivable by a recursive-SCC layer made the fact re-enter the layer's derived set during a delete round (it stops seeding the local fixpoint), tripping the sign-homogeneitydebug_assert(panic in debug builds, a needless sticky engine fallback in release). Now recovered with a non-sticky full re-materialization; the graph stays on the counting fast path. Found by theexplainretraction tests; regression-tested against the from-scratch oracle. -
Domain/range typing for TBox-orphan properties on the monotone OWL route (
sparq-reason) — on the batch single-pass path (rdfs_closurewith the property-orientation closure active, i.e. anyowl:inverseOf/owl:SymmetricPropertyaxiom present), a property with anrdfs:domain/rdfs:rangedeclaration but no subPropertyOf/inverseOf/symmetric/equivalentProperty edge emitted NO rdfs2/rdfs3 typing for its assertions (absent from the orientation map, the emission short-circuited) — diverging from the full fixpoint path. Such properties now fall through to the plain-RDFS emission (their expansion is the trivial identity). Regression-tested; conformance unchanged (1637/0/17 — no suite test hits the combination, which is how it survived).
Measured against native QLever on the same machine, compute-only, cold, min-of-N. The
methodology, the full per-query tables, and the speedup ranges are single-sourced in
bench/qlever-baselines.md (run bench/qlever-olympics/ / bench/qlever-synthetic/ to
reproduce):
- Synthetic 10M and 100M join/OPTIONAL queries: faster than QLever, in-memory and out-of-core (mmap) alike; the advantage holds from 10M to 100M.
- Real skewed data (DBpedia/Wallscope olympics, 1.78M triples): faster on every query.
- Honest caveats: QLever's on-disk index is compressed (smaller disk footprint at billion-scale); billion-scale real data (Wikidata/WDBench) is untested on this hardware; single-pattern/FILTER comparisons short-circuit via index range-counting and are excluded from the claims above.