DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/phase-10-render/phase-10-cr-render-v0_1.md

DUNIN7-M4 — INFRASTRUCTURE CHANGE REQUEST

CR-2026-021 — Phase 10: Render — RenderEvent as MemoryObject derivation, RenderDispatchAgent and render specialists, and the render-produced drift firing point (v0.1)

Version. v0.1 Date. 2026-04-22 Author. Claude (drafting) / Marvin Percival (approving). Target. /Users/dunin7/loomworks on DUNIN7-M4 (MacMini). Baseline reference. Tag phase-9-shaping on main at HEAD 6086fe9 (Phase 9 baseline) plus subsequent infrastructure landings (docs-archival-v0_1 migration plus Phase 8 CR v0.4, Phase 9 CR v0.3, standing notes, Phase 10 scoping note v0.1) on main. Phase 9 final state: 656 passed, 1 skipped. Phase 10 builds on this baseline. Priority. Standard. Confidential. Internal DUNIN7. Supersedes. No prior Phase 10 CR — v0.1 is the first. Companion to. docs/phase-scoping-notes/loomworks-phase-10-scoping-note-v0_1.md (the ten-scope-call resolution this CR drafts against); docs/phase-handoffs/loomworks-phase-10-kickoff-handoff-v0_2.md (the kickoff handoff that opened the scoping chat); Playground Specification v0.10 (Sec-C.1 R-C3 + R-C4 + R-C13; Sec-C.3 R-C9.2); methodology what-dunin7-is-building-v0_17.docx; loomworks-standing-note-shaping-render-boundary-v0_1.md (load-bearing per P5; cited binding in this CR's construction decisions); loomworks-standing-note-versioned-document-archival-v0_2.md (governs CR archival discipline; Phase 10 lands the CR at Step 0 per Phase 9 implementation notes Finding A); current-status-manifest-v0_3.md (orientation and Residue 3 awareness for kickoff-prompt path discipline); Phase 9 CR v0.3 (structural template); Phase 9 implementation notes v0.1 (deficiency patterns absorbed proactively); Phase 8 CR v0.4 (payload-only field carry-forward Finding I); Construction Brief v0.1 Sec-4.4 (Path C precedent origin). Status. Pre-execution CR. Ready for Operator review and approval. Step 0 pre-flight runs against this version against phase-9-shaping baseline.


Contents


1. Executive summary

Phase 10 makes Loomworks-the-engagement able to produce Renders — consumer-facing artifacts derived from confirmed Shapes — under settlement 1B (Render-only). Sec-C.1 R-C3 plus the Render-relevant parts of R-C4 and R-C13 land. Sec-C.4 (live maintenance) defers to Phase 11; Sec-C.5 (delivery to consumers) defers to Phase 12.

Under methodology v0.17's pipeline framing, extended by the Phase 10 scoping note's ten settlements: a confirmed Shape (produced and confirmed in Phase 9) becomes the input to a render specialist, which acts under a registered versioned instruction set against a declared render-type to produce a Render — a new MemoryObject persisted as a RenderEvent. The render is dispatched by a RenderDispatchAgent whose day-one mechanism is a lookup table on (engagement, declared_render_type) → registered specialist. Renders are not separately confirmed; they are deterministic derivations of confirmed Shapes (Position A on Scope Call 3). Drift detection extends to fire at render production through two checks (Check A' and Check B'); the third check from the Phase 9 anchor (Check C', transitive Seed-faithfulness) defers to Phase 11 live maintenance where periodic re-checking sits more naturally.

Architectural commitment crystallized in scoping — Position C on Field 11. The scoping chat surfaced a question the Phase 10 kickoff handoff was implicitly silent on: whether Renders are declared in the engagement seed parallel to declared shape-types, or are emergent from specialist registration, or some hybrid. Position C — the hybrid — was selected as the safest landing. A declared render-type carries strong discipline (rules pinned, specialist registration discoverable from seed, drift-checkable); ad-hoc renders are permitted with weaker discipline. The declared_render_type_ref field on RenderEvent is nullable per Position C; non-null marks declared, null marks ad-hoc. Phase 10 v0.1 builds the declared path only. Ad-hoc requests reject with a documented hook in RenderDispatchAgent's instruction. The hook keeps the future ad-hoc path visible without committing to it now.

Storage architecture — Path C precedent continued. RenderEvent persists through the Phase 2–9 convention: canonical write to memory_events; generic current-state cache via current_memory_objects; all fields live in the JSONB payload. In addition, Phase 10 introduces a render-events-specific materialized view render_events_view, projector-maintained, regeneratable from memory_events alone, with scalar columns tuned for Phase 11/12 consumption patterns. This continues the Path C precedent established by Phase 9's shape_events_view (Construction Brief v0.1 Sec-4.4 per-type-materialized-view pattern, applied at the second site where downstream-consumer read patterns justify).

Vocabulary convention (load-bearing; methodology-in-code, spec-citations preserved per P2). Methodology v0.17 names the pipeline with a process/product distinction. Render is the product — the MemoryObject produced by the specialist. The specialist is the producer. The spec's "projection render" is the methodology's render noun; R-C9.2's "Render Agent" terminology resolves to render specialist in implementation vocabulary. When citing R-C-series requirements verbatim, "projection render" and "Render Agent" are preserved at point of citation; everywhere else, methodology vocabulary applies. The handoff's "traffic cop" terminology and "playground" terminology (residue of pre-v0.8 era) are not used in this CR; the dispatch mechanism is named RenderDispatchAgent and the system is Loomworks.

The deliverables are:

The acceptance suite exercises the full pipeline through Render: a confirmed Shape with auto-trigger fires RenderDispatchAgent → looks up registered specialist → specialist produces Render content → RenderEvent emits to memory_events → projector propagates to render_events_view. A second acceptance test exercises the Memory-as-sole-write-target principle's extension to the Render layer: drift detected at render time (Check B' simulated) opens an upstream consideration, the consideration closes with amend, new Memory artifacts land, a new Shape gets confirmed, the auto-trigger fires again, a new Render emits, the old Render is invalidated and walkable.

Test count projection (per-file only, no aggregate per Phase 9 Finding F). Per-file projections appear in Section 16. The Phase 9 lesson — aggregate projections drift ~17–18% from execution reality — is honored by not reintroducing an aggregate projection here.


2. Rationale

2.1 Why now

Phase 9 is complete and tagged at phase-9-shaping. The Phase 10 kickoff handoff v0.2 settled five pre-scoping decisions (P1–P5) including the load-bearing P3 (Memory-as-sole-write-target extends to Render layer) and P5 (Shaping/Render boundary discipline binding). The Phase 10 scoping chat resolved all ten scope calls through Operator walk-through, with the trajectory preserved. Position C on Field 11 crystallized as the architectural commitment that constrains data model, registration model, trigger landscape, and drift scope. The scoping note v0.1 lands the resolutions; this CR drafts against it. No blocking item remains open.

The Render discovery project's resolutions on framework shape, dispatch locus, capability-declaration registration, and chain composition rules remain pending — but Phase 10 lands a Loomworks-local minimal shape under the handoff's defer-with-upgrade-compatibility posture. None of the discovery's possible resolutions can break Phase 10's data model or framework; the design choices are upgrade-compatible.

2.2 Why this shape (single CR)

The CR bundles Phase 10's deliverables into one unit because the components are mutually dependent. DeclaredRenderType without RenderEvent produces render-types nothing realizes; RenderEvent without RenderDispatchAgent produces an empty record; RenderDispatchAgent without registered specialists has nothing to dispatch to; the drift firing point without RenderEvent has nothing to fire on. Splitting the phase would produce sub-CRs each of which cannot be tested in isolation. The Phase 9 precedent of one bundled CR per phase carries forward.

2.3 Why these choices turn on the Memory-as-sole-write-target principle

The kickoff handoff v0.2 articulates the principle as extending across the downstream pipeline. Three Phase 10 design choices invoke the principle by name:

2.4 Why Path C continues

Phase 9 established Path C (event-log-canonical with type-specific materialized view) as the precedent for new MemoryObject types whose downstream consumers warrant scalar-column read patterns. Phase 10's render_events_view is the second instance of the pattern. The justifications:

The Phase 9 implementation-notes Finding E names the pattern: object types that acquire their own HTTP-layer query surface get a typed projection view; object types queried only inline from the generic projection do not. RenderEvent meets the criterion; the view lands.

2.5 Why Position C on Field 11

Three positions on the table during the scoping walk:

Position C was selected as the safest landing — preserves upgrade paths in both directions (toward A if specialists prove engagement-internal and declared-render discipline tightens; toward B if declared-render-type discipline turns out to add cost without benefit). Phase 10 v0.1 builds the declared-render path only and rejects ad-hoc with a documented hook (Section 14); the hybrid commitment is in the data model (nullable field) and in the dispatch agent's instruction (named hook), not in active machinery for ad-hoc.

The seed-shape consequence: the Loomworks engagement seed (loomworks-candidate-seed-v0_2.docx) gains an optional declared_render_types section parallel to the existing declared_shape_types. Loomworks-the-engagement may keep this section empty initially — Loomworks-the-engagement is the outlier where most products are shape-layer rather than render-layer. The three pending engagements (Enrollium, ExpenseDesk, FarmGuard) are more conventional and likely to surface render-level commitments through their separate induction solution. The seed v0.3 update is not a Phase 10 CR concern; this CR builds the engineering substrate that the seed update would consume.


3. Prerequisites

3.1 Tag and baseline

Phase 9 is complete at tag phase-9-shaping on main (HEAD 6086fe9); 656 tests pass with 1 environment-gated skip. Today's infrastructure landings (docs-archival-v0_1 migration, Phase 8 CR v0.4, Phase 9 CR v0.3, standing notes, Phase 10 scoping note v0.1) are on main after the tag. Phase 10 builds against main HEAD; tag history starting from phase-9-shaping remains the code-state reference.

3.2 Pre-flight ground-truth checks (Step 0)

CC verifies the Phase 9 codebase ground-truth before Step 1 runs. Specifically:

Any divergence stops execution and drives a CR v0.2 revision.

3.3 Auto-mode checkpoint declaration

Per D9 and Section 17:

3.4 Discovery state items (recorded as known-open, not blocking)

Per the scoping note Section 10, several items remain open pending Render discovery resolution. None block Phase 10 landing; each is a recorded pickup-point:

3.5 CR archival at Step 0 (per Phase 9 implementation-notes Finding A)

Phase 9 implementation notes Finding A surfaced that Phase 9's CR lived in conversation context only and was not archived to docs/ until Step 16. Phase 10 corrects this discipline: this CR (v0.1) commits to docs/phase-crs/phase-10-cr-render-v0_1.md at Step 0 before Step 1 begins, per loomworks-standing-note-versioned-document-archival-v0_2.md's archival discipline. Subsequent CR versions (v0.2, v0.3 if any) follow the same pattern — archival at Step 0 of each new version's pre-flight, not at Step 16 alongside implementation notes.


4. Construction decisions this CR closes

4.1 Construction decisions table

| ID | Decision | Source | |---|---|---| | D1 | DeclaredRenderType as MemoryObject via Phase 2–9 convention; no dedicated canonical table | Scope Call 1; Section 6.1 | | D2 | Two-trigger model: explicit_request foundational + declared_auto_on_shape_confirmed composed on top within Phase 10 | Scope Call 2; Section 8 | | D3 | Position A on Render confirmation: no Render-layer confirmation framework; Render inherits from confirmed Shape | Scope Call 3; Section 10 | | D4 | Two-check drift dispatch at render_produced (Check A' rules-conformance, Check B' translation-faithfulness); Check C' deferred to Phase 11 | Scope Call 7; Section 11 | | D5 | RenderEvent persists via convention plus render_events_view for downstream-consumer read patterns (Path C precedent continued) | Scope Call 8; Sections 6.2 and 13 | | D6 | RenderDispatchAgent in agent registry; declared-render-type registration for specialists; one specialist per declared render-type per engagement | Scope Calls 4 and 5; Sections 8 and 9 | | D7 | Reject-with-documented-hook for ad-hoc render requests; declared-render path only in Phase 10 | Scope Call 10; Section 14 | | D8 | Memory-as-sole-write-target extends to Render: drift findings route upstream; amend/retire/invalidate happens at Memory or Shape layer; Render layer state is minimal | Sections 10.3 and 11.5 | | D9 | Auto-mode checkpoint posture: A and B auto-proceed, C halts until Operator confirms | Section 17 | | D10 | Path C continues: render_events_view projector-maintained, regeneratable from memory_events alone | Section 13.5 | | D11 | Single-step only; no chaining concept in Phase 10. RenderEvent has single confirmed_shape_event_ref, no chain provenance fields | Scope Call 6; Section 6.2 |

4.2 Per-decision rationale

D1. DeclaredRenderType follows Phase 9's DeclaredShapeType pattern verbatim. The Phase 2–9 convention (canonical write to memory_events; generic view via current_memory_objects; payload in JSONB) handles MemoryObject persistence. No dedicated canonical table. The Position C nullability of declared_render_type_ref on RenderEvent is independent of DeclaredRenderType's own existence — DeclaredRenderType always persists when a seed declares a render-type; RenderEvent's reference is null when the Render is ad-hoc (rejected in Phase 10) or non-null when declared.

D2. The two-trigger model honors Phase 9's compose-on-top precedent (build the foundational mechanism, compose other triggers on top within the same phase). The auto-trigger path internally invokes the explicit-request mechanism with a system actor for triggered_by; one mechanism, two callers. The safer fallback (explicit-request only, defer auto-trigger composition to Phase 11 or composing layer) is recorded in the scoping note's trajectory; if CR-drafting or pre-flight surfaces auto-trigger composition cost as higher than expected, fallback to explicit-request only with auto-trigger as a Phase 11 pickup.

D3. Position A lands without empirical settlement of the CC-as-conformance-test question. Reasoning: Phase 10 is foundational; building a Render-layer confirmation framework now commits to machinery that may be lightly used (most renders are auto-trigger declared under Position C) and whose actual need is unsettled. Position B (always-confirm) over-applies confirmation in the deterministic-derivation case. Position C (selective confirmation) and the scaled-back-C variant (attest-only terminal wired) remain in trajectory as pickup-points if Phase 11+ practice surfaces a need.

D4. Resolution C on Scope Call 7. Confirmation and drift detection are independent mechanisms (Principle 2 from scoping note Section 13); Position A on confirmation does not settle the drift question. The Phase 9 principle (drift detected where derivation happens) extends naturally to Render. Check A' and Check B' catch real failure modes that pinning-and-conformance doesn't catch on its own. Check C' (transitive Seed-faithfulness) defers to Phase 11 where periodic re-checking is the natural framing.

D5. Path C continued. render_events_view is justified by Phase 11/12 read-pattern needs and by the Render-discovery-keeps-DB-level-subscriber-path-open consideration. Phase 9 implementation-notes Finding E names the pattern: object types acquiring their own HTTP query surface get a typed projection view. RenderEvent meets the criterion.

D6. Position A on Scope Call 4 (registered RenderDispatchAgent in agent registry) plus Resolution A on Scope Call 5 (declared-render-type registration). Lowest novelty cost; reuses Phase 9 ShapingAgent pattern; doesn't presuppose framework resolutions Phase 10 can't validate. Upgrade compatibility holds in both axes — if discovery resolves dispatch locus to protocol-property or framework-layer, RenderDispatchAgent becomes adapter or gets superseded; if discovery resolves registration to capability-declaration, the registration field set extends without breaking the lookup.

D7. Reject-with-documented-hook honors the Position C architectural commitment (ad-hoc is a recognized future capability) without building the ad-hoc path now. The hook in RenderDispatchAgent's instruction names ad-hoc as future capability; a clear error returns when an ad-hoc request arrives. Section 14 details the rejection mechanics.

D8. Memory-as-sole-write-target is the discipline that prevents Render-layer patching. Drift findings route upstream because remediation always happens upstream; the Render itself either gets retired (operator-driven) or invalidated (drift-triggered). New Memory → new Shape → new Render is the canonical remediation cycle.

D9. Phase 9 carry-forward; same posture.

D10. Path C continues. The projector contract for render_events_view (Section 13.5) is symmetric with shape_events_view's contract: per-event upsert; non-RenderEvent no-op; idempotence; rebuild-from-scratch parity; crash-mid-event recovery.

D11. Resolution A on Scope Call 6. The "intermediate-renders-that-are-themselves-consumed are independent productions" principle (scoping note Section 13 Principle 1) covers Loomworks-known cases without chain-internal provenance machinery. Chain provenance fields are not introduced in Phase 10; the third trigger value (chained_from_render) does not land. Recorded in trajectory for future pickup if a case surfaces.


5. Project layout after Phase 10

5.1 New files

5.2 Modified files (with explicit Literal mirror sites per Phase 9 Note item 1)

5.3 No new top-level packages

Phase 10 does not introduce src/loomworks/render/ or src/loomworks/dispatch/ as new top-level packages. The work lives in agents/ (RenderDispatchAgent and render specialists), engagement/ (orchestration), api/routers/ (HTTP), and extends memory/projector.py. Consistent with Phase 5 / Phase 8 / Phase 9 placement decisions.


6. New MemoryObject types — DeclaredRenderType and RenderEvent (event-log-canonical per Phase 2–9 convention)

6.1 DeclaredRenderType

A declared render-type is a versioned MemoryObject carrying what the Seed has committed this engagement to produce as a Render for a given consumer, sourced from a specific declared shape-type. Added, amended, and retired per the same R-C7-style discipline that governs DeclaredShapeType — through either seed amendment (when the change affects consumer commitments) or the Sec-A.0 configuration-change path (when the change does not). Persists through the standard Phase 2–9 convention: canonical in memory_events, generic current-state view in current_memory_objects. No dedicated canonical table.


class DeclaredRenderType(MemoryObject):
    object_type: Literal["declared_render_type"] = "declared_render_type"

    # What this render-type is for
    consumer_declaration: str                  # The consumer this render serves; aligns with R-A6 / R-C5
                                               # consumer-naming discipline at the seed layer.
    render_type_name: str                      # e.g. "irb_protocol_pdf", "quarterly_report_doc",
                                               # "regulatory_packet"; identifies the render-type
                                               # within the engagement.

    # What this render-type derives from
    source_shape_type_ref: MemoryRef           # Version-pinned reference to the DeclaredShapeType
                                               # whose confirmed Shapes this render-type produces from.
                                               # The source is one declared shape-type per declared
                                               # render-type; many render-types can share a source.

    # Format declaration
    render_format: str                         # Content-type-style identifier (MIME-like).
                                               # Used by render specialist registration to match
                                               # specialist capability and by the read surface
                                               # (Phase 12 will use it for content-typed download).

    # Rendering rules (per Field 6 — open per Scope Call 1.6; v0.1 carries rules in specialist instruction)
    rendering_rules_ref: MemoryRef | None = None  # Phase 10 v0.1: null. Rules are carried in the
                                                  # render specialist's per-instance versioned
                                                  # instruction set per R-A24. Discovery resolution
                                                  # may move rules to a separate object; field exists
                                                  # for upgrade-compatibility.

    # Lifecycle
    state: Literal["active", "retired"] = "active"
    retired_reason: str | None = None          # Populated on state transition to retired

Fields are a v0.1 draft position. Pre-flight and Operator review may refine.

Why one DeclaredRenderType references exactly one source DeclaredShapeType. Position C's discipline says declared renders carry strong discipline (rules pinned, registration discoverable, drift-checkable). A render that could derive from multiple shape-types weakens this — the auto-trigger path needs to know which shape-type's confirmation triggers the render. One source per declared render-type matches the auto-trigger lookup mechanism (Section 8). Multiple source shape-types per render would either require multiple registrations of the same render-type (fragmenting registration state) or a multi-source matching mechanism (heavier than declared dispatch warrants). One-to-many (one shape-type → many render-types) is supported because declared render-types can each independently reference the same source shape-type.

Registration, creation, and amendment go through memory_events parallel to DeclaredShapeType. Event kinds: declared_render_type_added, declared_render_type_amended, declared_render_type_retired — final naming per the existing event-kind vocabulary pattern (Section 7).

6.2 RenderEvent

The record of a Render produced from a confirmed ShapeEvent, by a registered render specialist, against a declared render-type. Standard MemoryObject. Canonical persistence in memory_events; generic materialized view in current_memory_objects; render-specific materialized view in render_events_view (Section 13.2).


class RenderEvent(MemoryObject):
    object_type: Literal["render_event"] = "render_event"

    # Provenance — single confirmed Shape (no chaining per D11)
    confirmed_shape_event_ref: MemoryRef       # Version-pinned reference to the ShapeEvent this
                                               # Render derives from. The referenced ShapeEvent
                                               # MUST be in confirmed state at write time;
                                               # writer enforces the precondition as an invariant.

    # Producer attribution — render specialist
    render_specialist_ref: ActorRef            # Instance-level identity of the registered specialist
                                               # that produced the Render.
    render_specialist_instruction_version: int # Per R-A24; the specialist's instruction-set version
                                               # in force at production time.

    # Trigger attribution
    trigger: Literal[
        "explicit_request",                    # Direct POST /renders/ request
        "declared_auto_on_shape_confirmed",    # Auto-trigger walked declared_render_types and dispatched
    ]
    triggered_by: ActorRef                     # Human contributor (explicit_request) or system
                                               # auto-trigger actor (declared_auto)

    # Declaration anchor — Position C nullable
    declared_render_type_ref: MemoryRef | None  # Non-null: declared render with strong discipline.
                                                # Null: ad-hoc render. Phase 10 v0.1 rejects ad-hoc
                                                # at dispatch (Section 14); the field remains nullable
                                                # in the data model for upgrade-compatibility.

    # Rendering rules (open per Scope Call 1.6; v0.1 carries them in specialist instruction)
    rendering_rules_ref: MemoryRef | None = None  # v0.1: null. Rules are pinned through
                                                  # render_specialist_instruction_version per R-A24.

    # Format
    render_format: str                          # Content-type-style identifier; carried from the
                                                # pinned DeclaredRenderType's render_format (when
                                                # declared) or asserted by specialist (when ad-hoc;
                                                # not Phase 10).

    # Content
    render_content: dict | bytes                # The Render itself. Inline JSONB for structured
                                                # and prose Renders (Phase 10 default per Scope Call 8).
                                                # External-storage path declared in trajectory but
                                                # not built; future phase may add render_content_ref
                                                # field for binary at scale.

    # Lifecycle state — minimal per Position A on Scope Call 3
    state: Literal[
        "produced",                             # Initial state on render_produced event
        "retired",                              # Operator-driven retirement
        "invalidated",                          # Drift-triggered invalidation; triggered Render is
                                                # walkable but no longer current
    ]

On render_content size and representation. The JSONB payload in memory_events carries render content through the sizes Phase 10 needs (structured/prose; the cases Loomworks-the-engagement actually produces). For binary Renders at scale (PDFs, large images), storage-by-reference (to an object store) becomes a candidate for a later phase — render_content would migrate to render_content_ref: MemoryRef or similar. Phase 10 does not build that indirection; the field's typing accepts dict | bytes to admit both inline-structured and inline-binary cases without committing to either by default.

Why the field set is smaller than ShapeEvent's. Phase 9's ShapeEvent carried confirmation lifecycle fields (confirming_party, confirmed_at, presented_form) because the Shape is held pending confirmation. Position A on Scope Call 3 means RenderEvent has no confirmation lifecycle to carry; those fields don't exist on RenderEvent. Similarly, no selected_memory_refs (the upstream Shape carries those) and no excluded_but_considered (the upstream Shape carries those). The Render is a derivation; its provenance points back to the Shape that already carries the Memory-selection fields.

Carry-forward discipline (per Phase 8 CR v0.4 Finding I). Payload-only fields on RenderEvent that helpers may read or write across version transitions — render_content, render_format, declared_render_type_ref, render_specialist_ref, trigger, triggered_by — must be explicitly propagated by every helper that writes a new RenderEvent version. The _advance_render_state and _invalidate_render helpers (Section 8) carry-forward all payload-only fields by default. The carry-forward regression test (Section 15.4) threads each field through state transitions and through the projector to verify.

6.3 Relationship vocabulary extension — declaresRenderType

Phase 10 adds one new term to Relationship.vocabulary at all four locations per Phase 5/9 convention:


vocabulary: Literal[
    "wasDerivedFrom",
    "wasRevisionOf",
    "wasInvalidatedBy",
    "wasAttributedTo",
    "wasGeneratedBy",
    "wasConsideredAndRejected",
    "relates-to",
    "corroborates",
    "wasConsideredAndAffirmed",
    "wasDeliberateExceptionTo",
    "declaresShapeType",          # Phase 9
    "declaresRenderType",         # NEW in Phase 10
]

Defence. Position C names declared renders as engagement commitments parallel to declared shape-types. The Seed → DeclaredRenderType edge is a declaration relationship — the same kind of relationship as Seed → DeclaredShapeType. declaresRenderType names the semantic exactly: the source (Seed) declares that the target (DeclaredRenderType) is one of the renders this engagement produces. Packing it into relates-to with a qualifier loses the first-class discoverability that makes the question "what renders does this engagement produce?" a simple filter.

Sites updated.

  1. src/loomworks/engagement/types.py — canonical Relationship definition.
  2. src/loomworks/engagement/assertions.pyadd_relationship public parameter; pre-flight confirms current line.
  3. src/loomworks/engagement/assertions.py_record_relationship private parameter; pre-flight confirms.
  4. src/loomworks/api/schemas.pyAddRelationshipRequest.vocabulary; pre-flight confirms.

6.4 The rendering_rules_ref open question (Scope Call 1.6) — v0.1 landing

Three positions on the table for how rendering rules are represented:

Phase 10 v0.1 lands Position B. The rendering_rules_ref field exists on both DeclaredRenderType and RenderEvent for upgrade-compatibility but defaults to null in v0.1; rules are carried in the render specialist's R-A24 versioned instruction. R-C3 deterministic-reproducibility is satisfied: Shape + specialist instruction version reproduces the Render; the instruction contains the rules.

Why Position B for v0.1. Lightest landing. No new MemoryObject type for rules. R-A24's per-instance versioned-instruction discipline is already mandatory for any specialist; the rules ride that infrastructure. The cost: rules and mechanics are conflated inside the specialist instruction, which may become awkward if multiple specialists need to share the same rules across specializations. The Render discovery's specialist-conformance work may surface a position on rules ownership that pushes toward A or C; in either case, the upgrade is straightforward — populate the existing nullable field, and introduce a RuleSet object type if A is taken.

Why not Position A for v0.1. Introducing a new MemoryObject type for rules is a separable piece of work that doesn't have a current-need argument. Phase 10 v0.1's specialists are likely to be 1–2 in number at land time (RenderDispatchAgent dispatches, but there's no concrete render specialist beyond stub for the testbed); rule sharing across specialists isn't yet a real problem. Pre-build is risky.

Why not Position C for v0.1. Hybrid is the most flexible but the heaviest; commits to two rule sources (default + override) without validation that overrides are needed.

The scoping note records all three positions in trajectory. Phase 10 v0.1 picks B; future phase may revisit.

6.5 Alignment with the spec's "projection" vocabulary

The spec's Sec-C.1 uses "projection render" and Sec-C.3 uses "Render Agent." For reader orientation:

| Methodology / code (Phase 10) | Spec (playground-spec-v0_10) | |---|---| | Render (as noun; the produced artifact) | Projection render | | RenderEvent (MemoryObject) | The record of a projection render produced | | DeclaredRenderType (MemoryObject in Seed) | The declared render-type in the seed (Position C; not in spec) | | RenderDispatchAgent | The dispatch mechanism; not in spec at this granularity | | render specialist | Render Agent (R-C9.2) | | render_content (RenderEvent field) | The Render that a projection render holds |

Spec citations throughout the CR preserve "projection render" and "Render Agent" at point of citation. Elsewhere, methodology vocabulary applies.


7. New event kinds for Render lifecycle

Phase 3 established that MemoryObject lifecycle transitions are recorded as event-log entries against the object by id. Phase 5 added consideration event kinds. Phase 9 added five Shape lifecycle event kinds (four writer-path plus one projector-reserved). Phase 10 adds three writer-path Render lifecycle event kinds plus three DeclaredRenderType lifecycle event kinds:

Render lifecycle (writer-path):

Why three writer-path kinds and not five (no projector-reserved name). Phase 9 had five kinds because the consideration framework uses a generic consideration_opened kind that conflicts with a specific shape_confirmation_opened writer-path kind (the projector reserves the name to prevent future accidental writer emission that would duplicate the generic event). Phase 10's confirmation framework is absent (Position A on Scope Call 3); no analogous conflict exists. The generic consideration_opened event kind is used by the upstream-routed drift consideration when one opens (Section 11.5); no Render-layer consideration opens, so no Render-specific consideration-open event kind is needed. Three writer-path event kinds suffice.

DeclaredRenderType lifecycle (writer-path):

These follow Phase 9's DeclaredShapeType event-kind pattern verbatim.


8. The RenderDispatchAgent

8.1 Class


# src/loomworks/agents/render_dispatch.py

from sqlalchemy.ext.asyncio import AsyncSession
from typing import Literal
from loomworks.agents.base import RegisteredAgent
from loomworks.engagement.types import (
    DeclaredRenderType, ShapeEvent, RenderEvent, MemoryRef, ActorRef,
)


class RenderDispatchAgent(RegisteredAgent):
    """
    Dispatches render production. One instance per engagement. Reads the
    engagement's declared render-types from the seed; on shape_confirmed
    or on explicit request, looks up the registered render specialist for
    the (engagement, declared_render_type) pair and enqueues a render_jobs
    row dispatching to that specialist via BackgroundAgentRunner.dispatch.

    Per-instance versioned instruction set per R-A24. The instruction names:
    - The lookup mechanism (the (engagement, declared_render_type) -> specialist table).
    - The auto-trigger walk (on shape_confirmed, find matching declared_render_types).
    - The reject-with-hook policy for ad-hoc requests (declared_render_type_ref null).
    - The failure-mode handling (logging, candidate-surface visibility, validation errors).
    - The invocation contract with specialists.
    """

    instruction_set_ref: MemoryRef               # Per R-A24
    engagement_id: str

    async def dispatch_for_shape_confirmed(
        self,
        *,
        confirmed_shape_event: ShapeEvent,
        db: AsyncSession,
    ) -> list["RenderJobDispatchResult"]:
        """
        Auto-trigger path. Called by the post-append hook when a
        shape_confirmed event fires. Walks the engagement's
        declared_render_types whose source_shape_type_ref matches the
        confirmed Shape's declared_shape_type_ref. For each matching
        declared render-type, looks up the registered render specialist
        and enqueues a render_jobs row.

        Returns the list of dispatched job ids and their target
        specialist refs. Empty list if no declared render-types match
        (the engagement has no declared renders for this shape-type;
        valid state, not an error).
        """
        ...

    async def dispatch_for_explicit_request(
        self,
        *,
        confirmed_shape_event_ref: MemoryRef,
        declared_render_type_ref: MemoryRef | None,
        triggered_by: ActorRef,
        db: AsyncSession,
    ) -> "RenderJobDispatchResult":
        """
        Explicit-request path. Called via POST /renders/. If
        declared_render_type_ref is None, raises AdHocRenderNotSupportedError
        per Section 14 (Position C v0.1 reject-with-hook). If
        declared_render_type_ref is supplied, looks up the registered
        specialist and enqueues a render_jobs row.

        Returns the dispatched job id and its target specialist ref.
        Raises if the declared render-type has no registered specialist
        (returns to caller as HTTP 409 per Section 12).
        """
        ...

8.2 Dispatch pattern — render_jobs operational table

Parallels Phase 9's shaping_jobs. Schema (migration 0022):


CREATE TABLE render_jobs (
    id UUID PRIMARY KEY,
    engagement_id UUID NOT NULL REFERENCES engagements(id),

    -- Inputs
    confirmed_shape_event_ref JSONB NOT NULL,        -- version-pinned MemoryRef
    declared_render_type_ref JSONB NULL,             -- nullable per Position C
                                                     -- (v0.1 always non-null since
                                                     -- ad-hoc rejects before dispatch)
    render_specialist_ref JSONB NOT NULL,            -- ActorRef; resolved at dispatch time

    -- Trigger attribution
    trigger TEXT NOT NULL,                           -- explicit_request | declared_auto_on_shape_confirmed
    triggered_by JSONB NOT NULL,                     -- ActorRef

    -- Status
    status TEXT NOT NULL,                            -- queued | dispatched | completed | failed
    render_event_object_id UUID NULL,                -- Populated on completed;
                                                     -- references RenderEvent object_id
                                                     -- (no FK because RenderEvent lives in
                                                     --  memory_events, not in a dedicated table)
    error_message TEXT NULL,                         -- Populated on failed

    created_at TIMESTAMPTZ NOT NULL,
    dispatched_at TIMESTAMPTZ NULL,
    completed_at TIMESTAMPTZ NULL
);

CREATE INDEX ix_render_jobs_engagement_status ON render_jobs (engagement_id, status);
CREATE INDEX ix_render_jobs_engagement_specialist
    ON render_jobs (engagement_id, (render_specialist_ref->>'actor_id'));

The render_event_object_id carries the MemoryObject's object_id; lookups against render_events_view or current_memory_objects resolve by that id. Pattern parallels Phase 9's shape_event_object_id per Phase 9 v0.2's revision (no FK because the canonical table is memory_events).

8.3 Non-blocking dispatch

The request handler returns immediately with the job id (HTTP 202). The caller polls GET /engagements/{eid}/render-jobs/{jobid} for completion or watches for the render_produced event via GET /engagements/{eid}/render-events. Matches Phase 5/9 non-blocking dispatch pattern.

8.4 Registration

RenderDispatchAgent registers through the existing agent registry (Phase 3). One instance per engagement. The agent's per-engagement versioned instruction (per R-A24) carries the lookup mechanism, the auto-trigger walk logic, the reject-with-hook policy for ad-hoc, the failure-mode handling, and the invocation contract with render specialists.

Render specialists register separately through the same registry. Each specialist instance is keyed by (engagement_id, declared_render_type_ref); the lookup mechanism in RenderDispatchAgent's instruction reads the registry by this key. A declared render-type without a registered specialist is flagged unproducible per Section 12.2's HTTP behavior (returns HTTP 409 with rationale naming the missing specialist; appears in GET /renders/candidates/ as pending-with-no-specialist).

8.5 Reject-with-documented-hook for ad-hoc requests

Per D7 and Section 14. When dispatch_for_explicit_request is called with declared_render_type_ref=None, RenderDispatchAgent raises AdHocRenderNotSupportedError. The error's message names ad-hoc as a future capability path and points the caller to the documented hook in the agent's instruction. The HTTP layer (Section 12.2) translates the error to HTTP 422 with a JSON body explaining the rejection. The agent's instruction (loaded via R-A24 versioned reference) includes the hook documentation as a structured section so future capability work can update the instruction without code changes to the dispatch agent.

8.6 Stub pattern for tests

Tests construct a RenderDispatchAgent instance with a stub instruction-set ref and stub render specialist references. The stubs short-circuit the LLM-instruction-evaluation path; lookup-table behavior is unit-testable directly. Render specialist stubs (Section 9.5) handle the specialist-side stubbing.


9. Render specialists — pattern, registration, instructions

9.1 The specialist as a registered agent

A render specialist is a registered agent in the engagement's agent registry. Per Resolution A on Scope Call 5, registration is one-instance-per-declared-render-type-per-engagement: the registration key is (engagement_id, declared_render_type_ref). Specialists are not framework-shared in Phase 10 (engagement-internal registration); the Render discovery's framework-shared resolution may inform Phase 11+ extensions.


# src/loomworks/agents/render_specialist.py

from typing import Protocol
from loomworks.agents.base import RegisteredAgent
from loomworks.engagement.types import MemoryRef, ActorRef, RenderEvent


class RenderSpecialist(RegisteredAgent):
    """
    Base class for render specialists. One instance registered per
    declared render-type per engagement. Per-instance versioned
    instruction set per R-A24, which carries:
    - The rendering rules (per Position B v0.1; rules bundled into
      instruction).
    - The render_content production logic (LLM prompt, deterministic
      template, format-specific rendering).
    - The invocation contract with RenderDispatchAgent.

    Subclasses or instructions define the actual production logic.
    """

    instruction_set_ref: MemoryRef               # Per R-A24
    engagement_id: str
    declared_render_type_ref: MemoryRef          # The render-type this specialist handles

    async def produce_render(
        self,
        *,
        confirmed_shape_event_ref: MemoryRef,
        declared_render_type_ref: MemoryRef,
        triggered_by: ActorRef,
        trigger: str,
        db,
    ) -> RenderEvent:
        """
        Called via BackgroundAgentRunner.dispatch through a render_jobs
        entry. Reads the confirmed Shape, reads the pinned
        DeclaredRenderType, applies the rendering rules from the pinned
        instruction set, produces the render content (LLM call in
        production; stub content in test mode), and writes a
        render_produced event to memory_events with the full
        birth-certificate payload. The projector then propagates to
        current_memory_objects and render_events_view.
        """
        ...

9.2 Versioned instructions per R-A24

Each specialist instance carries a MemoryRef to its instruction set. The instruction set is itself a versioned MemoryObject (per the R-A24 discipline established in Phase 7); when amended, the version bumps and subsequent renders pin the new version. Phase 10 does not introduce new instruction-set MemoryObject infrastructure — it reuses Phase 7's.

The RenderEvent's render_specialist_instruction_version field carries the integer version that was in force at production time. This is the load-bearing field for R-C3 deterministic-reproducibility: given the confirmed Shape plus the pinned specialist instruction version, the Render is reproducible. (Strictly: deterministic only if the specialist's underlying production logic is deterministic; LLM-based specialists are reproducible-modulo-LLM-temperature, which the instruction may pin.)

9.3 Rendering rules — v0.1 carries them in the specialist instruction

Per Section 6.4 (Position B v0.1 landing). The specialist's instruction set contains:

The rendering_rules_ref field on RenderEvent and DeclaredRenderType is null in v0.1 because rules are carried in the specialist instruction. Future-phase population is straightforward — the field already exists on the data model.

9.4 Specialist invocation contract

RenderDispatchAgent invokes a specialist via:


result = await specialist.produce_render(
    confirmed_shape_event_ref=...,
    declared_render_type_ref=...,
    triggered_by=...,
    trigger=...,
    db=...,
)

The specialist's produce_render returns a RenderEvent after writing the render_produced event to memory_events. The contract:

Failure modes the specialist may signal back to RenderDispatchAgent:

9.5 Stub pattern for tests

A StubRenderSpecialist produces deterministic stub content for tests:


class StubRenderSpecialist(RenderSpecialist):
    stub_render_content: dict | bytes
    stub_render_format: str = "application/stub+json"

    async def produce_render(self, **kwargs):
        # Skip LLM call; emit render_produced event with stub content.
        ...

Parallels Phase 9's StubShapingAgent and Phase 5's stub patterns.


10. Render confirmation — Position A landing (no Render-layer confirmation framework)

10.1 No Render-layer confirmation framework

Per D3 and Scope Call 3 Position A. The Render is a derivation of a confirmed Shape. There is no Render-layer consideration framework; no Render-layer confirmation lifecycle; no pending_confirmation state on RenderEvent. The state machine is minimal: produced, retired, invalidated.

This section exists in the CR (rather than being skipped) to make Position A explicitly visible — Discovery-record posture preserved. A future reader looking at the Render layer should see this section and understand why the Phase 9 Section 10 parallel does not have a corresponding Phase 10 implementation. The decision was made; the alternative was named; the framework is absent by choice.

10.2 Why Position A and not Position B or C

Three positions were considered in the scoping chat (Scope Call 3):

Position A landed. Reasoning:

10.3 Memory-as-sole-write-target as the discipline

Position A's discipline at the operational level: any apparent need to "confirm a Render" or "amend a Render" routes upstream through Memory and Shape. Specifically:

The Render is never edited. The principle's discipline holds.


11. Render-production drift detection — the render_produced firing point and the two-check dispatch

11.1 The firing point

Phase 10 introduces the new firing_point value "render_produced". The drift dispatch fires on every render_produced event for a declared Render (per D4 and Scope Call 7 Resolution C; ad-hoc renders' drift question defers with their other deferrals).

11.2 The two-check dispatch

After a render_produced event lands, the drift dispatcher fires two checks against the produced RenderEvent:


# In src/loomworks/agents/drift_detection.py

async def dispatch_render_produced_drift_checks(
    *,
    render_event: RenderEvent,
    db: AsyncSession,
) -> list[ConsiderationEpisode]:
    """
    Fires Check A' and Check B' against the produced Render. Returns the
    list of ConsiderationEpisodes opened (zero, one, or two).
    Considerations open at the upstream Memory or Shape layer per
    Section 11.5; the Render gets state-transitioned to invalidated if
    any check fires (per the routing-target's closure-driven state
    transition machinery).

    Per D4 and Section 11.7, fires only on declared Renders.
    """
    ...

The dispatcher reads the produced RenderEvent's payload and compares against:

11.3 Per-check semantics: Check A' and Check B'

Check A' — Render-against-rendering-rules. Catches:

Implementation: the specialist's instruction includes a "drift-check hook" section that the dispatcher invokes. The hook reads the rules and the produced render_content and returns either a clean signal or a drift finding with rationale. The hook is part of the specialist's R-A24 instruction; v0.1 specialists carry a default "no-drift-check" hook that returns clean unconditionally. Stub specialists in tests can override with stub_check_a_drift_detected=True to exercise the drift path.

Check B' — Render-against-faithful-Shape-translation. Catches:

Implementation: the dispatcher reads the confirmed ShapeEvent's selected_memory_refs, excluded_but_considered, and produced_shape_content, and compares against the produced render_content per the specialist's drift-check hook. Same hook mechanism as Check A'; v0.1 default returns clean.

11.4 Check C' deferral to Phase 11

Check C' (Render-against-Shape-against-Seed) is the transitive Seed-faithfulness check. It defers to Phase 11 live maintenance per D4 and Scope Call 7. Reasoning: the check is meaningful at production but is also a thing that can change over time (Seed evolves after Render is produced; the meaningful check is periodic, not just at production time). Phase 11's live-maintenance framing has the right shape — periodic re-check, possibly triggered by Seed amendment events — for Check C' to land cleanly. Phase 10 does not pre-build infrastructure for it.

11.5 Drift findings route upstream — to Memory or Shape layer

Per D8 and Memory-as-sole-write-target. When Check A' or Check B' detects drift, the dispatcher does NOT open a consideration on the RenderEvent. Instead:

In both cases, the RenderEvent itself transitions to invalidated via a render_invalidated event. The closing party of the upstream consideration drives the invalidation; the RenderEvent's state transition is captured in the render_invalidated event's payload (specifically the consideration_id and the routing_target metadata).

The closure terminals available on the upstream-routed consideration are Phase 5's existing five-terminal set: attest, amend, retire, escalate, no_change. The consideration framework is unchanged; the new triggering_reasons just route the consideration to the right layer. No new terminals are introduced.

11.6 New triggering_reason values

Two distinct values rather than one with metadata:

Why two values and not one with metadata. Phase 9 used "seed_drift_triggered" for all three checks (A, B, C) at shape production with metadata distinguishing which check. The Phase 10 scoping note specifies "new triggering_reason values" (plural) for the two checks. The substantive reason for two-distinct: the remediation paths differ meaningfully. Rule-conformance findings point at instruction/rules misalignment (remediation: bump instruction version); translation findings point at specialist behaviour (remediation: amend the specialist's instruction or upstream Memory). Distinct triggering_reasons make the remediation context queryable as a top-level dimension rather than a payload metadata field.

Alternative considered and set aside. Single "render_drift_triggered" value with metadata distinguishing checks (Phase 9 parallel). This was set aside because the two checks' remediation surfaces are sufficiently distinct that the triggering_reason itself carries the routing information. Recorded in trajectory; if pre-flight or Operator review prefers Phase 9 strict parallel, easy revision (one value, payload metadata distinguishes).

11.7 Scope: declared Renders only

Per D4 and Position C on Field 11. Drift detection at render time fires only when declared_render_type_ref is non-null on the produced RenderEvent. Since Phase 10 v0.1 rejects ad-hoc requests at the dispatch layer (Section 14), every produced RenderEvent in v0.1 carries a non-null declared_render_type_ref, so the scope distinction is currently moot — but the dispatcher's logic explicitly checks the field as a future-safety measure.

11.8 Configure_render_produced_dispatch all-or-nothing invariant (Phase 9 Note item 3 pattern carry-forward)

Phase 9 implementation notes Note item 3 surfaced the pattern: a dispatch configuration helper that wires multiple checks into the firing point should enforce an all-or-nothing invariant — either all checks are wired or none — so partial-wiring failure modes don't produce silently-degraded drift detection.

Phase 10's configure_render_produced_dispatch follows the pattern:


def configure_render_produced_dispatch(
    *,
    enable_check_a_prime: bool,
    enable_check_b_prime: bool,
) -> None:
    """
    Wires Check A' and/or Check B' into the render_produced firing point.
    Enforces all-or-nothing within Phase 10's two-check scope: either
    both flags are True (wire both; default for Phase 10 production),
    or both flags are False (wire neither; intended for test scenarios
    that need to disable drift dispatch). Mixed configurations
    (one True, one False) raise ConfigurationError.
    """
    ...

Tests that need to exercise individual checks toggle via the specialist's stub_check_X_drift_detected flags rather than via configure_render_produced_dispatch flags, preserving the all-or-nothing invariant at the wiring layer.


12. HTTP surface — render-type declaration, render production request, render read, ingestion candidate list

The HTTP surface set was settled in aggregate across Scope Calls 8 and 9 (scoping note Section 7). All routes Phase 10 ships:


POST   /engagements/{eid}/declared-render-types
GET    /engagements/{eid}/declared-render-types
GET    /engagements/{eid}/declared-render-types/{drt_object_id}
PATCH  /engagements/{eid}/declared-render-types/{drt_object_id}
DELETE /engagements/{eid}/declared-render-types/{drt_object_id}  (soft via state=retired)

POST   /engagements/{eid}/renders                  (explicit_request trigger)
GET    /engagements/{eid}/render-jobs/{jobid}      (job status poll)

GET    /engagements/{eid}/renders                  (within-engagement projection index — R-C13.1)
GET    /engagements/{eid}/renders/{object_id}      (individual render read)
GET    /engagements/{eid}/renders/candidates       (ingestion candidate list — R-C13.2)
POST   /engagements/{eid}/renders/{object_id}/retire   (operator-driven retirement)

12.1 DeclaredRenderType management

CRUD-shaped surface paralleling Phase 9's DeclaredShapeType management. Creation requires consumer_declaration, render_type_name, source_shape_type_ref (must reference an existing DeclaredShapeType), and render_format. Amendment follows R-C7 discipline: the route distinguishes configuration-change amendments from seed-amendment-requiring amendments based on which fields change.

12.2 Render production request — explicit_request trigger


POST   /engagements/{eid}/renders
       Body: {
         confirmed_shape_event_ref: MemoryRef,   # Version-pinned; the version MUST be confirmed
         declared_render_type_ref: MemoryRef,    # Required in Phase 10 v0.1; null is rejected per
                                                  # Section 14 with HTTP 422
         # trigger and triggered_by derived from authenticated contributor
       }
       Returns: 202, {
         render_job_id: UUID,
         render_event_object_id: UUID,           # Pre-allocated; populated on completion
       }
       Authorization: contributor with render-request authority for the engagement.
       Errors:
         - 422 if declared_render_type_ref is null (ad-hoc rejected per Section 14)
         - 422 if confirmed_shape_event_ref does not reference a confirmed ShapeEvent
         - 409 if no render specialist is registered for the
           (engagement, declared_render_type_ref) pair

The request handler returns immediately after enqueueing the render_jobs row; the actual render production happens asynchronously via the BackgroundAgentRunner.dispatch path.

12.3 Auto-trigger composition on shape_confirmed

The auto-trigger path is not a separate HTTP route. It is wired through the post-append hook on shape_confirmed events. When a shape_confirmed event lands in memory_events, the post-append hook invokes RenderDispatchAgent.dispatch_for_shape_confirmed for the engagement. The agent walks declared render-types whose source_shape_type_ref matches the confirmed Shape's declared_shape_type_ref; for each match, it enqueues a render_jobs row with trigger="declared_auto_on_shape_confirmed" and triggered_by=<auto-trigger system actor>. The system actor is a registered ActorRef with actor_kind="system" and actor_id="render_auto_trigger_system".

If no declared render-types match, the auto-trigger walk completes silently (no render is dispatched; this is a valid state).

If a matching declared render-type has no registered specialist, the auto-trigger walk logs the missing-specialist condition and the resulting candidate appears in GET /renders/candidates/ as pending-with-no-specialist.

12.4 Render read / retrieve


GET    /engagements/{eid}/renders
       Query params:
         ?declared_render_type_ref={ref}
         ?state={state}                          (produced | retired | invalidated)
         ?confirmed_shape_event_ref={ref}        (filter by source Shape)
         ?render_format={format}
         ?triggered_by_actor_id={uuid}
         ?from_created_at={timestamp}
         ?to_created_at={timestamp}
         ?limit={int}, ?offset={int}             (pagination; default limit 50)
       Returns: 200, { renders: [RenderEvent payload shape, ...], total_count: int }
       Implementation: queries render_events_view directly;
                       view is projector-maintained from memory_events.

GET    /engagements/{eid}/renders/{object_id}
       Query params:
         ?version={int}                          (default: latest)
       Returns: 200, full RenderEvent with all fields
                      (reads render_events_view; falls back to event-log walk
                      if a historical version is requested via ?version=)

R-C13.1 within-engagement is satisfied by GET /renders/. Cross-engagement R-C13.1 is deferred per the engagement-Memory boundary (Section 22).

12.5 Ingestion candidate list (R-C13.2)


GET    /engagements/{eid}/renders/candidates
       Query params:
         ?declared_render_type_ref={ref}        (filter by render-type)
       Returns: 200, {
         candidates: [
           {
             confirmed_shape_event_ref: MemoryRef,
             declared_render_type_ref: MemoryRef,
             registered_specialist_ref: ActorRef | null,
             reason: "no_registered_specialist" | "auto_trigger_pending" | "auto_trigger_failed",
             since: timestamp,
           },
           ...
         ]
       }
       Implementation: computed-at-query-time per Scope Call 9 Resolution A.
                       Joins current_memory_objects (for confirmed Shapes), the
                       seed's declared_render_types (for declared types), and
                       render_events_view (for existing Renders) to compute
                       candidates that have not yet produced a Render.

The computed-at-query-time approach is consistent with convention-first storage discipline — no new persistent state is introduced. If Phase 11 or Phase 12 surfaces a high-throughput need that justifies a materialized candidate view, the upgrade is a straightforward Path C addition (projector-maintained render_candidates_view).

12.6 Rejection-with-hook for ad-hoc requests

When POST /engagements/{eid}/renders arrives with declared_render_type_ref null, the handler returns:


HTTP 422 Unprocessable Entity
{
  "error": "ad_hoc_render_not_supported",
  "message": "Phase 10 dispatches declared renders only. Ad-hoc renders are a recognized future capability path documented in RenderDispatchAgent's instruction. To request this render, declare a render-type in the engagement's seed first.",
  "documentation_ref": {
    "instruction_set_ref": ...,                  # The dispatch agent's instruction set
    "section": "ad_hoc_handling"
  }
}

The hook documentation in the dispatch agent's instruction (referenced by documentation_ref) describes the future ad-hoc path so that a future capability work has a clear extension target.


13. Migration chain, render_events_view projection, and storage pattern

Phase 10 produces four Alembic migrations, one per logical concern. Migration numbers reflect Phase 9 ending at 0020; pre-flight at Step 0 confirms and renumbers if the baseline differs.

13.1 Migration 0021 — MemoryObject type registration (empty placeholder)


# migrations/versions/0021_phase_10_memory_object_type_registration.py

def upgrade():
    # Empty placeholder per Phase 3 migration 0006 pattern.
    # DeclaredRenderType and RenderEvent live in the existing memory_events
    # and current_memory_objects tables; object_type registration is a
    # code-level Literal extension in src/loomworks/engagement/types.py.
    # This migration records the object_type introduction in the lineage.
    pass

def downgrade():
    pass

13.2 Migration 0022 — render_events_view projector-derived table and render_jobs operational table


# migrations/versions/0022_phase_10_render_events_view_and_render_jobs.py

import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql


def upgrade():
    # render_events_view — projector-maintained; derived from memory_events;
    # can be regenerated at any time per Path C discipline.
    # NOT a canonical write target.
    op.create_table(
        "render_events_view",
        # Identity (matches MemoryObject identity)
        sa.Column("object_id", sa.UUID, primary_key=True),
        sa.Column("current_version", sa.Integer, nullable=False),
        sa.Column("engagement_id", sa.UUID, sa.ForeignKey("engagements.id"), nullable=False),

        # Provenance — single confirmed Shape per D11 (no chaining)
        sa.Column("confirmed_shape_event_object_id", sa.UUID, nullable=False),
        sa.Column("confirmed_shape_event_version", sa.Integer, nullable=False),

        # Producer attribution
        sa.Column("render_specialist_actor_id", sa.UUID, nullable=False),
        sa.Column("render_specialist_actor_kind", sa.Text, nullable=False),
        sa.Column("render_specialist_instruction_version", sa.Integer, nullable=False),

        # Trigger attribution
        sa.Column("trigger", sa.Text, nullable=False),  # explicit_request | declared_auto_on_shape_confirmed
        sa.Column("triggered_by_actor_id", sa.UUID, nullable=False),
        sa.Column("triggered_by_actor_kind", sa.Text, nullable=False),

        # Declaration anchor — Position C nullable
        sa.Column("declared_render_type_object_id", sa.UUID, nullable=True),
        sa.Column("declared_render_type_version", sa.Integer, nullable=True),

        # Format
        sa.Column("render_format", sa.Text, nullable=False),

        # Lifecycle state — minimal per Position A
        sa.Column("state", sa.Text, nullable=False),  # produced | retired | invalidated

        # Content (JSONB; payload-only carry-forward per Phase 8 CR v0.4 Finding I)
        sa.Column("render_content", postgresql.JSONB, nullable=False),

        # Rendering rules (Position B v0.1; null in v0.1; field exists for upgrade-compatibility)
        sa.Column("rendering_rules_object_id", sa.UUID, nullable=True),
        sa.Column("rendering_rules_version", sa.Integer, nullable=True),

        # Projector metadata
        sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=False),
        sa.Column("last_updated_at", sa.TIMESTAMP(timezone=True), nullable=False),
        sa.Column("last_event_id", sa.UUID, nullable=False),  # references memory_events.event_id; no FK
    )
    op.create_index("ix_render_events_view_engagement_state",
                    "render_events_view",
                    ["engagement_id", "state"])
    op.create_index("ix_render_events_view_engagement_declared_type",
                    "render_events_view",
                    ["engagement_id", "declared_render_type_object_id"])
    op.create_index("ix_render_events_view_engagement_shape",
                    "render_events_view",
                    ["engagement_id", "confirmed_shape_event_object_id"])
    op.create_index("ix_render_events_view_engagement_format",
                    "render_events_view",
                    ["engagement_id", "render_format"])
    op.create_index("ix_render_events_view_render_content",
                    "render_events_view",
                    ["render_content"],
                    postgresql_using="gin")

    # render_jobs — operational table for RenderDispatchAgent dispatch
    op.create_table(
        "render_jobs",
        sa.Column("id", sa.UUID, primary_key=True),
        sa.Column("engagement_id", sa.UUID, sa.ForeignKey("engagements.id"), nullable=False),
        sa.Column("confirmed_shape_event_ref", postgresql.JSONB, nullable=False),
        sa.Column("declared_render_type_ref", postgresql.JSONB, nullable=True),
        sa.Column("render_specialist_ref", postgresql.JSONB, nullable=False),
        sa.Column("trigger", sa.Text, nullable=False),
        sa.Column("triggered_by", postgresql.JSONB, nullable=False),
        sa.Column("status", sa.Text, nullable=False, server_default="queued"),
        sa.Column("render_event_object_id", sa.UUID, nullable=True),  # no FK; references MemoryObject.object_id
        sa.Column("error_message", sa.Text, nullable=True),
        sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=False),
        sa.Column("dispatched_at", sa.TIMESTAMP(timezone=True), nullable=True),
        sa.Column("completed_at", sa.TIMESTAMP(timezone=True), nullable=True),
    )
    op.create_index("ix_render_jobs_engagement_status", "render_jobs", ["engagement_id", "status"])


def downgrade():
    op.drop_index("ix_render_jobs_engagement_status", table_name="render_jobs")
    op.drop_table("render_jobs")
    op.drop_index("ix_render_events_view_render_content", table_name="render_events_view")
    op.drop_index("ix_render_events_view_engagement_format", table_name="render_events_view")
    op.drop_index("ix_render_events_view_engagement_shape", table_name="render_events_view")
    op.drop_index("ix_render_events_view_engagement_declared_type", table_name="render_events_view")
    op.drop_index("ix_render_events_view_engagement_state", table_name="render_events_view")
    op.drop_table("render_events_view")

13.3 Migration 0023 — firing_point and triggering_reason Literal extensions

Per Step 0 pre-flight (TEXT-vs-ENUM confirmation): firing_point and triggering_reason columns are TEXT with application-level Literal validation (Phase 9 confirmation). The Literal extension lands in engagement/types.py plus the api/schemas.py mirror sites at Step 4. This migration records the extension for provenance.


# migrations/versions/0023_phase_10_firing_point_and_triggering_reason_extensions.py

def upgrade():
    # firing_point and triggering_reason columns are TEXT with application-level
    # Literal validation. This migration records the Literal extension for provenance:
    #   firing_point adds: "render_produced"
    #   triggering_reason adds: "render_rule_conformance_drift", "render_translation_drift"
    # The Literal extension itself lives in code (engagement/types.py and the
    # api/schemas.py mirror sites per Section 5.2).
    pass

def downgrade():
    pass

13.4 Migration 0024 — declaresRenderType vocabulary extension record


# migrations/versions/0024_phase_10_vocabulary_extension.py

def upgrade():
    # Relationship.vocabulary Literal extension. The extension itself
    # lives in engagement/types.py and the three mirror sites per Section 5.2
    # and Section 6.3. This migration records the introduction in the lineage.
    pass

def downgrade():
    pass

13.5 Projector contract for render_events_view (Path C precedent continued)

The projector's apply_event_to_render_events_view extension follows the contract established by Phase 9's apply_event_to_shape_events_view:

The rebuild_render_events_view operational helper (per Phase 9 pattern) replays memory_events to reproduce view state. Test coverage in test_render_events_view_projection.py:

8 tests total in this file.

13.6 Cross-migration ordering

0021 → 0022 → 0023 → 0024 in sequence. Pre-flight at Step 0 confirms alembic upgrade head cleanly applies all four; alembic downgrade -4 cleanly reverses.

13.7 Rationale for four migrations

One migration per logical concern, per the Phase 9 pattern (and Phase 8 Finding F4 carry-forward — per-concern-per-migration):

Merging concerns into fewer migrations was set aside per the per-concern discipline; four migrations is the right granularity.


14. Ad-hoc render handling — reject-with-documented-hook

14.1 The rejection path

Per D7 and Section 8.5. When an explicit_request arrives with declared_render_type_ref=None, RenderDispatchAgent raises AdHocRenderNotSupportedError. The HTTP layer (Section 12.6) translates to HTTP 422 with a structured error body explaining the rejection and pointing to the documented hook.

Auto-trigger requests cannot be ad-hoc by construction — auto-trigger walks declared_render_types from the seed, so the resulting render request always carries a non-null declared_render_type_ref. The ad-hoc path is exclusively an explicit-request concern.

14.2 The hook in RenderDispatchAgent's instruction

The dispatch agent's per-engagement R-A24 instruction set carries an ad_hoc_handling section that describes:

The hook is documentation in the instruction, not active behavior. Phase 10 v0.1 ships with the documentation; the activation is a future-phase concern.

14.3 Discovery upgrade path

If the Render discovery resolves that:

In each case, the Phase 10 v0.1 reject-with-hook posture provides a clean upgrade target: the documented hook in the instruction is the location where new capability documentation gets written, and the rejection-error path in the HTTP layer is replaced with the new active path.


15. Test infrastructure

15.1 Session autobegin pattern (Phase 5/9 carry-forward)

After a full_engagement fixture has driven HTTP traffic on the shared db session, the session is in an autobegun transaction. Direct library calls from the same test body must NOT use async with db.begin(): wrappers. Phase 10 tests inherit the convention.

15.2 Deterministic time

Where a test depends on render_produced events ordering strictly — and especially in the auto-trigger composition path where shape_confirmed and render_produced events land in close temporal proximity — uses a fixed-clock fixture (Phase 7 pattern). Avoids timestamp-based flakiness.

15.3 Stub and live mode switching

The StubRenderSpecialist's stub_render_content field is the primary test override. Live-mode LLM calls inside specialists are guarded by ANTHROPIC_API_KEY per the existing environment-gate discipline. Tests that exercise the full render pipeline with a live specialist are gated; unit and integration tests use stubs.

The drift-check stubs (stub_check_a_drift_detected, stub_check_b_drift_detected on the StubRenderSpecialist) drive the drift-detection test paths deterministically without depending on actual LLM-based check evaluation.

15.4 Carry-forward regression fixture

test_render_carry_forward_regression.py specifically tests each new metadata field threading through object lifecycles and through the projector. Per Phase 8 CR v0.4 Finding I and Phase 9 Section 15.4 pattern:

8 tests total in this file (one per check + one per event-kind round-trip + one per payload-only-field carry-forward).


16. Acceptance test suite

The acceptance requirements map Phase 10 scope (R-C3 + the Render-relevant parts of R-C9.2 and R-C13 under settlement 1B) to a pytest suite. Per Phase 9 Finding F, no aggregate test count projection appears; per-file projections in Section 16.4 are authoritative.

16.1 Per-spec-requirement coverage

16.2 Construction-decision coverage

16.3 Memory-as-sole-write-target Render-extension acceptance test

test_render_drift_routes_upstream_acceptance.py::test_drift_routes_upstream_and_render_invalidated — the load-bearing acceptance test for the Render-layer extension of the principle. Walks:


@pytest.mark.asyncio
async def test_drift_routes_upstream_and_render_invalidated(
    db_session,
    full_engagement_with_shape_type_and_render_type,
    contributor_actor,
    stub_shaping_agent_with_content,
    stub_render_specialist_with_check_b_drift,
):
    # Step 1: Produce a Shape and confirm it.
    shape_event = await produce_and_confirm_shape(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        declared_shape_type_ref=full_engagement_with_shape_type_and_render_type.shape_type_ref,
        triggered_by=contributor_actor,
        db=db_session,
    )
    assert shape_event.state == "confirmed"

    # Step 2: Auto-trigger fires; render specialist produces RenderEvent.
    # Check B' drift detected (specialist hallucinated content per stub).
    render_events = await wait_for_render_events_for_shape(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        confirmed_shape_event_ref=shape_event.ref,
        db=db_session,
    )
    assert len(render_events) == 1
    render_event_v1 = render_events[0]
    assert render_event_v1.state == "produced"

    # Step 3: Drift consideration opened upstream (at Shape layer per Check B' routing).
    drift_considerations = await list_considerations(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        firing_point="render_produced",
        triggering_reason="render_translation_drift",
        db=db_session,
    )
    assert len(drift_considerations) == 1
    consideration = drift_considerations[0]
    # Routing target metadata identifies the Shape layer
    assert consideration.metadata["routing_target"] == "shape_layer"
    assert shape_event.ref in consideration.assertions_in_scope

    # Step 4: Close upstream consideration with `amend`. Render is NOT edited.
    closed = await close_consideration(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        consideration_id=consideration.id,
        terminal_state="amend",
        closing_party=contributor_actor,
        remediation_intent="specialist hallucinated; new Memory needed to constrain interpretation",
        db=db_session,
    )
    assert closed.terminal_state == "amend"

    # Step 5: render_invalidated event lands; RenderEvent state transitions.
    view_row_v1 = await query_render_events_view(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        object_id=render_event_v1.object_id,
        db=db_session,
    )
    assert view_row_v1.state == "invalidated"

    # Verify the Render content was NOT edited (Memory-as-sole-write-target)
    events = await list_memory_events_for_object(render_event_v1.object_id, db=db_session)
    produced_event = [e for e in events if e.event_kind == "render_produced"][0]
    invalidated_event = [e for e in events if e.event_kind == "render_invalidated"][0]
    assert produced_event.payload["render_content"] == render_event_v1.render_content
    # Carry-forward: invalidated event preserves payload-only fields
    assert invalidated_event.payload["render_content"] == render_event_v1.render_content
    assert invalidated_event.payload["consideration_id"] == consideration.id

    # Step 6: New Memory lands through normal paths (simulate via assertion commit).
    new_assertion = await add_and_commit_assertion(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        content="constraining-interpretation assertion",
        actor=contributor_actor,
        db=db_session,
    )

    # Step 7: Produce a new Shape and confirm it.
    shape_event_v2 = await produce_and_confirm_shape(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        declared_shape_type_ref=full_engagement_with_shape_type_and_render_type.shape_type_ref,
        triggered_by=contributor_actor,
        db=db_session,
    )
    assert shape_event_v2.engagement_version_at_production > shape_event.engagement_version_at_production

    # Step 8: Auto-trigger fires; this time no drift (stub flag flipped).
    render_events_v2 = await wait_for_render_events_for_shape(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        confirmed_shape_event_ref=shape_event_v2.ref,
        db=db_session,
        stub_check_b_drift_detected=False,
    )
    assert len(render_events_v2) == 1
    render_event_v2 = render_events_v2[0]
    assert render_event_v2.state == "produced"

    # No drift considerations on the new Render
    drift_considerations_v2 = await list_considerations(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        firing_point="render_produced",
        confirmed_shape_event_ref=shape_event_v2.ref,
        db=db_session,
    )
    assert len(drift_considerations_v2) == 0

    # View reflects both Renders in their terminal states
    view_row_v1_final = await query_render_events_view(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        object_id=render_event_v1.object_id,
        db=db_session,
    )
    view_row_v2 = await query_render_events_view(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        object_id=render_event_v2.object_id,
        db=db_session,
    )
    assert view_row_v1_final.state == "invalidated"
    assert view_row_v2.state == "produced"

    # Path C robustness: after regenerating the view from scratch, state matches
    await rebuild_render_events_view(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        db=db_session,
    )
    view_row_v1_after_rebuild = await query_render_events_view(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        object_id=render_event_v1.object_id,
        db=db_session,
    )
    view_row_v2_after_rebuild = await query_render_events_view(
        engagement_id=full_engagement_with_shape_type_and_render_type.id,
        object_id=render_event_v2.object_id,
        db=db_session,
    )
    assert view_row_v1_after_rebuild.state == "invalidated"
    assert view_row_v2_after_rebuild.state == "produced"

The regeneration step is load-bearing for Path C: it asserts that memory_events carries enough information that render_events_view can be rebuilt exactly.

A second, smaller acceptance test test_render_pipeline_acceptance.py::test_full_pipeline_through_render exercises the no-drift happy path end-to-end (Memory → Shape confirmation → auto-trigger → Render produced → query via view) without the drift remediation cycle, providing the lighter R-C4 three-object-relationship coverage.

16.4 Test count and per-file projections (no aggregate, per Phase 9 Finding F)

| Step | New test file(s) | Tests projected | |------|------------------|-----------------| | 1 | test_declared_render_type_model.py, test_render_event_model.py | 7 + 9 | | 3 | test_render_events_view_projection.py | 8 | | 4 | test_vocabulary_extension_phase_10.py | 5 | | 6 | test_render_dispatch_agent.py | 10 | | 7 | test_render_specialist_dispatch.py | 6 | | 8 | test_render_production_orchestration.py | 5 | | 9 | test_render_produced_drift_dispatch.py | 8 | | 10 | test_render_produced_dispatch_integration.py | 6 | | 11 | test_render_production_http.py | 16 | | 12 | test_ad_hoc_render_rejection.py | 5 | | 13 | test_render_carry_forward_regression.py | 8 | | 14 | test_render_pipeline_acceptance.py | 1 | | 14 | test_render_drift_routes_upstream_acceptance.py | 1 |

Per-file projections only, per Phase 9 Finding F discipline. Phase 9's projection-vs-execution drift (~17%) is project steady state; per-file slack of ~20% is anticipated, not projected as an aggregate ceiling.


17. Order of operations (steps with checkpoints — auto-mode posture declared)

Eighteen steps. Three checkpoints (A after Step 4, B after Step 10, C after Step 16). Step ordering follows Phase 9's pattern with the projector extension as its own early step (Phase 9 Finding B carry-forward: a projection that is part of the lifecycle lands as its own early step).

Auto-mode posture (per D9 and Section 3.3):

Step 0 — Pre-flight ground-truth checks and CR archival.

CC verifies the codebase ground-truth from Section 3.2: projector module path; migration starting number (confirms 0021 or renumbers); current_memory_objects schema; TEXT-vs-ENUM for firing_point and triggering_reason; agent registry shape; Phase 9 ShapingAgent class location. CR archival per Section 3.5: this CR commits to docs/phase-crs/phase-10-cr-render-v0_1.md at this step, before Step 1 begins. Any divergence from expected ground-truth stops execution and drives a CR v0.2 revision.

Commit: Phase 10 step 0: pre-flight checks and CR archival.

Step 1 — Migration 0021 (empty placeholder) and types.py Literal extensions for new MemoryObject types.

Create migration 0021 as empty placeholder. Extend src/loomworks/engagement/types.py with "declared_render_type" and "render_event" object_type Literal values and the two new MemoryObject classes per Section 6.1 and 6.2. Run alembic upgrade head. Write test_declared_render_type_model.py and test_render_event_model.py round-trip tests (memory_events + current_memory_objects, parallel to Phase 3's Assertion round-trip and Phase 9's DeclaredShapeType round-trip).

Commit: Phase 10 step 1: DeclaredRenderType and RenderEvent MemoryObject types.

Step 2 — Migration 0022 (render_events_view + render_jobs).

Create migration 0022 per Section 13.2. Run alembic upgrade head. No tests in this step (table existence is verified by Step 3's projector tests).

Commit: Phase 10 step 2: render_events_view and render_jobs schema.

Step 3 — Projector extension for render_events_view.

Extend src/loomworks/memory/projector.py with apply_event_to_render_events_view per Section 13.5. Add _RENDER_EVENT_UPSERTING_KINDS set. Implement rebuild_render_events_view operational helper. Write test_render_events_view_projection.py per Section 13.5's coverage (8 tests).

Commit: Phase 10 step 3: projector extension for render_events_view.

Step 4 — Migrations 0023 and 0024 + Literal extensions + vocabulary.

Create migration 0023 (firing_point and triggering_reason record). Create migration 0024 (vocabulary record). Extend Literals in src/loomworks/engagement/types.py:

Mirror sites in src/loomworks/api/schemas.py per Phase 9 Note item 1 (explicit checklist):

Run alembic upgrade head. Write test_vocabulary_extension_phase_10.py (5 tests including the Pydantic-validation test exercising the response shapes per the Phase 9 Note item 1 lesson).

Commit: Phase 10 step 4: Literal extensions, vocabulary, mirror sites.

Checkpoint A. Auto-mode-proceed.

Step 5 — RenderDispatchAgent class.

Create src/loomworks/agents/render_dispatch.py per Section 8. Implement the lookup mechanism, the auto-trigger walk, the explicit-request path, the reject-with-hook path. Write test_render_dispatch_agent.py (10 tests covering registration, lookup, auto-trigger walk, explicit-request, reject-with-hook, failure modes).

Commit: Phase 10 step 5: RenderDispatchAgent.

Step 6 — Render specialist base class and registration.

Create src/loomworks/agents/render_specialist.py per Section 9. Implement RenderSpecialist base class plus StubRenderSpecialist for tests. Write test_render_specialist_dispatch.py (6 tests covering specialist instantiation, registration keying, instruction-version pinning, the produce_render contract, stub mode, failure-mode signaling).

Commit: Phase 10 step 6: render specialist base class and registration.

Step 7 — Render production orchestration.

Create src/loomworks/engagement/render.py per Section 8. Implement the orchestration helpers (open_render_production, _advance_render_state, _invalidate_render, _retire_render). Write test_render_production_orchestration.py (5 tests).

Commit: Phase 10 step 7: render production orchestration.

Step 8 — Two-check drift dispatch at render_produced firing point.

Extend src/loomworks/agents/drift_detection.py with dispatch_render_produced_drift_checks per Section 11. Implement Check A' and Check B' invocation through the specialist's drift-check hook. Implement the configure_render_produced_dispatch all-or-nothing helper per Section 11.8. Implement the upstream-routing pattern per Section 11.5. Write test_render_produced_drift_dispatch.py (8 tests covering each check individually, both together, both clean, the all-or-nothing invariant, the upstream-routing-target metadata, and one negative test for the configure invariant).

Commit: Phase 10 step 8: two-check drift dispatch at render_produced.

Step 9 — Auto-trigger composition on shape_confirmed.

Wire the post-append hook on shape_confirmed events to invoke RenderDispatchAgent.dispatch_for_shape_confirmed. Implement the system actor for auto-trigger triggered_by. Write test_render_produced_dispatch_integration.py (6 tests covering the full auto-trigger composition path including the shape_confirmed → render_jobs queued → specialist invoked → render_produced sequence).

Commit: Phase 10 step 9: auto-trigger composition on shape_confirmed.

Step 10 — HTTP routers for render-type and render management.

Create src/loomworks/api/routers/renders.py per Section 12. Implement DeclaredRenderType CRUD, render production POST, render-jobs status GET, render index GET, individual render GET, candidates GET, retirement POST, ad-hoc rejection 422 path. Register the router in src/loomworks/api/routers/__init__.py. Write test_render_production_http.py (16 tests covering happy paths, sad paths, the ad-hoc rejection structure, the candidate-list computation, query filtering, pagination).

Commit: Phase 10 step 10: HTTP routers for render management.

Checkpoint B. Auto-mode-proceed.

Step 11 — Ad-hoc rejection test battery.

Write test_ad_hoc_render_rejection.py per Section 14 (5 tests covering the rejection error, the structured response body, the documented hook reference, the auto-trigger-cannot-be-ad-hoc invariant, the future-upgrade-path documentation).

Commit: Phase 10 step 11: ad-hoc rejection test battery.

Step 12 — Carry-forward regression test battery.

Write test_render_carry_forward_regression.py per Section 15.4 (8 tests including the payload-only field carry-forward per Phase 8 CR v0.4 Finding I).

Commit: Phase 10 step 12: carry-forward regression for new metadata.

Step 13 — Acceptance test suite — full pipeline.

Write test_render_pipeline_acceptance.py::test_full_pipeline_through_render per Section 16.3's lighter happy-path coverage.

Commit: Phase 10 step 13: acceptance — full pipeline through Render.

Step 14 — Acceptance test suite — drift routes upstream and Render invalidated.

Write test_render_drift_routes_upstream_acceptance.py::test_drift_routes_upstream_and_render_invalidated per Section 16.3's load-bearing remediation-loop coverage including the post-regeneration invariant check.

Commit: Phase 10 step 14: acceptance — drift routes upstream and Render invalidated.

Step 15 — Full suite run.

Run uv run pytest -v. Expected: per-file projections from Section 16.4 sum approximately, with ~20% per-file slack acknowledged. Any failures stop execution.

Commit: Phase 10 step 15: full suite green.

Step 16 — Implementation notes.

Write docs/phase-impl-notes/phase-10-implementation-notes-v0_1.md — the record of what execution surfaced that warrants preservation. The CR was already archived at Step 0 per Section 3.5; Step 16 does not re-archive.

Commit: Phase 10 step 16: implementation notes.

Checkpoint C. HALT until Operator confirms. Operator reviews suite state, implementation notes, and any execution-time surprises against the CR. Operator approves the tag.

Step 17 — Tag.

git tag -a phase-10-render -m "Phase 10 complete — Render under 1B, Position C on Field 11, Path C continued, declared-renders only with reject-with-hook for ad-hoc". Push.

Verification: git tag -l phase-10-* lists the tag.


18. Acceptance gate

All of the following must be true before Phase 10 is declared complete:

  1. uv run pytest -v passes all tests with only the expected environment-gated skip(s).
  2. DeclaredRenderType round-trips through the types.py discriminated union, memory_events, and current_memory_objects.
  3. RenderEvent round-trips through the types.py discriminated union, memory_events, current_memory_objects, and render_events_view with full birth-certificate fields populated.
  4. A render specialist registered against a DeclaredRenderType produces a RenderEvent via the render_jobs dispatch path; the render_produced event appends to memory_events; the projector propagates to render_events_view.
  5. RenderDispatchAgent correctly walks declared_render_types on shape_confirmed events and dispatches matching specialists; auto-trigger composition end-to-end.
  6. RenderDispatchAgent correctly handles explicit_request via POST /renders/ with a non-null declared_render_type_ref.
  7. Ad-hoc requests (declared_render_type_ref null) are rejected with HTTP 422 and the structured error body per Section 12.6 and Section 14.
  8. The two-check drift dispatch fires at the render_produced firing point; Check A' and Check B' can detect drift independently or together; up to two upstream considerations open per Render production; configure_render_produced_dispatch enforces the all-or-nothing invariant per Section 11.8.
  9. Drift findings route upstream — considerations open at Memory or Shape layer with triggering_reason = "render_rule_conformance_drift" (Check A') or "render_translation_drift" (Check B') per Section 11.5; routing_target metadata is correct.
  10. RenderEvent state transitions through producedretired (operator-driven) and producedinvalidated (drift-triggered); transitions appear in both memory_events and render_events_view.
  11. Vocabulary extension (declaresRenderType) present at all four sites and persisted; triggering_reason Literal extensions for "render_rule_conformance_drift" and "render_translation_drift" present at both engagement/types.py and the api/schemas.py mirror sites per Section 5.2; firing_point Literal extension for "render_produced" present at both sites.
  12. Migration chain cleanly upgrades and downgrades (executed numbers per Step 0 pre-flight; default 0021 → 0024).
  13. HTTP routes return correct status codes for per-endpoint happy and sad paths including the ad-hoc rejection 422 path.
  14. R-B28 dismissal-pattern query (Phase 5 carry-forward) returns correct aggregations when filtered by firing_point = "render_produced".
  15. test_render_drift_routes_upstream_acceptance::test_drift_routes_upstream_and_render_invalidated passes end-to-end including the post-regeneration invariant check.
  16. test_render_pipeline_acceptance::test_full_pipeline_through_render passes end-to-end (no drift; happy path).
  17. Carry-forward regression tests pass for firing_point, two triggering_reason values, three writer-path Render lifecycle event kinds, projector propagation, and payload-only field carry-forward per Phase 8 CR v0.4 Finding I.
  18. render_events_view can be regenerated from memory_events alone and produces the same state as uninterrupted operation.
  19. CR archived to docs/phase-crs/phase-10-cr-render-v0_X.md at Step 0 per Section 3.5 (corrects Phase 9 Finding A).
  20. Implementation notes file written and committed at Step 16.
  21. Tag phase-10-render pushed (after Checkpoint C Operator confirmation).

19. Post-CR state

After Phase 10 lands:

The Loomworks engineering substrate still cannot (deferred per 1B):


20. Dependencies and related changes

Depends on:

Enables:

Related future changes anticipated:


21. Kickoff prompt for the Claude Code session


> Read the Change Request document at the path I supply below. This is
> CR-2026-021 v0.1, the Phase 10 Change Request (first version; no prior
> Phase 10 CR exists). You are the executing agent named in the CR.
>
> CR path: ~/Downloads/phase-10-cr-render-v0_1.md
> (confirm the latest approved version if more than one is present in
> Downloads).
>
> v0.1 drafts against the Phase 10 scoping note v0.1
> (docs/phase-scoping-notes/loomworks-phase-10-scoping-note-v0_1.md)
> which resolved ten scope calls including the architectural commitment
> to Position C on Field 11 (declared_render_type_ref nullable; Phase 10
> v0.1 builds declared-render path only with reject-with-documented-hook
> for ad-hoc).
>
> Code baseline: tag phase-9-shaping at HEAD 6086fe9 plus today's
> main-branch landings (docs-archival-v0_1 migration, Phase 8 CR v0.4,
> Phase 9 CR v0.3, standing notes, Phase 10 scoping note v0.1).
>
> Run pre-flight (Step 0) per Section 3.2. The Step 0 checklist
> includes: projector module path; migration starting number (default
> 0021; renumber if baseline differs); current_memory_objects schema;
> TEXT-vs-ENUM for firing_point and triggering_reason columns; agent
> registry shape; Phase 9 ShapingAgent class location.
>
> Per Section 3.5 (corrects Phase 9 Finding A): archive this CR to
> docs/phase-crs/phase-10-cr-render-v0_1.md at Step 0 before Step 1
> begins. The standing-note discipline at
> loomworks-standing-note-versioned-document-archival-v0_2.md governs
> the archival.
>
> Per Section 17, eighteen steps with three checkpoints. Auto-mode
> posture: A and B accept auto-mode-proceed; C halts until Operator
> confirms. Pre-flight surprises (Section 3.2 ground-truth divergence)
> stop execution at Step 0 and drive a CR v0.2 revision; do not proceed
> through divergence.
>
> Per Section 5.2: api/schemas.py mirror sites for the new Literals
> (TriggeringReasonLit + ConsiderationEpisodeResponse inline,
> FiringPointLit + ConsiderationEpisodeResponse inline,
> AddRelationshipRequest.vocabulary) land in Step 4 explicitly. The
> Phase 9 Note item 1 lesson is built into the Step 4 checklist.
>
> Per Section 16.4: per-file test projections only; ~20% per-file slack
> is anticipated; aggregate projection is not declared per Phase 9
> Finding F.
>
> Implementation notes at Step 16: docs/phase-impl-notes/
> phase-10-implementation-notes-v0_1.md absorbs execution-time
> surprises and findings; CR v0.2 (if needed for absorbed deficiencies)
> drafts after the tag, archival-accurate per the post-execution CR
> pattern (manifest v0.3 Section 3 Entry 10).

22. What this CR does not specify (deferred to later phases or out of scope)


23. Findings surfaced during CR drafting

These are CR-drafting-time observations the drafter wants the Operator and CC to know before execution begins. They do not block execution.

F1 — Two-distinct-triggering_reason vs single-with-metadata. Phase 10 lands two distinct triggering_reason values (render_rule_conformance_drift and render_translation_drift) per scoping note Section 5 Scope Call 7's plural language and per the substantive remediation-path-distinction argument in Section 11.6. Phase 9 used a single seed_drift_triggered value with metadata distinguishing the three checks. If Operator review or pre-flight prefers Phase 9 strict parallel (one value with metadata), revision is straightforward — collapse to one value, push check identity into payload. The two-distinct landing reads as the cleaner fit but is an interpretive call.

F2 — Position B v0.1 landing on rendering_rules_ref. Phase 10 carries rules in the render specialist's R-A24 instruction (Position B). The rendering_rules_ref field exists on RenderEvent and DeclaredRenderType but is null in v0.1. If the Render discovery's specialist-conformance work surfaces a position on rules ownership that pushes toward Position A (separate object) or Position C (hybrid), the field already exists for upgrade. Recorded here as a known v0.1 simplification.

F3 — Three writer-path event kinds vs Phase 9's four. Phase 10 has three writer-path Render lifecycle event kinds (render_produced, render_retired, render_invalidated); no projector-reserved fifth name. Position A on Scope Call 3 (no Render-layer confirmation framework) is the reason — the Phase 9 generic-vs-specific consideration-event-kind tension that produced the projector-reserved shape_confirmation_opened does not arise at the Render layer. If Position C on Scope Call 3 is later picked up, a render_confirmation_opened projector-reserved name would be added at that point.

F4 — Auto-trigger composition cost is the v0.1's most likely surprise area. The post-append hook on shape_confirmed → RenderDispatchAgent.dispatch_for_shape_confirmed → render_jobs queue → BackgroundAgentRunner pickup → specialist invocation → render_produced event chain has more wiring than Phase 9's explicit-request-only path. Pre-flight or Step 9 may surface that the auto-trigger composition is heavier than the scoping note's middle resolution anticipated. Fallback: drop to explicit-request only and defer auto-trigger composition to a later phase. The fallback is recorded in scoping note Section 5 Scope Call 2.

F5 — Two acceptance tests rather than one. Phase 9 had one big acceptance test. Phase 10 splits into two: test_render_pipeline_acceptance.py (no-drift happy path, lighter) and test_render_drift_routes_upstream_acceptance.py (drift remediation cycle, the load-bearing one). The split reads as cleaner because the two test the two halves of Memory-as-sole-write-target's Render-layer extension separately. If Operator review prefers one combined test, easy revision.

F6 — CR-archival-at-Step-0 discipline first applied here. Per Phase 9 implementation notes Finding A, Phase 10 archives the CR at Step 0 (Section 3.5 and Step 0 of Section 17) rather than at Step 16 alongside implementation notes. This is the first phase to apply the discipline at Step 0. The standing note loomworks-standing-note-versioned-document-archival-v0_2.md governs.

F7 — Render discovery state was queried twice during scoping; both queries returned no settled position. Scoping note Section 4 records this. The CR's design choices are made under the defer-with-upgrade-compatibility posture. None of the discovery's possible future resolutions can break Phase 10's data model or framework, but several recorded pickup-points (Section 10 of the scoping note) become live work whenever the discovery resolves.


24. Changes from prior versions

v0.1 (2026-04-22). First version of Phase 10 CR. No prior Phase 10 CR exists; v0.1 is the trajectory's starting point. Drafts against loomworks-phase-10-scoping-note-v0_1.md which resolved ten scope calls in a single scoping chat on 2026-04-21 to 2026-04-22.

Discovery-record trajectory preserved through scoping note. The ten scope calls in scoping note Section 5 each record alternatives considered and set aside alongside the landed resolution. This CR's construction decisions (Section 4) cite the corresponding scope calls; the trajectory is one document level deeper but is not lost.

Phase 9 implementation-notes findings absorbed proactively at v0.1 (rather than as v0.2 deficiencies):

These absorptions are the "small updates where the Phase 10 scoping note surfaced a reason to update" plus "deficiencies-anticipated-at-v0.1-rather-than-deferred-to-v0.2-or-archival-v0.N" pattern. The CR drafter (Claude) made these absorptions in the v0.1 draft; Operator review and CC pre-flight may surface additional items.


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-021 — Phase 10: Render — RenderEvent as MemoryObject derivation, RenderDispatchAgent and render specialists, and the render-produced drift firing point — v0.1 — 2026-04-22