Teaching the system without a developer
The checks, the document types and the tolerances that make one firm different from another can now be added as data, tried before anyone relies on them, and kept as a record of what people decided. Five pictures: two kinds of shape, building a check, teaching a new document, the record of decisions, and what runs on its own.
Two kinds of shape
Everything the system checks against comes in two layers. The compiled layer is what I ship: the workflows, the document types, their rules and letters. The version layer is what you add on top: a rule from the builder, a drafted type, a tolerance for one client, a pack imported from another install. A version sits in front of the compiled shape until a date you chose, then steps aside. If it turns out to be wrong you do not wait for the date: withdraw it and the shape rolls back a step that moment. Nothing you add can break the engine, because the engine never changes.
Under the hood
The Definitions view (sidebar → Definitions) shows every workflow and
document type as it resolves now. The badges: offered means members can
run it and its rules are in the manifest; not offered means withdrawn, past
runs kept, rules hidden; compiled means no version is in front of it;
version N until date means a database version wins until that date;
withdrawn on a document type means it is out of the pickers and the
classifier. Rules carry error or warning, and one the live
version added carries from version N, so it is visible what withdrawing
would take away. Two actions live on the page because neither edits a definition:
Withdraw version N archives the version being served and resolution
falls back to the next live one or to the compiled shape, and Override
in a rule’s Tolerance column sets how far apart two figures may be before that rule
says anything, for the install or for one collection, with a reason and its own expiry.
Export pack downloads the lot as a file; Import pack publishes every entry of such a
file as the next version of its own, with one expiry, overwriting nothing (earlier
versions stay, the compiled definition stays underneath). Versions live in
workflow_definition_versions and extract_schema_definition_versions;
the highest published, unexpired one wins; the compiled definition is the fallback for
everything.
Building a check from a sentence
A senior writes what should agree with what. A bounded model turns it into one declared rule in one of five kinds, using only the fields the workflow already exposes; it cannot write code and cannot decide a verdict. The rule is tried on nineteen made-up engagements and on the client’s own last run, with nothing written, and then published at warning. A ratio or a percentage is refused, and the refusal says why. So is a check that already exists — two rules for one comparison is noise — which is why changing one is a button rather than a sentence: press Edit beside it and the same four steps become a replacement, keeping the rule’s id and its severity. Before you write anything, the page lists what this workflow already checks, so you can see what is there.
Under the hood
Definitions → Build a check. The proposal is forced by schema into
reconcile, continuity, unique, rowReconcile or
rowUnique; the same validator that gates an imported definition runs on the
workflow with the rule appended. The dry run calls validateAssembled twice, with
and without the rule, over evals/corpus/environments and over the collection’s
latest run re-read through RLS. Publish goes through the shared
publishDefinitionVersion with a mandatory expiry. Tolerances are absolute
amounts, there is no subtraction (A less B equals C is written C plus B equals A), and
the fire rate of a warning rule shows on the Feedback page before a person promotes it.
An edit pins the rule’s id in three places rather than hinting at it: the brief says
the rule is being revised, the prompt lifts its do-not-duplicate rule for that rule only,
and the answer’s id is forced back — so a revision cannot land as a second check
firing beside the one it was meant to replace. Asked instead to loosen a check, the model
will say that a tolerance change is an override and not a new rule, which
is right, and that is set from the Definitions view.
evals/rule-builder.eval.ts gates the drafter in CI, with the duplicate refusal
and the same rule named as an edit sitting side by side so the exemption cannot quietly
cost the refusal.
Teaching it a new document
Pick three examples the client already sent, name the type, say which fields matter. The model reads them with the names already replaced and answers with a list of fields, never a schema; code builds the schema, so it is right by construction. Each example is extracted twice and only what both runs agree on can be confirmed, after you have reviewed it in the Verify tab. Publishing re-extracts every confirmed example and refuses a type that no longer reproduces them.
Under the hood
Definitions → Draft a type. The drafter returns a declaration (names, kinds,
instructions, required, up to three repeated groups) and zodForProposal builds
the Zod object and JSON Schema, proved to hydrate before a draft version row is
written. A try persists the first run’s extract under the draft’s name, grounded, so
the Verify tab and its correction route work unchanged; the second run exists only to be
intersected with the first (stableValues). Confirm records the stable paths plus
every corrected one in extract_schema_examples. Publish needs three confirmed
examples and 90% of confirmed values recovered on a fresh run; the same check runs on a
live type any time. The extraction model ignores temperature, so a value it has to infer
is not stable between runs: when the gate names such a field, the fix is the wording of
the request (say what to leave null), not the bar. A drafted type gets extraction,
classification, grounding and review from its schema; a code check, a derive rule or a
letter for it is still a deploy.
The record of decisions
The system used to keep only the latest state of what people decided. Now it keeps the decisions themselves: a finding accepted and later reopened, a run signed off and delivered, a letter sentence rewritten, a chase list sent and answered. The Feedback page reads them back, and that is the material the next tolerance, rule and prompt change is made from, by a person, with the evidence in front of them.
Under the hood
Append-only tables, one per decision: workflow_run_finding_reviews (acceptances
carry forward by fingerprint, which deliberately excludes the definition version so a
tolerance tweak does not orphan them), workflow_run_outcomes (signed off,
delivered with the artefact, came back wrong with a reason, withdrawn; delivery never
clears), workflow_run_report_edits (the redacted narrative before and after,
keyed to the slot; a regeneration writes what it replaced as superseded), and
workflow_job_messages (the chase list as sent: the classified gaps it asked for
in clear, the recipient and body encrypted under the vault key, the reply as a person
marks it, and the documents that arrived since counted at read time). The chase list
itself is serialised by code from workflow_gaps: client gaps only, documents
never figures, no internal vocabulary, the preparer’s gaps as an internal note.
Notes are scrubbed of structured identifiers, not of names.
What runs on its own, and what never does
A job can now run itself: every Monday at seven, or on the second of the month, queued exactly as if you had pressed the button, on the budget of the person who set it. The exams run on every change, and the revalidation on every new document. But nothing reaches a client and nothing is published without a person’s click. The schedule runs the job; you do the rest.
Under the hood
A Schedule card on the job page stores a cadence (daily, one weekday, or one day of the
month, at an hour in the install zone) in workflow_schedules with the owner and
the next due instant, computed across daylight saving in
lib/workflows/schedules/cadence.ts. One Inngest cron every fifteen minutes queues
due jobs with the owner as requester (queueJobs, then workflow/job.run);
a busy job, an owner past their spend cap or a withdrawn workflow is skipped with the
reason on the card, and the schedule advances to its next due time rather than retrying
every quarter hour. The periodic tasks a person keeps: send the chase list after an
incomplete run, sign off and mark delivered, read Feedback weekly, promote warning rules
after a fortnight of data, re-check each bespoke type’s golden set monthly and after a
model change, renew versions before they expire, export the pack before an instance
sync.