DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path inspection-briefs/loomworks-b6-step-0-findings-v0_1.md

Loomworks — B-6 Step 0 findings — v0.1

Version. 0.1 Date. 2026-08-03 Executed by. Claude Code on DUNIN7-M4, one session. Brief. inspection-briefs/loomworks-b6-step-0-inspection-brief-v0_1.md — confirmed to exist before any check, and the highest version present (numeric sort; v0.1 is the only one). Charter. standing-notes/dunin7-standing-authorization-charter-v0_1 — R-5. Status. Complete. Five questions answered. One unread, named at §7.

Headline. Shaping is not Rendering, and the difference is not size — it is authority. Confirming a Shape automatically enqueues render jobs, so an act in this room produces artifacts in another. And a failed production is indistinguishable from a running one from anything the room can read: the ShapeEvent has no failure state, the failure lands on a job row, and no route reaches that row except by an id only the requesting caller ever held.


1. Environment

| | | |---|---| | Operator Layer | main 50252e1, tag render-flow-v0_1. Tree clean. | | Engine | main ffc29af, tag discovery-source-mode-v0_1. Tree clean. |

Fences honoured. No dev server, no build, no test run, no database connection, no perimeter call, no npm install. No branch, commit or edit in either repository. Static reads only.

Nothing inherited. Both grounding documents predate these merges; every carried claim below is marked re-verified or unverified.


2. Question one — the engine's shape surface

2.1 The routes — twenty, across three routers

| Router | Routes | |---|---| | shapings.py | POST / GET /shapings; PATCH /shapings/{id}; PATCH /shapings/{id}/executor; DELETE /shapings/{id}; GET /shapings/{id}; GET /available-executors | | shape_events.py | POST /shape-events; POST /shape-events/self-consumer; GET /shape-events; GET /shape-events/{object_id}; POST …/{id}/confirm; POST …/{id}/retire; PUT …/{id}/title; GET /shaping-jobs/{shaping_job_id} | | declared_shape_types.py | POST, GET on /declared-shape-types (with trailing-slash variants) |

All four capabilities the brief listed exist — list, retrieve, retrieve at a version (version: int | None = Query(default=None, ge=1) at shape_events.py:475, the same query-parameter shape as Manifestations), and create.

2.2 The lifecycle — five states, and the transitions are not all operator-driven

ShapeEvent.state (engagement/types.py), documented in the class docstring:


held                 - ShapingAgent dispatched; Shape not yet produced.
pending_confirmation - Produced; confirmation consideration opened.
confirmed            - attest or no_change closure.
amended_superseded   - amend closure; a later Shape supersedes this one.
retired              - retire closure.

The question the brief said decides the room's shape: is any transition automatic? Yes — two are.

(a) held → pending_confirmation is automatic. It is written by the agent on completion — agents/shaping.py:711 and engagement/shaping_skill.py:404. The Operator does not perform it and cannot.

(b) Confirming a Shape automatically enqueues renders. This is the material one. engagement/shape_confirmed_dispatch.py is a post-append hook registered against event_kind == "shape_confirmed", and its own docstring states it plainly:

> "When a ShapeEvent transitions to state='confirmed' … this hook fires on that event and invokes RenderDispatchAgent.dispatch_for_shape_confirmed for the engagement, walking the engagement's declared render-types and enqueuing one render_jobs row per matching declared_render_type."

So an act in the Shaping room produces artifacts in the Rendering room, without a second decision.

> This does not contradict the seed, and the change request should not treat it as a defect. The render side's state transitions are operator-driven — CR-2026-162 established that producing a render never retires another, and D-3 recorded that as the seed implemented correctly. What fires here is production, not a state change on an artifact the Operator already owns. The Operator confirmed a Shape; the engine acted on a render-type the Operator had previously declared. The declaration is the authority. > > But the room must say so. A confirm control that reads as a local act, when it may enqueue several renders, is the surface understating what the Operator is about to do. That is a design finding for the change request, and it has no precedent in the three built rooms.

Two guards worth carrying: matching render-types with no registered specialist are skipped and logged; an engagement with no RenderDispatchAgent is a silent no-op, deliberately, so "engagements that haven't adopted rendering yet must not be impacted by confirming a Shape."

2.3 The executors

Shaping carries an executor, addressable directly: PATCH /shapings/{id}/executor and GET /available-executors. engagement/executor.py:103 and :121 branch on executor.kind == "agent" and == "skill", and shaping_skill.py:271 rejects a non-skill executor. So the surface can both list executors and reassign one — a capability no built room has.

2.4 What is asynchronous

Shape production is dispatched, not awaited. POST /shape-events returns 202 ACCEPTED (shape_events.py:229), via runner: AgentRunner. POST /shape-events/self-consumer is also 202.

The 202 body is ShapeProductionResponse: shaping_job_id and shape_event_object_id — so the ShapeEvent exists immediately, in held.

How a caller learns it finished: by polling GET /shaping-jobs/{shaping_job_id}ShapingJobResponse with status: Literal["queued","dispatched","completed","failed"], shape_event_object_id, error_message.


3. Question two — the async dimension, and the gap

3.1 ReadState today — re-verified, unchanged since CR-2026-160


export type ReadState<T> =
  | { status: "unloaded" }
  | { status: "loading" }
  | { status: "failed"; error: unknown }
  | { status: "empty";     readAt: number }
  | { status: "populated"; readAt: number; items: T[] };

Byte-identical to the shape CR-2026-160 authored. Neither CR-2026-161 nor CR-2026-162 touched it.

3.2 Can populated represent a Shape still being built? Yes — the contract needs no new state.

Because the running condition is on the resource, not outside it. ShapeEventResponse carries state among its thirty fields, and GET /shape-events returns those items directly. So a completed read returns real items, one of which is in held. populated is the honest answer: the read succeeded, and this is what the record says.

> This is the opposite of the shape the brief anticipated. The concern was that a running job is "not any of" the five states. It does not need to be — the job's running-ness is a property of the item, not of the read. The contract distinguishes did the read land; the item distinguishes is the work done. Two different questions, and conflating them is what would have required a sixth state.

3.3 The gap: a failed production is indistinguishable from a running one

This is the finding, and it is stated as a gap rather than a proposal.

  1. ShapeEvent has no failure state. The literal is five members and none is failed — a sweep of the class returns zero mentions.
  2. Failure lands on the job row only. engagement/shaping_jobs.py:123mark_failed runs UPDATE shaping_jobs SET status = 'failed'. The ShapeEvent is not touched. agents/shaping.py:400 calls it on any exception after dispatch, and the module header says background tasks "must never propagate."
  3. So a failed production leaves the ShapeEvent in held — permanently, and looking exactly like one still running.
  4. And the room cannot reach the job. shaping_jobs does carry shape_event_object_id, so the link exists in the database — but the only route is GET /shaping-jobs/{shaping_job_id}. There is no route to find a job by shape-event id, and none to list an engagement's jobs. The shaping_job_id is returned once, in the 202, to the caller who requested production. After a reload, or in any other session, it is gone.

The consequence, stated plainly: a room that lists shape-events sees held and cannot honestly say whether that Shape is being produced or failed hours ago. Under the governing rule it may say only "not produced yet" — which is true of both, and which is the least the Operator needs to know.

Whether the answer is a new engine route, a failure state on the ShapeEvent, or a surface that declines to distinguish, is the change request's to decide. This session does not design across it.


4. Question three — the mapping is written down and used as a selector, not enforced as a constraint

It is written down, on the render-type side. DeclaredRenderType.source_shape_type_ref: MemoryRef"Version-pinned DeclaredShapeType ref" (engagement/types.py). DeclaredShapeType carries no reference to a render-type; the mapping is one-directional.

It is used, at three sites:

No rejection path was found. Nothing raises on a mismatched combination; the mapping decides what is offered and what auto-fires, not what is permitted.

So, against the brief's three options: neither purely "enforced" nor "neither". It is exposed, stored version-pinned, and load-bearing for selection — which is stronger than "the surface must carry it" (it must not; the engine already does) and weaker than "the engine would reject the rest."

Consequence for the change request: the surface should not re-implement the mappingGET /renders/candidates already answers "what can be rendered from this Shape." A second copy is the divergence the brief warned about.


5. Question four — the wall

Re-verified at 50252e1. Case-sensitive substring scan over src/, run as a Vitest test.

5.1 shaping is a forbidden term (vocabulary-wall.test.ts:30), as is shape_event (:27). rendering is not. The discriminator is shape (room-labels.ts:27{ key: "shaping", label: "Shaping", content: "shape" }), and ROOM_LABEL_TERMS exempts shaping only in lib/room-labels.ts.

5.2 The full collision set is two, and it is smaller than feared. A sweep of every identifier in src/ containing shaping returns exactly:

| Identifier | Where | Status | |---|---|---| | shaping | lib/room-labels.ts | exempted by ROOM_LABEL_TERMS | | shapingRunning | lib/api/activity.ts | passes only because that file is a boundary file |

reshaping does not occur anywhere in src/. The brief named it as the plausible trap; it is real as a rule but has no current instance.

5.3 The adapter path rule holds, and the file does not exist. src/lib/api/shape.ts is absent. The rule stands: lib/api/shape.ts, never shaping.ts — the wall reads import specifiers, so a room-named adapter trips every consumer.

5.4 WIRE_BOUNDARY_FILES is 15 entries. Would a Shaping adapter need one? Almost certainly yes — unlike the render-download adapter, which projects no wire shape, a Shaping adapter must project ShapeEventResponse, whose fields include shaping_agent_actor_id, shaping_agent_instruction_version and shaping_agent_display_name. Those carry shaping. CR-2026-162's rule is that an adapter needing no exemption should not get one — this one needs it, and the entry should name those fields as the reason.


6. Question five — the room as it stands

6.1 The placeholder. RoomView.tsx:78-81 — the shape branch renders EmptyRoom with the two strings. Unchanged since CR-2026-160.

6.2 The two strings, verbatim at 50252e1 (lib/strings.ts:1152-1154):


shapeEmpty:     "This room isn't built yet."
shapeEmptyHint: "Shaping arranges organized memory for a particular reader.
                 The screen is coming; nothing has been checked."

Both assert the screen, not the record. They become false in the other direction when the room is built — the room will exist — so they are replaced, deliberately, not overwritten.

6.3 Which rooms are on the contract — re-verified:

| Room | ReadState | |---|---| | MemoryRoom | yes | | ManifestationRoom | yes | | RenderingRoom | no — still derives its own ladder |

CR-2026-162 touched RenderingRoom twice and did not migrate it, which is consistent with its scope. The majority pattern is the contract, two rooms to one, and Shaping should join it — but the count is two-to-one, not three-to-nothing, and the change request should not describe it as universal.


7. Unread

One.

| # | Unread | Why | What would settle it | |---|---|---|---| | 1 | Whether a held ShapeEvent whose job failed is ever cleaned up — by a sweeper, a cadence, or an operator action — or whether it remains in held forever. | Establishing it means reading the cadence and drift machinery end to end, which is a larger sweep than these five questions, and confirming absence of a cleanup path is not decidable from the reads performed. | A targeted read of the cadence/drift modules for shape-event state repair, or a query against a long-lived database — the latter outside these fences. §3.3's gap does not depend on the answer; a cleanup would change how long the ambiguity lasts, not whether it exists. |


8. Corrections preserved

1. The change request C findings called Shaping "a read plus a state machine plus a job poll." Re-verified and accurate, but understated in one respect: it is also a trigger for work in another room. The auto-dispatch on confirmation was not in that sizing.

2. The brief's framing of the async gap is corrected by the read. It anticipated that ReadState might need a sixth state for a running job. It does not — the running condition is a property of the item, not of the read (§3.2). The real gap is elsewhere: failure is unreachable, not un-modellable.

3. reshaping is named in both the change request C findings and this brief as a substring trap. Re-verified: the rule is correct, and there is no current instance in src/. Recorded so the change request does not go looking for one.

4. The change request C findings recorded WIRE_BOUNDARY_FILES at 15 entries. Re-verified: still 15 — CR-2026-162 added none, having established its adapter needed none.


9. What this session did not do


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — B-6 Step 0 findings — v0.1 — 2026-08-03 The room where confirming one thing produces another, and where "not finished" and "failed" look the same.