Goldie Blocks

Buy the Mechanics, Build the Document Semantics

Which parts of Goldie Blocks lean on established packages, which stay custom because they are the product's own rules, and which frameworks are deliberately not introduced

Decision

Standard packages own the mechanics; Goldie's own code owns the document semantics. No published package models a Folio hierarchy with per-save-point schedules, legal assembly, agent proposals and the Stack. Reaching for a general framework there would mean translating Goldie's rules into someone else's abstractions and working around every place they do not fit. Where a mechanic is generic — editing text in a browser, validating a payload, wrapping a database function, ordering a list, diffing two strings — an established package is preferred over hand-rolled code, and the repository already carries most of them.

In plain terms: buy the filing cabinet, the typewriter and the photocopier; write the precedents yourself. The value of the firm is in the precedents.

Owner direction 2026-09-02, on an external review of the repository's dependencies against its hand-written code.

Standard packages, at the boundary where they belong

MechanicPackageStatus
Backend, transactions, schedulingConvexIn use
The agent runtime@convex-dev/agentIn use
Schema migrations@convex-dev/migrationsInstalled; the hand-written convex/folioMigration/ was deleted with the Stage A seed compiler (ruled, landed 2026-09-03), and any later live migration uses the package
Shared checks in front of a function familyconvex-helpers customMutationGB-173 — in use; convex/folio/folioOperation.ts declares every executor so the prologue runs before the handler
One payload contract, deriving type and validatorZod with the convex-helpers bridgeGB-172 — in use for the field contract (packages/domain/src/field/field-schema.ts, derived in convex/folio/fieldValidators.ts); GB-182 (landed 2026-09-06) — the four twins the Stage D review found now derive the same way: the field-operation vocabulary (field/block-field-operation-schema.ts → convex/blockFieldSchema.ts), the proposal schedule (proposed-change/ → convex/folio/proposalSchedule.ts), and the block payloads with numbering (folio/schema-about-card.ts and numbering/start-level.ts → convex/folio/validators.ts, convex/displaySchema.ts); convex/derivedValidators.test.ts fails if any derived validator carries v.any(). Then each new write boundary; never a repository-wide conversion mid-migration. Scope ruled 2026-09-04, below. The Zod 4 bridge requires convex-helpers ≥ 0.1.123: under 0.1.118 it fails the Convex typecheck on the commitTs validator kind Convex 1.45 added
Shared checks in front of a read familyconvex-helpers customQueryGB-178 — in use; convex/folio/folioRead.ts declares every Folio read so the read authority runs before the handler
Editor mechanics: selection, undo, IME, paste, Markdown round-trip, decorationsTiptap over ProseMirrorGB-58 — the feasibility spike must pass its six proof points before any further custom content-editor behaviour
Collaborative text, if it is ever needed@convex-dev/prosemirror-syncGB-174 — recorded ahead of need, post-cut-over
Text difference beneath a proposaldiff (jsdiff), a maintained Myers implementation — line grain for authored source, word grain for prose on the reading faceGB-176, landed 2026-09-15 in src/shared/ui/molecules/source-diff; Goldie keeps scope, storage, cascade, acceptance, rendering and authorization
Sibling ordering keysFractional indexing, evaluatedGB-175 — a bounded spike after cut-over; the gapped-integer arithmetic in pinOrder.ts stays until it passes
Form state for nested Data blocksTanStack Form or React Hook Form, not introducedGB-177 closed 2026-09-04: not for the field editors, not for About editing, and not for the planned nested Data-block editor. Each control is a few lines of draft state saved independently on blur and reset by prop or remount key; the domain already owns nested schema, transitions and typed refusals, and field-level errors arrive through the server refusal path (GB-181). Nothing a form library would delete exists. Reopen condition: a whole-block submit is designed that must validate collections client-side before one save, or a second copy of a nested-path draft appears beside the first
UI primitives, drag-and-drop, testingshadcn/Base UI, dnd-kit, Vitest, Playwright, StorybookIn use

Where Zod owns the shape, and where it stops (owner ruling, 2026-09-04)

Zod is used selectively, never as a replacement for Convex validators. Five layers, each owning one thing:

LayerOwns
Zod schemaThe authoritative persistent domain structure, and intrinsic validity
z.inferThe TypeScript type — never a second hand-written declaration
zodToConvexThe storage-boundary validator, derived rather than authored
React component stateEphemeral editing and draft state
Domain and application logicContextual and behavioural rules

In plain terms: one master definition of what a thing is; the type and the filing clerk's checklist both printed from it; the half-filled form on the desk is nobody's master copy; and the rules that depend on who is asking, or on what else is in the file, stay written out as rules.

The deciding question for whether a rule belongs in the schema: can this record ever be valid if the condition is false? If no, it is intrinsic and belongs in the schema — minItems <= maxItems cannot describe a valid field either way round. If the answer depends on document context, sibling fields, role, workflow or product behaviour, it is domain logic — a collection's item_sum totalling 100 is a rule about answers, not about whether the field declaration is well formed.

Three consequences worth stating, because each is a way the refactor could go wrong:

  • Native v.* stays for simple database-only shapes and straightforward function arguments — the table definitions, folioOperationArgs, a read's address. Converting them for consistency buys nothing.
  • z.unknown() is refused where it reaches storage. The bridge derives it to an unconstrained validator, which would make a contract superficially DRYer while weakening the boundary it exists to guard — the opposite of the refactor's purpose. A JSON-valued member takes the bounded-JSON domain type instead. The bounded levels stay written out, because Convex validators cannot recurse; Zod states them once rather than twice.
  • Zod does not own the little editors. A React field editor keeps its own draft state and consumes the authoritative type; it does not become a schema-generated form. That is why a form framework stays an evaluation scoped to the nested Data-block editor and is not a route to a repository-wide conversion.

Custom, because it is the product

Folio identity and kinds; live head versus sealed version; recursive pin schedules and save-point behaviour; exact-edition generation; the operation catalogue, kind applicability and the lock rules; Builder, Maker and Reader authority; structural proposals and cascade acceptance; Goldie's authority to edit or propose; reference resolution, numbering and conditional composition; the Stack interaction model. The permission policy stays custom and its enforcement is centralised in one prologue — a policy language such as Casbin would replace half of resolveFolioOperations and leave the menu metadata and refusal reasons behind.

Deliberately not introduced

A global state framework (Redux or similar); a generic workflow or event-sourcing engine; a full policy language; CRDT infrastructure; a schema-driven UI generator; a second component system beside shadcn/Base UI; a repository-wide form-framework conversion. Each would add abstraction and integration checks without shortening the migration.

Revisit

When a listed evaluation passes its spike, amend this table in the same commit that adopts the package. When a "custom" row starts to look generic — the policy grows role-by-condition matrices, or ordering needs concurrent edits — that is the trigger to re-open the row, not to reach for a framework quietly.

On this page