Version. 0.1
Date. 2026-07-28
Status. Change request draft. Awaits Operator review, then Claude Code execution in a fresh session.
Author. Claude.ai (drafting layer). Operator: Marvin Percival.
CR number. CR-2026-155, assigned from the current sequence in loomworks-record at drafting time. Highest present at assignment: CR-2026-154, verified by Claude Code repo-wide search on 2026-07-28; no four-digit variants. Per PF-2, the executing session re-checks the sequence before any commit carries this number.
Companion documents. loomworks-graph-stage-2-cr-scoping-note-v0_1 (scoping note, filed at scoping-notes/, commit bbcaa9e), loomworks-graph-stage-2-step-0-inspection-brief-v0_1 and loomworks-graph-stage-2-step-0-inspection-report-v0_1 (both at inspection-briefs/).
Provenance. Executes Stage 2 of loomworks-graph-findings-implementation-strategy-v0_1. Every live-code claim in this draft is grounded in the step-0 inspection report, executed by Claude Code against loomworks-engine main at 355fe7a (the CR-2026-154 merge) on 2026-07-28. Nothing here is asserted from memory of prior chats.
Stage 2 is the forward walk: given an assertion, find what was built from it. This change request delivers two items. Item 2a adds an indexed query and endpoint — GET /engagements/{eid}/assertions/{aid}/dependents — returning every shape event in the engagement whose selected_memory_refs contains the given assertion, at any pinned version, with the matched version surfaced per result. One migration: a GIN index on shape_events_view.selected_memory_refs. Item 2b makes the supersede (revise) and retract responses carry a dependents count, so the Operator learns what was built from an assertion at the moment of the decision. Nothing is blocked, invalidated, or changed automatically — the signal is a signal, per the Operator-authority principle.
One correction from step-0 is carried into this draft: the scoping note asked step-0 to confirm selected_memory_refs never crosses engagement boundaries. It does cross — universally, but only for the seed element, and never for an assertion — so item 2a's design stands unchanged. Section 3 states the correction and the reasoning in full.
Decision needed from the Operator: none beyond approving the draft. The two decisions the report left to the draft (subclass count for 2b; honest index sizing) are settled in §5 and §7 with reasoning.
In scope: the 2a query, index, and endpoint; the 2b response field on revise and retract. Out of scope: Stages 3 and 4, relationship-vocabulary edge walking, render-event forward walk, needs_you dashboard integration, any automatic action on dependents, supersession severity.
Per step-0 report §A, all figures as of 2026-07-28:
main at 355fe7a (CR-2026-154 merge, tagged graph-hygiene-v0_1), working tree clean.0101. This CR's migration takes the next revision in sequence.355fe7a, not re-measured): 3408 passed / 68 skipped / 1 failed. The one failure is tests/test_stele_router_mount.py::test_stele_router_mounts_and_begin_resolves_end_to_end, waived per its own front note, unrelated to any Stage 2 surface. The gate for this CR is no new failures against that baseline; the executing session re-runs the suite at pre-flight to establish the measured baseline before any change.Relationship objects projected into current_memory_objects; Stage 2's field lives on shape_events_view, a separate projection table with a separate projector path. No shared write path, no shared table. Stage 1 does not gate this work.selected_memory_refs. No relationship-vocabulary edges, no render-event forward walk.id, matched version surfaced per result. The scoping note §2 recommended this; step-0 confirmed it is implementable exactly as written: all 436 live refs are version-pinned (report §B.8), the element shape is uniformly two keys (id, version; report §B.6), and @> '[{"id": …}]' matches regardless of version with default jsonb_ops — no generated column, no expression index, no jsonb_path_ops variant (report §C.12). The alternative — version-exact match — was set aside at scoping because the case this work serves is "an old version was just superseded; who selected it?", which an exact-match-only query answers but an any-version query answers more completely, with the pinned version available to distinguish.
Prior position (scoping note §2, superseded). "Step-0 confirms selected_memory_refs never crosses engagement boundaries — if it can, that is a design question for the change-request draft."
Corrected position (report §E, current). The field crosses engagement boundaries in 100% of shape events. The crossing is fully characterised: every crossing ref is a seed; every crossing ref is element 0 of the array (the shaping paths seed the list with manifestation.seed_ref before appending assertion refs — agents/shaping.py:672, engagement/shaping_skill.py:356); every crossing ref points at the administrative engagement (ADMINISTRATIVE_ENGAGEMENT_ID, engagement/bootstrap.py:26), where seeds live by design. Of 436 live refs: 33 crossing seed refs, 403 same-engagement assertion refs, zero unresolvable.
Why the correction does not change 2a's design. The two claims are distinct and must not be conflated:
selected_memory_refs crosses engagement boundaries" — true, universally, for the seed element.An engagement-scoped dependents query is therefore correct for assertions and stays scoped, consistent with every other engagement-scoped read in the engine. It would silently become wrong only if 2a were ever extended to seeds — which is guarded by a comment at the query (§4, requirement 2a-R4), not a design change.
Second finding folded in (report §B.7). The shaping agent is not the field's only writer. credit/proposal_applier.py:372 writes selected_memory_refs from the Accounting reconciliation path, referencing a proposal, not an assertion. The field is genuinely heterogeneous — seeds, assertions, proposals. The implementation consequence is the same as above: key on the caller-supplied assertion id; never enumerate refs and assume their type.
selected_memory_refs: jsonb NOT NULL on shape_events_view — a projector-maintained projection table (migration 0018), not a SQL view; an index on it is an ordinary table index with no refresh semantics.{"id": "<uuid>", "version": <int>}. Exactly two keys across the whole corpus.ix_shape_events_view_produced_content (0018:118–125), naming convention ix_shape_events_view_<column>. (The scoping note cited Phase 10's render_events_view.render_content; the same-table precedent is closer and governs naming.)
2a-R1 — Migration: GIN index. One Alembic migration, next revision after 0101, creating:
op.create_index(
"ix_shape_events_view_selected_refs",
"shape_events_view",
["selected_memory_refs"],
postgresql_using="gin",
)
Default jsonb_ops, matching both existing GIN indexes on these projection tables. Downgrade drops the index. No new table, no new event kind, no payload change, no data migration.
Honest sizing (report §C.11, correction 4). At 33 live shape events the sequential scan is trivially fast; the index buys nothing measurable today. Its value is preventing a scan that grows linearly with shape events per engagement. The migration docstring and this CR claim it exactly that way — growth prevention, not a present-day performance fix.
2a-R2 — Query. Containment query keyed on the caller-supplied assertion id:
... WHERE engagement_id = :eid
AND selected_memory_refs @> jsonb_build_array(jsonb_build_object('id', :aid))
(Executing session derives the exact SQLAlchemy form from the live query conventions in the shape-events read path; the predicate shape above is what step-0 verified against live data at §C.11–C.12.) Per-result, the matched pinned version is extracted from the ref whose id equals the queried assertion id — never from positional assumptions, per §3's heterogeneity finding.
2a-R3 — Endpoint. GET /engagements/{eid}/assertions/{aid}/dependents, engagement-scoped, same auth posture as the existing assertion GET on the same router (api/routers/assertions.py). Response: a list built on the existing shape-event response schema used by the read-side serializers in api/routers/shape_events.py, extended additively (subclass, per the CR-2026-154 item 1c pattern) with one field per result: matched_version: int — the pinned version of the queried assertion in that shape event's refs. The executing session derives the base schema name and field set from the live serializer, not from this draft.
2a-R4 — Scope-guard comment. At the query site, a comment stating: the engagement-scoped predicate is correct because assertion refs never cross engagement boundaries (step-0 report §E.18); if this query is ever extended to seed refs, the scoping assumption breaks — seeds live in the administrative engagement — and the extension must revisit the scoping, not widen the predicate in place.
2a-R5 — Ref-heterogeneity discipline. The implementation keys on the supplied assertion id and treats matches by containment only. It never enumerates selected_memory_refs assuming element type or position (element 0 is a cross-engagement seed in every live shape event; proposals also appear via proposal_applier).
2a-R6 — FORAY. No new attestation. The dependents query is a read; it performs no recordable action. revise and retract attest at the substrate today and are unchanged by 2b's additive field.
revise — POST /engagements/{eid}/assertions/{aid}/revise (revise_assertion_route, api/routers/assertions.py:404). There is no distinct supersede terminal state for assertions; the prior version stays in history, the revision becomes current, held and awaiting commit. (amended_superseded exists as a state on shape events, not assertions.) The scoping note was right not to assume otherwise.POST /engagements/{eid}/assertions/{aid}/retract (retract_assertion_route, assertions.py:434), moves the assertion to retracted, requires a rationale, preserves history.AssertionResponse, which is shared across six routes (commit, discard, revise, retract, redirect, GET). A field added directly to it would land on four routes where it is meaningless and would have to be computed or stubbed on every call.AddAssertionResponse(AssertionResponse) — a purely additive subclass, with the route's response_model re-pointed at it.
2b-R1 — One shared subclass. The report left subclass count (one shared, or one each for revise/retract) to this draft. Settled: one shared subclass, e.g. AssertionWithDependentsResponse(AssertionResponse), used by both routes. Reasoning: both routes want the identical field with identical semantics, computed the same way; two subclasses would duplicate a one-field schema with no divergence to justify it. Set aside: per-route subclasses — the right shape only if the two routes' dependents semantics ever diverge, which nothing on the horizon suggests. Exact class name aligns with live schema naming conventions at execution time.
2b-R2 — The field. dependents_count: int — the number of shape events returned by 2a's query keyed on the assertion id being revised or retracted, computed via the same query path (no second implementation). Always present on the two routes, including when zero. Count only, not an inline list: the 2a endpoint is the list; duplicating it inline couples the response to a second schema for no reader who cannot make the follow-up call. Set aside: inline list (duplicates 2a; grows the response unboundedly), and a URL pointer field (derivable from the route shape; adds nothing).
2b-R3 — Semantics unchanged. No block, no warning dialog, no required confirmation, no terminal-state change. The Operator revises or retracts exactly as today; the response says more.
2b-R4 — Four routes untouched. commit, discard, redirect, and the plain GET keep AssertionResponse unchanged, byte-for-byte at the schema level.
wasDerivedFrom, wasConsideredAndRejected, and siblings).render_events_view if wanted, not an extension of 2a.needs_you dashboard integration (Phase 39 pattern) — real, separable, a small follow-on CR against 2b's field if the Operator wants it.Floor (net new, exact count recorded as-shipped in implementation notes):
ix_shape_events_view_selected_refs, downgrade drops it.matched_version.revise and retract responses carry dependents_count, correct when >0 and when 0.commit, discard, redirect, GET response schemas unchanged.All tests on synthetic per-verification engagements only — never E0060, E0007, E0030, or the shared E0006 E2E fixture. Gate: no new failures against the pre-flight measured baseline; the Stele router-mount failure remains waived per its own front note.
ix_shape_events_view_selected_refs exists with GIN/jsonb_ops on shape_events_view.selected_memory_refs; query plan for the id-only containment predicate uses it once row counts warrant (correctness gate is the predicate's results, per test 2 — the plan flip is growth-dependent and not gated).GET /engagements/{eid}/assertions/{aid}/dependents returns correct results per tests 2–4.revise and retract return the shared subclass with dependents_count; the other four AssertionResponse routes unchanged.
Standard loop. Fresh Claude Code session. Pre-flight: re-verify main at or beyond 355fe7a with clean tree; re-run the suite to convert the carried baseline (§1) into a measured one; re-check the CR sequence in loomworks-record — if anything above CR-2026-154 has landed since drafting, halt and report before any commit carries this number. Branch from main. Stage by explicit path, never git add -A. Halt before every push; report paths and SHA. If any live fact diverges from this draft's grounding (route line numbers, schema names, index inventory), halt-surface per the substrate-friction discipline rather than absorbing in flight.
v0.1 (2026-07-28). First version. No prior Stage 2 CR. Carries the step-0 report's §E correction to the scoping note's §2 cross-engagement premise (stated with prior position preserved at §3) and settles the report's three draft-level items: subclass count (§5, 2b-R1: one shared), ref heterogeneity at the query (§4, 2a-R5), honest index sizing (§4, 2a-R1).
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Graph Stage 2 (Forward Walk / Dependents) — Change Request — CR-2026-155 — v0.1 — 2026-07-28