Version. 0.1
Date. 2026-07-27
Status. Draft for Operator review, then Claude Code execution.
CR number. CR-2026-154, assigned 2026-07-27 from the current sequence in loomworks-record (CC read-only check: highest present is CR-2026-153, change-requests/cr-2026-153-operator-tools-menu-v0_1.md; working-tree scope, filenames and contents). Prior position, preserved: the draft first carried CR-2026-146 provisionally, inferred from CR-2026-145 in the inspection report (E.19, path 7) — the record check superseded it. Pre-flight check PF-2 re-verifies at execution time in case the sequence advances between filing and execution.
Consumer. Claude Code on DUNIN7-M4, executing against /Users/dunin7/loomworks-engine.
Provenance. Stage 1 of loomworks-graph-findings-implementation-strategy-v0_1 (loomworks-record, strategies/). Scoped by loomworks-graph-hygiene-cr-scoping-note-v0_1. Grounded by loomworks-graph-hygiene-step-0-inspection-report-v0_1 (loomworks-record, inspection-briefs/, main 1e79120) — the report is authoritative for all live-code facts in this CR; where this CR and the live tree disagree at execution time, halt and surface. Drafting handoff: loomworks-graph-hygiene-cr-drafting-handoff-v0_1 (decisions D-1 through D-5).
One change request, four items, no split. 1a: redirect writes a wasDerivedFrom edge from the target assertion to the retracted source, owned by the target engagement, plus an idempotent backfill (a no-op in dev, which holds zero live redirects). 1b: the fourteen-value relationship vocabulary moves to one definition site in memory/base.py; preventive, not repair — no drift has occurred. 1c: POST /assertions accepts an optional alternatives list; each alternative enters as an ordinary held assertion and receives a wasConsideredAndRejected edge from the primary. 1d: closes as verification-only — every Companion as-current read path already filters correctly — plus tests and one small hardening (a named as-current constant). No migration. The suite gate is "no new failures," with one pre-existing Stele router-mount failure explicitly waived and filed to the Stele front. The Operator's next actions after review are listed at the end of the companion filing set, and the CC kickoff prompt is §9.
loomworks-graph-findings-implementation-strategy-v0_1 (orientation; Stage 1 scope).loomworks-graph-hygiene-cr-scoping-note-v0_1 (items 1a–1d, boundaries, out-of-scope §7).loomworks-graph-hygiene-step-0-inspection-report-v0_1, executed 2026-07-27 against main 79262c5. Authoritative for live-code facts. Where the report corrects the scoping note, the report wins; the corrections are preserved in §5 below, not smoothed.loomworks-graph-hygiene-cr-drafting-handoff-v0_1 (settled decisions D-1 through D-5, carried into §3).
| Fact | Value at inspection | Source |
|---|---|---|
| Branch / HEAD | main at 79262c5 | report A.1 |
| Working tree | one modified file, uv.lock — pre-existing, not step-0's; do not touch, do not stage | report A.1 |
| Suite | 3379 passed / 68 skipped / 1 failed | report A.2 |
| Known failure | tests/test_stele_router_mount.py::test_stele_router_mounts_and_begin_resolves_end_to_end (401 where 200 expected; deterministic; pre-existing; outside every 1a–1d surface) | report A.2 |
| Alembic head | 0101, single head | report A.3 |
| Queue position | Stele contract phase and CR-2026-098 both landed; not blockers | report A.4 |
| Dev redirects | zero live; one orphaned projection row on hard-deleted engagements | report B.10 |
| Dev relate anomalies | 5 unresolvable-target, 4 retracted-target relationships | report B.8 |
The acceptance gate for this CR is: no new failures relative to the execution-time baseline; the known Stele router-mount failure unchanged. The 3379 / 68 / 1 figures above are the inspection-time reference; the execution-time re-baseline (PF-1) supersedes them. The failure is not absorbed here — it is filed to the Stele front (loomworks-stele-front-note-router-mount-test-failure-v0_1). If the failure changes character during this build (starts passing, or fails differently), halt and surface: that is signal about the Stele path, not about this CR.
git status --porcelain, full-suite counts, alembic heads at execution time. The inspection baseline (§2.1) is expected to have drifted; that is fine. Halt only if: suite has failures other than the known router-mount one; alembic has multiple heads; or the tree carries modifications beyond uv.lock.loomworks-record (filenames and contents, working tree) at execution time. If ≥ 154, renumber this CR to highest+1 — filename, title, header, all three — before archiving, and report the renumber to the Operator in the build summary.docs/phase-crs/, commit on the build branch.redirect.py retract_assertion call still ends the try block near line 360 with the orphan-cleanup except following; (b) add_relationship signature at engagement/assertions.py:610 still matches report B.7; (c) wasderivedfrom_backfill.py still exists with the idempotency check at lines 51–66; (d) the four vocabulary sites of report C.12 still exist at their paths; (e) AddAssertionRequest at api/schemas.py:2448 still carries content / grammar_element / normative_force. Line numbers may drift; structure must hold. Structural divergence halts.cr-2026-154-graph-hygiene from main HEAD.None. Report finding F-confirms-9: nothing in sections A–E surfaced a migration need. If one appears during the build, halt and surface — do not absorb (scoping note §6).
Settled by the Operator (D-1 through D-5, drafting handoff) or by this draft where the handoff delegated the choice. Each names the alternative set aside.
CD-1 — The target engagement owns the redirect edge (D-1). The edge is written with engagement_id = target_engagement_id. Rationale: the target is the living assertion; provenance walks start there. Consequence, stated plainly: because MemoryRef carries no engagement id and relationship reads filter on engagement_id (report B.8a), the edge is visible only from the target side. The redirect_from_* metadata therefore stays load-bearing for source-side traversal, not merely historical — this corrects the scoping note §2's "historical mechanism" framing. The one-edge-one-end asymmetry is filed as a provenance-threads reframe finding, not fixed here. Alternative set aside: source-engagement ownership (inverts the asymmetry; provenance walks would then start at the dead end).
CD-2 — Qualifier key is relationship_kind (draft decision, per report B.9). The edge qualifier is {"relationship_kind": "assertion_redirect"}. The scoping note's illustrative {"derivation": "redirect"} matched no existing convention; relationship_kind is the established discriminator for "which kind of wasDerivedFrom is this" (wasderivedfrom_backfill.py:102), introduced for exactly this coexistence problem. "assertion_redirect" over bare "redirect": it sits beside "engagement_derived_from_founding_seed" in the same discriminator space and names the object kind. Alternative set aside: inventing a third key (rejected — report B.9 recommendation followed).
CD-3 — Insertion point is redirect.py:361 (report B.6): immediately after the retract_assertion await, still inside the try block — inside the orphan-cleanup guard and the request transaction. The write-time position between lines 349 and 354 produces the same stored object but reads worse; set aside.
CD-4 — Backfill is a new library-level module modeled on wasderivedfrom_backfill.py (D-4). Not the Phase 28 HTTP scripts — the scoping note's reference to those was wrong (it named an amendment script), and all three fail at import on the deleted loomworks.stele.session module (report B.11; that repair is not this CR's job). The new module:
src/loomworks/engagement/redirect_edge_backfill.py;memory_events joined to engagements, never from current_memory_objects alone — a naive projection sweep would hit hard-deleted engagements and raise EngagementNotFound (report B.10, blocks-item 3);wasderivedfrom_backfill.py:51–66 pattern: skip where an edge with the same vocabulary + qualifier->>'relationship_kind' + source id already exists;Verification is test-level (§6, T-1a-3): synthetic engagement pair, redirect-shaped metadata without the edge, run, assert the edge. The dev run is recorded in the build summary as a no-op — dev holds zero live redirects (report B.10) — and cannot serve as functional evidence; the test does that. Alternative set aside: repairing and reusing the Phase 28 HTTP scripts (heavier, session-minting, and the wrong layer for something that must be atomic with the projection).
CD-5 — Vocabulary single site is memory/base.py (draft decision, per report C.14). A RelationshipVocabulary Literal alias beside ActorKind (memory/base.py:17–23), which is the exact precedent — same module, same motivation, same "single source of truth" comment. Dependency direction already works: engagement/types.py and api/schemas.py both import from memory/base.py today. The four sites of report C.12 become importers. Alternative set aside: engagement/types.py (works, but heavier module and puts a shared vocabulary behind an engagement-layer import).
CD-6 — The Phase 11 guard test is rewritten, not retired (draft decision, delegated by handoff). tests/test_vocabulary_extension_phase_11.py:44–70 currently asserts the four sites agree — which is why drift never fired (report C.13). After the refactor it becomes trivially true but still catches re-duplication, so: rewrite it to assert that all four consumption sites resolve at runtime to the canonical RelationshipVocabulary args (regression guard against re-divergence), and add one new test asserting the definition site is unique (a grep-level or import-level assertion that no other module defines the fourteen-value Literal). The scoping note's proposed "one new test asserting the sites agree" is superseded — that test has existed since Phase 11; 1b is preventive, not repair, and the build summary records it as such.
CD-7 — 1c extends AddAssertionRequest, API-only (report D.15 recommendation; API-only per scoping note §4 and handoff). AddAssertionRequest gains an optional alternatives: list[AlternativeItem] field; AlternativeItem carries content (required) and grammar_element (default "definition"), mirroring the primary. The route fans out: primary as today, then one add_assertion per alternative, then one add_relationship per alternative, all in the one request transaction (api/deps.py:38–55; in-tree precedent for add-plus-edge in one transaction: boundary.py:247–268, report D.16/D.18). Deliberately out of scope for this version, named so the coverage gap is visible: POST /contributions (ContributionRequest carries no grammar_element; extending it would give alternatives a field the primary cannot express — report D.15), and both Companion-mediated paths (add_knowledge, remember_about_me). Those surfaces can adopt the attach point later without rework.
CD-8 — Alternatives are ordinary held assertions, visible in the held tray (D-2). Each alternative is marked in metadata with {"alternative_of": "<primary assertion object_id as string>"} — flat snake_case per the qualifier/metadata conventions of report B.9. They consume display numbers, appear in the held tray, and commit/discard behave normally (report D.16.1). No held-tray special case. The split rule does not fire on this reading (report D.18 caveat 2 — the split condition was Operator-elected hiding, which the Operator declined by settling D-2).
CD-9 — The rejection edge. wasConsideredAndRejected, source = primary, target = alternative, both refs current-pointing (version=None), engagement_id = the contribution's engagement, qualifier {} — no discriminator needed while this is the sole producer. The edge persists regardless of eventual states; the protocol's semantics are about what was considered, not what survived (Loom Protocol v0.1 §3; held→held edges are permitted with 8 live precedents, report D.17).
CD-10 — 1d closes as verification-only, plus one small hardening (report E.20; scoping note §5 named this outcome as legitimate). All fourteen Memory-reading Companion paths already exclude retracted and superseded material — explicitly by state filter, and structurally because current_memory_objects holds exactly one current-version row per object (report E.20–E.21). The work is: (a) tests per answer-composing path (§6, T-1d); (b) the hardening: a named constant AS_CURRENT_STATES = ("held", "committed") in engagement/assertions.py, with the two Python-side filter paths (find_files at router.py:995, correction-target matching at router.py:260) routed through it. Rationale: those two paths are one deleted line away from surfacing retracted assertions as current (report E.20 caveat), and the scoping note §1 wants 1d's filter reusable by Stage 4 as a hard filter. The fuller list_as_current_assertions(...) helper is set aside as more than this CR needs; the constant is the minimum durable move. No retrieval work; nothing anticipates Stage 4 beyond the constant existing.
CD-11 — This CR relies on the relate path's current permissiveness, and says so (D-5). 1a's edge is cross-engagement-targeted and points at a retracted assertion; both are possible only because add_relationship / _record_relationship / the route perform no existence, engagement, or state validation (report B.8 — dev already holds 5 unresolvable-target and 4 retracted-target relationships). If relate-path validation lands later (filed as a queued-directions candidate, not fixed here), the redirect edge write and the backfill are the two sites that need an explicit carve-out for cross-engagement and retracted targets. This CR does not add validation and does not depend on validation being absent forever — it depends on the carve-out being remembered, which this paragraph exists to make findable.
Write site. src/loomworks/engagement/redirect.py, after the retract_assertion await (line 360 at inspection), inside the try:
await add_relationship(
engagement_id=target_engagement_id, # CD-1
source_ref=MemoryRef(id=new_assertion.object_id, version=None),
target_ref=MemoryRef(id=assertion_id, version=None),
vocabulary="wasDerivedFrom",
qualifier={"relationship_kind": "assertion_redirect"}, # CD-2
actor=actor,
db=db,
)
Exact parameter names derived from the live add_relationship signature at build time (PF-4b), not from this sketch. redirect.py gains the add_relationship and MemoryRef imports it currently lacks (report B.6). The three redirect_from_* metadata keys are untouched — they remain load-bearing for source-side traversal (CD-1).
Backfill. Per CD-4. Selection query joins memory_events (assertions whose payload metadata carries redirect_from_assertion_id, at their current version) to engagements (live only). For each hit without an existing assertion_redirect edge, write the edge with the same shape as the live write site. The verification query the build summary names carries the same join.
RelationshipVocabulary to src/loomworks/memory/base.py beside ActorKind, carrying the fourteen values verbatim in declaration order from report C.13 (wasDerivedFrom, wasRevisionOf, wasInvalidatedBy, wasAttributedTo, wasGeneratedBy, wasConsideredAndRejected, relates-to, corroborates, wasConsideredAndAffirmed, wasDeliberateExceptionTo, declaresShapeType, declaresRenderType, hasShaping, explains), with a single-source-of-truth comment mirroring ActorKind's.Literal unions with the alias: engagement/types.py:349 (Relationship.vocabulary), engagement/assertions.py:615 (add_relationship), engagement/assertions.py:659 (_record_relationship), api/schemas.py:2982 (AddRelationshipRequest.vocabulary).AlternativeItem model in api/schemas.py: content: str, grammar_element: Literal["definition", "constraint"] = "definition".AddAssertionRequest.alternatives: list[AlternativeItem] | None = None.add_assertion_route (api/routers/assertions.py:196): after the primary add_assertion, loop the alternatives — add_assertion with metadata={"alternative_of": str(primary.object_id)} and normative_force inherited from the primary; then add_relationship per CD-9. One transaction; all-or-nothing (report D.16).alternative_ids list to the response; CC confirms against the live response model at build time and keeps the change additive.AS_CURRENT_STATES = ("held", "committed") in engagement/assertions.py; router.py:995 and router.py:260 route through it (CD-10).GET .../history), at-version reads (GET .../at-version/{version}), seed induction history, state_of_engagement_at_version, state_of_object_at_time (memory/queries.py), consideration surfacing (considerations.py:328,382), relationship reads (list_relationships_for_assertion — docstring-explicit), and the 1a backfill (must reach the retracted source). None of these acquires the filter.
| # | Prior position (scoping note / brief) | Corrected position (report) |
|---|---|---|
| 1 | Baseline main 129d124, suite 2933/46/0, alembic 0086 | Main 79262c5, suite 3379/68/1, alembic 0101; suite not green — one known failure, waived-explicit (D-3) |
| 2 | redirect_from_* metadata is "the historical mechanism" | Metadata stays load-bearing for source-side traversal; one edge is visible from exactly one end (CD-1; asymmetry filed to provenance-threads reframe) |
| 3 | Qualifier illustration {"derivation": "redirect"} | {"relationship_kind": "assertion_redirect"} per established convention (CD-2) |
| 4 | Backfill template: Phase 28 script amend_loomworks_dst_drt_abbreviations.py | Wrong file; all three Phase 28-era scripts broken at import; template is engagement/wasderivedfrom_backfill.py (CD-4) |
| 5 | 1b: "at least thirteen" values; "one new test asserting the sites agree" | Fourteen values; the agreement test has existed since Phase 11 and is why drift never fired; 1b is preventive, not repair (CD-6) |
| 6 | 1c split-rule worry: fan-out might break a boundary/admission invariant | Empty — contribution routes never enter the boundary path; in-tree precedent for the exact shape at boundary.py:247–268 |
| 7 | "The contribution path" (singular) | Four entry points, two schemas; AddAssertionRequest extended; the other three named out of scope (CD-7) |
| 8 | 1d: "at least two" read paths | Fourteen; all filter correctly; 1d closes verification-only (CD-10) |
| 9 | Exempt list includes "trajectory views" | No trajectory read path exists; corrected exempt list in §4.4.3 |
wasDerivedFrom relationship in the target engagement with relationship_kind = "assertion_redirect", source ref = target assertion, target ref = retracted source; assert it is absent from the source engagement's relationship reads (the asymmetry is asserted, not discovered later).add_assertion failure; assert no orphan edge, no orphan assertion (transaction + orphan-guard hold with the new write inside them).EngagementNotFound.RelationshipVocabulary args, order-exact.wasConsideredAndRejected edges primary→alternative, alternatives carry alternative_of metadata.alternatives behaves exactly as today (additive-only).git add -A; uv.lock is not staged. Halt before push. Report paths and SHA. Merge to main and tag only on Operator instruction.Checkpoints are report-and-proceed unless a halt condition fires; the Operator may tighten any checkpoint to report-and-wait at kickoff.
Stages 2–4; provenance-threads reframe implementation (the one-edge-one-end finding is filed, not designed around); supersession severity; terminal-state changes; compound contributions as a general feature; relate-path validation (filed as queued-directions candidate); the hard-delete orphaned-projection repair (filed as queued-directions candidate); the Stele router-mount failure (filed to the Stele front); repair of the three broken Phase 28-era scripts; POST /contributions and Companion-path alternatives attach points.
Read /Users/dunin7/loomworks-engine/docs/phase-crs/cr-2026-154-graph-hygiene-v0_1.md
(if not yet archived there, read it from ~/Downloads/ and archive per PF-3).
Also read, from loomworks-record:
- inspection-briefs/loomworks-graph-hygiene-step-0-inspection-report-v0_1.md
(authoritative for live-code facts)
- the graph-hygiene scoping note and drafting handoff for orientation.
Execute CR-2026-154 against /Users/dunin7/loomworks-engine.
Step 0 first: PF-1 re-baseline, PF-2 CR-number verification against
loomworks-record (renumber if 146 is taken), PF-3 archive, PF-4 grounding
spot-checks, PF-5 build branch cr-2026-154-graph-hygiene. Halt conditions
per CR §2.3. Report Checkpoint A before building.
Then items in order: 1b, 1a, 1c, 1d, per CR §4 and §7, suite run after
each item. Suite gate per CR §2.2: no new failures; the known Stele
router-mount failure unchanged and waived.
Signatures and placements are derived from the real code at build time,
not from the CR's sketches — verify each before writing. Synthetic
engagements only for live verification; never E0060, E0007, E0030, or
E0006. Stage by explicit path; never git add -A; do not stage uv.lock.
Halt before every push. Report paths and SHA before push.
If a migration need, a boundary-invariant surprise, or any structural
divergence from the CR's grounding surfaces: halt and report, do not
absorb.
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-154 — Graph Hygiene (Stage 1) — v0.1 — 2026-07-27