… the Composer client-fold + two N+1s
persist v17.5.0 (edge v13.1.0 lockstep) ships resolve_scores/list_scores
(FSD-005 Appendix C, CIRISPersist#455/#456) — the composed
(subject, dimension, trust, time, state) -> verdict fold now runs
substrate-side as a composite op (the #329 pattern). This retires the
fetch-whole-history-then-fold-in-Rust pattern at both demand-survey sites.
Site 1 — compose_policy.rs::compose_for_key:
before: list_attestations_for(subject) (whole signed history) + an
in-Rust fold + an N+1 has_effective_role pair per licensure attester
IN THAT HISTORY.
after: ONE resolve_scores(caller, filter, policy, trace) per the issue.
The pin-once AttestationFilter carries {subject, dimension_exact,
valid_at: now, lifecycle: Live, attester_filter: Explicit(pinned)};
trust gate + staleness + CEG §6.1 precedence + latest-wins +
aggregation all run in persist's .so. Verdict (ConfidenceBand + n's +
open trace) mapped into the existing Composition return type. The
licensure N+1 collapses to co-steward resolution over the fold's HEAD
attesters only (bounded by contributor_count).
kept: the pure Composer::compose (CC 4.4 normative surface, adversarial
tests untouched); TrustSet::pin_co_steward; CC 3.4.9 licensure cap +
CC 3.4.5 self-emission re-check (client-side, per CC 3.4.7).
fidelity deltas (doc'd on compose_for_key): signature gained `dimension`
(verdict is per (subject,dimension), always one Verdict); persist owns
value/band; CC 4.4.1 weighting + CC 4.4.3.9 Policy D + the
Detector/Enumerated/PositiveOnly polarities not yet substrate-side
(flagged); no refusal ledger (gated rows excluded server-side).
Site 2 — graph_config.rs:
before: list_attestations_by(node) filtered client-side to SCORES &&
config:v1, then a full list_attestations_by re-scan + revocations_for
probe PER row (config_key_revoked) — the O(N^2) revocation N+1.
after: ONE cursor-paged list_scores({attester:node, subject:node,
type:scores, dimension_exact: config:v1, lifecycle: Live}) seek;
retracted rows excluded server-side. config_key_revoked deleted.
Wire behavior (get/set, version chain, latest-wins, Null tombstones,
revoke-reads-as-absent) preserved; recant targeting now speaks the
CEG §6.1 canonical references_attestation_id (test updated).
Pins: ciris-persist v17.3.0->v17.5.0, ciris-edge v13.0.9->v13.1.0 (root +
crates/ciris-lens-core); release-gate TARGET_PERSIST/TARGET_EDGE bumped.
Closes #267
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MT2o477HcDTJJmY8GAystF
persist v17.4.0 shipped
resolve_scores/list_scores(FSD-005 Appendix C) — the composed(subject, dimension, trust, time, state) → verdictfold now executes substrate-side as a composite op (the #329 pattern, CIRISPersist#455/#456 executor notes honored). This PR adopts both handles, deletes the Composer client-fold's substrate reads, and removes the two N+1s the 2026-07-14 demand survey flagged.Pins
ciris-persistv17.3.0 → v17.5.0 andciris-edgev13.0.9 → v13.1.0 in lockstep (edge v13.1.0 is the tag that re-pins persist v17.5.0; edge and server share persist types across theEngine/SqliteBackendseam, so the graph must unify on ONE persist). Applied to the root manifest (sqlite + postgres + dev-deps) andcrates/ciris-lens-core. Verify stays v10.2.0 (matches edge v13.1.0's own pin).Site 1 —
src/compose_policy.rs::compose_for_keyBefore:
list_attestations_for(subject)fetched the WHOLE signed history; the fold (type==SCORES, trust set, staleness, group-by-(dim,subject), aggregation, licensure cap) ran client-side — plus an N+1: twohas_effective_rolecalls perlicensure:*attester in that history.After: ONE
resolve_scores(caller, filter, policy, trace=true)per the issue. The pin-onceAttestationFiltercarries{subject_key_id, dimension_exact, valid_at: now, lifecycle: Live, attester_filter: Explicit(pinned trust set)}; the policy iscc-4.4.2-signed-mean/cc-4.4.2-boolean-minfrom the dimension's CC 3.1 polarity column. Trust gating, staleness, CEG §6.1 retraction precedence, per-attester latest-wins, and the aggregation all run inside persist's.so; the verdict comes back as aConfidenceBand+ n's + the open trace, and is mapped into the existing publicCompositionreturn type.N+1 removed: co-steward resolution now runs over the fold's head attesters only (bounded by
contributor_count, never by history size) — and only forlicensure:*dimensions.Kept intact (per the issue's constraints):
Composer::compose(&[Attestation])— the CC 4.4 normative surfacetests/compose_policy.rsdrives adversarially — is untouched, as are all its tests;TrustSet::pin_co_stewardremains (consumer pins resolve first, substratehas_effective_rolesecond);compose_for_keystill returnsComposition(no callers existed in-tree; grep confirmed).Composition-fidelity deltas (documented in the doc comment)
dimension— persist's verdict is per(subject, dimension); the returnedCompositioncarries exactly oneVerdict, ALWAYS (an empty fold is now a visibleDecision::Undeterminedverdict rather than an absent one).valueand the band:Verdict::valueis the fold's traceaggregate;InsufficientWitnesses→Undetermined, otherwise the consumerthresholddecides over the aggregate as before.weight = 1.0; persist's fold marks the server-tier column-resolver refinement as its own TODO.Detector/Enumerated/PositiveOnlydimensions currently fold under the signed-mean default (flagged;Verdict::polaritystill reports the CC 3.1 column).Composition::refusalsnow only carries the CC 3.4.5 re-check below.single_source_licensure = true; the persist-ownedvalue/band are NOT re-capped.capacity:*head that somehow passed the substrate gate is surfaced as aSelfEmissionrefusal and the verdict fails closed toUndetermined.slashing:*on testimonial-onlyevidence_refs) needs the raw corpus; it runs at admission + in the pure compose tier only (flagged).Site 2 —
src/graph_config.rsBefore:
list_attestations_by(node)(every row, every type) filtered client-side toSCORES && dimension == config:v1, then per surviving row a second fulllist_attestations_by(node)walk + arevocations_forprobe (config_key_revoked) — the O(N²) revocation re-scan N+1.After: ONE cursor-paged
list_scores("", {attesting_key_id: node, subject_key_id: node, attestation_type: scores, dimension_exact: config:v1, lifecycle: Live}, cursor, 512)seek over the V106 projection. Withdrawn/superseded/recanted rows are excluded server-side;config_key_revokedis deleted. The filter is built once and reused across pages (pin-once, Appendix C.4;#[non_exhaustive]-safe construction).Wire behavior preserved: get/set semantics, version chaining, latest-wins fold,
ConfigValue::Nulltombstones, and revoke-reads-as-absent are unchanged (the per-keyversionfold is config semantics and deliberately stays client-side). One convention note: retraction targeting now speaks the CEG §6.1 canonical envelope memberreferences_attestation_id(which persist's lifecycle fold honors) instead ofconfig_key_revoked's ad-hocattested_key_id/subject_key_idsmatching — nothing in production ever emitted the ad-hoc shapes;tests/graph_config.rs::recant_rowwas moved to the canonical member and the recant-reads-as-absent pin still passes.Gates
cargo build --release -j 1— clean (16m, 0 warnings)cargo test --release -j 1— 373 passed, 0 failed, 13 ignored across 41 test binaries (incl. thecompose_policy,graph_config, andrelease_gatessuites)cargo clippy --all-targets -j 1— no new warningscargo fmt --all— appliedThe one release-gate that asserts the substrate pin (
gate1_substrate_pins_at_target) was bumped to v17.5.0 / v13.1.0 alongside the manifest.Closes #267
🤖 Generated with Claude Code
https://claude.ai/code/session_01MT2o477HcDTJJmY8GAystF