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

Loomworks — B-9 Step 0 inspection findings — v0.1

Version. 0.1 Date. 2026-08-03 Author. Claude Code (inspection session). Operator: Marvin Percival. Brief. inspection-briefs/loomworks-b9-step-0-inspection-brief-v0_1 — confirmed present and the highest version (it is the only version; ls | grep -i b9 returns one file). Charter. standing-notes/dunin7-standing-authorization-charter-v0_1. R-5 inspection run. Build-list item. B-9 — change request E. Build list standing-notes/dunin7-build-list-v0_9 line 43 carries the promise verbatim. Status. Read-only. No code change, no change request, no design.


0. Environment

| Repository | Branch | SHA | Tree | |---|---|---|---| | /Users/dunin7/loomworks-engine | main | ffc29af (Merge CR-2026-162 (engine) into main: B-28, the discovery source mode) | clean — git status --porcelain empty | | /Users/dunin7/loomworks | main | f477c87 (Merge CR-2026-163 into main: B-6, the Shaping room) | clean — git status --porcelain empty |

Both SHAs match the brief's header exactly.

No server was started, no build was run, no test was run, no database was connected to, and no perimeter call was made. playground_dev was not touched. Every claim below is read back from source files at the two SHAs above, from git show / git log -L against history, or from documents in loomworks-record. Two directory listings of loomworks-engine/data/renders were taken; both are read-only and the directories contain zero files (counted).


1. Question one — is "click any statement" buildable? No. Not as written, and not derivably.

Per-statement linkage does not exist, and it is not derivable from anything stored. The answer is the brief's second outcome (§3): B-9 as promised requires the render to carry provenance it does not currently carry.

The finding rests on four reads, in the order the brief asked for them.

1.1 What a render's stored content actually is

RenderEvent.render_content is typed dict | bytes (engagement/types.py:1503), discriminated by content_kind ∈ {inline_dict, binary_blob, external_reference, multi_file} (types.py:1515-1520).

The only producing path in the tree writes inline_dict. AnthropicRenderSpecialist._produce_content returns {"text": text_out}, declared_render_type.render_format (agents/render_specialist.py:993-994), where text_out is completion.text — the model's raw output string. The class docstring states the contract outright: "Output shape: ({"text": <markdown string>}, render_format)" (render_specialist.py:915-916).

The stored content is one opaque string. Not a structured document, not a list of paragraphs — a single text key holding whatever the model emitted.

The surface confirms it independently: lib/api/renders.ts:81-86 reaches into render_content.text, checks typeof content.text === "string", and hands the whole string to new Blob([output.html], { type: "text/html" }) for a new tab (RenderingRoom.tsx:27-34). Nothing between the model and the browser inspects the string's interior.

1.2 Whether anything inside it carries a reference back to a source assertion

Nothing. A case-insensitive sweep of src/loomworks for citation, anchor, data-assertion, and assertion_id returns no rendering-side hit — the assertion_id hits are all in credit/ (conversion_credit_override.py, grant_decision_dispatcher.py) and concern grant proposals, not renders. RenderEvent has no field for a marker, an index, or a parallel map; its 17 carry-forward fields are enumerated in engagement/render.py:336-344 and none of them is one.

1.3 How the content is produced — does anything record which assertion produced which sentence?

No, and the specialist could not do it even if asked, because it is never told the identifiers.

AnthropicRenderSpecialist._build_user_prompt (render_specialist.py:996-1020) assembles exactly six lines: render-type name, render format, consumer declaration, a blank, the literal header Confirmed Shape content:, and confirmed_shape.produced_shape_content["text"]. The Shape's prose is the entire substantive input. No selected_memory_refs, no assertion ids, no Manifestation reference reaches the render specialist.

One layer up, the same is true. ShapingAgent._build_user_prompt (agents/shaping.py:600-646) lists the Manifestation's committed assertions as


- ({grammar_element}/{normative_force}) {content}

(shaping.py:642-645) — content and classification, no identifier. The Shaping model likewise cannot cite what it was never shown. Its output is wrapped as {"text": text_out} (shaping.py:598), a second opaque string.

The system prompt reinforces it rather than counteracting it: "Preserve what the Shape carries; do not add information not present. Return only the render text." (render_specialist.py:901-903).

1.4 Whether the Shape names its selected assertions, and whether that survives into the render

The Shape names them. It does not survive into the render.

ShapeEvent.selected_memory_refs: list[MemoryRef] is version-pinned per R-C2 (types.py:1255). Both production paths populate it identically — seed reference first, then every committed assertion on the Manifestation:

Two qualifications, both read rather than assumed. First, this is not a selection: it is the Manifestation's whole committed set. The docstring says so — "selected_memory_refs is populated from the Manifestation's committed_assertions (plus the seed_ref) as a Phase 9 baseline" (shaping.py:664-666). Second, excluded_but_considered is hardcoded [] on both paths (shaping.py:714, shaping_skill.py:407); R-C12 is honoured by the field existing.

The render's only tie to the Shape is confirmed_shape_event_ref — one reference to the whole Shape (types.py:1480), carried forward verbatim through every state transition (render.py:374). There is no per-fragment structure on either side of that reference for a per-fragment link to attach to.

1.5 What this means for the two builds

The render-level walk is buildable today, entirely over routes that exist. This document was built from this Shape, from this Manifestation at this version, from these assertions, by these people. Section 2 establishes every hop.

The per-statement walk is a production-pipeline change and a substrate change. At minimum it requires: identifiers reaching the shaping and render prompts, a specialist output contract that is structured rather than a single string, a storage shape that can hold the structure, and a validator that admits it. That is not a walk; it changes what a Shape and a Render are.

This session does not design across the gap and takes no position on which to build. Per the brief §7 that is the Operator's decision.


2. Question two — the five hops, re-verified at ffc29af

All five hops have routes. None requires database inspection. Every hop can be version-pinned. The walk audit's own §4.2 recorded that it performed the hops over the API, not the database — that is re-verified here against the current tree, hop by hop.

| # | Hop | Route | Pinning | Response field(s) | |---|---|---|---|---| | 1 | Render → Shape | GET /engagements/{engagement_id}/renders/{object_id} (renders.py:1239-1256) | ?version= (renders.py:1252) | confirmed_shape_event_ref{id,version} (renders.py:169-172) | | 2 | Shape → Manifestation + version | GET /engagements/{engagement_id}/shape-events/{object_id} (shape_events.py:463-476) | ?version= (shape_events.py:475) | manifestation_object_id, manifestation_version (shape_events.py:142-143); also selected_memory_refs (:137) and shaping_object_id/shaping_version (:144-145) | | 3 | Manifestation at version → memory references | GET /engagements/{engagement_id}/manifestations/{manifestation_id} (manifestations.py:490-513) | ?version= (manifestations.py:502) | organized_groups.groups[].assertion_refs[]{assertion_id,version} (manifestations.py:278, types.py:1723-1748) | | 4 | Memory reference → assertion at version | GET /engagements/{engagement_id}/assertions/{assertion_id}/at-version/{version} (assertions.py:786-811) | path segment | full AssertionResponse | | 5 | Assertion → attributed contributor | same response | — | contributed_by, committed_by (assertions.py:133-135) |

Hop 1 is also traversable forward. GET /renders accepts confirmed_shape_event_id as a query filter (renders.py:628). Hop 4→2 is traversable forward too: GET /engagements/{id}/assertions/{assertion_id}/dependents (assertions.py:699-756, CR-2026-155) returns every produced Shape that selected the assertion, each carrying matched_version"the forward walk from a contribution to the work built on it" (:705-706). B-9 can walk both directions with what exists.

2.1 W-16 is struck, and the mechanism of the miss is worth recording

get_manifestation_route declares version: int | None = Query(default=None, ge=1) and load_manifestation honours it with a distinct object_version = :ver query (engagement/manifestation.py:692-708).

It has been there since the route was born. git log -L 499,513:src/loomworks/api/routers/manifestations.py resolves to 5af3195 (2026-04-23, "Phase 11 step 9: Manifestation HTTP surface"), and git show 5af3195:… shows the version parameter and the Default: current; ?version=: pinned docstring in the introducing commit. The capability predates the walk audit.

The audit's wording shows how it was missed: "There is no at-version route for manifestations (routes are: manifestations, /derive, /preview, /{manifestation_id}…)" (walk-audit report line 349). It enumerated paths. Manifestation pinning is a query parameter on an existing path, not a path of its own — unlike assertions, where pinning genuinely is a separate path segment. The sweep encoded the assertion route's shape as the expected shape of the answer. That is discipline-clause item 4 in a new dress: a sweep pattern must not encode an assumption about its target.

2.2 Hop 3 has a real gap, and hop 2 makes it survivable

Manifestation.organized_groups is ManifestationOrdering | None, and None is the documented Phase 18 value (types.py:1819-1822). ManifestationResponse carries no committed_assertions field — only assertion_count, relationship_count, and organized_groups (manifestations.py:255-279). So for a Manifestation derived before Phase 19, hop 3 returns counts and nothing enumerable.

This does not break the walk. Hop 2 already delivers selected_memory_refs — the same assertion set, version-pinned, on the Shape itself. Hop 3 is confirmatory, not load-bearing, and a walk should be built knowing hop 3 can legitimately return an unenumerable Manifestation.

2.3 Hop 5 — the actor kinds, and two terminal cases a walk must not flatten

ActorKind = Literal["contributor", "agent", "person", "companion"] (memory/base.py:23), documented at base.py:97-118. B-25 (CR-2026-161, merged a6a3ed6) corrected which kind is written where — step 1 (725ef21) replaced hand-built ActorRef(kind="contributor", id=person.id, …) with actor_from_person, and its message records the acceptance sweep returning zero remaining sites. Kind is now true at the write sites. Two things it did not change, and both bear on how a walk renders its terminus:

(a) companion carries the person's id. actor_from_companion sets kind="companion" with id = the person's UUID — "there is no separate companions table because every Companion is a person's interface to their own substrate" (stele/actor.py:42-46). So a walk terminating on companion has the person in hand and need never dead-end. What differs from person is only the display_name — the per-person Companion name, which CR-2026-161 step 4 made a genuine read rather than the hardcoded "Companion" (assertions.py:383-417 in that diff). This is walk-audit W-6, and it is not fixed: a walk that renders display_name still says the Companion's name for anything contributed by talking. It is now correctable at render time rather than at the substrate, because the id is right.

agent is the one kind with no person behind it — its id is a minted agent-actor UUID (e.g. uuid.uuid4() at engagement/render.py:228).

(b) There is a fifth terminus, and it is a sentinel, not a person. LEGACY_UNRESOLVED_ACTOR_ID = UUID("00000000-0000-0000-0000-00000001e9ac") (memory/base.py:62), written with kind="contributor" on the legacy bearer-token path where "The identity is genuinely absent, not discarded" (base.py:42). The comment is explicit that the kind was chosen for its authority consequences, not its truth value: agent/companion "would REVOKE COMMIT AUTHORITY on records a human actually made" (base.py:46-48). A walk that renders this id as a contributor's name states a person where the engine deliberately recorded an unknown. It reads as an ordinary contributor and must be special-cased by id.

2.4 Hop 5's larger imprecision — the walk's terminus is the committer, not the contributor

contributed_by on a committed assertion names the person who committed it, not the person who said it. commit_assertion mints the new version with provenance=Provenance(wasAttributedTo=actor, …) where actor is the committer (engagement/assertions.py:340-347); revise_assertion does the same for the reviser (:454-458). The API serializer states the consequence in its own comment: "for state='committed' / 'retracted' the field reflects the actor who caused the latest transition (the committer / retracter). That's a known imprecision" (assertions.py:117-124).

B-25 did not touch this. The only CR-2026-161 change to assertions.py is the companion-name read at the discard route (verified by git diff e106917 a6a3ed6 -- src/loomworks/api/routers/assertions.py, which is that hunk and nothing else).

It matters directly to B-9's promise. "Walk backward to the person who said it" asks for the originator. Commit gates reject agent and companion (assertions.py:284-287, against NON_HUMAN_ACTOR_KINDS = frozenset({"agent", "companion"}) at base.py:31), while add_assertion admits every kind — "Both humans and agents MAY add (R-B34)" (assertions.py:167-168). So the originator and the committer are routinely different actors of different kinds, and for anything contributed by talking the originator is the Companion and the committer is the person.

It is derivable, over an existing route. GET /engagements/{id}/assertions/{assertion_id}/history returns "every version of an assertion, earliest first" (assertions.py:759-783); v1's contributed_by is the originator. Hop 5 is therefore two reads, not one — and the two answers are different sentences: who said it and who approved it. Both are true and a walk that shows only one is answering a question it was not asked.


3. Question three — provenance.wasGeneratedBy

What it now is. append_event requires the caller to pass the same event_id it stamped into object.provenance.wasGeneratedBy, and writes it as the row's event_id"the object the caller holds and the row written here carry the same identifier by construction, with no correction, copy or back-patch anywhere" (memory/events.py:154-160). The parameter is required, not defaulted, deliberately: "A default would leave a silent path back to the fabricated value this parameter exists to eliminate" (:162-166). event_id is the primary key of memory_events (events.py:97-99). B-29 is verified at the code, not inherited.

What it makes possible. wasGeneratedBy is a primary-key lookup into memory_events. The row carries what no reference-walk hop reaches: event_kind, timestamp, engagement_version, actor_id / actor_kind / actor_instruction_version, and — the one with no equivalent anywhere in the reference chain — commit_webauthn_attestation, "WebAuthn proof recorded at engagement commit" (events.py:120-126).

So the hop it opens is real and it is not merely a debugger's hop: which event produced this version, under which engagement-version, by which actor at which instruction version, and with what proof of presence. The reference-walk answers what was selected; this answers what act produced it.

Is there a route that reads it today? No — and the gap is two-sided.

Exposing it is therefore new work on both sides: a field on a response, and a route that resolves it. Reported, not recommended, per the brief §7.


4. Question four — the surface

4.1 Where the walk would live — no room has an affordance that could open one

The four rooms exist (lib/room-labels.ts:26-31; components under src/app/operator/engagement/[engagement_address]/). Every interactive control in all four is a mutation or an export. None navigates, and none opens a detail view. Counted by reading each file:

| Room | Controls | Kind | |---|---|---| | Memory (MemoryRoom.tsx) | Confirm, Confirm-with-voice, Dismiss, Move, plus a source-file link that opens a new tab (:322-332) | mutations + one external open | | Manifestation (ManifestationRoom.tsx) | none — zero onClick in 253 lines | — | | Shaping (ShapingRoom.tsx) | Confirm (:125-128) | mutation | | Rendering (RenderingRoom.tsx) | View (blob → new tab, :27-34), Download (:99-104) | exports |

And the surface reaches no version-pinned route at all. A sweep of src/ for at-version, selected_memory_refs, confirmed_shape_event, dependents, and ?version returns zero matches (counted, not read off the audit — the audit's equivalent claim at line 401 is re-verified). The B-9 walk would be the first consumer of every pinned read in the product.

Three of the walk's hops already cross the wire and are discarded in projection. This is where a build would start, and it is smaller than it looks:

4.2 ReadState carries a walk — per hop. The sequence is the item's business.

ReadState<T> is the five-value union unloaded | loading | failed | empty | populated (hooks/usePagedList.ts:50-55).

It is already used outside usePagedList for a single-value read. ManifestationRoom holds useState<ReadState<RoomRead>>({status:"unloaded"}) and drives all five states by hand from one fetch (ManifestationRoom.tsx:192-217), wrapping its single result as items:[result]. A walk hop is exactly that shape, so each hop can carry its own ReadState with no contract change.

The distinction a walk needs most is the one ReadState was built to make. A hop that legitimately returns nothing (empty, which "cannot be constructed without the readAt of a completed read") and a hop that could not be read (failed) are different sentences — "we could not look" and "there is nothing" are different sentences, and only one of them is true (ManifestationRoom.tsx:12-14). A provenance walk that conflated them would report a broken chain where there is only an unreachable server. And unloaded correctly describes a hop not attempted because an earlier one failed.

Nothing is missing from the contract. What a walk needs beyond it — which hop is current, how a partial chain renders, whether a failed hop halts or the walk continues past it — is composition over N ReadState values, not a sixth value. This is B-6's answer again: the question was answered by the item, not by the contract.

4.3 The vocabulary wall — this is the CR-2026-163 case, but there is a cheaper shape

The wall (tests/components/vocabulary-wall.test.ts) forbids 11 terms (:22-37, counted) with 15 current WIRE_BOUNDARY_FILES entries (:43-96, counted). It is a case-sensitive line.includes(term) scan of every .ts/.tsx/.js/.jsx/.css file under src/ (:116, :144).

A walk adapter cannot avoid the tokens. It would carry, at minimum:

So this is CR-2026-163's case, not CR-2026-162's. CR-2026-162 struck an exemption for render-download.ts because that adapter "projects no wire shape … Its path segments are the plural nouns, none of which the wall forbids" (lib/api/render-download.ts:14-19). A walk adapter is the opposite on both counts — it projects four wire shapes and one of its paths is a walled word.

But the cheapest build needs no new entry at all. Every wire read the walk requires can land in an adapter that is already exempt: hop 1 in renders.ts, hop 2 in shape.ts, hop 3 in compose.ts, hops 4–5 in memory.ts or assertions.ts. Each addition is a version-pinned sibling of a read that file already performs. One new adapter would need one new entry; four extensions to existing adapters would need none. Which is right is a change-request decision, not this session's.

One naming constraint, whichever shape is chosen. The wall reads import specifiers as ordinary lines, so a file named for a walled word forces an exemption on every consumer — the lesson compose.ts and shape.ts both carry (vocabulary-wall.test.ts:76-91). A walk adapter must be named from an unwalled word. And a related trap has now fired twice: "a comment about forbidden vocabulary is still scanned as vocabulary" (render-download.ts:21-24; hit again in B-6 per status brief v0.15 §4).


5. Corrections to the grounding documents

Every grounding document predates at least one merge. Three carried claims are corrected here; the rest re-verified as stated.

  1. W-16 is struck — walk-audit report line 349, "There is no at-version route for manifestations." Version-pinned retrieval has existed since 5af3195 (2026-04-23). The brief already recorded the strike; §2.1 above establishes it independently and names the mechanism of the miss.
  2. W-6 is NOT closed by B-25 — walk-audit report §W-6. B-25 corrected which kind is written; it did not change the fact that a Companion-path contribution terminates in the Companion's name. §2.3(a) records what B-25 did change: the id was always the person's, and the display name is now a genuine per-person read.
  3. The walk audit's hop-5 result generalizes narrowly. Its terminal read returned Walk Audit Operator because on that assertion the contributor and the committer were the same person. §2.4 establishes that in general they are not, and that the current-version field names the committer.

Re-verified as stated, not inherited: the B-25 sizing findings' §5 conclusion that the demonstrated walk does not touch wasGeneratedBy — §2's five hops run entirely through references and versions (confirmed against the current routes); the walk audit's finding that no Lane B surface exposes any hop — §4.1's zero-match sweep.


6. Unread

Stated rather than closed with a plausible reading.

  1. No live data was examined. No database, no perimeter. Every claim about what is stored is a claim about what the code writes. In particular: how many live Manifestations carry organized_groups = null (§2.2), and how many live assertions carry the legacy sentinel (§2.3(b)), are uncounted — both are counts, and the brief's discipline forbids reading a figure in place of counting one. Neither is countable without the database this session may not touch.
  2. The bytes arm of render_content was not traced to a producer. types.py:1504-1509 calls it vestigial after Phase 35; no current specialist writes it. Whether any historical render carries it is a database question.
  3. binary_blob, external_reference, and multi_file renders were not examined for interior addressability. The reasoning in §1 covers inline_dict, the only kind the in-tree specialists produce. data/renders holds zero files locally, so no stored artifact of any other kind was available to read.
  4. The two non-Anthropic specialist classes were read only far enough to establish their content shape. StubRenderSpecialist returns self.stub_render_content verbatim (render_specialist.py:884); the external-specialist path (test_external_specialist.py, poll_external_work) was not traced end to end. Neither is a production prose path.
  5. No test file was read as evidence of behaviour, only as evidence of the wall's own definition.

7. Summary for the change request

Question one, plainly: per-statement linkage does not exist and is not derivable. The Shape is one opaque string, the render is another, neither prompt is shown an identifier, and the only tie between them is a single whole-Shape reference. B-9 as promised is a pipeline and substrate change.

The render-level walk is buildable now. Five hops, five routes, all version-pinnable, zero database reads required, plus two forward routes that already exist. Its substantive costs are on the surface, not the engine: no room has an affordance to open it, three of its hops are already served over the wire and discarded at the adapter boundary, and hop 5 needs two reads to answer who said it rather than who approved it.

Three things a walk must not flatten, each with an anchor above: the four actor kinds (§2.3), the legacy sentinel that reads as an ordinary contributor (§2.3(b)), and the committer/originator distinction (§2.4).

wasGeneratedBy opens a genuinely different hop — the act rather than the selection, including the WebAuthn presence proof — and nothing reads it today, on either side. What to do with it is §5's report, not a recommendation.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — B-9 Step 0 inspection findings — v0.1 — 2026-08-03 The walk between records is proven and buildable. The walk into a document is not, because nothing inside a document points anywhere.