Skip to content

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 via jbarrow/commonforms

  • in-tree pipeline; Anthropic-API correctness via claude-api skill + context7; AI-SDK Elements via elements.ai-sdk.dev; in-tree component inventory) + direct verification of the live migrations and components.json. Every external claim was fetched, not recalled.

Headline reframes vs RESEARCH.md (read these first):

  1. 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.
  2. 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.
  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.
  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.

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:

OQRESEARCH askedOwner decisionGrounded status
OQ-1Cherry-pick vs wholesale ExtendAdopt broadly (all doc components), but see the wholesale-vs-rewire nuanceLive: two independent axes — primitives (Coss UI vs our components/ui) and icons (Hugeicons vs lucide). §10
OQ-2Who 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-3bbox-citations: derive coords vs text-highlightUnlock Extend Bounding Box CitationsTwo surfaces, not one. Fill-slots → cheap (commonforms). Citations → still needs derivation. §4
OQ-4Migrate incumbent content-renderer.tsx now or defer?Fold the migration inIt is the only react-markdown site in scope; feeds qa-pair-renderer. §6
OQ-5Schema Builder fit — spikeSpike (feasibility) + integrate commonformsVerdict: does not fit → custom editor. §5
OQ-6Theming adaptation budgetDon’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:

RefOwner’s questionAnswerSection
Q-ADoes Extend have an equivalent (battle-tested) upload backend?No — Extend is client-only, zero server code. Keep our BI-9 backend.§3
Q-BUse commonforms for pixel/region bbox coords (form_creator.py)?Yes, for fill-slots — geometry already exists, is discarded at the DB write.§4
Q-CAre we using Anthropic Files/caching/etc. correctly for the domain?Mostly yes; one real caching defect + a Files-API opportunity.§7
Q-DIs AI-SDK Elements workflow feasible for the state stepper?No — wrong shape (React-Flow graph canvas). Build custom.§8
Q-EDo 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)WasLoad-bearing columns
The form item (the item is the form, BI-1)form_instancesform_templatesprocessing_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_fieldsform_template_fieldsfield_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_questionsform_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)
Answersform_responsesresponse_text, review_status, overall_score, source_record_ids[], version
Reusable requirement catalogue (BI-24, Path-C)form_requirement_templatesform_template_requirementsrequirement_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 trackingtemplate_completions(col template_idform_instance_id)
Citations (id-58, BI-37)citationscited_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) and form_questions/form_responses (semantic answerable slots). Keep them separate; they have different owners and different UI.


Distribution model (confirmed from github.com/extend-hq/ui registry root apps/v4/): Extend is a shadcn source-copy registrypnpm 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. No registries blockextend-hq/ui is not registered.
  • package.json: no @extend/extend-* dep. Already present and relevant: streamdown ^2.5.0, react-pdf ^10.4.1, parsers docx/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):

  1. Add a registries block to components.json pointing at the extend-hq/ui registry (the @extend/* namespace).
  2. shadcn add each adopted component; the source lands under components/ (domain subdir — never root, per components/CLAUDE.md).
  3. 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.
  4. Some Extend components pull @extend-ai/react-docx / @extend-ai/react-xlsx as 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 componentKey props / inputFeeds which surfaceNotes for the spec
PDF Viewersrc:string (URL/blob), zoom/toolbar flags, renderPageOverlay(), page-pointer callbacks; renders via EmbedPDF/pdf.jsDocument 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 Viewersrc: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 Viewersrc:string, isDark + onIsDarkChange (required)Document viewer (price schedules)xlsx is a parser; this is the render. Pulls @extend-ai/react-xlsx.
CSV Viewerdata:string (raw text, NOT a URL — inconsistent with the others), needs #portal div + Next ssr:falseDocument viewer (CSV)Data-fetch layer must pass raw string, not a URL.
DOCX / Excel Editor (experimental)Viewer surface + in-place edit; full props unpublishedOwner: enable manual edits / fill missing answersv1 (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 UploadonFilesAccepted:(File[]), onFilesChange, type/size/count/dup validation client-sideUpload 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 ViewerUpload → previewPure client shell, no network at all.
File System / Finderitems:FileSystemItem[] (flat S3/R2 ListObjectsV2 shape), getFileUrl(), loadPreviewImageUrl(), loadChildren() async callbacksMulti-document browse (BI-16)We supply URL-signing + thumbnails via callbacks. Must reconcile with the multi-document item page (owner’s note).
Bounding Box CitationsHumanReviewPanel / ReviewField[] with location:{page, area}; coord format = §4Citation overlay (BI-37) and fill-slot reviewThe flagship. See §4 for the coordinate + data-model plumbing.
Schema Builderschema:SchemaBuilderSchema, onSchemaChange, serializeSchema()→JSON Schema(candidate) fillable structure / catalogueDoes not fit BI-20/BI-24 — §5. Reserve for a future JSON-extraction feature.
File Thumbnailfile:{name,type}, previewImageUrl (externally generated)Multi-doc gridRenders shell/loading/error only; we generate the preview image. Zero deps.
Document Splitssplits:DocumentSplit[] ({id,title,pages[]}), activePage, onSelectPage, onSplitsChange, thumbnailImages mapPacket-split multi-form tendersThumbnail-image driven; splitting UI (segment one file into logical docs).
Document Viewer Sidebarchildren (thumbnails), open, inlineall required; composition-onlyMulti-doc navigationNo page-data prop, no thumbnail generation — we supply every thumbnail.
Layout BlocksOcrBlocksPanel (blocks:OcrBlock[] OCR overlay) + a resizable JSX shellItem-page shell compositionExpectation correction below.
E-Signature (block)file?:string (only prop); signature fields + signer = internal state, no callbacks, no persistence, no networkSigned 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 CSS left:${left}%). Not pixels, not 0–1, not bottom-left. (An alternate ReviewCitation.polygon path takes pixels + pageWidth/ pageHeight.)
  • commonforms internally computes a detection-layer BoundingBox that is normalised [0,1], top-left origin (YOLO/image-render space — proven by its from_yolo + sort_widgets path). form_creator.py::rect_for then converts that away into a PDF /Rect in points, bottom-left origin (explicit y-flip + CropBox offset) to write the AcroForm widget.
  • Our pdf.py calls commonforms.prepare_form and reads the /Rect back — so PdfDetectedField.bbox is 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.py produces bbox + page_number on every detected field.
  • orchestrator.py::_pdf_result_to_extracted_form drops it — keeps only question_text, repurposes table_index ← page, row_index ← sequence; bbox/widget_kind are not carried.
  • form_instance_fields has no pixel-geometry column (only the logical table_index/ row_index/col_index cell coords). Confirmed against the W1c migration (“slot model … coords, mapping_status, fill_status unchanged” — where “coords” means the logical cell indices, not geometry).
  • citations has 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:

SurfaceData availablev1 approachNotes
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 spansOption B — derive coords: B1 text-layer getClientRects (primary) + B2 Claude vision coordinates (fallback) — see belowText-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-% → Extend HighlightArea.
  • 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 = Extend HighlightArea (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_templates is 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 in SchemaBuilderProperty; 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_responses is 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.


  • 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 by qa/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: replace content-renderer’s react-markdown with Streamdown, reusing the existing hardening shim (lib/okf/prepare-streamdown-content.ts normaliseInternalMdLinksForStreamdown) so internal relative links survive Streamdown’s default rehype-harden pass. 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.

AreaVerdictDetail
Prompt caching (draft.ts)GAP — highest ROI fixcache_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 APIOPP (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 blocksOKPDF uses native type:"document" blocks (text + visual/layout) — correct.
DOCX/XLSX deliveryOPPPre-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.
CitationsOK — already nativeDraft 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).
StreamingOKmessages.stream() + .finalMessage() for the SSE draft flow; Python streams large-max_tokens calls to dodge the 10-min guard.
Structured outputGAP (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 / pricingGAP (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.

DirCountKEEP (domain logic)REPLACE (shells)
qa/7all 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/64 (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/5all 5 (the true template-coverage panels live here; procurement/ only has the fill/completion side)0
admin/q-a-pairs/dedup-proposals/86 (+ span-label util)2 (filter-bar, empty-state)
item-detail/4all 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.tsx renders 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 Extend resizable shell + 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_groups row is a LINK, not a container (BI-27/28): it does not own or scope its forms, and form_instances.engagement_group_id is 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_instances row carries filename/storage_path/mime_type, and the current surface lists Supabase Storage bucket tender-documents/<form_id>/ (app/api/procurement/[id]/route.ts) — already an ListObjectsV2-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_items to forms/engagements, and no attachments/form_documents/engagement_documents join table anywhere. It needs a new store keyed by nullable form_instance_id + nullable engagement_group_id + a role enum (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:

#DecisionRulingRef
1Icon policyRe-point lucide everywhere (retire Hugeicons on vendor-in)DR-063, §10.1
2Primitive policyKeep our components/ui as app default; re-wire shared chrome; self-contained viewers may retain Coss UI internalsDR-063, §10.2
3Fill-slot geometryOption A, v1 — add geometry column to form_instance_fields + un-drop commonforms coords; spatial fill-slot review is v1 (PDF-only)DR-064, §4c
4Citation overlayOption B, v1 — B1 text-layer primary + B2 Claude-vision fallback; DOCX/XLSX overlay out of v1DR-064, §4c
5DOCX/Excel Editor + E-SignatureProcurement v1 (E-Signature requires a fork)DR-066, §3
6Anthropic backend itemsSeparate backlog item (bl-460), not the UI spec-chain§7
7Multi-doc vs FinderHybrid custom frame + Extend shells; Finder rejectedDR-068, §9b
Streamdown incumbentFold content-renderer.tsx migration into {145.19}§6, OQ-4
DOCX/XLSX spatial fillBacklog (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: nullable form_instance_id + nullable engagement_group_id + role enum
    • storage_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.py un-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-4 content-renderer migration 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)”
ClaimMethodResult
Extend distribution = shadcn copy-in, no runtime dep, client-only, no backendgithub.com/extend-hq/ui registry source (apps/v4/) + every component "use client"; grep for fetch/POST in File Upload/Dropzone = noneCONFIRMED
Extend HighlightArea = %-0–100, top-leftbounding-box-citations.tsx source (HighlightArea type + CSS left:${}%)CONFIRMED
commonforms internal coords = normalised [0,1] top-left; /Rect = points bottom-leftjbarrow/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 bboxscripts/cocoindex_pipeline/form_extractors/pdf.py + orchestrator.pyCONFIRMED
form_instance_fields has no pixel-geometry col; citations is text-spanW1c 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.sqlCONFIRMED
database.types.ts stale (pre-rename); regen held behind W1 pushcontinuation prompt (S470) + type file still on old namesCONFIRMED
components.json no registries block; iconLibrary: lucide; @extend absentcomponents.json + package.json grepCONFIRMED
draft.ts cache breakpoint on volatile per-question system blocklib/domains/procurement/ai/draft.ts:281-308, :413-440CONFIRMED
Native PDF document blocks + Search Result Citations used; no Files APIextract-questions.ts:240-247,522-536; draft.ts:255-271; citations.ts; grep files.upload = noneCONFIRMED
AI-SDK workflow = React Flow node-graph (@xyflow/react)elements.ai-sdk.dev/examples/workflow + registry JSONCONFIRMED
Schema Builder = data-extraction schema editor, no domain metadataschema-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.