From 44cfd88bddc1c62d497301cf7645b6fb8cfe226f Mon Sep 17 00:00:00 2001 From: Blake Bertuccelli-Booth <46652+bbertucc@users.noreply.github.com> Date: Thu, 21 May 2026 14:47:26 -0700 Subject: [PATCH 1/7] feat(pipeline): form reconstruction subagent emitting composable form blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Realigns form handling to the table/list/image pattern instead of a standalone step. The structure pass now detects forms (new has_forms page attribute), which gates a reconstruct_form tool on the page-correction agent — so the orchestrator only pays for form work on pages that actually have a form, and forms compose with the other per-page corrections. The form subagent reads the form from the page image and returns a structured FormReconstructionResult (legend + typed fields). A deterministic renderer turns that into a self-contained ```form block (a small composable DSL inspired by Obsidian custom code-block processors / Meta Bind), which the page agent splices in via str_replace. Rendering the block to accessible controls is the viewer's / downstream renderer's job; the block contract is documented in docs/reference/form-block.md. - has_forms on PageAttributes + structure-analysis prompt detection - src/agents/prompts/form_reconstruction.py (subagent prompt) - procedures/page_correction/content/has_forms.md (gated page guidance) - FormFieldType / FormFieldSpec / FormReconstructionResult models - _render_form_block / _form_dsl_escape / _run_form_reconstructor + tool wiring and token accounting in pipeline_viewer - docs/reference/form-block.md spec + pipeline-phases updates - 18 unit tests (renderer, escaping, has_forms composition, subagent wrapper) Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/reference/form-block.md | 100 ++++++++ docs/reference/pipeline-phases.md | 5 +- src/agents/prompts/form_reconstruction.py | 99 +++++++ .../page_correction/content/has_forms.md | 54 ++++ src/agents/prompts/structure_analysis.py | 7 + src/services/pipeline_viewer.py | 208 +++++++++++++++ src/services/pipeline_viewer_models.py | 62 +++++ .../unit/services/test_form_reconstruction.py | 241 ++++++++++++++++++ 8 files changed, 774 insertions(+), 2 deletions(-) create mode 100644 docs/reference/form-block.md create mode 100644 src/agents/prompts/form_reconstruction.py create mode 100644 src/agents/prompts/procedures/page_correction/content/has_forms.md create mode 100644 tests/unit/services/test_form_reconstruction.py diff --git a/docs/reference/form-block.md b/docs/reference/form-block.md new file mode 100644 index 0000000..01943db --- /dev/null +++ b/docs/reference/form-block.md @@ -0,0 +1,100 @@ +# The `form` block + +When the pipeline finds a fillable form on a page, it does not emit raw inline +HTML. Instead the `page_content` form subagent (see +[pipeline-phases.md](pipeline-phases.md)) replaces the flattened form text with +a single self-contained fenced **`form` block** — a small, composable DSL that +describes the whole form. A downstream renderer (the viewer, a WordPress +plugin, an Obsidian-style code-block processor) compiles the block into +accessible form controls. + +This keeps the converted markdown clean and round-trippable: one block per form, +trivially diffable, and validated by a deterministic renderer rather than +trusting an LLM to emit correct HTML. The pattern is modelled on Obsidian custom +code-block processors (e.g. Meta Bind input fields). + +> **Accessibility note:** the `form` block is an *intermediate representation*. +> It is only accessible once a renderer turns it into labelled controls. Any +> surface that ships the converted document to end users must run a `form`-block +> renderer; raw, unrendered blocks are not accessible on their own. + +## Grammar + +```` +```form +legend:
# optional, at most one, must be first +- [*] |