DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/companion-commit-affordance/loomworks-companion-commit-affordance-completion-record-v0_1.md

Loomworks — Companion Human Commit Affordance Completion Record — v0.1

Version. 0.1 Date. 2026-06-25 (filed). Built, verified, merged, and pushed 2026-06-24. Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (CR drafting) on DUNIN7-M4; CC executing on DUNIN7-M4. Status. Completion record. CR-2026-121 (Companion Human Commit Affordance) is built, verified on playground_dev + playground_test, and merged + pushed to both product repos' main: loomworks-engine at 0ae231b (08d0c0c..0ae231b, a --no-ff merge of the five-commit branch) and loomworks (Companion) at bafdb07 (c17539a..bafdb07, a --no-ff merge of the one-commit branch). loomworks-ui (Workshop) untouched — port reference only. Markdown primary (technical consumer).

Governing CR. cr-2026-121-companion-commit-affordance-v0_1.md (planned trajectory). Grounded by Step 0 inspection companion-commit-step0-v0_1.md (2026-06-24).


Plain-language summary

What this did. When an Operator adds knowledge through the Companion, it lands as a held draft awaiting their approval — but until now the Companion had no way to give that approval. The only place to commit a held assertion was the Workshop, the harder surface the Companion exists to replace. This change gives the Companion its own commit affordance, two ways that share one addressing scheme:

Two correctness properties are built in. First, commit is the Operator's reserved act (R-B20): every commit records the human as the confirming party, never the Companion — including the verbal path, which executes as the authenticated human Operator. Second, the Companion's language stops blurring state: a held draft is "held, awaiting your commit", and only after commit "committed to the record".

Why it is the keystone. This is build item #1 of the Companion build order. Until commit works on the Companion, nothing downstream (deriving a Manifestation, creating a Shape, producing a Render) can be reached or tested there, because each needs committed knowledge to operate on. The open loop from the build map is now closed.


1. What was built

A durable, lifecycle-stable held number (engine). Assertions already had a per-engagement display_number, but it was allocated at commit and was None while held — so no held assertion had a speakable handle. The number is now allocated at add_assertion from the engagement's existing next_display_number counter (the same single counter commit-time allocation has always used, so held and committed share one sequence — no collision). Commit adopts the existing number rather than minting a fresh one: one number for the assertion's whole life ("held 3" → "committed 3"). Migration 0094 backfilled every currently-held assertion that carried no number.

Feed enrichment. The held number travels to the Companion. The held_assertion arm of /me/dashboard/needs_you emits it (mirroring the sibling pending_shape arm), and the operator-vocabulary translation that the Companion actually consumes (/operator/dashboard, /operator/inbox) carries it through DashboardInboxPreview and InboxItem. The number rides in the payload, decoupled from feed order.

An actionable Companion held surface. The dashboard "Needs you" zone renders held assertions (operator vocab: waiting_note) as an actionable card — visible "Held N", a selection checkbox, and zone controls "Commit selected (N)" / "Commit all" that loop the one-per-call commit endpoint through the Operator's own session. Selection / commit affordances appear only where a commit is possible (no disabled controls).

A commit_assertion converse intent. Classifier (IntentLabel gains commit_assertion; taxonomy + held_reference extraction with confirmation carry-forward), router dispatch branch, _route_commit_assertion (two-turn confirm-back; resolves spoken number → held assertion; "all" → every held; unknown number commits nothing), and an instruction template.

Honest fidelity language. Held vs committed are named distinctly in the card, the confirm-back, and the post-commit reply. The confirm-back and post-commit replies are server-composed (delegated_response) so the held-vs-committed distinction is guaranteed, not left to the responder LLM (the established behaviour-guarantee bypass pattern).


2. Design decisions (settled with the Operator, gated where noted)

| # | Decision | Resolution | |---|---|---| | D1 | Commit shape | Both — card (primary) and conversational intent, sharing one identifier scheme. | | D3 | Identifier durability | Durable add-time held number, not an ephemeral read-time ordinal. | | D4 | Identifier lifecycle | Lifecycle-stable — the add-time number carries through to committed. Gated on V1. | | D6 | Verbal addressing | The Operator speaks the held number; the intent resolves it within the engagement. | | D7 | Authority (R-B20) | The verbal commit executes as the authenticated human Operator, recorded as the human confirming party. Gated on V6. | | D8 | Fidelity | Confirm-back before commit; "held, awaiting your commit" vs "committed to the record" never confused. |


3. Pre-flight verifications (V1–V7)

Run read-only before any change. V1 and V6 are the two gated decisions; both were cleared by the Operator before the build began.


4. The build (per-step commits; suite green at each step)

loomworks-engine — merged at 0ae231b

| # | Commit | What | |---|---|---| | 1 | 464dc28 | Migration 0094 — backfill held-assertion display numbers (run + reversible; dev & test at head 0094) | | 2 | 9b7c93c | Allocate held number at add_assertion (lifecycle-stable); commit adopts existing | | 3 | 48ef463 | needs_you feed emits the held number | | 4 | e79dd71 | Held number reaches the operator-vocab feed (DashboardInboxPreview / InboxItem + translators) | | 5 | 40eff76 | commit_assertion converse intent (classifier + router + _route_commit_assertion + template) |

Alembic 0093 → 0094. Migration verified on playground_dev (Fenwick E0060 → held numbers 1, 2, 3; E0007 #14 revise-from-committed untouched; projection and event-log agree) and up/down/up roundtrip-clean on playground_test. Engine suite 2938 passed / 0 failed / 46 skipped at head. Merged --no-ff into main at 0ae231b (16 files, +916 / −24); origin/main confirmed 08d0c0c pre-merge.

loomworks (Companion) — merged at bafdb07

| # | Commit | What | |---|---|---| | 7 | e53080a | Actionable held card (held number, multi-select, Commit selected/all), fidelity language, verbal-path wiring via existing converse |

DashboardInboxPreview + InboxItem types gain display_number; new commitAssertion helper (direct authenticated POST); HELD_COMMIT strings block. Companion suite 470 passed / 0 failed; ESLint clean; tsc adds zero new errors (3 pre-existing TagChipArea.test.tsx errors, unrelated). Merged --no-ff into main at bafdb07 (5 files, +307 / −2); origin/main confirmed c17539a pre-merge.

loomworks-ui (Workshop)

Unchanged. Port reference only (HeldAssertionCard).


5. Acceptance gate (§6) — all ten met

  1. V1–V7 verified (V1/V6 cleared by Operator before the build).
  2. Held number visible & stable — the card shows "Held N"; the number is durable (add-time) and does not change while held.
  3. Lifecycle-stable — committing keeps the same number; display_number unchanged across held→committed (DB-confirmed).
  4. Card commit — selecting one + "Commit selected" commits it; assertion_committed event with committed_by = the human; item leaves the held surface.
  5. Batch commit — "Commit all" / multi-select each commit all targeted held, one event per assertion.
  6. Verbal commit — "commit held N" confirm-back, commits as the human on confirmation; "commit all" works; unknown number declined without committing.
  7. Authority — no commit anywhere records the Companion as committed_by; every commit is the human (R-B20).
  8. Fidelity — held never called "saved" / "in the record"; the transition is named "committed to the record".
  9. Engine suite green (2938/0); Companion build + tests green (470/0); new tests cover held-number allocation at add, lifecycle-stable carry, feed emits the number, and the commit_assertion route (resolution, human authority, confirm-back, commit-all, unknown rejection, no-authority refusal).
  10. Fenwick E0060 (manual, post-build) — closed below.

Fenwick E0060 — the open loop closed (non-destructively)

E0060's three held drafts now carry stable numbers 1, 2, 3. Both commit paths were exercised against the real engagement as Marvin Percival (operator), inside a rolled-back transaction so the drafts stay held:

DB re-verified afterward: three held, numbers 1/2/3, untouched.


6. As-built notes (caught at halt points / by the suite, each resolved on the merits)

  1. V3 refinement — held is not uniformly numberless. revise_assertion carries a committed number forward onto a held revision (E0007 #14). The migration backfills only display_number IS NULL; the downgrade restores null only for assertions with no committed version anywhere in their history (the never-committed set add-time allocation introduces). This refinement also de-risks D4: the carry-forward mechanism already existed on the revise edge; only the initial add edge was new.
  2. Feed enrichment was two-layer. The first cut enriched only the Phase-39 needs_you arm; the Companion consumes the Phase-40 operator-vocab /operator/dashboard (and /operator/inbox), so the held number also had to thread through translate_needs_you_item / translate_needs_you_to_inbox into DashboardInboxPreview / InboxItem. Without it the number stopped at the engine boundary.
  3. asyncpg rejects multiple commands in one prepared statement — the migration downgrade was split into two op.execute() calls.
  4. IntentLabel enumeration guards — two tests (test_phase_54, test_phase_55) hardcode the intent count; updated 22 → 23.
  5. EngagementNotFound contract preserved — moving the allocation before append_event in add_assertion re-raises EngagementNotFound for a missing engagement, keeping the prior caller/test contract.
  6. Behaviour-guarantee bypass — the confirm-back and post-commit replies are server-composed (delegated_response), guaranteeing D8 fidelity rather than relying on the responder LLM. The two-turn idiom survives because turn-recording (step 7) stamps the companion turn's classified_intent independently of the delegated path.

7. Production migration status — alembic 0094 PENDING in prod

The pushes moved code; they did not run the migration in production. The engine repo has a single GitHub workflow — api-docs.yml (API-docs freshness) — and no deploy or alembic upgrade workflow. Where 0094 has run: dev (playground_dev) and test (playground_test), both local, at head 0094. Where it has not: production (the engine DB behind api.loomworks.dunin7.com). Until 0094 runs there, prod held assertions will not carry backfilled numbers and new held assertions will not be born with one — the code is live on main, the schema/data change is not. 0094 is a counter-drawing data backfill; it should run as part of the same deploy that ships this code (it is order-independent and idempotent on the display_number IS NULL predicate). The held-commit feature is not fully functional in prod until 0094 runs there.


8. Parked and outstanding


Completion record. Pre-flight V1–V7, five engine commits + one Companion commit, full-suite green on both repos, and a non-destructive end-to-end proof on real Fenwick E0060 data. The keystone commit affordance is live on both main branches; production migration 0094 is the single remaining deploy-time step.