Forms class taxonomy + classifier (epic #2806, Phase B)
tools/formspec/formspec/form_class.py gives the AI form rebuild
(formspec.from_detected) class-specific design knowledge. Real-world forms
cluster into a small number of recurring kinds β a W-4, a patient-intake
packet, a purchase order β and each kind has a conventional shape people
expect: the section order, where the signature goes, where the legal fine
print sits. Phase B captures that knowledge as data plus a classifier; Phase
B2 (later) wires it into the rebuild prompt.
The taxonomy
FormClass β 12 classes:
| Class | Covers |
|---|---|
tax_government | W-4/W-9/1099-style official filings, benefits/DMV forms |
employment_application | job applications |
patient_intake | new-patient / visit intake packets |
consent_waiver | permission slips, releases of liability, photo/medical consent |
registration_enrollment | program/class/camp/event sign-up |
order_form | purchase orders with line items and totals |
survey_feedback | satisfaction/evaluation questionnaires |
membership | join/renew an association, club, gym |
incident_report | accident/injury/safety event records |
timesheet | pay-period hour grids with dual approval |
invoice | bills requesting payment |
generic | explicit βnone of the aboveβ fallback |
Template fields (ClassTemplate, one per class in CLASS_TEMPLATES)
display_name/descriptionβ human name and what the class covers (the description also feeds the AI classification prompt).canonical_sectionsβ the conventional reading order as(section name, short guidance)pairs. The order is the knowledge (e.g. patient intake: demographics β insurance β medical history β medications/allergies β emergency contact β consent/signature).autocomplete_hintsβ case-insensitive label-regex β HTML autofill token (WCAG 1.3.5). These are class-specific complements to the generic mapper inworkers/api/src/services/form-label-jury.ts(inferAutocomplete), not a copy: only a tax form should map βfiling statusβ, only an intake form should map βsexβ. Sensitive identifiers (SSN/EIN, member IDs) map tooffβ a valid token that satisfies 1.3.5 while preventing browser autofill. All tokens are validated in tests againstVALID_AUTOCOMPLETE_TOKENS(the juryβs emit-set plus a few class-specific WHATWG tokens:sex,honorific-prefix,tel-extension).signature_conventionβnone/single/dualplus a placement note (e.g. timesheet: employee certifies, then supervisor approves).legal_text_placementβnone/top/bottomplus a note (e.g. a waiverβs terms MUST precede the signature; a surveyβs anonymity note goes in the introduction).prompt_guidanceβ 2-4 sentences of best-practice instruction destined verbatim for the rebuild prompt.
Classifier
Two layers, deterministic-first:
classify_form_heuristic(text, field_labels) -> (FormClass, confidence)β zero-cost weighted keyword scoring over the case-folded document text + field labels (word-bounded regexes, so OCR-ish text works; each pattern counts once so repetition canβt swamp the vote). Confidence scales linearly to 1.0 at a score of 6; ties are broken by enum order with damped confidence. No signal at all (including empty text) β(generic, 0.0).classify_form(text, field_labels, ai=None) -> FormClassificationβ whenai(an injectedprompt -> reply_textcallable, followingformspec.aiβs injection pattern so tests need no SDK/network) is given, the model is asked to pick one class + confidence + one-line rationale as strict JSON. The reply is validated against the enum; on any parse failure, unknown class, exception, or confidence < 0.5 it falls back to the heuristic.ai=Noneβ heuristic only. The resultβssourcefield ('ai' | 'heuristic') always says which path answered; it never raises for a bad model reply.
FormClassification carries form_class, confidence (clamped 0-1),
source, rationale, and a .template convenience property.
Prompt rendering
prompt_addendum(classification) -> str renders the classβs
prompt_guidance, numbered canonical sections, and signature/legal-text
conventions into a text block ready to append to the rebuild prompt. Sections
are framed as guidance, not hard rules (βskip a section when no field
belongs to it; never invent fields to fill oneβ), preserving
from_detectedβs guardrail philosophy: the model only ever controls
presentation β canonical field identity is always re-imposed.
Phase B2 wiring (DONE β epic #2806)
formspec/from_detected.pyclassifies before the rebuild β an explicitform_classoverride wins (source"override", confidence 1.0; unknown values are logged and ignored), otherwiseclassify_formruns AI-first through the sameai._call_messagesseam with the heuristic as fallback β and appendsprompt_addendum(classification)to_build_promptβs prompt. Auto-classified GENERIC omits the addendum (no signal β no boilerplate); an explicit GENERIC override is honoured with it.- The report carries
report["classification"](class_id,confidence,source,ai_attempted,rationale, plus that callβs owninput_tokens/output_tokens/cost_usd);report["cost_usd"]is the TOTAL across the classification and rebuild calls, and the form-place executor books it tocost_ledgerwhenever it is non-zero (a fallback rebuild can still carry real classification spend). - The rebuild choice dialog (
apps/formsRebuildChoiceDialog) has a βWhat kind of form is this?β select β auto-detect default plus the 12 classes β threaded throughuseFormDetect.confirmβPUT /api/forms/detect/:jobId/confirm(formClass, allowlist-validated againstFORM_CLASS_IDS) β theform-placeSQS message β the sidecarβs/rebuildbody (form_class). The id list is mirrored in three places (Python enum, route allowlist, dialog constant) β keep them in sync. - Still open: apply the classβs
autocomplete_hintswhen emitting widgets, as a class-aware complement to the generic form-label-jury pass.
Tests
tools/formspec/tests/test_form_class.py β every class reachable via the
heuristic with a representative fixture, AI happy path + all fallback paths
(malformed reply, unknown class, low/non-numeric confidence, raising
callable), confidence bounds/clamping, prompt_addendum content and
ordering, and CLASS_TEMPLATES completeness (all 12 present, all fields
non-empty, all autocomplete tokens valid regexes drawn from the valid set).
Run: cd tools/formspec && python3 -m pytest