ID-147 Empirical Grounding — resolved OQs + investigation record (pre-spec-chain)
ID-147 Empirical Grounding — resolved OQs + investigation record
Section titled “ID-147 Empirical Grounding — resolved OQs + investigation record”Type: Supplementary grounding doc. NOT a spec. It sits between
{147.1}RESEARCH.md and the UI spec-chain ({147.2}PRODUCT →{147.3}TECH →{147.4}PLAN, which gates{145.19}). Its job: turn the owner’s feedback (id-147-initial-user-feedback.md) + the 6 RESEARCH open questions into settled decisions grounded in empirical fact, so the Planner authors the spec-chain from truth, not from the RESEARCH doc’s (now partly superseded) recommendations.Authored 13/07/2026. UK English (DD/MM/YYYY, “colour”, “organisation”). Track:
/procurement(ca-s435-procurement-baseline). Not committed to code; lives in the docs-site spec dir alongside RESEARCH.md.Investigation method: five parallel read-only agents (Extend component props via
ui.extend.ai+github.com/extend-hq/ui; commonforms bbox bridge viajbarrow/commonforms
- in-tree pipeline; Anthropic-API correctness via
claude-apiskill + context7; AI-SDK Elements viaelements.ai-sdk.dev; in-tree component inventory) + direct verification of the live migrations andcomponents.json. Every external claim was fetched, not recalled.Headline reframes vs RESEARCH.md (read these first):
- Owner chose BROAD Extend adoption, not the RESEARCH-recommended minimal cherry-pick. The live decision is no longer which components, but how they are vendored (primitives
- icons + theming) — §2, §10.
- Extend ships ZERO backend. Every component is a client-only shell. The owner’s “is Extend’s upload backend battle-tested?” question dissolves — there is no Extend backend to adopt; our hardened BI-9 backend stays and we bind Extend’s callbacks to it — §3.
- The bounding-box gap is smaller than RESEARCH feared — for fill-slots. commonforms already computes the exact coordinate shape Extend wants; the app just throws it away. For citations the gap stands — §4.
- The data-model table names in RESEARCH.md and the pipeline docstrings are STALE. The
{145.6}W1c rename (this branch) renamed nearly every form table. §1 is the authoritative map; the Planner must use it.
0. OQ resolutions (the spine)
Section titled “0. OQ resolutions (the spine)”STATUS (round 2, 13/07/2026): FULLY RESOLVED. Every OQ and follow-up question below is settled by owner feedback; the consolidated ruling table + residual spec-author choices are in §11, and the load-bearing rulings are in the Decision Register (DR-063…DR-068). This doc is ready to author
{147.2}/{147.3}from.
The owner’s feedback file answers the 6 RESEARCH OQs plus raises new investigation questions. Consolidated, with the empirical grounding that now backs each:
| OQ | RESEARCH asked | Owner decision | Grounded status |
|---|---|---|---|
| OQ-1 | Cherry-pick vs wholesale Extend | Adopt broadly (all doc components), but see the wholesale-vs-rewire nuance | Live: two independent axes — primitives (Coss UI vs our components/ui) and icons (Hugeicons vs lucide). §10 |
| OQ-2 | Who owns the first registry install? | ID-147 owns it (ID-117 took a different path, is done) | components.json has no registries block; @extend absent. Install slice = §2 |
| OQ-3 | bbox-citations: derive coords vs text-highlight | Unlock Extend Bounding Box Citations | Two surfaces, not one. Fill-slots → cheap (commonforms). Citations → still needs derivation. §4 |
| OQ-4 | Migrate incumbent content-renderer.tsx now or defer? | Fold the migration in | It is the only react-markdown site in scope; feeds qa-pair-renderer. §6 |
| OQ-5 | Schema Builder fit — spike | Spike (feasibility) + integrate commonforms | Verdict: does not fit → custom editor. §5 |
| OQ-6 | Theming adaptation budget | Don’t apply Warm Meridian wholesale — install as-is first (white doc backgrounds), tweak after | §10 |
New questions raised by the feedback, answered in this doc:
| Ref | Owner’s question | Answer | Section |
|---|---|---|---|
| Q-A | Does Extend have an equivalent (battle-tested) upload backend? | No — Extend is client-only, zero server code. Keep our BI-9 backend. | §3 |
| Q-B | Use commonforms for pixel/region bbox coords (form_creator.py)? | Yes, for fill-slots — geometry already exists, is discarded at the DB write. | §4 |
| Q-C | Are we using Anthropic Files/caching/etc. correctly for the domain? | Mostly yes; one real caching defect + a Files-API opportunity. | §7 |
| Q-D | Is AI-SDK Elements workflow feasible for the state stepper? | No — wrong shape (React-Flow graph canvas). Build custom. | §8 |
| Q-E | Do the flagged in-tree components matter for the rework? | Yes — 48 are KEEP domain logic, 6 are REPLACE shells. | §9 |
1. Authoritative data-model map (post-W1c) — the Planner MUST use these names
Section titled “1. Authoritative data-model map (post-W1c) — the Planner MUST use these names”The {145.6} W1c rename migration (supabase/migrations/20260712062000_id145_w1c_rename_reshape.sql,
this branch) renamed nearly every form table. RESEARCH.md, the pipeline docstrings
(form_extractors/pdf.py), and supabase/types/database.types.ts itself are all still on the
PRE-rename names — the type regen is HELD behind the W1 staging push (see the S470 continuation
prompt; grep-gate must read 0 first). Read the migrations, not the generated types, for current
shape.
| Concept (RESEARCH / feedback name) | Authoritative table (post-W1c) | Was | Load-bearing columns |
|---|---|---|---|
| The form item (the item is the form, BI-1) | form_instances | form_templates | processing_status (was status), workflow_state (10-state axis, orthogonal), reference_number, estimated_value, engagement_group_id, ingest_source ∈ {app_upload, minted}, mime_type ∈ {docx,xlsx,pdf} |
| Mechanical structural fill-slots (commonforms/OOXML detected) | form_instance_fields | form_template_fields | field_type, placeholder_text, table_index/row_index/col_index (logical cell coords — NOT pixel geometry), mapping_status, fill_status, form_instance_id FK (new) |
| Answerable questions (BI-7/BI-20) | form_questions | form_questions (workspace-keyed) | question_text, word_limit, section_name/section_sequence, evaluation_weight, status, assigned_to, template_requirement_id, form_instance_id FK (NOT NULL) |
| Answers | form_responses | — | response_text, review_status, overall_score, source_record_ids[], version |
| Reusable requirement catalogue (BI-24, Path-C) | form_requirement_templates | form_template_requirements | requirement_type, requirement_text, matching_keywords[], matching_guidance, is_mandatory, word_limit_guidance, section_ref, sector_applicability[], domain/subtopic classification; embedding moved to record_embeddings (DR-036) |
| Completion tracking | template_completions | (col template_id → form_instance_id) | — |
| Citations (id-58, BI-37) | citations | — | cited_text, cited_start/cited_end, cited_location_kind ∈ {block,char,page} — text-span, NO pixel/bbox column; q_a_pair kind dormant until BI-37 activates it |
Naming defect to reconcile in the spec: RESEARCH.md §4 maps “Fillable field structure (BI-20)” onto Extend Schema Builder, but BI-20 spans two distinct current tables —
form_instance_fields(mechanical structural slots, the bbox target) andform_questions/form_responses(semantic answerable slots). Keep them separate; they have different owners and different UI.
2. Extend adoption + install slice (OQ-2)
Section titled “2. Extend adoption + install slice (OQ-2)”Distribution model (confirmed from github.com/extend-hq/ui registry root apps/v4/): Extend
is a shadcn source-copy registry — pnpm dlx shadcn@latest add @extend/<name> copies the
component into our tree as source. There is no npm runtime dependency on Extend. Base
primitives = Coss UI (registry/base-ui/ui); icons = Hugeicons. All components are
client-only ("use client").
Current install state (verified):
components.json:style: new-york,baseColor: neutral,cssVariables: true,iconLibrary: lucide. Noregistriesblock →extend-hq/uiis not registered.package.json: no@extend/extend-*dep. Already present and relevant:streamdown ^2.5.0,react-pdf ^10.4.1, parsersdocx/docx-preview/mammoth/xlsx(parsers, not viewers),@react-pdf/renderer(generation),@tiptap/*(editor),lucide-react ^0.575.0.
ID-147 install slice (net-new, since ID-117 didn’t land it):
- Add a
registriesblock tocomponents.jsonpointing at theextend-hq/uiregistry (the@extend/*namespace). shadcn addeach adopted component; the source lands undercomponents/(domain subdir — never root, percomponents/CLAUDE.md).- Resolve the icon fork and primitive-stack decisions (§10) at vendor-in time — the cost
is per-component, so decide the policy before the first
add, not after. - Some Extend components pull
@extend-ai/react-docx/@extend-ai/react-xlsxas real npm deps (the DOCX/XLSX render engines) — those are runtime dependencies, distinct from the copied-in shell. Budget for them.
3. Extend component → Procurement mapping (REVISED for broad adoption)
Section titled “3. Extend component → Procurement mapping (REVISED for broad adoption)”Per-component API surface (from the doc site + GitHub source). “Backend?” is uniformly “client only” — Extend ships no server code anywhere in the registry. Callbacks bind to our backend.
| Extend component | Key props / input | Feeds which surface | Notes for the spec |
|---|---|---|---|
| PDF Viewer | src:string (URL/blob), zoom/toolbar flags, renderPageOverlay(), page-pointer callbacks; renders via EmbedPDF/pdf.js | Document viewer (owner: adopt for consistency, replacing react-pdf as the standard) | renderPageOverlay is the hook for bbox-citation overlays (§4). No bytes prop — blob URLs only. |
| DOCX Viewer | src:string, isDark + onIsDarkChange (both required) | Document viewer (DOCX SQ/ITT) | Fills a real gap (docx-preview/mammoth are converters, not viewers). Must own dark-mode state. Pulls @extend-ai/react-docx. |
| XLSX Viewer | src:string, isDark + onIsDarkChange (required) | Document viewer (price schedules) | xlsx is a parser; this is the render. Pulls @extend-ai/react-xlsx. |
| CSV Viewer | data:string (raw text, NOT a URL — inconsistent with the others), needs #portal div + Next ssr:false | Document viewer (CSV) | Data-fetch layer must pass raw string, not a URL. |
| DOCX / Excel Editor (experimental) | Viewer surface + in-place edit; full props unpublished | Owner: enable manual edits / fill missing answers — v1 (DR-066) | Experimental maturity — spike the editors early to de-risk. Fill mechanism is a TECH decision (Claude-side R5) separate from the editor UI. |
| File Upload | onFilesAccepted:(File[]), onFilesChange, type/size/count/dup validation client-side | Upload affordance (BI-9, DR-014) | No POST, no backend — bind onFilesAccepted to our hardened upload. Q-A resolved. |
| PDF Dropzone (block) | No props; internal URL.createObjectURL → PDF Viewer | Upload → preview | Pure client shell, no network at all. |
| File System / Finder | items:FileSystemItem[] (flat S3/R2 ListObjectsV2 shape), getFileUrl(), loadPreviewImageUrl(), loadChildren() async callbacks | Multi-document browse (BI-16) | We supply URL-signing + thumbnails via callbacks. Must reconcile with the multi-document item page (owner’s note). |
| Bounding Box Citations | HumanReviewPanel / ReviewField[] with location:{page, area}; coord format = §4 | Citation overlay (BI-37) and fill-slot review | The flagship. See §4 for the coordinate + data-model plumbing. |
| Schema Builder | schema:SchemaBuilderSchema, onSchemaChange, serializeSchema()→JSON Schema | (candidate) fillable structure / catalogue | Does not fit BI-20/BI-24 — §5. Reserve for a future JSON-extraction feature. |
| File Thumbnail | file:{name,type}, previewImageUrl (externally generated) | Multi-doc grid | Renders shell/loading/error only; we generate the preview image. Zero deps. |
| Document Splits | splits:DocumentSplit[] ({id,title,pages[]}), activePage, onSelectPage, onSplitsChange, thumbnailImages map | Packet-split multi-form tenders | Thumbnail-image driven; splitting UI (segment one file into logical docs). |
| Document Viewer Sidebar | children (thumbnails), open, inline — all required; composition-only | Multi-doc navigation | No page-data prop, no thumbnail generation — we supply every thumbnail. |
| Layout Blocks | OcrBlocksPanel (blocks:OcrBlock[] OCR overlay) + a resizable JSX shell | Item-page shell composition | Expectation correction below. |
| E-Signature (block) | file?:string (only prop); signature fields + signer = internal state, no callbacks, no persistence, no network | Signed forms — v1 (DR-066) | Must be FORKED, not configured — to drive fields from our data + persist a signed PDF we add callbacks/persistence. Budget the fork; not a drop-in. |
Layout Blocks — expectation correction (material to the item-page plan). The owner wants
“Layout Blocks for composing state/deadline/questions/coverage/documents panels.” Extend’s “Layout
Blocks” are not a general dashboard-panel composition system. They are (a) OcrBlocksPanel — a
PDF OCR-region overlay — and (b) a resizable shell primitive you fill with your own JSX
children (no declarative panel manifest). So the item-page shell = our resizable shell +
our KEEP domain components (§9), not an Extend block that magically arranges domain panels. This
is fine (it matches the owner’s “helps where we need custom layouts” intuition) — but the spec must
not assume a turnkey panel system exists.
4. Bounding-box citations & fill-slot geometry (OQ-3 / Q-B) — flagship
Section titled “4. Bounding-box citations & fill-slot geometry (OQ-3 / Q-B) — flagship”The single most important reconciliation in this doc. There are two different “region” surfaces, and Extend’s one Bounding Box Citations component can serve both — but they have different data sources:
4a. The coordinate alignment (the good news)
Section titled “4a. The coordinate alignment (the good news)”- Extend
HighlightArea={left, top, width, height}in percentages 0–100, page-relative, top-left origin (applied as CSSleft:${left}%). Not pixels, not 0–1, not bottom-left. (An alternateReviewCitation.polygonpath takes pixels +pageWidth/pageHeight.) - commonforms internally computes a detection-layer
BoundingBoxthat is normalised [0,1], top-left origin (YOLO/image-render space — proven by itsfrom_yolo+sort_widgetspath).form_creator.py::rect_forthen converts that away into a PDF/Rectin points, bottom-left origin (explicit y-flip + CropBox offset) to write the AcroForm widget. - Our
pdf.pycallscommonforms.prepare_formand reads the/Rectback — soPdfDetectedField.bboxis points/bottom-left, and the Extend-friendly normalised-top-left form is computed internally and discarded.
Conversion is therefore trivial IF we recover the normalised form: normalised-top-left × 100
= Extend HighlightArea. Same origin, no y-flip. Recoverable by a ~4-line normalise in pdf.py
(page height is already in hand) or via commonforms’ lower-level extract_widgets(). If we instead
keep the points/bottom-left /Rect, we owe a y-flip + page-dimension normalisation at render time.
4b. Where the geometry is dropped (the gap)
Section titled “4b. Where the geometry is dropped (the gap)”pdf.pyproducesbbox+page_numberon every detected field.orchestrator.py::_pdf_result_to_extracted_formdrops it — keeps onlyquestion_text, repurposestable_index ← page,row_index ← sequence;bbox/widget_kindare not carried.form_instance_fieldshas no pixel-geometry column (only the logicaltable_index/row_index/col_indexcell coords). Confirmed against the W1c migration (“slot model … coords, mapping_status, fill_status unchanged” — where “coords” means the logical cell indices, not geometry).citationshas no bbox column either — it is text-span anchored (cited_start/cited_end+cited_location_kind).
4c. Two surfaces — RESOLVED (owner: both v1, DR-064)
Section titled “4c. Two surfaces — RESOLVED (owner: both v1, DR-064)”Owner ruling: Extend Bounding Box Citations is unblocked and both surfaces are v1 (reverses RESEARCH’s “BLOCKED”). Two slices, different data sources:
| Surface | Data available | v1 approach | Notes |
|---|---|---|---|
Fill-slots (form_instance_fields, BI-20 mechanical) | commonforms geometry exists (detected, then discarded) | Option A — un-drop geometry in pdf.py/orchestrator.py (recover the normalised-top-left form, ~4 lines) + add a geometry column to form_instance_fields storing normalised-top-left + page → ×100 = Extend HighlightArea (no y-flip) | Reuses existing detection; PDF-only |
Citations (citations, BI-37 q_a_pair) | No detected geometry — targets are arbitrary text spans | Option B — derive coords: B1 text-layer getClientRects (primary) + B2 Claude vision coordinates (fallback) — see below | Text-anchored-only Option C is not the chosen path; PDF-only |
Option B mechanisms (grounded against platform.claude.com/docs/en/build-with-claude/vision-coordinates):
- B1 — text-layer
getClientRects(primary). Resolve the citation span (cited_text+cited_start/cited_end) to on-page rectangles via the rendered PDF’s text layer (react-pdf/EmbedPDF). Deterministic and exact, no extra API call. Works for text-based PDFs (most UK procurement forms). Normalise the rects to page-% → ExtendHighlightArea. - B2 — Claude vision coordinates (fallback for scanned / flat / no-text-layer PDFs). Claude
returns absolute pixel
[x1,y1,x2,y2], top-left origin (ask for pixels, never normalised); divide by the resized image dims → relative [0,1] →×100= ExtendHighlightArea(origin matches, no flip). Critical caveat: native PDF support rasterises pages server-side at dimensions you don’t control, so those coords are unmappable — you must rasterise the pages to images yourself and pre-resize (per the doc). Vision coords are approximate (“spot-check visually”; small targets lose precision on downscale) — hence fallback, not primary.
Consequence: fill-slots (Option A) is the low-cost win — commonforms already computes the exact shape Extend wants. Citations (Option B) is a real build: B1 for text PDFs, B2 for scanned. DOCX/XLSX spatial overlay is out of v1 (no detected geometry, no text layer) → backlog bl-459 (PDF-convert-then-revert for fill).
5. Schema Builder spike (OQ-5) — feasibility verdict
Section titled “5. Schema Builder spike (OQ-5) — feasibility verdict”Schema Builder edits a custom tree SchemaBuilderProperty = {id, key, type, description, enumValues?, properties?, items?} (types: string/number/integer/boolean/null/object/array/enum)
and serialises to standard JSON Schema via serializeSchema(). It is a data-extraction
schema editor — “define the typed JSON shape to extract from a document.” It carries no domain
metadata (no mandatory flag, no word limit, no matching keywords, no section refs, no evaluation
weight).
Verdict: does NOT fit BI-20 or BI-24 → build a custom editor.
- BI-24
form_requirement_templatesis a requirements taxonomy (matching_keywords[],matching_guidance,requirement_type,sector_applicability[],word_limit_guidance,section_ref,is_mandatory, domain/subtopic classification). None of these has any representation inSchemaBuilderProperty; conversely Schema Builder’s scalar/array/object type system is irrelevant to a free-text requirement. The mapping is empty in both directions. - BI-20
form_questions/form_responsesis a fillable question/answer slot model (question text, word limit, evaluation weight, assignee, drafted answer, review status, version). Again unrepresentable in the property tree.
Where Schema Builder could earn a place (reserve, don’t adopt now): a future “extract these
fields from an uploaded RFP/spec” JSON-extraction step — its native Extend use case — where
serializeSchema()→JSON Schema would drive an Anthropic structured-output extraction. That is a
different feature from BI-24/BI-20 and should not be conflated with them in this spec-chain.
6. Streamdown / markdown migration (OQ-4)
Section titled “6. Streamdown / markdown migration (OQ-4)”- DR-040 (adopted): all markdown rendering uses Streamdown, new surfaces first; incumbents migrate later as a backlog sweep.
- The incumbent is a single site in scope:
components/item-detail/content-renderer.tsx(react-markdown+remark-gfm), consumed transitively byqa/qa-pair-renderer.tsx. It is the only react-markdown render site across the flagged component areas (§9). Rich editing is Tiptap (content-editor,response-editor,qa-answer-display), which is orthogonal. - Owner decision: fold the migration into
{145.19}. Concretely: replacecontent-renderer’sreact-markdownwithStreamdown, reusing the existing hardening shim (lib/okf/prepare-streamdown-content.tsnormaliseInternalMdLinksForStreamdown) so internal relative links survive Streamdown’s defaultrehype-hardenpass. New draft-stream answer surfaces (BI-40/41 SSE) use Streamdown natively (streaming caret + partial-parse) — that is the DR-040 “new surface” case and needs no migration.
7. Anthropic API correctness for the domain (Q-C) — mostly right, one real bug
Section titled “7. Anthropic API correctness for the domain (Q-C) — mostly right, one real bug”Owner’s reframe: not a Bedrock audit (we don’t use InvokeModel/Converse — confirmed) but “are we
using Anthropic functionality correctly / as intended for procurement doc+form processing?”
Grounded against the claude-api skill + context7 live docs.
| Area | Verdict | Detail |
|---|---|---|
| Prompt caching (draft.ts) | GAP — highest ROI fix | cache_control sits on a systemText block that is rebuilt per question (word limit, headings, key points, tone, regen instructions appended after the stable skill/role text). The breakpoint’s bytes change every call → cache_read ≈ 0; and being the first breakpoint, it invalidates the 3 downstream search_result breakpoints too. Effectively nothing caches except an exact same-question regen within TTL. Fix: split system into a leading cached block (skills + role) + a trailing uncached block (per-question rules). |
| Prompt caching (Python pipeline) | OK — the model to copy | _cached_system_block caches the stable prompt; volatile content_text is the uncached user suffix. Reference-correct. |
| Files API | OPP (moderate) | Everything inlines base64; the same tender PDF is re-sent to both extractPDFQuestions and extractTenderMetadata. Upload-once + file_id saves bytes + enables cross-call cache_control on the document, but repetition is low (~1–2 sends/PDF) — bandwidth/future-proofing, not a step-change. |
| Native PDF blocks | OK | PDF uses native type:"document" blocks (text + visual/layout) — correct. |
| DOCX/XLSX delivery | OPP | Pre-flattened to HTML client-side (mammoth/SheetJS) — legitimate (no native .docx/.xlsx block exists) but loses the visual grid. A DOCX/XLSX→PDF→document-block route would restore layout signal for column-laid-out forms. Weigh vs conversion cost. |
| Citations | OK — already native | Draft Pass-2 uses native Search Result Citations (search_result blocks + citations.enabled), parsed in citations.ts. Correctly kept separate from structured output (the two are mutually exclusive). |
| Streaming | OK | messages.stream() + .finalMessage() for the SSE draft flow; Python streams large-max_tokens calls to dodge the 10-min guard. |
| Structured output | GAP (consistency) | Four strategies coexist. Good: draft.analyseQuestion (output_config.format), extract-questions (strict tool_use). Below-par: batch-reclassify (non-strict tool, no additionalProperties:false), from-instance.classifyField (raw JSON prompt + manual fence-stripping — weakest). Standardise onto the proven output_config/strict path. |
| Model IDs / pricing | GAP (mild) | Pins are a gen behind (Opus 4.6 drafting; Sonnet 4.5 analysis) — none retired. Tiered IDs are env-overridable; a few hardcoded literals need edits. pricing.ts silently falls back to Sonnet-4.5 rates for unknown models — fill the map before any bump, or make the fallback log/throw. |
Scope note for the Planner: most of §7 is backend answer-quality, not UI foundation. It is in this doc because the owner asked and because the draft/answer surfaces the UI renders depend on it. Recommendation: carve §7 items 1–2 (caching fix + structured-output consistency) into a separate backlog item rather than inflating the ID-147 UI spec-chain — flag to the Curator. The Files-API / DOCX→PDF items are genuine but lower priority.
8. Workflow-state stepper (Q-D) — AI-SDK Elements workflow is the wrong shape
Section titled “8. Workflow-state stepper (Q-D) — AI-SDK Elements workflow is the wrong shape”The AI-SDK Elements workflow component is a React Flow (@xyflow/react) node-graph / flow-canvas
builder — draggable nodes, animated edges, pan/zoom, connection handles. It is distributed as a
shadcn-style copy-in registry (Apache-2.0, Vercel) but pulls @xyflow/react as a real npm dep.
Verdict: overkill / wrong shape — not recommended for BI-13. BI-13 is a linear 10-state workflow stepper (ordered states + current-state badge, non-colour-only for accessibility). A graph canvas makes the ordered-step, accessible presentation harder, not easier, and imports React Flow for nothing. Build a small custom Warm Meridian stepper (ordered states + current-state badge + lucide icon/label + semantic tokens); no first-party shadcn stepper primitive exists, and the component is small enough to own. BI-18 (outcome capture) is a form, unrelated to this component. Reserve the AI-SDK workflow component for a genuine branching-graph need later (its permissive licence + copy-in model make it low-risk to adopt then).
9. In-tree component inventory (Q-E) — reuse vs replace
Section titled “9. In-tree component inventory (Q-E) — reuse vs replace”54 components across the 6 owner-flagged areas. KEEP = domain-logic component to reuse behind
any Extend shell; REPLACE = a layout/composition shell that an Extend resizable-shell + our
KEEP components would supersede.
| Dir | Count | KEEP (domain logic) | REPLACE (shells) |
|---|---|---|---|
qa/ | 7 | all 7 (viewer, answer-display, pair-renderer, preview-list, revision-history, row, batch-table) | 0 |
procurement/ | 24 | ~19 (all question-*/response-*, procurement-context-provider = state hub, workflow-indicator, forms-card, readiness-checklist, template-field/completion-review, wizard, tender/kb/outcome) | 5 (question-navigator, procurement-list-card, procurement-export-menu, template-fill-progress, draft-recovery-dialog) |
source-document-detail/ | 6 | 4 (derived-pairs-list, document-citations-panel, document-version-list, source-document-provenance) | 2 (section-error-state, source-document-detail-client = prime Extend Layout Block target) |
coverage/ | 5 | all 5 (the true template-coverage panels live here; procurement/ only has the fill/completion side) | 0 |
admin/q-a-pairs/dedup-proposals/ | 8 | 6 (+ span-label util) | 2 (filter-bar, empty-state) |
item-detail/ | 4 | all 4 (Tiptap content-editor + editor-toolbar, revision-diff-view, content-renderer) | 0 |
Cross-cutting facts:
- Markdown: exactly one react-markdown site —
item-detail/content-renderer.tsx(the §6/OQ-4 target). No Streamdown or react-pdf anywhere in these scopes yet. - Citations: only
source-document-detail/document-citations-panel.tsxrenders citations — text-only today (0 rows), no spatial rendering to reuse → bbox-citation UI is greenfield (matches §4). - Composition shells to replace are the item-page/list/section wrappers, chiefly
source-document-detail-client— these are where the Extendresizableshell + KEEP components compose the panels the owner described.
9b. Multi-document item page (OQ-7) — RESOLVED: hybrid, Finder rejected
Section titled “9b. Multi-document item page (OQ-7) — RESOLVED: hybrid, Finder rejected”Owner asked whether Extend’s File System / Finder or a custom multi-document page is the better UX for an item that carries multiple documents (worked example: an engagement with several forms plus attached reference/evidence — e.g. a technical person’s CV — where “seeing what’s attached” is valuable).
Recommendation (grounded): HYBRID — custom domain frame + Extend viewer shells. File System/Finder rejected as the top-level surface (DR-068).
- Why not Finder. Its folder/containment metaphor contradicts the data model — an
engagement_groupsrow is a LINK, not a container (BI-27/28): it does not own or scope its forms, andform_instances.engagement_group_idis nullable (ON DELETE SET NULL). Finder would also flatten stateful forms and inert evidence files into identical rows. - What ships with zero schema change. “A form owns its uploaded documents” already works: a
form_instancesrow carriesfilename/storage_path/mime_type, and the current surface lists Supabase Storage buckettender-documents/<form_id>/(app/api/procurement/[id]/route.ts) — already anListObjectsV2-shaped listing. Pair it with Extend’s viewers + Document Viewer Sidebar + File Thumbnail. - What is GREENFIELD (scope flag). “Attach a labelled reference/evidence document (a CV) at
form OR engagement level” does not exist: no FK from
source_documents/reference_itemsto forms/engagements, and noattachments/form_documents/engagement_documentsjoin table anywhere. It needs a new store keyed by nullableform_instance_id+ nullableengagement_group_id+ aroleenum (form-source vs reference/evidence) +storage_path. This is v1 (owner ruling, DR-068) — the{147.3}TECH author fixes the exact shape; see §11. - Proposed layout (progressive disclosure). Form-as-page header → an engagement sibling-forms LINK rail shown only when grouped → a Documents tab whose Extend Sidebar splits FORM SOURCE vs REFERENCE / EVIDENCE feeding an Extend Viewer preview. A single ungrouped form with no attachments collapses to today’s simple list.
10. Theming, primitives & icons (OQ-1 / OQ-6) — the remaining live decisions
Section titled “10. Theming, primitives & icons (OQ-1 / OQ-6) — the remaining live decisions”The owner chose broad component adoption, but “broad adoption” and “wholesale” are different axes. Three decisions remain, each a per-component cost the spec must budget:
1. Icons — Hugeicons vs lucide. Extend components import Hugeicons; the tree is
lucide (components.json iconLibrary: lucide, every component imports lucide-react). Options:
(a) re-point vendored Extend components lucide at vendor-in time (RESEARCH’s recommendation; keeps
one icon system, adaptation cost per component); (b) accept a second icon lib — add hugeicons
and let Extend components keep their icons (owner floated this: “could we just use Hugeicons where
Extend requires them?”). Trade-off: (b) is faster to vendor but ships two icon systems (bundle +
visual-consistency cost); (a) is the clean long-term surface. Recommend (a) for shared chrome,
tolerate (b) transiently inside document-viewer internals where icons are Extend-internal and rarely
seen. Owner to confirm.
2. Primitives — Coss UI vs our components/ui. Extend is built on Coss UI
(registry/base-ui/ui — a Base-UI-derived primitive set: button, tooltip, select, popover, dialog,
menu) which overlaps our Radix-based shadcn components/ui/*. Adopting Extend components as-is
copies their Coss UI primitive deps → a second primitive layer in the tree. Re-wiring each
Extend component to our existing components/ui primitives avoids the duplication at a re-wire cost.
This is the real “wholesale vs cherry-pick” axis. Recommend: keep our primitives as the app
default; allow the copied-in document viewers to retain their Coss UI internals (they are
self-contained, rarely themed) but re-wire any Extend component that surfaces shared chrome
(buttons/menus the user sees app-wide). Owner to confirm the tolerance.
3. Warm Meridian theming — as-is first (OQ-6 owner decision). Do not re-theme on vendor-in.
Install components as-is (white document backgrounds are preferred for document viewers), verify
they render, then tweak to Warm Meridian semantic tokens incrementally. This is compatible with the
token system: Warm Meridian’s semantic tokens are bare shadcn (--background, --foreground,
--primary, --border…) and components.json sets cssVariables: true, baseColor: neutral, so
Extend’s CSS-variable model maps onto our tokens when we get to it — the hard rule (no raw
Tailwind colours; see components/CLAUDE.md F3) applies to our code, and re-pointing vendored
Extend source to bare semantic tokens is the eventual adaptation, not a day-one gate. DOCX/XLSX
viewers’ required isDark/onIsDarkChange props mean we own dark-mode state regardless.
11. Decisions — RESOLVED (owner round-2, 13/07/2026) + residual spec-author choices
Section titled “11. Decisions — RESOLVED (owner round-2, 13/07/2026) + residual spec-author choices”All 7 RESEARCH OQs and the follow-up questions are now settled (owner feedback round 2). The spec-chain authors from settled ground:
| # | Decision | Ruling | Ref |
|---|---|---|---|
| 1 | Icon policy | Re-point lucide everywhere (retire Hugeicons on vendor-in) | DR-063, §10.1 |
| 2 | Primitive policy | Keep our components/ui as app default; re-wire shared chrome; self-contained viewers may retain Coss UI internals | DR-063, §10.2 |
| 3 | Fill-slot geometry | Option A, v1 — add geometry column to form_instance_fields + un-drop commonforms coords; spatial fill-slot review is v1 (PDF-only) | DR-064, §4c |
| 4 | Citation overlay | Option B, v1 — B1 text-layer primary + B2 Claude-vision fallback; DOCX/XLSX overlay out of v1 | DR-064, §4c |
| 5 | DOCX/Excel Editor + E-Signature | Procurement v1 (E-Signature requires a fork) | DR-066, §3 |
| 6 | Anthropic backend items | Separate backlog item (bl-460), not the UI spec-chain | §7 |
| 7 | Multi-doc vs Finder | Hybrid custom frame + Extend shells; Finder rejected | DR-068, §9b |
| — | Streamdown incumbent | Fold content-renderer.tsx migration into {145.19} | §6, OQ-4 |
| — | DOCX/XLSX spatial fill | Backlog (bl-459) — PDF-convert-then-revert | §4c |
Residual choices left to the spec-chain (not owner blockers — spec-author scope):
- Reference/evidence attachment store (§9b) — v1 (owner ruling, DR-068). The “attach a labelled CV
at form/engagement level” half is greenfield; it is in v1 scope. The
{147.3}TECH author fixes the exact shape (new table: nullableform_instance_id+ nullableengagement_group_id+roleenumstorage_path) — that shape is the residual choice, not the v1/defer question. The form-owns-docs half needs no schema change and ships alongside.
- Editor fill mechanism (R5) — the DOCX/Excel Editor UI is v1; how answers are written (Claude-side vs in-editor) is a TECH decision, separate from adopting the editor component.
- E-Signature fork surface — the persistence/callbacks to graft onto Extend’s block.
What feeds which artefact:
{147.2}PRODUCT — §0 decisions, §3 surface mapping, §6 Streamdown, §8 stepper, §9 reuse boundary, §10 theming posture (behavioural invariants for each adopted surface).{147.3}TECH — §1 data-model map, §2 install slice, §4 geometry plumbing (the migration +pdf.py/orchestrator.pyun-drop + coord conversion), §5 custom-editor decision, §10 primitive/icon vendor-in mechanics.- Gates
{145.19}(item-page UI) — the item-page shell (§3 Layout Blocks correction + §9 REPLACE shells + §9b hybrid layout) + the OQ-4content-renderermigration land there. - Backlog (already created) —
bl-460(Anthropic caching + structured-output, §7);bl-459(DOCX/XLSX spatial fill, §4c). - Decision Register (already written) — DR-063 (adoption posture), DR-064 (bbox geometry), DR-065 (Schema Builder → custom), DR-066 (Editor/E-Sig v1), DR-067 (custom stepper), DR-068 (multi-doc hybrid).
12. Empirical verification block (13/07/2026)
Section titled “12. Empirical verification block (13/07/2026)”| Claim | Method | Result |
|---|---|---|
| Extend distribution = shadcn copy-in, no runtime dep, client-only, no backend | github.com/extend-hq/ui registry source (apps/v4/) + every component "use client"; grep for fetch/POST in File Upload/Dropzone = none | CONFIRMED |
Extend HighlightArea = %-0–100, top-left | bounding-box-citations.tsx source (HighlightArea type + CSS left:${}%) | CONFIRMED |
commonforms internal coords = normalised [0,1] top-left; /Rect = points bottom-left | jbarrow/commonforms form_creator.py::rect_for (+ from_yolo/sort_widgets) | CONFIRMED |
pdf.py reads back /Rect (points/bottom-left), discards normalised form; orchestrator.py drops bbox | scripts/cocoindex_pipeline/form_extractors/pdf.py + orchestrator.py | CONFIRMED |
form_instance_fields has no pixel-geometry col; citations is text-span | W1c migration + citations schema (cited_start/end, cited_location_kind) | CONFIRMED |
| Table renames (form_templates→form_instances, form_template_fields→form_instance_fields, form_template_requirements→form_requirement_templates) | 20260712062000_id145_w1c_rename_reshape.sql | CONFIRMED |
database.types.ts stale (pre-rename); regen held behind W1 push | continuation prompt (S470) + type file still on old names | CONFIRMED |
components.json no registries block; iconLibrary: lucide; @extend absent | components.json + package.json grep | CONFIRMED |
| draft.ts cache breakpoint on volatile per-question system block | lib/domains/procurement/ai/draft.ts:281-308, :413-440 | CONFIRMED |
| Native PDF document blocks + Search Result Citations used; no Files API | extract-questions.ts:240-247,522-536; draft.ts:255-271; citations.ts; grep files.upload = none | CONFIRMED |
AI-SDK workflow = React Flow node-graph (@xyflow/react) | elements.ai-sdk.dev/examples/workflow + registry JSON | CONFIRMED |
| Schema Builder = data-extraction schema editor, no domain metadata | schema-builder.tsx SchemaBuilderProperty + serializeSchema() | CONFIRMED |
Investigation artefacts (this session, ephemeral scratchpad — detail folded into this doc): Thread A (Extend props), B (commonforms bbox), C (Anthropic), D (AI-SDK), E (component inventory).
End of empirical grounding — ID-147. Feeds {147.2}/{147.3}; gates {145.19}. Not committed
to code.