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
| Mechanic | Package | Status |
|---|---|---|
| Backend, transactions, scheduling | Convex | In use |
| The agent runtime | @convex-dev/agent | In use |
| Schema migrations | @convex-dev/migrations | Installed; 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 family | convex-helpers customMutation | GB-173 — in use; convex/folio/folioOperation.ts declares every executor so the prologue runs before the handler |
| One payload contract, deriving type and validator | Zod with the convex-helpers bridge | GB-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 family | convex-helpers customQuery | GB-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, decorations | Tiptap over ProseMirror | GB-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-sync | GB-174 — recorded ahead of need, post-cut-over |
| Text difference beneath a proposal | diff (jsdiff), a maintained Myers implementation — line grain for authored source, word grain for prose on the reading face | GB-176, landed 2026-09-15 in src/shared/ui/molecules/source-diff; Goldie keeps scope, storage, cascade, acceptance, rendering and authorization |
| Sibling ordering keys | Fractional indexing, evaluated | GB-175 — a bounded spike after cut-over; the gapped-integer arithmetic in pinOrder.ts stays until it passes |
| Form state for nested Data blocks | TanStack Form or React Hook Form, not introduced | GB-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, testing | shadcn/Base UI, dnd-kit, Vitest, Playwright, Storybook | In 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:
| Layer | Owns |
|---|---|
| Zod schema | The authoritative persistent domain structure, and intrinsic validity |
z.infer | The TypeScript type — never a second hand-written declaration |
zodToConvex | The storage-boundary validator, derived rather than authored |
| React component state | Ephemeral editing and draft state |
| Domain and application logic | Contextual 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.
Builder Goldie Lives in the Side Panel
Owner ruling 2026-09-28 — the Builder talks to Goldie in the same fixed side panel a Maker has, about whatever is selected; one conversation per document, each turn anchored to the selection at send time; the per-card Goldie trigger and floating panel are gone.
One Domain Rulebook, Concept-Sliced Convex, Standard FSD
Why the codebase uses one runtime-free canonical domain package shared by frontend and Convex, concept-sliced Convex adapters, and standard Feature-Sliced Design for the frontend — and which structural rules each brand promise obliges