DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-160-manifestation-room-v0_1.md

DUNIN7-M4 — OPERATOR LAYER CHANGE REQUEST

CR-2026-160 — B-5: the Manifestation room and the room state contract — v0.1

Version. v0.1 Date. 2026-07-31 Author. Claude.ai (drafting session). Approving: Marvin Percival. Charter. standing-notes/dunin7-standing-authorization-charter-v0_1. No session executes a change request it drafted (§1). Target. /Users/dunin7/loomworks at main 99f7f64. No work in loomworks-engine — see §3. Build-list item. B-5. This is change request A of the completion arc. CR number. CR-2026-160. Highest confirmed at drafting is CR-2026-159. [EXECUTING SESSION: verify against change-requests/ and both repositories' docs/phase-crs/; advance if taken.] Grounding. inspection-briefs/loomworks-operator-layer-cr-a-step-0-findings-v0_1 (Operator Layer, all anchors); inspection-briefs/loomworks-b29-b27-b5-engine-inspection-findings-v0_1 (the engine side); scoping-notes/loomworks-completion-scoping-note-v0_2 (the governing rule); candidate-seeds/loomworks/loomworks-candidate-seed-v0_12 (Manifestation, Operator authority, only-show-what-is-available). Status. Pre-execution.


1. The governing rule, and what this CR owes it

A room states only what it has read back. Three faces, and this CR is accountable for all three in the Manifestation room:

Face 1 — a room cannot say "nothing here" without having looked. Already largely held: usePagedList initialises loading to true, so no empty state renders before a read settles. The property is real and incidental. This CR makes it structural.

Face 2 — a room states what it knows, not what it guesses. Currently violated, verbatim and totally. Four strings assert the record while no read stands behind them.

Face 3 — the room shows what it was derived from and whether the source has moved since. Not built at all today; the Manifestation room does not exist.


2. What exists, from the Step 0 read

The Manifestation room is a placeholder. RoomView.tsx dispatches content: "compose" to EmptyRoom with two strings. There is no component.

No surface code calls any Manifestation route. A full grep finds the room key, three display strings, and a spend adapter — no call site. The proxy is a single wildcard rewrite (/api/:path* → engine), so nothing needs adding to reach the engine. What is missing is a caller.

The engine side is open and established. Four Manifestation routes exist, version pinning works via ?version=, and GET /engagements/{eid}/memory-status returns both the derivation version and the source's current version in one call.

The shared layer is src/hooks/usePagedList.ts. Both live rooms fetch through it. It already holds every input a combined state needs and simply does not combine them, which is why each room re-derives the same three states with its own if ladder — and why canMove invents a fourth, wrongly.

The wall permits the capitalised noun everywhere. The scan is case-sensitive substring matching, so Manifestation is invisible to it and manifestation is not. No wall change is needed and no Operator ruling is needed.


3. In scope and out of scope

In scope

  1. The room state contract in hooks/usePagedList.ts, as an addition.
  2. ManifestationRoom.tsx — the real room, replacing the "compose" placeholder branch.
  3. The staleness marker, from memory-status.
  4. The four false strings, corrected to assert the surface.
  5. canMove — a failed read must stop being indistinguishable from a true-empty read.
  6. RenderingRoom's missing refresh path.

Out of scope


4. Construction decisions

D-1 — the contract is a derived value inside usePagedList, not a replacement for its fields.


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

Every existing field stays, so no consumer breaks. empty and populated both carry readAt — the timestamp of the completed read that produced them. That is the whole mechanism: the two states that make a claim about the record cannot be constructed without evidence of a read. unloaded and loading carry none, because they claim nothing.

fetchPage must keep being read through its ref, or every room refetches on every render.

D-2 — unloaded is reachable, and that is deliberate. Today loading initialises true, which is safe but conflates never asked with asking. The contract separates them so the distinction is expressible. A room renders unloaded and loading identically — this is not a visible change, it is a change in what can be said.

> The resetKey window, from the Step 0 read. On a resetKey change the reset happens inside the effect, so one render shows the previous engagement's items. Stale-briefly, not falsely-empty. The contract makes it expressible: readAt belongs to the prior read, and a consumer that cares can see the value is stale rather than inferring it. This CR does not change the timing; it makes the condition nameable.

D-3 — the room surfaces movement; it never re-derives on its own. The seed is explicit that automatic state transitions on artifacts the Operator has authority over are a category error. The room shows that Memory has moved and offers the action. The Operator takes it. No effect, no interval, no auto-refresh on focus.

D-4 — the staleness marker leads with the semantic signal and shows the arithmetic underneath.

The engine has two measures and they can disagree: memory_has_changed is driven by content-category counts, while the version pair is a raw delta. E0005 is twenty-five versions behind and may still be correctly flagged unchanged.

Showing only one is wrong in both directions. The flag alone hides real movement; the delta alone cries stale over bookkeeping. The room states the flag as a sentence and the pair as its supporting detail:

> Memory has changed since this was organized. > Organized from Memory at version 41. Memory is now at version 66.

and when they disagree, the sentence is what the Operator acts on and the numbers explain why the counter moved without the picture going out of date. A disagreement is visible rather than hidden, per corrections-preserved.

D-5 — naming, given the wall. The wall is case-sensitive substring matching over src/. So:

Free: ManifestationRoom, ManifestationRoom.tsx, type names, every user-visible string. Forbidden outside lib/room-labels.ts: any lowercase manifestation — including test ids, CSS classes, camelCase identifiers, and prose in comments. Substring matching means manifestationId and reshaping trip it too.

Test ids use the discriminator, matching the repo's existing lowercase convention: compose-room, compose-room-stale, compose-room-empty. The wall is not weakened and no exemption is added.

D-6 — the four strings assert the surface, because that is what is known.

The two Manifestation strings are deleted; the room replaces them. The two Shaping strings are corrected in place, because B-6 has not been built and the room stays a placeholder:

| String | Today — a claim about the record | Corrected — a claim about the surface | |---|---|---| | shapeEmpty | "No shapes waiting on you yet." | "This room isn't built yet." | | shapeEmptyHint | "Shapes appear here once there's settled memory to shape." | "Shaping arranges organized memory for a particular reader. The screen is coming; nothing has been checked." |

> Why the wording matters more than it looks. The Step 0 read found the sharpest evidence inside one file: renderingEmpty"No finished outputs yet." — is the same grammatical form making the same kind of claim, and it is honest, because a completed read stands behind it. The surface has taught the Operator that this sentence form means the engine was asked and said none. Using that form where the engine was never asked is what makes the false one false. The correction must break the form, not soften the words.

D-7 — canMove stops deriving a claim from a failed read.


const canMove = otherEngagements.length > 0;     // today

otherEngagements is [] both when the Operator has no other engagement and when the fetch rejected — the .catch sets [] as best-effort. The Move action is then silently hidden, which reads to the Operator as you have nowhere to move this.

The fix follows the contract: the summaries fetch carries a ReadState, and Move appears only on populated. On failed the room says the picker could not be loaded rather than implying there is nothing to pick.

> This sits against the seed's only-show-what-is-available. That constraint forbids disabled and greyed-out controls; it does not license a surface stating a fact it has not established. Hiding on a known empty is correct. Hiding on a failed read is face 2 in a control rather than a sentence.

D-8 — RenderingRoom gains the refresh path it lacks. Its reset key omits refreshNonce; the nonce is neither a prop nor threaded from RoomView, so no conversation-side event can refresh it and nothing in the room mutates its own data. Thread refreshNonce through RoomView to both rooms and fold it into the reset key, matching MemoryRoom.


5. The change

5.1 src/hooks/usePagedList.ts — add the ReadState type and derive it from the fields already tracked. Export it. Record readAt when a fetch settles. No existing field removed or renamed.

5.2 src/lib/api/manifestations.ts (new) — adapter for the current Manifestation and for GET /engagements/{eid}/memory-status. [EXECUTING SESSION: this file is a wire boundary and must be added to WIRE_BOUNDARY_FILES in tests/components/vocabulary-wall.test.ts alongside memory.ts and renders.ts, with a comment naming why — the existing 15 entries are the pattern.]

5.3 ManifestationRoom.tsx (new) — renders, per the contract:

5.4 RoomView.tsx — the "compose" branch renders ManifestationRoom; thread refreshNonce to both live rooms. The discriminator is untouched.

5.5 src/lib/strings.ts — delete the two Manifestation strings; correct the two Shaping strings per D-6.

5.6 MemoryRoom.tsxcanMove per D-7.

5.7 RenderingRoom.tsx — reset key includes refreshNonce per D-8.


6. Order of operations

Per-step commits. npm test at each step — the vocabulary wall is a test, so a wall violation fails the suite rather than surfacing at review.

Step 0 — pre-flight. Verify HEAD, tree state, CR number. Record the baseline failure set by name; compare against it thereafter rather than assuming green. Confirm the Step 0 findings' anchors still resolve.

Step 1 — the contract. §5.1. Nothing consumes it yet.

Step 2 — the adapter. §5.2, including the wall exemption.

Step 3 — the room. §5.3 and §5.4.

Step 4 — the strings. §5.5.

Step 5 — the two adjacent gaps. §5.6 and §5.7. [If either proves larger than the findings suggest, halt and report rather than absorbing it — they are included because they are small, and that premise is checkable.]

CHECKPOINT A — report, then proceed. Failure set versus baseline; a live pass on the dev server showing the room in each of its five states, including a deliberately failed fetch rendering the error rather than an empty claim; and the staleness block against an engagement where the two measures disagree. No Operator confirmation. Halt only on: a failure outside the baseline set; a wall violation needing an exemption not named here; or any charter §6 anomaly.

Step 6 — implementation notes.

CHECKPOINT B — merge, tag, push. Authorized under R-2 including the push. Deployment is never autonomous (F-1).


7. Acceptance gate

  1. No failure outside the Step 0 baseline set; the vocabulary wall passes with no new exemption beyond §5.2's.
  2. ReadState is exported from usePagedList and every existing field survives.
  3. No empty or populated value can be constructed without a readAt.
  4. The Manifestation room renders all five states; failed never renders an empty claim.
  5. The room shows the Manifestation, its version, its derivation source, and the D-4 staleness block.
  6. No lowercase manifestation or shaping outside lib/room-labels.ts — including comments and test ids.
  7. No string on any room asserts the record without a completed read behind it.
  8. canMove is false on a failed read only in the sense that Move is absent and the failure is stated.
  9. RenderingRoom refreshes on a refreshNonce bump.
  10. No change in loomworks-engine.
  11. The status brief is appended, per charter §7.

8. What this CR does not do


9. Claude Code kickoff


CR-2026-160 — B-5, the Manifestation room and the room state contract.
Execution session.

CR: loomworks-record/change-requests/cr-2026-160-manifestation-room-v0_1.md
Confirm it is the highest version present before starting.

Grounding, read both before the CR:
  inspection-briefs/loomworks-operator-layer-cr-a-step-0-findings-v0_1.md
  inspection-briefs/loomworks-b29-b27-b5-engine-inspection-findings-v0_1.md

Charter dunin7-standing-authorization-charter-v0_1 governs.

Target: /Users/dunin7/loomworks at main 99f7f64 or later.
NO work in loomworks-engine — CR-2026-159 holds it.

playground_dev is the live production database and is not touched.

The vocabulary wall is a Vitest scan, case-sensitive substring over src/.
Capitalised Manifestation is free everywhere. Lowercase manifestation is
forbidden outside lib/room-labels.ts — including comments and test ids.
Test ids use the discriminator: compose-room, not manifestation-room.

Record the baseline failure set at Step 0. "No new failures" is the criterion.

Steps 0 through 5 auto, per-step commits, npm test at each.
CHECKPOINT A reports and proceeds on a clean report.
Step 6 auto. CHECKPOINT B merges, tags, pushes.
Deployment is never yours.

Halt and queue on: a failure outside the baseline set; a wall violation needing
an exemption the CR does not name; either Step 5 gap proving larger than the
findings suggest; any charter section 6 anomaly.

Append the outcome to current-status/dunin7-status-brief at close.

DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-160 — B-5: the Manifestation room and the room state contract — v0.1 — 2026-07-31 The rooms already read before they speak. This makes them unable to do otherwise.