Reviewer curates, per promoted table, which columns to use over the full catalog list (suggested pre-selected + bold); selection persists into schema_linking.json + the decision ledger and softly guides SQL generation (Option 1). Dedicated structured schema-linking gate widget (Approach A), staged commit, inline gate + single columns modal. Hard SQL enforcement is an explicit follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
11 KiB
F4 schema-linking — per-table column curation — design
Date: 2026-07-06 Status: approved (brainstorming), pending plan
The F4 "Schema linking: tabelle/colonne/esclusioni" reviewer gate currently lets the
reviewer only promote/exclude tables; columns are chosen by the model and never
surfaced for review. This feature lets the reviewer, per promoted table, curate the
columns to use — over the table's full catalog column list — with the model's
suggested columns pre-selected. The curated selection is persisted (into
schema_linking.json + the decision ledger) and guides SQL generation downstream.
Excluded tables get a read-only column view.
Decisions locked in brainstorming:
- Approach A — a dedicated structured
schema-linkinggate widget (not a flatreviewer_decide). - Option 1 (soft) — the curated column set is persisted and the model is instructed to honor it for output columns; no hard SQL validator. Hard enforcement is an explicit follow-up (needs live model + VPN).
- Staged commit — column checkboxes are staged in widget state and sent once, on the gate's Confirm.
- Interaction — the F4 gate stays inline (as today's
reviewer_decide); each table row opens one columns modal (no nested modals).
Scope
| In scope | Out of scope (explicit) |
|---|---|
| Only the F4 tabelle/colonne/esclusioni gate (replay gate 7) | The F4 join proposte gate (gate 8) — unchanged |
| Table rows with description + rationale under the name | Hard SQL enforcement (reject SQL using non-approved columns) — Option 2, follow-up |
| Per-table columns modal (name + description, checkbox), suggested pre-selected + bold | Any change to the Pi RPC wire protocol beyond the new descriptor/response payloads |
Persist curated columns into schema_linking.json + review_decisions.jsonl |
Reviewer editing of column descriptions (read-only from catalog) |
| Excluded tables: read-only column modal | Column curation for tables the reviewer excludes |
1. Data contract
1a. Gate descriptor (harness → frontend)
A new widget kind replaces the flat reviewer_decide for this gate only:
{
"id": "<pi ui id>",
"widget": "schema-linking",
"title": "F4 — Schema linking: tabelle/colonne/esclusioni",
"tables": [
{
"id": "t-ablazione",
"name": "fact_studio_elettrofisiologico_endocavitario_ablazione",
"kind": "promote", // "promote" | "exclude"
"recommended": true, // pre-enacted (checkbox checked) when true
"description": "<table comment from physical.yaml>",
"rationale": "<model's promote/exclude motivation>",
"columns": [
{ "name": "cod_paz", "description": "<column comment>", "type": "bigint", "pk": true, "suggested": true },
{ "name": "num", "description": "<column comment>", "type": "bigint", "pk": false, "suggested": false }
]
}
],
"reserved": ["back", "exit", "other"]
}
columnsis populated deterministically from the catalogartifacts/mschema/physical.yaml(per table:comment; per column:type,pk,comment). The model supplies only: promote/excludekind,rationale, and thesuggestedset.tht-gate.jsmerges model-proposal + catalog into the descriptor, so the model transcribes little (fewer tokens, no invented column names).- For
kind: "exclude",columns[].suggestedis ignored by the UI (read-only view).
1b. Gate response (frontend → harness)
Structured response, sent once on gate Confirm:
{
"id": "<same ui id>",
"kind": "schema-linking",
"tables": [
{ "id": "t-ablazione", "enacted": true, "columns": ["cod_paz", "num", "data_time_key", "ablazione_transcatetere"] },
{ "id": "t-impianto", "enacted": true, "columns": ["cod_paz", "num", "data_time_key"] },
{ "id": "x-sostituzione", "enacted": true } // exclude enacted; no columns key
]
}
enactedmirrors the row checkbox (the reviewer may decline a recommended promote/exclude).columnsis present only for enactedpromotetables; it is the reviewer's final selection (suggested ∖ deselected ∪ newly selected).
2. Interaction model
The gate renders inline (same surface as today's reviewer_decide), as a card
listing every table. Column detail lives only in the modal (requirement 3: no
column rows in the list).
Promote row
- Line 1: table
name(prominent). - Below:
description(muted) thenrationale(the "why include"). - A
PROMUOVIbadge. - A row checkbox = enact this promote decision (checked by default when
recommended). - A "Colonne k/n" button (k = currently selected, n = total) → opens the columns modal.
Columns modal (promote)
- List of the table's fields. First cell = checkbox.
suggestedfields are pre-selected.- Selected field ⇒ its name + description render bold; deselected ⇒ normal. This applies to both pre-selected fields and fields the reviewer selects later; on deselection they return to normal (requirement 1).
- Selection is staged in the widget's state (not committed until the gate's Confirm). Closing the modal keeps the staged selection; the row's "Colonne k/n" count updates.
Exclude row
- Line 1: table
name. - Below:
description+ exclusionrationale(requirement 2). - An
ESCLUDIbadge; row checkbox = enact the exclude decision. - A "Colonne" button → opens the columns modal in read-only mode: checkboxes disabled, all fields rendered normal (no preselection, no bold), purely informational (requirement 2).
Confirm
- The gate's Confirm builds the structured response (§1b) from staged state and
calls
onRespond. Reserved controls (back/exit/other) behave as today.
3. Frontend components
| File | Change |
|---|---|
frontend/src/widgets/SchemaLinkingGateWidget.tsx |
New. Inline card; staged state Map<tableId, { enacted: boolean; selected: Set<string> }> initialized from recommended (enacted) and suggested (selected). Builds the §1b response on Confirm. |
frontend/src/widgets/SchemaColumnsDialog.tsx |
New. components/ui/dialog modal listing columns; prop readOnly for excluded tables; bold-when-selected rule; writes staged selection back to the parent on toggle. |
frontend/src/widgets/registry.ts |
Register schema-linking → SchemaLinkingGateWidget. |
frontend/src/widgets/types.ts |
Descriptor + option types for the new payload. |
frontend/src/api/types.ts |
Extend UiResponse with the structured schema-linking shape (tables[]). |
frontend/src/shell/WidgetHost.tsx |
Pass the structured response through (it already forwards UiResponse; verify setLastUserEntry summary handles the new kind). |
Reuse the v2 visual vocabulary: cards border-border/70 + shadow-sm, .thot-label
(mono) for the section/field micro-labels, rounded-* radii, first-action-primary
button convention. No new color tokens.
4. Harness changes
| File | Change |
|---|---|
harness/.pi/extensions/tht-gate.js |
Emit the schema-linking descriptor (merge model proposal + catalog columns). Add a response handler for widget:"schema-linking": per enacted table → tht decision add (table_promoted/table_excluded, as today); per selected column of a promoted table → new column_promoted decision (subject = "table.col"); suggested-but-deselected → column_excluded. Then invoke the deterministic reconcile (below). |
catalog reader (tht) |
A deterministic reader returning a table's columns + comments from physical.yaml ([{name, description, type, pk}]). Verify whether an existing tht schema …/search command already exposes this; add a thin command if not. |
schema_linking.json reconcile (tht) |
A deterministic tht command that sets a promoted table's column candidates to exactly the reviewer's selection (selected → decision: "promoted"; deselected suggested → dropped or "excluded"; newly selected → added). Makes the curation authoritative rather than relying on model transcription. |
harness/tht/decisions.py |
Add column_promoted / column_excluded to DecisionType. |
harness/workflow.yaml |
Add column_promoted / column_excluded to F4 emits. |
harness/.pi/skills/tht-sessione/SKILL.md |
F4: describe the column-curation step + the new gate. F6/F7: instruct the model to treat schema_linking.json promoted columns as the reviewer-approved output set, while remaining free to use other columns as join keys / filter predicates (Option 1, soft). |
Sequencing of "model proposes → gate reconciles schema_linking.json" is a detail to
pin down in the plan (the reconcile must run after the tables exist in the artifact).
5. Downstream (Option 1, soft)
harness/tht/cli/sql_cmd.py:promoted_tables_forstays. Optionally add apromoted_columns_forhelper (read promoted column candidates) for future use; it is not wired into validation now — no SQL is rejected on column grounds.- The only behavioral coupling now is via
SKILL.mdguidance + the curatedschema_linking.jsonthe model reads when composing CTEs/SQL.
6. Testing & replay (no VPN)
- Augmented replay fixture. A script reads
physical.yamlfor the 6 tables of the recorded psd session and rewrites replay gate 7 as aschema-linkingdescriptor with realcolumns. This makes the new widget visually verifiable in replay (server on :5333) before any harness wiring exists. Touchestools/replay/extract.mjs+ regeneratestools/replay/replay.json. - Frontend unit (vitest + MSW): widget staging (suggested pre-selected, count), bold-toggle on select/deselect, read-only for excluded, structured response shape on Confirm, reserved controls.
- Harness unit:
tht-gate.jsresponse handler (structured response → the righttht decision addcalls + reconcile invocation); pytest for the catalog reader and theschema_linking.jsonreconcile command. - End-to-end with the live model is deferred (no VPN); the soft SQL guidance can only be spot-checked in replay.
7. Phased delivery (one spec, plan sequenced in phases)
- Frontend + contract + replay fixture — new widget, columns modal, structured
UiResponse, registry; augmented replay fixture. Fully verifiable offline. - Harness — gate emission (catalog merge +
suggested), structured-response handler, ledger types,schema_linking.jsonreconcile,thtcommands,SKILL.mdguidance.
Follow-up (separate spec, when VPN is available): Option 2 hard SQL enforcement.
8. Risks & open decisions
- Interaction shape (§2): inline gate + single columns modal is chosen over gate-as-modal + nested modal to avoid nested-dialog complexity. Revisit only if the reviewer explicitly wants the gate itself modal.
- Reconcile ordering (§4): the deterministic
schema_linking.jsonreconcile must run after the table candidates exist; exact sequencing pinned in the plan. - Deselecting a key column (e.g.
cod_paz): under Option 1 this does not block —SKILL.mdnotes join keys remain usable even when not selected as output columns. A soft UI hint ("key column") is possible but not required for v1. - Catalog reader existence (§4): confirm whether
thtalready exposesphysical.yamlcolumns; add a thin command only if missing.