DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path foray-reference/loomworks-foray-r2-forward-gap-scoping-note-v0_3.html

Loomworks ��� FORAY R-2 Forward-Gap Closure ��� Scoping Note ��� v0.3

Loomworks — FORAY R-2 Forward-Gap Closure — Scoping Note — v0.3

Version: v0.3 Date: 2026-08-20 Status: Scoping note. Not a change request. Step 0 has run and FORAY has responded to it. A CR may be drafted from this note once the two rulings at §10a are made. Changes from v0.2: FORAY’s response to the census absorbed. §4d reweighted — the two directions of the registry gap are not equally consequential and v0.2 gave them equal weight (§4d). Site 12 reframed: three of the seed’s four rooms already emit through it today, which changes what closing its gap is worth (§3c). The dynamic-dispatch lesson stated once as a shared finding, with both projects’ errors named (§7b). Two rulings pulled out of the body into §10a so they are not lost in prose. v0.1 and v0.2 stand as siblings; v0.1’s wrong §7 premise stays preserved at §7a and is not rewritten. Scopes: Stage 2 of loomworks-foray-coverage-resolution-proposal-v0_3 §10 — R-2, ruled at D3. Grounded on: the Step 0 census at engine main e951e60c, 2026-08-20; foray-response-to-loomworks-census-v0_1; loomworks-foray-coverage-resolution-proposal-v0_3; loomworks-foray-mapping-v0_2 (engine eb9392f5); foray-api-reference-for-loomworks-v0_1. Being passed to: the FORAY project — §8 and §9 only. The rest is Loomworks-side. FORAY has answered nothing in §9 yet; those questions stand.


Plain-language summary

R-2 is the smallest real piece of FORAY work Loomworks has, and everything else waits behind it. At sixteen places in the engine, a marker sits where a FORAY record will one day be built. At most of them the useful facts — who did it, which engagement, which grant, which proposal — are sitting right there in the function, often passed into Loomworks’ own audit write one line above, and then dropped. Closing the forward gaps means forwarding them. Nothing transmits, nothing anchors, nothing changes in production.

The inspection this note required has now run, and it corrected the note. Sixteen sites is still exactly right — no work since the mapping added any. What v0.1 predicted would be found was not there, and §7a says so plainly rather than quietly dropping the claim.

The inspection did find something larger, which no amount of reading the mapping would have surfaced. Memory’s site forwards two things, one of which is a priority hint looked up in a registry. That registry has twenty-nine entries. Forty-nine event kinds that are actually written have no entry at all — so for most of what Memory records, the hint resolves to nothing. Fourteen entries point the other way, naming events never written anywhere. These two are not equally serious. Dead entries are visible clutter. Unregistered live kinds are a silent gap, which is the shape that survives unnoticed. Two of the dead entries do have live tails worth fixing: one is still being filtered for in a database query that therefore returns nothing, and another names an event that lands in a different table entirely.

And the census answered a question nobody asked it. Three of the seed’s four rooms — Manifestation, Shaping, Rendering — were understood to have no FORAY presence at all. They have no dedicated call site, which is what was checked. But their work is already flowing through Memory’s site today, unregistered and unprioritised. So closing that one site’s gap is worth more than a payload fix: it is the only path those three rooms currently have.

Three other things travel in the same stage: a confirmed ordering bug at the suspension site, now proven rather than suspected; the same bug a second time in a four-row loop; and two settings sites carrying byte-identical emit code that should share one builder.

Two contract changes still need FORAY’s word, unchanged from v0.1: the anchor endpoint gained a duplicate check, and the validator appears to have stopped rejecting references that point outside a record — which, if confirmed, removes the ceiling the cross-record question has been resting on. Those are §8 and §9.


1. What R-2 is

From the resolution proposal’s brief: the forward-gap sites are closed, or a reason is stated for each. Ruled close them all at D3.

A forward gap is not missing data. It is data that exists, in scope, at the call site, and does not reach the emit payload. The proposal’s words: in every case the identifier is in scope at the call site, and at the settings sites it is passed into Loomworks’ own audit write one line above the FORAY emit and then dropped.

Why this stage is first: it touches no new room, changes no architecture, is blocked by nothing, and it is the prerequisite for any cross-record linkage. A reference cannot be built out of a value the emit never received.

2. What closing a gap means, exactly

For each site: pass the in-scope values into _foray_reserved_emit’s payload dict. That is the whole change at most sites.

What it does not mean. No emitter is built. No network call is made. _foray_reserved_emit remains a no-op stub under its # FORAY_RESERVED_LOCATION marker. No record is constructed, validated, or anchored. Production behaviour does not change anywhere — the payload argument is passed to a function that discards it.

Why do it before there is an emitter. The payload argument is the specification of what each site can attest. Building the emitter against sites that drop their identifiers would produce records that validate and say nothing, and the fix would then be a change to every call site instead of one module.

3. The gap inventory

Confirmed current at e951e60c. Seven of the eight host files are unchanged since the mapping was taken. The eighth, memory/events.py, was touched by CR-2026-226, but the diff widens a database check constraint only — the emit call and its payload are byte-identical. One refinement, at §3b.

Line numbers are deliberately not reproduced; the CR pins them at drafting time.

3a. Credit family — credit/flows.py, credit/room_consumption.py, credit/proposal_applier.py

# Event kind In scope and dropped
1 credit.issuance grant_id — which grant this issuance claims
2 credit.provisioning authorized_by — the operator identity, when supplied
3 credit.consumption_token (×4) engagement_id
4 credit.consumption_credit engagement_id
5 credit.suspension requested_by, expires_at
6 credit.reactivation auth_method
7 credit.deletion deletion_kind
8 credit.balance_zeroing (0–3×) Nothing dropped; payload is complete for what the site holds
9 credit.referral_credit converted_person_id — which conversion this credit rewards
10 credit.consumption_token / _credit (×3) engagement_id, room
11 credit.corrective proposal_id, shape_event_id, applied_by

Site 11 is the one to read twice. proposal_id is the thing the correction corrects, and it reaches neither the payload nor the metadata dict. Forwarding it does not by itself produce a reference — a single-Action record has no component for an F21.ref to resolve against. Forward it anyway: it makes the value present for whatever linkage mechanism is eventually ruled. Do not let R-2 be scoped into designing that mechanism.

Site 9 is unreached code. write_referral_credit_flow has no callers. Close its gap anyway — it is live, correct, and will fire when Phase 48 wires a caller.

3b. Governance-shaped family

# Event kind In scope and dropped
12 memory.<event_kind> object.id, object.object_type, engagement_id, actor.id, actor.kind — payload carries only event_id and anchor_priority
13 audit.setting_change (enum) person_id, engagement_id, current, committed
14 audit.setting_change (numeric) person_id, engagement_id, current_f, committed
15 conversation.turn person_id, engagement_id, actor.id — payload carries only turn_id and actor_kind
16 audit.setting_change (button) person.id from the authenticated principal, previous, normalized

Site 12 is the largest gap in the set, and §4d makes it larger than the mapping recorded. It is Memory’s single write path, carrying every memory event kind. The object id, engagement id, actor id and actor kind are all present, and none reach the payload — which is exactly why the mapping had to put the stated placeholder "UNATTRIBUTED" in F3. Closing this gap is what makes a Memory record able to name its own subject.

Refinement from the census: actor.kind at site 12 now ranges over five values — person, companion, contributor, agent, system — not four. CR-2026-226 added system. None of them reaches the payload, so the gap is unchanged in kind and one value wider in range.

Sites 13, 14 and 16 share one shape three times independently: the identity is passed to Loomworks’ own audit write one line above and then not passed to the emit. Worth stating as one finding rather than three coincidences.

3c. Site 12 is the only FORAY path three rooms currently have

New in v0.3, from FORAY’s response to the census.

foray-loomworks-audit-coverage-requirement-v0_1 — the document that opened this arc — states that three of the seed’s four rooms have no FORAY presence. FORAY has withdrawn that statement. The search behind it looked for call sites, correctly found none in any Manifestation-, Shaping- or Rendering-named module, and drew a conclusion the search could not support.

What the census shows: render_produced, shape_produced, composition_* and shaping_* are live append_event kinds today, flowing through site 12, unregistered, with _ANCHOR_PRIORITY.get() returning None for each.

What this changes for R-2. It does not add work — site 12’s gap was already the largest in the set and already in scope. It changes what closing it is worth. A Rendering act reaching FORAY through Memory’s generic write path, in a payload carrying neither the object it produced nor the engagement it belongs to, is the current state of three rooms’ attestability. Stage 3’s dedicated Rendering sites remain the right destination; until they exist, site 12 is not a placeholder for those rooms, it is their only path.

What it does not license. Site 12 still emits under memory.<event_kind>, and R-2 does not re-badge Memory’s write path as a Rendering site or invent room-specific emission there. Forward what is in scope; leave the room postures to Stages 3–5.

4. Four items in the same stage that are not forward gaps

4a. The suspension-site ordering defect (D8) — now confirmed, not suspected. write_suspension_flow calls db.add(flow), emits, then await db.flush(). Its siblings flush before emitting. The mapping could not confirm whether flow.id was populated because it depended on the primary-key default. The census read it:

id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True,
    server_default=text("gen_random_uuid()"))

Server-side. SQLAlchemy has no visibility into a server default before flush, so a pending row’s .id is unpopulated. This is a confirmed defect on every call, not an ordering inconsistency. Fix the ordering to match the siblings. It is a real bug independent of FORAY and should be fixed on those grounds.

4b. The duplicated settings emit construction. Sites 13 and 14 carry byte-identical emit calls in two independent functions. room_consumption.py’s _emit helper is the pattern. A shared payload builder, not a centralised emit — canon Principle 1 is one emit per business event. The helper builds the dict; each site still calls the emit.

4c. Site 3’s flush timing — the same defect, second instance. Four rows are db.add-ed and the emit loop reads row.id before any flush. The same server-side default applies, so this is 4a again rather than a benign variant. Fix both.

4d. The anchor-priority registry has drifted from what Loomworks actually writes. New in v0.2; not visible from the mapping, only from the census.

_ANCHOR_PRIORITY holds 29 entries. Site 12 forwards _ANCHOR_PRIORITY.get(event_kind) as one of its two payload values. Against the 65 distinct event kinds actually passed to append_event across 94 call sites:

Count Consequence
Live event kinds with no registry entry 49 anchor_priority resolves to None for most of what Memory records
Registry entries with no write anywhere 14 Dead entries; the hint can never fire for them

So one of site 12’s two forwarded values is empty for the majority of real writes. That reframes the site: not merely a thin payload, but a thin payload whose non-identifier half usually says nothing.

The two directions are not equally consequential, and v0.2 was wrong to weigh them evenly. FORAY’s response names the asymmetry and it is correct:

The exceptions are the two dead entries with live tails, which are ordinary bugs and not tidiness:

Two dead entries have live tails and are ordinary bugs:

Six of the fourteen dead entries are deliberate and correct: agent_session_minted, agent_session_closed, agent_session_refused, agent_identity_minted, agent_identity_frozen, agent_session_silent. These were pre-registered under standing obligation O7, which says new significant events get their seam while the emitter sleeps. They are not drift and must not be cleaned up. The remaining six — agent_registered, induction_cycle_recorded, candidate_engagement_discarded, contribution_credential_issued, contribution_credential_claimed, contribution_credential_revoked — need a disposition each: pre-registered like the O7 six, or genuinely dead.

Scope for R-2, ordered by the asymmetry above:

  1. Make the 49 visible. Record the unregistered live kinds as a stated finding in the CR, so the silent half stops being silent. R-2 does not close it.
  2. Fix the two bugs — the engagement_committed_divergent query, and the membership_created entry.
  3. Dispose of the six undisposed entries — a ruling each, at §10a.
  4. Leave the six O7 pre-registrations alone.

Do not mass-add entries for all 49. Assigning anchor priority to every memory event decides what deserves anchoring, which is room-posture work at Stages 3–5. FORAY’s response is explicit that it takes no position on whether every live kind needs an entry — what that registry is for is a Loomworks design question, and R-2 is not the place it gets answered. R-2’s business is that the registry stops lying about what exists, and that the gap is visible rather than assumed away.

5. What binds this work

Constraints, not choices; each already ruled or forced.

  1. Substrate only. Emission lives in the engine. Never in the Operator Layer, never in a surface, never anywhere a replaceable component could decline to emit and leave no trace. (D6; architecture §09.)
  2. One emit per business event. The reserved-location pattern with grep markers is the mechanism. No new emission mechanism is invented here, and no emit is centralised into a shared writer.
  3. Reserved stays reserved. Nothing in R-2 makes _foray_reserved_emit do anything.
  4. The payload is the specification. What a site forwards is what it will one day attest. Forward what is genuinely in scope; do not fabricate, do not reach outside the function.
  5. Do not resolve the linkage question inside R-2. Where a forwarded value obviously wants to be a reference — proposal_id at site 11, the site 3/4 token-to-credit relationship, the FIFO lot linkage at site 10 — forward the value and state the wish. The mechanism is Stage 6.
  6. The PROVIDER: scheme break at sites 3 and 10 stands. Provider token assets are genuinely Anthropic-denominated; claiming LOOMWORKS: over them would be false. Interacts with D14, unruled — see §10.
  7. Do not clean up the O7 pre-registrations. A registry entry with no writer is exactly what O7 asks for while the emitter sleeps. §4d names which six.

6. Sizing

The proposal calls Stage 2 small enough for a single change request. With the census in, that holds. The site set did not grow, the gap closures are payload-dict additions across three files, and 4a/4c are a two-line ordering fix each.

§4d is the one item that could grow it. The reconciliation itself is small; the six undisposed entries need a ruling each, and the engagement_committed_divergent query fix touches read behaviour rather than emit behaviour. If drafting finds that fix reaching further than expected, split it out rather than letting it swell the CR.

7. Step 0 — run, and what it returned

Run read-only at engine main e951e60c on 2026-08-20. Findings are folded into §3 and §4 above rather than repeated. The headline results:

7a. Correction — v0.1’s §7 premise was wrong

v0.1 asserted that engine work since the mapping had added FORAY call sites, and instructed the inspection to report them separately on the grounds that their payloads had never been screened. No such sites exist. The census found zero new call sites under any label.

The prior position and the correction, both preserved:

The inference was reasonable and it was still wrong, and the mechanism it missed is worth keeping: a dynamic dispatch site absorbs new event kinds without producing new call sites. A census that counts call sites will therefore under-report growth in what is being emitted. §4d is what that growth looks like when you go and check.

v0.1 also used the label “H0” in an instruction aimed at a reader with no exposure to it — it is Boundaries-arc vocabulary and appears nowhere in the engine. Naming conventions do not travel between arcs by themselves.

7b. The same mistake, from both sides, twice in two days

Worth stating once because it has now produced an error in each project.

The reasoning The error
Loomworks (v0.1 §7) New significant events happened, O7 requires call sites for them, so new call sites exist Over-counted. Predicted sites that were never added
FORAY (audit-coverage v0.1) No call site exists in any Manifestation, Shaping or Rendering module, so those rooms have no FORAY presence Under-counted. Missed activity already flowing through an existing site

Both projects treated a dynamic dispatch site as if it were static. Site 12 emits memory.<event_kind> for whatever kind it is handed. New kinds arrive at it without new code; existing kinds route through it without naming their room. So:

The general form: where dispatch is dynamic, the census must be of the values dispatched on, not the places dispatch occurs. Neither project’s search was careless — both asked a precise question and got a correct answer to it. The question was the wrong shape.


8. For the FORAY project — what changed in the anchor contract

Unchanged from v0.1 and v0.2, and not yet answered. The resolution proposal’s §8 tabulated the anchor contract as of 2026-08-18; foray-api-reference-for-loomworks-v0_1, dated 2026-08-20 and verified against live source the same day, contradicts it in one row.

8a. Idempotency — the proposal’s §8 row is now wrong.

Proposal §8 (2026-08-18) API reference (2026-08-20)
Idempotency None. Two identical submissions anchor twice 409 when SHA-256 of the submitted bytes matches an already-anchored body. Byte-identical only

What this changes and what it does not. A byte-identical resubmission is now caught and spends nothing — a real improvement that removes the worst version of the double-anchor risk.

It does not rescind the no-retry rule. The check is byte-identical only, and Loomworks constructs an emit-time timestamp at most sites precisely because no source timestamp exists, so byte-stability across a retry is not something Loomworks can guarantee. And the 409 withholds leafSalt, jobId and fee, so a retry that is caught still does not recover the custody package the lost response carried.

Proposal §9 constraints 1–3 stand, and D11/D12 stand as leaned. One narrowing: constraint 2’s rationale — a retry guarantees a second independent anchor — becomes a retry either recovers nothing or creates a second anchor, and Loomworks cannot tell in advance which.

8b. Material the proposal did not have.

9. For the FORAY project — the question that may unblock Stage 6

The item worth FORAY’s attention above everything else in this note.

The API reference describes N4 as a well-formed reference in F22F24 or an F21.ref that does not resolve to a component present in the record — and states this is not an error. As of 2026-08-20 a reference leaving the record’s own contents is recorded and reported, the record stays valid: true, and this is described as the intended shape for lineage pointing at a prior record or anything outside FORAY’s holdings. It further narrows E9 to fire only on a ref_type mismatch against a component that is present.

If that is current settled behaviour, the structural ceiling the cross-record question rests on is gone.

The proposal’s §7 records the position as: wire strictly in-record via E7; under a single-Action record an in-record reference cannot exist at all; FORAY has escalated Validation Scope R2 for an Operator ruling; Stage 6 deferred until it lands.

Three questions, in the order they matter:

  1. Is N4’s permissive treatment of out-of-record references the ruling on Validation Scope R2, or a separate change that leaves R2 open? The reference reads as the former. Loomworks would rather be told than infer it.
  2. If it is the ruling — what is E7 now? Both the mapping and the proposal attribute the in-record constraint to E7, and E7 remains in the current code space. Either its meaning narrowed or one of those documents was wrong. Loomworks needs the current definition before designing anything that depends on it.
  3. The F4 catalog — the contribution is now real. D13 offered Loomworks’ namespaced event kinds as input, on FORAY’s own concern that inventing the list unchecked risks immediate revision. The census has produced it: 65 distinct event kinds in live use across 94 append_event call sites, plus the six pre-registered agent-lifecycle kinds not yet written. This is a larger and more accurate list than D13 anticipated. It can travel with FORAY’s answers to 1 and 2 whenever FORAY wants it.

What Loomworks does meanwhile: R-2 proceeds either way. Forwarding a value that wants to be a reference is correct whether or not the reference mechanism exists, which is why R-2 is sequenced first.


10a. Two rulings before the CR is drafted

Pulled out of the body so they are not lost in prose. Both are small and neither is FORAY’s.

Ruling 1 — the six undisposed registry entries. agent_registered, induction_cycle_recorded, candidate_engagement_discarded, contribution_credential_issued, contribution_credential_claimed, contribution_credential_revoked. Each is either a deliberate pre-registration in the O7 pattern — a seam waiting for a writer — or genuinely dead and removable. They cannot be treated as a block; the credential three and the engagement-lifecycle three plausibly differ.

Ruling 2 — where the engagement_committed_divergent query fix lives. It changes read behaviour, not emit behaviour, and the query currently returns nothing by construction. Either it rides in the R-2 CR as an incidental fix, or it splits out as its own small change so R-2 stays purely a forward-gap CR. §6 leans split-if-it-grows; this decides it up front instead.

10. Decisions still open that touch this stage

None block R-2 starting; each is recorded so R-2 does not silently answer it.

11. What this note does not do

It does not draft a change request. It does not rule §10a. It does not design the emitter, the custody store, or any linkage mechanism. It does not rule D10–D14. It does not answer §9’s questions on FORAY’s behalf. It does not assign anchor priority to the 49 unregistered event kinds — that is a design act about what deserves anchoring and belongs at Stages 3–5. And it does not clean up the six O7 pre-registrations, which are correct as they stand.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — FORAY R-2 Forward-Gap Closure — Scoping Note — v0.3 — 2026-08-20