Authoring notations
This is the how-to companion to notation.md. That doc defines the vocabulary (Template, Notation,
Questionnaire, Question, Answer, Rule); this one is the procedure — how you write a notation, what the toolchain
enforces, what runs after a client finishes intake, and what is still on the roadmap. If a word here is unfamiliar,
notation.md is the source of truth for what it means.
What a notation is, in one paragraph
A Template is a static blueprint: one markdown file with YAML frontmatter, checked into templates/. A Notation
is that Template come to life — one running instance bound to a Person (the respondent), exactly
one Project, and optionally an Entity — advancing through two state
machines the Template declares. In client English a Notation-in-a-Project is the Engagement (or Retainer). The
Template declares; Restate runs. Everything below is about writing good Templates and growing what their workflows
can do.
Where templates live
Every template lives under exactly one of two shelves:
templates/forms/— government form-backed templates. Paths mirror the public assets bucket, sotemplates/forms/united_states/nevada/state/nv__llc_formation.mdcorresponds togs://<assets-bucket>/forms/united_states/nevada/state/nv__llc_formation.pdf.templates/neon_law/— Neon Law product work: product retainers, engagement letters, product-specific documents, and shared firm documents such as the closing letter.
Every template declares jurisdiction: using a seeded jurisdiction code from store/seeds/Jurisdiction.yaml. Form
templates are stricter: their filename stem, code, and form binding must match, and that code starts with the
jurisdiction prefix (nv__llc_formation, us__form_990, ca__...). Product retainers may keep runtime codes such as
onboarding__retainer_nest while living at author-friendly paths such as templates/neon_law/nest/retainer.md.
Anatomy of a template file
For a plain-English, attorney-facing tour of every frontmatter key — across notation templates, events, blog posts, and board minutes — see <frontmatter.md>. This section is the authoring-focused version.
Every template has two parts: YAML frontmatter (the contract) and a markdown body (the document, with
{{question_code}} placeholders). Here is the shared retainer frontmatter from templates/neon_law/shared/retainer.md
(the real file wraps this block in --- fences, then the prose body follows):
title: Retainer Agreement
respondent_type: person_and_entity
jurisdiction: NV
code: onboarding__retainer
confidential: true
questionnaire: # the intake Q&A — what we ask the client
BEGIN: { _: person__client }
person__client: { _: project__engagement }
project__engagement: { _: END }
END: {}
workflow: # what happens after intake — render, review, sign
BEGIN: { intake_submitted: intake_persisted__client }
intake_persisted__client: { retainer_rendered: staff_review }
staff_review: { approved: generate_pdf__retainer_pdf, changes_requested: reask__client, rejected: END }
reask__client: { intake_resubmitted: staff_review } # re-collect flagged answers, loop back
generate_pdf__retainer_pdf: { pdf_persisted: sent_for_signature__pending }
sent_for_signature__pending: { signature_received: END, signature_declined: END }
END: {}
The body below the frontmatter is plain prose carrying placeholders for declared answers. At render time each is
replaced with the client's answer — {{person__client.name}} becomes the actual name and {{project__engagement.name}}
the matter.
Frontmatter fields:
title— the human document title (N101 requires it non-empty).respondent_type— one ofperson,entity,person_and_entity(N102).jurisdiction— a seeded jurisdiction code (NV,CA,US); required even for product templates.code— the stable, unique identifier (onboarding__retainer,nv__llc_formation); how every surface refers to it.confidential— an explicittrue/falsedecision, never defaulted (N105).origin_url— forms only; the HTTPS government page where the blank form can be obtained.form— forms only; the bundled form code, matchingcodeand the filename stem.questionnaire:— the intake state machine:BEGIN → typed_state → … → END. Each step's_:is the "answered" transition — and the only condition a questionnaire may use: the block must be one linear_chain fromBEGINtoENDcovering every declared state.QuestionnaireSpecrejects anything else at parse time, andN118flags it at authoring time. State names are<type>__<role>; the prefix before__must be a registered Question Type.workflow:— the post-intake state machine: render, staff review, signature, filing. Transitions fire on named signals (approved,pdf_persisted,signature_received).
Two machines, one journal: questionnaire and workflow are hosted on a single Restate virtual object keyed by the
notation's id, so their signals serialize and can never interleave. State is append-only — every transition writes a
notation_events row, and the current state is the latest row's to_state. Nothing is ever updated in place, so the
full history of a matter is replayable for audit.
The typed question grammar
A questionnaire state name is <type>__<role> — a question type and the role it plays in this matter, e.g.
person__trustor, entity__company, address__for_company. The <type> half is a closed set (the registry in
store::question_registry); N113 rejects a typed state whose <type> is not
registered. The <role> suffix is free naming that keeps two answers of one type distinct — entity__company and
entity__subsidiary are two separate entities, not one.
Every type is one of three kinds:
- Record — the answer creates or links a
store::entityrow:person,entity,address,role,filing,credential,disclosure,issuance,signature,notarization. - Reference — the answer selects an existing seeded row:
jurisdiction,country(thejurisdiction_type = 'country'subset, rendered as a country picker),entity_type,product,statute,project. - Custom — a primitive value living in the answer JSON, no SQL grounding:
custom_text,custom_phone,custom_yes_no,custom_single_choice,custom_multiple_choice,custom_usd,custom_datetime. A custom state readscustom_<type>__<prompt_key>and needs a matchingprompts:entry (N104).
Prefer the glossary — both its models and their fields — and reach for custom_* only when nothing in the glossary
fits. The registry exists so a questionnaire is composed of real domain nouns, not free text. Two checks, in order,
before any custom_:
- Is the answer a glossary model? Use the type — a trustee is
person__trustee, the companyentity__company, its officeaddress__for_company, its memberspeople__members, its state of licensurejurisdiction__licensure. - Is the answer a field of a glossary model? Use the model and read the field with a dotted placeholder, never a
custom string: a trustee's name is
{{person__trustee.name}}, the entity's legal name{{entity__company.name}}, the office city{{address__for_company.city}}, a bar number{{credential__bar.license_number}}. Each glossary term's Schema link (store::entity::…) is the list of fields a type exposes.custom_text__trustee_nameis the anti-pattern — a name is a field of a Person, not free text.
Only a value that is neither a glossary model nor a field of one is custom — a fee status
(custom_single_choice__fee_status), a one-off date (custom_datetime__filed_on), a consent flag
(custom_yes_no__recording_consent). This keeps answers queryable, linkable, and glossary-grounded.
custom_text has one extra guardrail: N117 is a strict allowlist. Every custom_text__<role> must appear in the
rules crate's ALLOWED_CUSTOM_TEXT_ROLES (each entry carries its rationale in a comment), so a new free-text state
fails closed until it is either typed or adjudicated onto the allowlist. Roles that name a glossary-backed noun —
custom_text__client_name, custom_text__client_email, custom_text__healthcare_agent,
custom_text__country_of_birth, custom_text__daytime_phone, custom_text__product_description — can never be
allowlisted (a meta-test bars the tokens). Use person__client, person__healthcare_agent, country__of_birth,
custom_phone__daytime_phone, and their dotted fields, or product/project render context such as the retainer's
{{custom_clauses}} slot.
Singular and plural. Each record/reference type has a singular form (one row) and an explicit aggregate form (an
array of the singular's shape) collected under one question — person→people, entity→entities,
address→addresses, and so on. Add a plural only where a matter actually collects several.
__for_ children. A <type>__for_<role> state creates a child row that FKs to an earlier role-addressed parent:
address__for_trustor follows person__trustor (or entity__trustor) and hangs off that answer. N114 requires the
parent to precede the child in the questionnaire graph and bars aggregates from __for_ (their children are inline).
Body placeholders. Prose uses {{code}} for a bare answer, {{type__role.field}} for a field off a typed answer,
and {{#for x in <aggregate>}} … {{x.field}} … {{/for}} to walk a plural answer. N115 resolves every path and
iterator against the questionnaire's declared states; signature blocks ({{client.signature}}) stay N107's specialty.
How to create one — the five-step recipe
New legal matters follow a fixed order (see agent-workflows.md for the long form).
Feature-first, so the composition is specified before the prose exists:
- Write the composition
.featurefirst. Describe the matter as a sequence or branching graph of reusable workflow steps, using only Person / Entity nouns fromglossary.md. The feature is the product-level spec; the template satisfies it by composing already-known steps. - Write the template + questionnaire. Create the markdown file under
templates/forms/...for a government form ortemplates/neon_law/<product>/...for a product template. Declare thequestionnaire:walk and theworkflow:states. Body prose uses{{question_code}}placeholders. If a questionnaire state uses acustom_*__prompt_keycode, add a siblingprompts:map with that English prompt key and the exact prompt to ask. - Seed the questions. Add each new question
codetostore/seeds/Question.yaml(prompt,question_type, help text). The questionnaire's state prefixes must resolve to these codes or N104 fails. - Declare the workflow YAML. Compose the post-intake flow from the shared step registry (below) — never a one-off
handler. Reuse
staff_review, signature, and document steps so the flow stays auditable. - Wire the durable handlers. Bind new workflow steps onto the existing
workflows-serviceworker. Never stand up a per-workflow pod — one worker hosts every flow.
A template is not legally usable until an attorney has reviewed the body copy. The staff_review state is mandatory
(N106) precisely so a licensed human is always in the loop before anything is sent or filed.
Question codes should stay minimal and reusable. A Question is the stable prompt/fact type (citizenship_status,
passport_expiration_date, registered_agent); an Answer is the respondent's time-bound value for that question. If a
future workflow needs to know whether an answer is still fresh, add freshness/expiration metadata to the answer side
rather than minting a new one-off question code. That keeps questionnaires short: reuse a still-valid answer when it can
lawfully answer the question, and ask again only when the recorded answer has expired or is no longer adequate for the
matter.
The validation contract
Three rule families guard every template, enforced identically in your editor, in cli validate, and in CI — because
all three call the same rules crate. A template that is clean on your laptop is clean in the merge gate.
- N-family (notation template shape, structural). N101 title present; N102 valid
respondent_type; N103 snake_case filename; N104 both machines declareBEGIN, reachEND, questionnaire states resolve to real Question codes, and workflow states resolve to known workflow-step prefixes; N105confidentialis an explicit bool; N106 theworkflow:has a barestaff_reviewstate (the suffix formstaff_review__for_grantordoes not satisfy it — the human-review gate must be unconditional); N108codeis the stable Template identifier; N110/N111/F110 enforce theforms/orneon_law/shelf, seededjurisdiction, and jurisdiction-first form codes. N-family rules are diagnostic-only: a human must resolve them, the tool will not auto-rewrite legal structure. - M-family (markdown hygiene, ~50 rules). Headings, lists, fences, tables, spacing. Most carry a safe autofix.
- S101 (line length). 120 Unicode scalars per line, every
.md. Frontmatter is linted too; folded YAML scalars let a long value wrap and still pass.
Run it before committing any .md change:
cargo run -p cli --quiet -- validate <path>
Authoring in markdown with the LSP
navigator-lsp is a single Rust binary speaking LSP over stdio. It shares the exact rules engine the CLI uses, so the
editor and CI can never disagree. Supported editors ship copy-paste configs under lsp/ docs: VS Code,
Neovim, Helix, Emacs, Zed. The authoring loop for a non-engineer legal author:
- Type. Open
templates/neon_law/northstar/nv__simple_will.mdin your editor. Write legal prose and frontmatter — no proprietary tool, no markup beyond markdown. - Live diagnostics. On every keystroke the LSP lints the buffer and shows squiggles: N101 if
title:is missing, N104 if the questionnaire/workflow shape is broken or a questionnaire state is not in the canonical question seed list, S101 past 120 chars, M-rules on shape. Hover any squiggle for a plain-English explanation of the rule. - Fix-all on save.
source.fixAllrewrites every mechanical issue — tabs, trailing whitespace, blank-line spacing, heading spacing — automatically. What remains is the semantic work only a human can do (an unmadeconfidentialdecision, a workflow that never reachesEND). - Open a PR. The clean
.mdis committed as a plain-text diff. CI runs the identical engine. An attorney reviews readable prose; the linter has already signed off on structure.
Why markdown + frontmatter + git, not a proprietary tool
- Ergonomics. One free binary attaches to whatever editor the author already knows. Fix-on-save removes the entire class of formatting fiddling; hover tooltips teach the rules in context, lowering the floor for a non-engineer.
- Correctness. A single rules engine is the authority — editor, CLI, and CI cannot diverge. Invariants that matter
legally (every workflow has a
staff_reviewgate,confidentialis an explicit choice, every workflow code resolves) are machine-enforced before merge, not left to reviewer vigilance. - Auditability. The template is plain text under git: every change is an attributable, reviewable diff, gated by PR. The rules themselves are versioned Rust with snapshot tests. A proprietary document-automation tool hides the document in an opaque format with no line-level diff and no enforceable structural contract.
What runs after intake — the step registry
Once the questionnaire reaches END, the workflow machine takes over. Steps are resolved from a state-name prefix to a
StepKind and an actor class (System / Staff / Respondent) in workflows/src/step.rs. Honest status of what is wired
today:
| Step | Status | Notes |
|---|---|---|
email_send__<slug> | Implemented | Durable SendGrid send via two ctx.run journals; only welcome renders today. |
intake_persisted__* | Implemented | Pass-through wait state recorded on the journal. |
staff_review | State-only | Mandatory gate; dev auto-approves. No prod review UI wired to the worker. |
client_review | State-only | Respondent approves attorney-reviewed drafts on the Phase A review surface. |
reask__* | State-only | Re-collects flagged answers after changes_requested, then loops back to staff_review. |
document_intake__<slug> | Implemented | Worker files a provided artifact (text/file/link) via ingest_bytes. |
extract__* | Seam | Northstar: estate inputs mined from the transcript by AIDA/Gemini; advanced on completion. |
analysis__* | Seam | Contract review: web (Vertex Gemini) flags playbook deviations; System wait state. |
document_drafts__* | Implemented | Northstar: web renders drafts into review_documents rows (System wait state). |
generate_pdf__retainer_pdf | Implemented | Worker-dispatched: render + storage persist wrapped in ctx.run. |
sent_for_signature__pending | Implemented | Wait state; e-signature webhook signals signature_received → END. |
notarization, _signature | State-only | Trust/will signing states; a human act, no worker side effect. |
firm_signature | State-only | Firm (staff) signs the closing letter ending a matter; a human act, no side effect. |
mailroom_send | Implemented | Worker records a filings row in ctx.run; reached only after staff_review. |
certified_mail, e_filing, filing__* | Implemented | Worker submission steps; record filings post-review. |
onchain__* | Scaffolded | Node attestation → durable attestations row; null attestor keeps it pending. |
mailroom_receive | State-only | Inbound mail logged by the SendGrid webhook, not a workflow step. |
witnesses | State-only | Respondent's witnesses sign (will); resolves to the Signature step kind. |
Durability is Restate's: each side effect is wrapped in ctx.run, so a replay reuses the cached result instead of
re-emailing or double-inserting. In prod the worker dials Restate Cloud; in KIND it dials the in-cluster Operator. The
"State-only" rows are the contract for steps with no worker side effect yet. The drift-guard test
workflows::step::tests::drift_guard_every_step_prefix_is_documented fails if step_kind_for gains a prefix
(STEP_PREFIXES) this table never mentions, so the status here cannot silently rot.
The onchain__* row is "Scaffolded": the step kind, the dispatch arm, and the durable attestations table are
implemented and tested, but the on-chain write itself is deferred. The chain is isolated behind the
workflows::attest::Attestor trait exactly as GCS is isolated behind cloud::StorageService — selecting Solana (or a
second chain) is a new impl Attestor, never a workflow edit. The default NullAttestor records no transaction, so the
row stays pending and no live retainer can claim an on-chain record that does not exist. The step is therefore not yet
wired into the binding onboarding__retainer_node workflow; that one-line YAML edge lands together with the
SolanaAttestor (whose open questions — firm key custody, the client wallet, public-chain confidentiality of the hash,
and finality — are decisions, not code). See workflows::attest and the Neon Law Node product page.
The registry is deliberately small. Template authors should compose these prefixes with discriminators
(generate_pdf__articles_pdf, mailroom_send__notice_of_representation) rather than creating per-product verbs. If a
workflow needs a genuinely new act, add a reusable StepKind first, document it here, cover the mechanics in Rust, and
then compose it from a feature spec.
Adding a reusable step — the recipe
A "reusable step" is one StepKind that many notations bind to by naming a <prefix>__<slug> state — email_send__*,
generate_pdf__*, document_intake__*. Two reference implementations show the shape; the next one is a single registry
entry, not a second dispatch match.
- Signature is the seam reference:
web::signature::SignatureProvideris a trait with a stub for KIND/tests and a concreteDocuSignSignatureProviderfor prod, selected fromAppState. Reach for a trait when the step calls an external system that has more than one real implementation you swap at runtime. - Document-intake is the registry reference:
document_intake__<slug>files a provided artifact (a transcript, an executed PDF, an ID scan) into the matter throughstore::documents::ingest_bytes. It has exactly one implementation, so it is a plain dispatch fn behind oneStepKind, not a trait.
The step layer routes through one registry, workflows::dispatch_step, keyed by StepKind. Both callers — the
workflows-service worker (notation_service::workflow_signal, which wraps the call in ctx.run) and the in-process
dev/BDD runtime (DispatchingRuntime::maybe_dispatch, which calls it inline) — share that one arm, so a new step is
added once, not twice. To add a step kind with a worker side effect:
- Name the prefix + kind. Add a
StepKindvariant and its(prefix, StepKind)row toSTEP_PREFIXESinworkflows/src/step.rs, plus the actor class inStepKind::actor. Document it in the status table above (the drift-guard test enforces this). - Write the dispatch fn + payload. Add
<kind>.rswith a serde payload (internally tagged onkind, likeDocumentPayload/IntakeArtifact) and anasync fn dispatch_<kind>(deps…, payload) -> Result<_, _>that performs the one side effect and returns — noctx.run, no journaling; durability is the caller's. - Register one arm. Add the
StepKindtodispatches_side_effectand one match arm todispatch_stepinworkflows/src/dispatch.rs, decoding the payload from the signalvalueand calling your dispatch fn with theStepDepsproviders (email,storage, optionaldb). The worker and the in-process runtime pick it up for free. - Thread the payload from the trigger. The surface that fires the transition into the step (a
webhandler) builds the payload, JSON-serializes it, and passes it as the signalvalue. The artifact for an intake step is phone-friendly: a text paste, a file, or a link — never "scan a PDF".
Keep the ctx.run boundary in the worker, never inside dispatch_step: a registry that journaled its own side effect
would reintroduce the duplicate-effect bug on replay.
Documents and PDFs
What we have. A dedicated pdf crate renders a Typst document to PDF bytes in pure Rust (no shell-out), in the firm
typeface Noto Serif, with a redaction helper. Preview, offline rendering, form-fill, and final document generation share
one notation evaluator for bare placeholders, dotted fields, and aggregate loops; final signed documents evaluate data
placeholders first, then expand signature anchors into Typst blocks and the e-signature manifest. The retainer flow
threads the evaluated Typst source to the worker as a DocumentPayload on the approved signal; the
generate_pdf__retainer_pdf step calls pdf::render and persists the bytes through the cloud::StorageService seam
(FsStorage in dev, GCS in prod) at notations/<id>/retainer.pdf, wrapped in ctx.run for replay-idempotent
durability. web reads the PDF back from storage to hand to the signature provider. This is one-directional: template →
fresh PDF.
Rendering a template to PDF offline — navigator render. For an ad-hoc PDF outside the durable workflow (a demand
letter to send by hand, a draft for review), navigator render <template.md> --out <file.pdf> takes any
validation-passing notation template and compiles it in pure Rust. Because templates are authored in Markdown but
the pdf crate compiles Typst, the body is converted by pdf::markdown::to_typst (headings, emphasis, lists, block
quotes, inline code, links) before rendering. The command validates the file against the same rule set as navigator validate, refuses to render a template with any violation, and fills placeholders through the same notation evaluator
used by preview and final PDF generation.
Output formats — the letterhead seam. How the document is dressed is an OutputFormat (pdf::format): plain
(page geometry + firm typeface) or letter (Neon Law letterhead with the embedded logo). A template declares its
default in an optional output: frontmatter field (validated by rule N109); --format overrides per render. New
forms — pleading paper, a fax cover — are a new OutputFormat variant plus its Typst chrome preamble; the conversion
and embedded logo are shared. Fill {{placeholder}} tokens with repeated --answer code=value flags; unfilled tokens
render verbatim.
Filling fillable government PDFs — done. pdf::fill_acroform(blank_pdf, fields) opens an existing fillable PDF (a
Nevada SoS articles form, an IRS Form 990) via lopdf, walks its AcroForm /Fields, sets each /V, and sets
/NeedAppearances so a viewer regenerates the field appearances — a read-modify-write path distinct from the Typst
render path. Blank forms live as templates in the cloud::StorageService seam (forms/<slug>.pdf); a
generate_pdf__<form> sub-slug dispatches the fill through the same worker step as the retainer PDF (via
DocumentPayload::Acroform). The output is attorney-review-ready, never auto-filed: the workflow spec parks it at
staff_review before any filing step, enforced by workflows::staff_review_gates_filing (a spec-graph check + test).
Two loud-failure guardrails — XFA-based forms (Adobe's XML form layer, unsupported by any Rust crate) are detected and
rejected rather than silently emitting a blank, and a field name that matches no form field errors rather than being
silently dropped. Hierarchical (kids / dotted /T) field names remain out of scope.
External integrations
- Email — implemented.
email_send__*→ SendGrid viareqwest, durable, with the message id captured for the event webhook join. This is the one integration wired end-to-end. - E-signature — implemented (the production dead-end is closed). The
SignatureProvidertrait now ships a concreteDocuSignSignatureProvider(DocuSign eSignature REST viareqwest,.env-driven; the stub stays for KIND / tests). At send time the provider'senvelopeIdis persisted onnotations.signature_request_id. The inbound webhook at/webhook/esignature/:secret(web::esignature_webhook) verifies an HMAC-SHA256 signature over the raw body before parsing it — fail-closed whenDOCUSIGN_HMAC_KEYis configured (a prod invariant) — then resolves the envelope id back to its notation and signalssignature_received→ END. Only acompletedevent advances state; other events ack with 200. The engagement terms are attorney-reviewed atstaff_reviewbefore the document is sent, so signature receipt is a ministerial transition with no second human gate. Covered by a.feature(happy + forgery) and an end-to-end integration test through the real provider against a mocked endpoint. - Google Drive — fully removed. The per-Project archive is the append-only git repo served from
web(seegit-project-repos.md); theprojects.drive_folder_idcolumn, theDriveSyncRestate workflow, theaida_drive_*MCP tools, the web/CLI sync surfaces, thecloud::driveREST client, and thecli driveOAuth door (login/ls) have all been dropped. Drive is no longer a document-ingest surface, and nothing in the dependency graph speaks to it — a matter is reached by cloning its git URL.
Roadmap
Ordered by value, each item independently shippable. Reliability fixes are split out from the features they ride with so neither blocks the other.
Recently shipped. The full ten-template catalog is now bundled into the canonical seed, so a fresh cluster carries
every template (LLC, trust, will, annual report, dissolution, three nonprofit forms, NV MBT) without an import pass.
The signature loop is closed — DocuSignSignatureProvider plus the HMAC-verified /webhook/esignature/:secret
route advance a signed retainer to END (see External integrations above).
Close the signature loop.Shipped. A realDocuSignSignatureProviderplus an inbound webhook that verifies the provider's HMAC signature over the raw body before signalingsignature_received; the provider request-id is persisted on the notation for correlation. This ended the production dead-end atsent_for_signature__pending.AcroForm form-filling.Shipped.pdf::fill_acroform(blank_pdf, fields)(lopdf) fills a fillable government form; agenerate_pdf__<form>sub-slug dispatches it through the worker step, with blank forms held incloud::StorageService. Output is attorney-review-ready, never auto-filed — the spec-graph guardrailstaff_review_gates_filingproves no fill→file path skipsstaff_review. XFA forms and unmatched field names fail loudly rather than emitting a silent blank.Promote the planned filing/mail steps to real handlers.Shipped.mailroom_send,certified_mail,e_filing, andfiling__*are worker-dispatched steps that record a durablefilingsrow (the firm's proof of what was filed) inctx.run; compliance flows (e.g. the Nevada annual report) run end-to-end to END instead of parking.staff_review_precedes_submissionproves — on every bundled spec — that no submission side effect fires before the review gate. (notarizationstays a human act;mailroom_receiveis inbound.)Make language access explicit in intake.Shipped.persons.preferred_language(BCP-47, defaulten) plus aquestion_translationstable of attorney-reviewed localized prompts;notation_sessionrenders every questionnaire prompt in the person's language (web form + AIDA MCP/A2A surfaces, one convergence point), falling back to the English base when a translation is absent. Spanish ships seeded for the retainer questions. Translation is reviewed copy, not runtime machine translation — thestaff_reviewgate and legal copy stay attorney-reviewed in each language. The questionnaire prompt is the only localized surface here: the template body — the binding document a client signs — stays English-only regardless of the client's language. See the English-first rule in../CLAUDE.md.Template storage and scoping.Shipped. Template bodies moved from the inlinetemplates.bodyTEXT column to blob storage (templates.blob_id→ a Blob viacloud::StorageService);templates.project_idplus two partial unique indexes add project-scoped templates alongside the shared catalog, resolved bystore::templates::resolve(prefer Project, fall back to shared). The seed +navigator importpaths ingest bodies into blobs; render paths read them back viastore::templates::body. Seenotation.md.
Why this matters — access to justice
The whole point of the notation system is to make routine legal work cheap, fast, and repeatable without removing the
attorney. Each design choice traces back to that mission (mission.md):
- One template, many matters. A lawyer encodes a matter once; every future client walks the same validated questionnaire. The marginal cost of the next LLC, trust, or annual report trends toward zero, which is what lets the firm serve people a billable-hour model prices out.
- Faster resolution, lower cost. A guided questionnaire plus automatic document generation collapses what used to be multiple back-and-forth meetings into a single self-serve intake the client finishes in minutes — answered in their own words through AIDA, on whichever surface they already use.
- The human stays in the loop.
staff_reviewis mandatory by rule, not by convention. Automation does the repetitive assembly; a licensed attorney signs off on the substance. Faster and accountable, not faster instead of accountable. - Auditable by construction. Append-only
notation_eventsmeans every matter has a complete, replayable history — the transparency a public-interest practice owes the people it serves, and the record that lets one attorney safely oversee far more matters than a paper process ever could.
Speed here is not a convenience feature; it is the access-to-justice mechanism. Every minute and dollar the notation system removes from a routine matter is one that a person who could not otherwise afford a lawyer gets to keep.