Goldie Blocks

Proposals Schema

The proposals table — how the agent files a structural change against one Working Document, and how a Builder accepts or rejects it whole

Re-addressed to Folios 2026-09-15 (GB-248)

The filing, acceptance and rejection doors below are live against the unified folios tables: convex/folio/proposalWrites.ts, convex/folio/proposalDecisionWrites.ts and convex/folio/proposalReads.ts, with the column validators in convex/folio/proposalSchedule.ts. Every address is a folio key; the baseline is the Document's working revision.

Current — the proposals table, the ScheduledOperation domain types, the fileProposal (internal) / acceptProposal / rejectProposal mutations and the openProposal query are implemented and tested. Filing validates shape, admitted membership, backward-only refs, the anchor's reach and the one-open rule; acceptance replays the schedule through the executor bodies a Builder's own click reaches, running the operation prologue for each, inside one Folio transaction at the filed revision.

Goldie files from her turn action (GB-277, convex/goldie/tools.ts), and the canvas shows and decides the filed proposal (GB-201): the page subscribes to openProposal, the Review Feature (src/features/review-folio-proposal) binds Accept and Reject and holds the pending and refusal state, and the canvas draws the memo on the card the proposal is anchored to (folioKey): Reject / Accept inside the reply row beside Send, and each change on the thing it changes. The conversation carries no memo line of its own (owner, 2026-09-15: Goldie's reply already says what she proposes, and a line that vanished on decision shifted the card). A stale baseline is shown in the card's notice row as the server's own sentence.

Landed 2026-09-15 (GB-201, GB-176, GB-278): a filed rewrite of a content block is marked up word by word on that block's reading face (the diff package beneath it, /docs/decisions/buy-versus-build); a proposed insertion is drawn as a provisional card where it would land — dashed, striped insertion spine, "Accept insertion / Reject insertion / View" in the reserved action row (src/widgets/drafting-editor/model/proposal-provisional-rows.ts); and a container is decided whole or item by item (rule 10, below): a Steps menu beside the pair and each provisional card's own bar decide one step, decideProposalItem records it in itemDecisions, and the row resolves on its last item. The memo names the kind of thing each step touches.

A proposal is one filed, not-yet-applied structural change to one Working Document. The agent files it; a Builder (the role holding write-structure) accepts or rejects it. Nothing moves until acceptance.

In plain terms: a marked-up draft that a junior sends up the line, sitting beside the file rather than in it. Accepting it is the moment the changes are engrossed; rejecting it leaves no mark that it was ever suggested.

Why a separate table

A proposal is its own row, never a Block/Module version and never a column on the row it targets. A rejected proposal must leave no trace in version lineage — versions record what happened, and a declined suggestion did not happen. And a proposal may create entities that do not exist yet, or span several Modules and Blocks, so no single canonical row could hold it.

The proposals row

Indexed metadata the app can query without opening the change, plus the change itself as one ordered schedule.

Prop

Type

Timestamps. Convex _creationTime is the filing timestamp; there is no proposedAt and no createdAt (Convex rules).

What each step replaces (befores)

fileProposalFor reads each step's old words from the heads just before it inserts the row, and stores them in befores, one entry per step beside operations. They are written once at filing and never rewritten: once a step is accepted the head reads only the new, so nothing later can recover the old. The chat's proposal list shows old → new from them, and they are the base of the drafting history (which AI changes were accepted).

StepRecorded before
renameThe folio's old name (its label, else its handle, as resolved for display).
write-edit contentThe head version's wording (payload.text).
write-edit fields rename or deleteThe field's old name, found by fieldKey in the head's payload.fields.
write-edit about, write-module-about, write-document-guideThe head version's guide.

An entry is null when the step replaces nothing: an insert; a target that is a placeholder ref ($new1, $new1.block) made by an earlier step; a field add; a guide or wording that was empty; or a target that cannot be found. Rows filed before this field existed have no befores; the list falls back to the live name.

In plain terms: the memo keeps a photocopy of each passage it proposes to change, so the file always shows what the draft said before, even after the change is accepted.

Indexes

TableIndexPurpose
proposalsby_key on (key)Business-identifier lookup.
proposalsby_document_status on (documentKey, status)The open-proposal check, and "show me this document's open proposal".
proposalsby_thread on (threadKey)Trace a proposal back to its conversation.

Proposals queue on a document

Owner ruling 2026-10-07 (Proposals Wait for an Explicit Accept): a document may hold several open proposals. Filing never accepts or blocks on the one already open; the new one joins the queue. openProposal returns the head of the queue (the oldest open row). Accepting one moves every other open proposal filed against the same revision up to the revision the accept produced (advanceQueue in proposalDecisionWrites.ts), so the queue is not stranded as stale. MVP rule: a document has at most one open proposal — retired 2026-10-07.

In plain terms: memos queue in the tray; signing the top one does not make the ones under it out of date.

The operations schedule

The change is an ordered array of ScheduledOperation — a type defined once in packages/domain/src/proposed-change/, imported by both the filing and the acceptance path. It embeds the existing FolioOperation value (packages/domain/src/folio/operation-resolution.ts) as its discriminant, and its args are the executor's own arguments with the target's address beside them — { operation: FolioOperation; args }.

There is deliberately no forked operation vocabulary and no opaque blob: a Builder's click and Goldie's proposal name the same catalogue row, and acceptance replays the row through the very executor the click reaches.

In plain terms: the memo lists the very actions the menu offers, each naming the card it is about, so the clerk applies an accepted memo with the same stamps a partner's own edits get.

Admitted operations

Each row names the executor body acceptance replays it through — exported from the executor's own module, so the click and the replay share one body and one refusal wording.

Admitted operationEmbedded FolioOperationArgsReplays through
Insert module{ kind: "insert", child: "module" }parent (the Document), optional after (a sibling module), optional guide and tagsinsertFolioChild, then Goldie's tag door (compositionWrites.ts)
Insert content block{ kind: "insert", child: "block", payload: "content", source: "new" }parent (a module), optional after (a sibling block), optional guide and tagsinsertFolioChild, then Goldie's tag door
Insert data block{ kind: "insert", child: "block", payload: "fields", source: "new" }parent, optional after, optional guide and tagsinsertFolioChild, then Goldie's tag door
Keep a Library copy (block){ kind: "insert", child: "block", payload, source: "library" }parent, optional after, copy (the staged copy's own key), entry (folioKey, libraryKey), optional alternatives (up to two, each with label and fit), optional guide and tagsclears folios.stagedAt on the copy, then writes the proposed guide and tags; reject removes the copy (library/stagedWrites.ts)
Keep a Library copy (module){ kind: "insert", child: "module", source: "library" }as aboveas above
Content-block text{ kind: "write-edit", facet: "content" }target, textwriteContentOnHead (contentWrites.ts)
Data-block field structure{ kind: "write-edit", facet: "fields" }target, operation (a FieldStructureOperation)applyFieldStructure (fieldStructureWrites.ts)
Block guide{ kind: "write-edit", facet: "about" }target, guide (text or null)writeGuide (aboutWrites.ts)
Module guide{ kind: "write-module-about" }target, guidewriteGuide
Document guide{ kind: "write-document-guide" }target, guidewriteGuide
Rename any folio{ kind: "rename" }target, labelrenameFolioRow (namingWrites.ts)

Edits embed write-edit, never propose-edit. The propose-edit / write-edit pair encodes the card-level review flow, and the proposal mechanism on this page is the review — embedding propose-edit would nest one review inside another. Two consequences follow, both wanted: at acceptance the schedule performs writes, and the filing-time scope check evaluates the write-edit operation's required capability, the one actually exercised.

Field-structure args

The fields facet carries the field-structure executor's own operation verbatim — fieldStructureOperationSchema (packages/domain/src/field/block-field-operation-schema.ts), from which the executor's Convex validator and this column both derive: add (optional label, naming the field as it is born; optional type from the retype vocabulary, Text when absent; optional afterFieldKey, the existing field the new one lands directly below, and optional atStart, which lands it first — one or the other, never both, and appending when neither); rename (fieldKey, label); retype (fieldKey, type); move (fieldKey, direction); delete (fieldKey); set_required (fieldKey, required). There is no second field vocabulary for proposals.

A rename changes the typed name and the handle projected from it; the fld_ identity underneath is stable, and references bind to that identity, never to the handle. A proposed rename is therefore not reference-breaking and needs no carve-out; the executor's sibling-handle guard is the last word.

A field is never addressed by placeholder. add mints its fld_ key at acceptance and no later step can reach it — there is no $new for a field — so a new field is named in the add step itself ({ kind: "add", label }). Filing refuses a placeholder fieldKey in those words (owner's stuck memo, 2026-09-15: "add" then "rename $new1" was accepted at filing and refused at acceptance as a field not found). An add's afterFieldKey addresses an existing field, so the same rule covers it: a placeholder there is refused at filing, and an anchor the block does not hold is refused at acceptance rather than quietly appended.

In plain terms: a suggestion can say which existing question the new one should sit under, but it cannot point at a question the same suggestion is still about to create.

What is excluded, and why

ExcludedWhy
Field valuesA Maker's writes and the side panel's job (The Side-Panel Goldie Is Maker Only); never travel through structure proposals.
TagsOut of phase 1 by owner ruling 2026-09-15: no tag writer exists, and Goldie proposing tags is its own later ruling.
A block's handleMachinery nobody dictates to Goldie; a name is proposed through rename.
Arrangement and lifecycle — move, remove, duplicate, delete, restore, save versionRearrangement a Builder does by hand. Any may join later as one more admitted row.
Library insertsAdmitted since 2026-09-30 as the rows above: the copy is staged at filing and the memo keeps or returns it (Goldie Finds a Clause and Stages It).

Target: proposal capability reaches user capability

The admitted list is an incremental delivery boundary, not a permanent ceiling. Each addition extends the shared typed catalogue and the one replay pipeline; none creates a feature-specific proposal path.

Addressing

Payloads store stable folio identities only — folios.key, whatever prefix a row wears (fol_, or a legacy tier prefix a migrated row still carries) — never a version key, never a handle. A key's prefix says nothing about its kind: the kind is a fact the filing mutation reads from the row and hands the scope resolver as data (ProposalFolioFact).

The Document itself is addressed by its own key, like any other folio. Filing refuses any address that does not sit beneath the proposal's Document as unknown-folio, so a schedule can never reach into another file.

In plain terms: the note names each card by its file number, and the clerk checks every number is in this file before the note goes in the tray.

Resolution is sequential — per operation, mid-replay. Each operation resolves its addresses against the document as the operations before it in the same schedule left it: a rename in step two changes the name step four is shown under, and a folio created in step one has no key until step one has run.

Baseline staleness

Settled 2026-09-06, before Stage E: the baseline is the Document's working revision, not its head version key alone. The live head is edited in place and keeps its folioVersions.key between saves, so a baseline made only of the key would accept a proposal reviewed against prose that has since been rewritten. The root Document row carries a monotonic workingRevision (Folios → The working revision); every material write anywhere in the tree advances it exactly once, a no-op or a refusal never does, and a multi-operation acceptance advances it once for the whole accepted transaction.

  • Filing (fileProposal) records the Document's current revision as baselineRevision (and the head key as baselineVersionKey, for the reader). Filing writes nothing material, so it never moves the revision.
  • Acceptance (acceptProposal) runs inside runFolioTransaction with expectedRevision: baselineRevision: the transaction is refused as stale before any operation runs when the revision has moved, and otherwise replays the whole schedule and advances the revision once. A refused acceptance changes nothing; the agent files afresh against the new baseline.
  • Sealed historical editions are unaffected: the revision is a fact about the working Document, and versions carry nothing of it.

In plain terms: the suggestion says which edit count of the file it was written against. If the file's count has moved on, the suggestion is handed back for a fresh look rather than applied to a file its author never saw.

The invariant. A proposal identifies the exact state it proposes to apply — the Document, its revision, and stable folio identities for every target — and acceptance either applies that reviewed state atomically or refuses it as stale. There is no third outcome.

One baseline per proposal, and it is the Document's. Per-target baselines — one per Module or Block touched, so an unrelated edit elsewhere does not stale the whole proposal — are a later non-breaking loosening, not MVP.

Placeholder refs

A later operation in a schedule may target an entity an earlier operation creates; refs resolve to real keys only at acceptance. The syntax and binding rule below were settled by owner ruling on 2026-08-18, and packages/domain/src/proposed-change/placeholder-refs.ts implements them.

Syntax. A ref is the string $new1, $new2, … — a $new prefix plus a 1-based ordinal counting creating operations in schedule order. It appears anywhere a folio key would appear, and the replay substitutes the minted key at acceptance.

Binding rule. A creating operation binds the entity it creates; where the underlying write creates more than one entity, the extra bindings are named fields of the same ref rather than further ordinals. Concretely, insert-module binds both the Module and the starter content Block that is born with it, because insertFolioChild returns the starter with the module: $new1 is the Module, and $new1.block is its starter content Block. Insert-content-block and insert-data-block bind one entity each, addressed as the bare ref.

Validation. A ref must be bound by an earlier operation in the same schedule; a forward or dangling ref is refused at filing time, not at acceptance. A created folio is within its creator's reach wherever it lands, so a proposed new clause can be given its words in the same memo.

In plain terms: a suggestion may say "add a new clause, then rename it" — and the new clause has no name yet when the note is written. $new1 is a placeholder that means "the thing step 1 creates", filled in only when the changes are actually made.

Validation timing

The split below was settled by owner ruling on 2026-08-18; convex/folio/proposalWrites.ts and convex/folio/proposalDecisionWrites.ts implement it.

At filing, the mutation validates: the schedule's shape (each entry parses as a ScheduledOperation, args match the embedded discriminant); admitted-operation membership (nothing outside the admitted MVP table); scope (every operation is permitted by the anchor × capabilities resolver for this invocation); placeholder refs bind backwards; and the one-open-proposal invariant. Rename entries also check the shared naming rules against stored siblings and earlier names in the schedule. Deleted siblings still reserve names and handles. A naming conflict returns to Goldie before the proposal appears, so she can correct it; stable folio keys remain the references throughout.

At acceptance, the mutation re-validates baseline freshness, resolves placeholder refs to minted keys as each creating operation runs, and then replays the schedule through the executor bodies, running the operation prologue (requireFolioOperation) for every operation on the folio it targets. The executors' own guards — authority, live head, policy, handle uniqueness, field identity — remain the last word. Acceptance never bypasses or duplicates them; it is the one composite executor the prologue guard admits beside the builder.

In plain terms: the suggestion is checked for form when it is filed, so an impossible one never reaches the Builder's tray. It is checked for substance when it is accepted, because that is the only moment the document's real state matters — and the ordinary save rules still apply, exactly as if a person had typed the change.

Scope is anchor × capabilities

What Goldie may propose is the product of which folio she is anchored to (folioKey) and what the caller may do (their capabilities), resolved by one resolver (packages/domain/src/proposed-change/proposal-scope.ts) that reads capability and applicability from the Folio operation resolution and decides only the anchor's reach (One Goldie Per Folio).

AnchorReach
Any folioEdits to itself and its descendants — a block's Goldie edits that block; a module's Goldie edits the module and its blocks; the Document's Goldie reaches every folio, the Document's name and guide included.
Any folioInsertions whose parent is within that reach, or that land immediately after the anchor itself.
Any folioEvery folio the same schedule creates, wherever it lands.

| A block | A new module immediately after the module she sits in — one semantic destination, never further down, inside another module, or above (owner ruling 2026-09-15, GB-278); the Builder moves an accepted module with the ordinary reorder. |

Naming. Code always says agent and proposal; Goldie appears only in visible copy, read from AGENT_DISPLAY_NAME (packages/domain/src/agent/display-name.ts).

Accept and reject

Retired 2026-10-07 — nothing applies without an explicit accept (the ruling). Sending Goldie the next message, filing a new proposal, or turning to another card accepts nothing; the turn-start settle and the canvas's moving-on settle are removed. The paragraph below is history.

Moving on accepts (owner ruling, 2026-09-15). A proposal is not left open. It is shown on the card whose conversation filed it for as long as that conversation is the one open; the moment the Builder moves on — sends Goldie the next message anywhere on the document, opens another card's conversation, shuts the panel, or returns to a document with a memo waiting — the proposal is accepted without a click, the way a coding agent's shown diff takes effect. Reject is the one deliberate act, pressed while the card is open; pressing Accept only does early what moving on would do. When the acceptance is refused because the document moved on since filing, the proposal is set aside — rejected, so the tray is clear for her next memo — and the card's notice row says so in the server's words. Server side this runs at the start of every turn (convex/goldie/settleProposal.ts); on the canvas it runs when the panel leaves the anchor card (features/review-folio-proposal).

In plain terms: the junior's memo is taken as signed when you turn to the next thing. Sending it back is the only decision you ever need to make.

Accept is all-or-nothing and atomic. One Convex mutation checks the baseline, then replays the whole schedule in order through the canonical authoring mutations. Real rows and versions exist only from that moment, and the result is indistinguishable from structure a person authored by hand.

Reject flips status to rejected, sets decidedAt, and touches nothing else. The document is unchanged and its version lineage records nothing.

Subset acceptance is a deliberate non-goal for MVP. "Accept some of it" is a conversation: reject with feedback and the agent files a narrower re-proposal. The operations-list shape leaves the door open — subset acceptance would be a later non-breaking extension, not a reshaping.

In plain terms: you sign the marked-up draft or you send it back. There is no initialling three amendments out of five, because a half-applied set of structural changes can leave the document saying something nobody drafted.

Item by item — ruled 2026-09-07 (owner; the Folio Canvas record, rule 10), built 2026-09-15 (GB-201): a proposed container is decided whole or item by item. A proposed module is one proposal with a schedule beneath it. Accept on the container takes every step still open and Reject sends them all back — the whole route above — or the reviewer decides each step on its own (decideProposalItem, on the thing each step changes — a provisional card's own review bar, or the Reject / Accept pair on a data block's tinted field row (owner ruling 2026-09-22: one vocabulary at every grain, not "Keep" for a field beside "Accept" for the whole memo)), and the container resolves when its last step does: applied, replaying only the steps taken, when anything was; rejected when nothing was. Each step's decision is recorded against the schedule entry (itemDecisions) and the row's status derives from them. The schedule's refs impose one rule (packages/domain/src/proposed-change/item-decisions.ts): deciding a creating step decides its undecided dependants with it, taken or sent back, transitively — so Accept on a provisional card lands the new folio with its words, its name and anything proposed inside it — a dependant cannot be taken while its creator stands rejected, and a creator cannot be sent back once a dependant was taken. Placeholder ordinals stay positional over the whole schedule, so a rejected step never renumbers the steps after it. This is the "later non-breaking extension" the subset paragraph reserved.

Where a step is decided — ruled 2026-09-15 (owner), built the same day (GB-282). A Steps dropdown beside the pair used to list every numbered change with its own Accept and Reject. It is retired: a change is judged where it is drawn, not in a list of sentences about it. So each step carries its decision on the thing it changes, in the shape of assistant-ui's Reviewable Diff element — the pair at the top right of the hunk. The reply row keeps Accept and Reject for the whole memo, and moving on still applies everything left open.

Narrowed 2026-09-16 (owner): a prose clause and an About row carry no pair. Only a provisional card and a data block's field row still decide themselves. On a clause and on an About row the pair asked the question the reply row below already asks, so what is drawn there is the markup alone — and, where the memo touches something, a gold dot: on the card's trailing corner for a clause, and on the panel's About tab for an About row (since 2026-09-22; before that beside the About this ... fold-out), which is what an unchosen tab has to say about what it is hiding.

The dot is gold whatever the rewrite does. Colouring it by operation restated the lie the field-row tints already refuse — that a rewrite is an "addition" — and put a three-way code in six pixels; the markup and the dot's accessible name both say what changed.

In plain terms: you initial each amendment where it can be initialled, and sign or send back the whole memo at the bottom.

The marker on the conversation's rail

Ruled 2026-09-15 (owner), built the same day (GB-281). A filed proposal is recorded in the anchor card's conversation as a quiet note — the shared transcript marker, as muted text at the left of the rail (no rail line and no date line, owner ruling 2026-09-16) — written at filing and never removed. It reads Proposed · 3 changes while open and 2 of 3 accepted · 1 open while it is being decided.

A decided memo draws two lines, not one — ruled 2026-09-16 (owner), superseding the single mutating marker above. The record is the series: the filing line, the reply that announced it, then what was decided. One line whose words changed could show only the last of those, so a reader could not see that a proposal had been made at all. The outcome line reads Accepted · 3 changes, Accepted 2 of 3, 1 sent back, Sent back, or Set aside · <the server's sentence> — the count survives the decision, so the two lines read as one note in two states.

The filing line is placed against the reply it announces, not by the clock: the proposals row is written after that message is saved, so _creationTime alone put the caption underneath the sentence it captions. The outcome line keeps sorting by decidedAt, so conversation that happened while the memo sat open falls between the two.

This retires the rule that deciding must not move the card (2026-09-15): an outcome line is a new row. What holds is the top edge — the card grows downward, so the reader's place above the click is kept.

Nothing new is stored. The marker is a view of this row: _creationTime is where it sits on the rail, itemDecisions and status are what it says, and the thread's memos are reached through the by_thread index (goldie/threads:listProposals). The one thing the row does not hold is the refusal a settle came back with; that sentence belongs to the turn that met it (settleOpenProposalForTurn), so the tab that saw it puts it on the marker and every later reader sees the plain "Set aside".

In plain terms: the file note that says "memo filed", and later "signed" — never crossed out.

Drafting rulings

The document-scoped pair — proposing the document's name and its guide — was admitted by owner ruling on 2026-08-18, on the same terms as everything else here: from the Document's own Goldie, and inside the propose-then-accept loop, so the agent still holds no direct write path. The guide is the author's standing instruction to the agent and never renders as document content; writeDocumentGuide is its canonical human executor, and the replay reuses its body.

All of this page's once-open drafting points were settled by owner ruling on 2026-08-18, as implemented: the prop_ key prefix, the decidedBy column (server-derived, so a decision always has an actor), the placeholder-ref syntax, and the filing/acceptance validation split.

On this page