From bb02dfb7529ffd60a3c1000ca0c05e7f06ce925e Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Tue, 9 Jun 2026 21:47:35 +0000 Subject: [PATCH] docs: ADR-0044 relationship visualization (V1); accepted Two-table connector diagrams (Style A) for relationships, resolving ADR-0016 OOS-1 and the open half of requirements.md V1. Reach is 'relationship-relevant' (show relationship / show table / relationship DDL echoes; incidental echoes keep prose). App-side rendering, width- adaptive side-by-side vs vertical, compound-FK pair routing, bold box title rows. Revised after a /runda DA pass corrected three inverted- architecture claims (App-side rendering, untracked output width, prose-in-worker show-relationship). Index updated per ADR-0000. --- docs/adr/0044-relationship-visualization.md | 492 ++++++++++++++++++++ docs/adr/README.md | 1 + 2 files changed, 493 insertions(+) create mode 100644 docs/adr/0044-relationship-visualization.md diff --git a/docs/adr/0044-relationship-visualization.md b/docs/adr/0044-relationship-visualization.md new file mode 100644 index 0000000..3d7c0c8 --- /dev/null +++ b/docs/adr/0044-relationship-visualization.md @@ -0,0 +1,492 @@ +# ADR-0044: Relationship visualization (two-table connector diagrams) + +## Status + +Accepted (2026-06-09) + +Resolves **ADR-0016 OOS-1** ("Relationship visualization — two +structures side-by-side with an arrow; its own ADR; will compose +`render_structure`"). Partially **supersedes ADR-0016 §5** (the +plain-text `References:` / `Referenced by:` relationship block) and +**extends ADR-0016 §4 and §6** (adds layout width-awareness and +per-span styling). Builds on ADR-0028 (styled output runs), +ADR-0043 (compound, list-based relationship endpoints), ADR-0013 +(named 1:n relationships), and the V5/V5a `show` family. Honours +ADR-0009 (DSL conventions) and ADR-0002 (no engine name in +user-facing strings). + +Closes the substantive open half of requirement **V1** +(`docs/requirements.md`): "a selected relationship as two tables +joined by a line." + +## Context + +The table-structure half of V1 is done: `show table ` renders a +box-drawn structure table (columns / types / constraints / indexes), +and relationships appear **as prose** beneath it — +`References:` / `Referenced by:` blocks formatted `A.col → B.col` +(`output_render.rs render_structure`, per ADR-0016 §5). The same +prose appears in V5a's `show relationship ` detail view. + +The open piece is the **visual** form: drawing a relationship as two +tables side by side joined by a connecting line, with cardinality and +referential actions. ADR-0016 deliberately deferred this (OOS-1) and +**pre-sized the structure renderer to compose two of itself**. This +ADR cashes that in. + +Three facts from the current architecture shape the design: + +1. **Rendering is App-side; the worker returns structured data.** + *(Corrected from the initial draft after direct verification — an + earlier survey had this backwards.)* The database worker (`db.rs`) + returns `TableDescription` / `DataResult`; the **App** (`app.rs`) + calls the `output_render.rs` helpers to format them into + `OutputLine`s. Verified: every `render_structure` / + `render_data_table` call site is in `app.rs` (557, 1669, 1732, + 1752, 1814, 1678, …); **none in `db.rs`**. Width and theme + therefore live App-side. The **one exception** is the V5/V5a + `show []` family: `do_show_one` / `show_list` build + prose `Vec` **in the worker** from `RelationshipSchema` / + index metadata, carrying *no* `TableDescription` — so + `show relationship ` is the path that must be restructured + (§6). The output buffer is a flat `VecDeque` — a + historical log of lines, not re-renderable widgets (live + re-rendering is V4 territory). +2. **Relationships are list-based (ADR-0043).** `RelationshipSchema` + /`RelationshipEnd` endpoints are `Vec` column lists, + positionally paired. Single-column FKs are the one-element case; + the diagram must render compound FKs too. +3. **Only 1:n relationships exist.** Relationships are declared 1:n + (ADR-0013); m:n (requirement C4) is unbuilt, and there is no + UNIQUE-target 1:1. So cardinality is always **1 on the parent + (referenced) side, n on the child (referencing/FK) side**. + +The user has chosen the design direction across three decisions +(recorded below as made): **visual style**, **where it appears**, and +**multi-relationship layout**. + +## Decision + +### 1. Scope and trigger — diagrams where relationships are the subject + +Relationship diagrams replace the prose relationship form on the +surfaces where **relationships are the subject of the command**; no +new command or sigil is added (ADR-0009: one sigil, keyword grammar). +This is the user-chosen **"relationship-relevant"** reach (over a +global replacement of every structure echo, or a show-commands-only +scope — see the DA pass). + +Diagram surfaces: + +- **`show relationship `** (V5a) renders **one** diagram: the + parent and child tables as **full structure boxes** joined by a + connector. The canonical base unit. +- **`show table `** keeps T's full structure box at the top + (unchanged from ADR-0016 §5), then a **Relationships** section: + **one compact connector diagram per relationship**, stacked + vertically (§4). +- **Relationship DDL auto-shows** — the structure echo after + `add 1:n relationship`, `drop relationship`, and a future + `modify relationship` (C3a) — render their relationships as + compact diagrams (§4), since the user just acted on a relationship. + +Prose-retained surfaces (**unchanged** from ADR-0016 §5): + +- **Incidental DDL auto-shows** — the structure echo after + `create table`, `add`/`drop`/`rename`/`change column`, + `add`/`drop index` — keep the terse `References:` / + `Referenced by:` prose. A simple `add column` on a heavily-related + table should not print a wall of diagrams. + +So this **partially supersedes ADR-0016 §5**: the prose block is +replaced by diagrams on the relationship-subject surfaces and +retained on incidental ones. No information is lost on either — +relationship name, endpoints, cardinality, and `on delete` / +`on update` actions all appear in both forms. + +Mechanically (§6) this is a **render mode** on `render_structure` +(`Diagram` vs `Prose`), selected by the calling command; the items/ +side panel (`ui.rs render_items_panel`) is a navigation tree and is +untouched. + +A future **user-configurable setting** (e.g. always-prose / +always-diagram / auto-by-width, for small-screen users) is a clean +follow-up and is **out of scope here** (OOS-7) — the per-command +default above is the v1 behaviour. + +### 2. The base diagram — Style A (structure + connector) + +A relationship diagram is two boxes plus a connector. Reading +convention, applied **uniformly** (this is what makes +multi-relationship lists scannable): + +- **Child (FK holder) on the left, parent (referenced) on the + right.** The connector arrow always points **left → right**, in the + direction of the reference (child *references* parent). +- **Cardinality** sits at the connector ends: **`n`** at the child + (left) end, **`1`** at the parent (right) end. +- **Referential actions** (`on delete …`, `on update …`) label the + connector beneath the line. `no action` is shown verbatim + (ADR-0009 wording), abbreviated to the action keyword when space is + tight. + +#### 2.1 Box anatomy — the table name must stand out + +Every box (full or compact) carries the table name as a **bold title +row separated from the columns by a rule** (`├─┤`), styled in the +table-name theme class (§5). This directly addresses the requirement +that a name never read as just another column row. + +Full structure box (used in `show relationship`): title row, then the +ADR-0016 column/type listing, with key markers (§2.2): + +``` +┌──────────────────────┐ +│ orders │ ← bold title row (table name) +├──────────────┬───────┤ +│ id (PK) │ int │ +│ customer_id ●│ int │ ● marks the FK column in this relationship +│ total │ real │ +└──────────────┴───────┘ +``` + +Compact box (used in `show table`'s stacked list): title row, rule, +then **only the column(s) participating in this relationship** — the +focal table's full structure is already shown above, so the compact +box stays small and the focal table is not redrawn in full N times: + +``` +┌──────────────┐ +│ orders │ ← bold title row +├──────────────┤ +│ customer_id ●│ only the FK column(s) for this relationship +└──────────────┘ +``` + +#### 2.2 Key markers + +- The PK is annotated `(PK)` (consistent with the existing structure + view's constraint column). +- The **endpoint columns of *this* relationship** are marked with a + filled dot `●` adjacent to the column name — on the child FK + column(s) and the parent referenced column(s). The connector + attaches at these `●` rows. + +#### 2.3 The connector (single-column) + +`show relationship Customers_Orders` (child `orders.customer_id` +→ parent `customers.id`): + +``` + orders customers +┌──────────────────────┐ ┌─────────────────────┐ +│ orders │ │ customers │ +├──────────────┬───────┤ 1 ┌──●│ id (PK) │ +│ id (PK) │ int │ │ ├─────────────┬───────┤ +│ customer_id ●│ int │ n ───────────┘ │ name │ text │ +│ total │ real │ │ email │ text │ +└──────────────┴───────┘ └─────────────┴───────┘ + on delete cascade · on update no action +``` + +**Connector routing.** The two `●` endpoint rows are generally at +different heights. The connector leaves the child box's right edge at +the child-`●` row, travels horizontally into a **gutter channel** +between the boxes, jogs vertically to the parent-`●` row, then enters +the parent box's left edge with an arrowhead. Box tops are aligned. +(Exact glyphs/spacing are pinned by insta snapshots in +implementation; the mockups here are representative, not normative.) + +#### 2.4 Compound foreign keys (ADR-0043) + +For an n-column FK, the n child `●` columns and n parent `●` columns +are each listed, and **one connector is routed per positional pair**. +A summary line states the pairing explicitly so it is unambiguous +even if the routed lines are visually close: + +``` + orders customers +┌──────────────────────────┐ ┌──────────────────────┐ +│ orders │ 1 ┌●│ region (PK) │ +├──────────────┬───────────┤ │ │ id (PK) │ +│ cust_region ●│ text │ n ─────────┤ ├────────────┬─────────┤ +│ cust_id ●│ int │ n ─────────┘ │ name │ text │ +│ … │ … │ └────────────┴─────────┘ +└──────────────┴───────────┘ + (cust_region, cust_id) ──► customers.(region, id) + on delete cascade · on update no action +``` + +### 3. Width handling and the narrow-terminal fallback + +The diagram is rendered **once, App-side, at the output-panel width +current when the command runs**. The `App` does **not** track the +panel width today — `note_output_viewport(visible_rows, +total_wrapped_rows)` records only row counts for scroll math +(verified; `App` has no width field). This ADR adds a +`last_output_width: u16` to `App`, set from `ui.rs` where the output +panel's `inner.width` is already computed (next to the existing +`note_output_viewport` call), with an `80` default before the first +render. The App-side render path then chooses layout per diagram: + +- **Side-by-side** (§2) when both boxes plus the gutter fit the + available width. +- **Vertical stack** when they do not: parent box above, child box + below, connector running **downward** through a short vertical + channel, cardinality and actions labelling it. This keeps a real + diagram at any width instead of degrading to prose: + +``` +┌──────────────┐ +│ orders │ +├──────────────┤ +│ customer_id ●│ n +└──────────────┘ + │ on delete cascade + ▼ on update no action +┌──────────────┐ +│ customers │ +├──────────────┤ +│ id (PK)●│ 1 +└──────────────┘ +``` + +- **Last-resort helper.** Only when even a single vertical diagram + cannot fit (e.g. an extremely narrow pane, or a box wider than the + pane), `show table` emits a one-line pointer per relationship — + ` · run `show relationship ` for the + diagram` — so nothing is silently dropped. `show relationship` + itself always renders (it is the detail view); if its single box is + wider than the pane, ratatui's existing right-edge handling applies + (ADR-0016 §4), no new truncation. + +**No live reflow.** Because the output buffer is a historical line +log, resizing the terminal *after* a diagram is rendered does not +reflow it — identical to how every other rendered output behaves +today. Reflow-on-resize belongs to V4's re-renderable journal and is +explicitly out of scope (OOS-2). + +This **extends ADR-0016 §4**: that ADR added no width-awareness +(relying on ratatui truncation); this ADR adds width-awareness for +the **layout decision** only. It still performs **no per-cell +truncation** (ADR-0016 OOS-4 stands). + +### 4. `show table` multi-relationship layout — stacked compact diagrams + +After T's full structure box, a **Relationships** heading, then one +**compact** diagram (§2.1) per relationship, stacked vertically. +Ordering matches the current prose order: **outbound** (T is the +child — "References") first, then **inbound** (T is the parent — +"Referenced by"), each group ordered by relationship name. The +child-left/parent-right rule (§2) is applied per diagram, so the +focal table appears on the left for its outbound relationships and on +the right for its inbound ones — consistent with the reference +direction. + +``` +orders +┌──────────────┬──────┐ +│ orders │ │ +├──────────────┼──────┤ +│ id (PK) │ int │ +│ customer_id │ int │ +│ total │ real │ +└──────────────┴──────┘ + +Relationships +┌──────────────┐ n 1 ┌─────────────┐ +│ orders │────────────────│ customers │ +├──────────────┤ on delete ├─────────────┤ +│ customer_id ●│ cascade │ id (PK) ●│ +└──────────────┘ └─────────────┘ +┌──────────────┐ n 1 ┌─────────────┐ +│ order_items │────────────────│ orders │ +├──────────────┤ on delete ├─────────────┤ +│ order_id ●│ cascade │ id (PK) ●│ +└──────────────┘ └─────────────┘ +``` + +Stacking (rather than a single focal-centred subgraph with fan-out +connectors) is chosen because: it never produces crossing lines; it +scales to any number of relationships via the pane's existing +vertical scroll; each diagram is only two boxes wide, so it fits +nearly any terminal; and it keeps the per-diagram width logic of §3 +trivial. The cost — the focal table name repeats per diagram — is +mitigated by the compact box showing only the participating +column(s). + +### 5. Styling + +Styling uses the **ADR-0028 styled-run mechanism** (`OutputSpan` / +`OutputStyleClass` on `OutputLine`), not raw text. New style classes +(theme-defined, legible light/dark per ADR-0016 §6 / NFR-7): + +- **table name** — bold, accent colour (the "stand out" requirement); +- **key marker** (`●`, `(PK)`) — a distinct accent; +- **connector** (box-drawing line + arrowhead) — muted; +- **cardinality** (`1` / `n`) — emphasised; +- **referential action** label — muted/secondary. + +This **extends ADR-0016 §6** (which set up `OutputKind::System` +styling but no per-element theming) by reusing the per-span +`OutputSpan` path ADR-0028 later introduced — which is **already +produced App-side** (`output_render.rs:299-332` builds styled runs +for the explain-plan tree; `app.rs` constructs `OutputLine`s with +`styled_runs: Some(..)`; `ui.rs:863` renders them). So **no +worker→UI contract change is needed**: the new `output_render` +diagram functions return styled lines directly on the App side +(§6). + +### 6. Implementation + +All rendering is **App-side** (per the corrected Context fact 1), in +`output_render.rs` (hand-rolled, ADR-0016 §7), returning styled +lines that `app.rs` pushes as `OutputLine`s with `styled_runs` set. +No worker `available_width` and no worker→UI contract change. + +- **New renderer functions in `output_render.rs`**, composing the + existing box primitives and emitting styled spans (§5): + - `render_relationship_diagram(child, parent, rel, width, full) -> + Vec` — `full` selects full vs compact boxes; + internally decides side-by-side vs vertical (§3) from `width`; + routes the connector(s) including compound pairs (§2.4). + - A shared helper for the box title row + key markers. + - The two call paths supply `width` from the new + `App::last_output_width` (§3). +- **`show table ` (already App-side).** The focal + `TableDescription` reaches `app.rs` and is rendered by + `render_structure` today. `render_structure` is refactored to emit + the focal structure box, then a **Relationships** section built + from the focal description's `RelationshipEnd`s (which already + carry neighbour table name + participating columns + cardinality + + actions — **enough for the compact box**; no neighbour + full-structure fetch needed). `render_structure` gains a + **relationship render mode** (`Diagram` | `Prose`); the caller + selects it (§1): `show table` and the relationship DDL echoes pass + `Diagram`, incidental DDL echoes pass `Prose`. The generic + `handle_dsl_success(command, description)` (`app.rs:1669`) already + has the `Command`, so the mode is a function of the command + variant; the incidental-only call sites (`app.rs:557/1732/1752/ + 1814`) pass `Prose`. +- **`show relationship ` (must be restructured).** Its + worker path `do_show_one` currently returns prose `Vec` + from a `RelationshipSchema` only. To draw the **full** diagram the + App needs **both** endpoint `TableDescription`s. The relationship + detail reply is upgraded to carry the `RelationshipSchema` plus + both endpoints' `TableDescription`s (the worker already has + `do_describe_table`); the App renders the diagram. The + "No relationship named `X`." not-found line is preserved. +- **Self-referential FKs** (`parent_table == child_table`; supported + in the grammar): render as two boxes bearing the same table name + (child-left copy with the FK column, parent-right copy with the + referenced column), connector as usual — clearer than a self-loop + glyph in a TUI. Covered by a snapshot test. +- **Out of these surfaces:** the items/side panel (`ui.rs:582` + `render_items_panel`) is a navigation tree, not a relationship + view, and is unchanged. +- **Database engine name** never appears in any rendered string + (ADR-0002). + +### 7. Testing + +- **Insta snapshots** (Tier 2) pin exact rendered output for: + single-column 1:n (`show relationship`); compound FK; the + narrow-terminal vertical fallback; the last-resort helper line; + a self-referential FK; and a `show table` with both an outbound and + an inbound relationship (stacked compact list). +- **Unit tests** (`output_render.rs`): the side-by-side-vs-vertical + **width-threshold** decision at boundary widths; connector routing + for endpoints at differing row heights; compound-pair routing; + key-marker placement. +- **Tier-3 integration**: `show relationship ` and + `show table ` produce diagram output (not prose) end-to-end + through the worker. +- **Existing tests/code to update** (enumerated from a DA grep — the + supersession of ADR-0016 §5 is not abstract): + - `output_render.rs:121,135` — the `References:` / `Referenced by:` + prose-emitting code in `render_structure`; + - `output_render.rs:78` — the docstring deferral note (OOS-1); + - `output_render.rs:793` — unit test asserting `"Referenced by:"`; + - `src/snapshots/…render_structure_with_relationships.snap` — the + prose snapshot; + - `tests/it/walking_skeleton.rs:477,530` — integration asserts on + `"Referenced by:"` (and the comment at 433); + - `src/dsl/command.rs:984` — a comment referencing the prose (no + behaviour). + If the scope fork resolves to "all `render_structure` call sites," + the DDL-echo snapshots for create-table / add-column / drop-index / + change-column auto-shows also churn — to be re-recorded with + `cargo insta`. No test is deleted to hide a regression; each change + is a deliberate format update. + +### 8. Out of scope + +- **OOS-1.** Live reflow-on-resize of already-rendered diagrams — + V4's re-renderable journal (requirement V4). +- **OOS-2.** Whole-database ER diagram / export (requirement V3). +- **OOS-3.** m:n relationships (requirement C4, unbuilt). Only the + existing 1:n form is rendered. +- **OOS-4.** ASCII fallback for terminals without box-drawing + (inherits ADR-0016 OOS-5). +- **OOS-5.** Per-cell colouring of column *data* values (ADR-0016 + OOS-3 / NFR-5) — unrelated to relationship structure. +- **OOS-6.** Cell-level truncation with ellipsis (ADR-0016 OOS-4 + stands). +- **OOS-7.** A user-configurable relationship-display setting + (always-prose / always-diagram / auto-by-width — useful for small + screens). The §1 per-command default is v1; the setting is a clean + later follow-up (user-flagged 2026-06-09). + +## Consequences + +- Requirement **V1** is fully satisfied: relationships render as + two-table connector diagrams in both `show relationship ` + (full) and `show table ` (stacked compact), superseding the + prose form. +- `output_render.rs` grows a relationship renderer that **composes** + the box primitives exactly as ADR-0016 anticipated; the worker→UI + show contract becomes styled-line-based, a small generalisation + reusable by other styled `show` output later. +- The worker gains an `available_width` input for `show` — the first + width-aware formatting in the codebase. The decision is a snapshot + at command time; no resize machinery is introduced. +- Compound FKs (ADR-0043) get their first dedicated visualization, + with explicit positional-pair labelling. +- The historical-log output model is preserved (no widget/reflow + model); V4 remains the home for a re-renderable journal. +- Box-drawing is required (no ASCII fallback), consistent with + ADR-0016. + +## Devil's Advocate / runda pass (2026-06-09) + +A planning-time DA pass (empirical, against the code) corrected three +foundational errors carried in from an initial code survey, and +surfaced one open fork: + +1. **Rendering boundary was inverted** (fixed in Context 1 / §6): + `render_structure` / `render_data_table` run **App-side** + (`app.rs`), not in the worker. Rendering, width, and theme are all + App-side — simpler than the draft assumed. +2. **Width was claimed "already tracked"; it is not** (fixed in §3): + `note_output_viewport` records only rows. A new + `App::last_output_width` (set from `ui.rs`) is required. +3. **`show relationship ` renders prose in the worker** (fixed + in §6): `do_show_one` carries only a `RelationshipSchema`. The + detail reply must be upgraded to include both endpoint + `TableDescription`s for the full diagram. +4. **Styled-line contract was over-stated** (fixed in §5): styled + runs are already produced App-side (explain plan); no worker→UI + contract change. + +**Resolved fork (user, 2026-06-09): "relationship-relevant" reach.** +Diagrams render on the surfaces where the relationship is the subject +(`show table`, `show relationship`, relationship DDL echoes); +incidental DDL echoes keep prose (§1) — avoiding a wall of diagrams +after an `add column`. A future user-configurable display setting is +noted as OOS-7. This also records V1's **deliberate scope expansion** +beyond its literal "a selected relationship" wording into +multi-relationship surfaces, to be reflected in `requirements.md`. + +All design forks are now resolved and the architecture is corrected. +The user accepted this revised ADR on 2026-06-09; status is +**Accepted** and implementation proceeds test-first. diff --git a/docs/adr/README.md b/docs/adr/README.md index 055f0d6..ff69ab8 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -49,3 +49,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0041 — Copy the output panel to the system clipboard](0041-copy-output-to-clipboard.md) — **Accepted 2026-06-02 (issue #11)**, amends ADR-0003's app-command registry (adds **`copy`** / `copy all` / `copy last`). The friction it removes: filing a bug report meant terminal-selecting the output panel and fighting wrapping/borders. New **app-level command** (sigil-free, both modes): `copy` / `copy all` copy the whole panel; `copy last` copies from the most recent echo line to the end. **Mechanism — OSC 52 *and* native (`arboard`), always both**, because OSC 52 acceptance is undetectable (no terminal ack), so a true "fall back when unsupported" can't be built: emit the OSC 52 escape (no new dep — `base64`+`crossterm`; works over SSH; tmux-passthrough-wrapped via `$TMUX`), then a best-effort native write whose failure is ignored (headless host — OSC 52 carried it); the two carry identical content. **Format — plain text verbatim as rendered** (tags, `✓`/`✗`, box-drawing) joined by `\n`, without viewport padding/wrapping; a drift-lock test pins `OutputLine::plain_text` to `render_output_line`. `arboard` added **`--no-default-features`** (drops the `image` crate; X11-only on Linux — `wayland-data-control` deliberately omitted as it ~doubles the dep tree and OSC 52 covers native-Wayland). Security: write-only, scans clean for arboard's tree (cargo audit / osv-scanner / grype), 1Password-maintained, minimal surface. OOS: Markdown export, selection/range, a keybinding, OSC 52 read, `screen` passthrough - [ADR-0042 — H1a parse-error pedagogy in the grammar-tree era](0042-h1a-parse-error-pedagogy-grammar-tree.md) — **Accepted 2026-06-03.** Continues **H1a** from ADR-0021 against the ADR-0024 grammar tree (ADR-0021's chumsky mechanism is dead). Records the **baseline already shipped** — per-command `usage:` block (38 `parse.usage.*` templates), available-commands fallback, structural "after `…`, expected …" wording, source-derived ident slot labels ("table name"/"column name"), curated `parse.custom.*` near-miss messages, and the ADR-0027/0033/0036 schema-aware `[ERR]` diagnostics — so H1a is *substantially* delivered at the intent level. Defines the remaining work as **(1)** a verified per-command **near-miss matrix** (`tests/typing_surface/` + `tests/it/parse_error_pedagogy.rs`) as the definition of done, test-first; **(2)** **friendlier literal expectation labels** — optional prose glosses on `Word`/`Punct`/`Flag` positions that *add* role context while always keeping the exact literal visible (e.g. "a filter clause: `where …` or `--all-rows`"); **(3)** **advanced-mode SQL** near-miss parity (RETURNING scope, CTE-arity positioning, `CROSS JOIN … ON`, INSERT…SELECT count) — **in scope**, kept distinct from ADR-0019 §OOS-2 which covers advanced-SQL *engine*-error sanitisation, a different layer. Catalog/anchor-phrase discipline (ADR-0019) preserved; no public API change. OOS: I3/I4, spell-correction, multi-error reporting, verbosity-gating the usage block - [ADR-0043 — Compound-primary-key foreign-key references (T3)](0043-compound-pk-foreign-key-references.md) — **Accepted + implemented 2026-06-09** (all four forks confirmed at the recommended option: full-PK matching, house-style uniform lists, parenthesized DSL syntax, bare-SQL-FK auto-expansion). Closes `requirements.md` **T3** `[x]` — the relationship model went list-based across six layers (single-column preserved, no migration), DSL `from P.(a,b) to C.(x,y)` + SQL `FOREIGN KEY (a,b) REFERENCES P(x,y)` parse/execute/enforce, 12 tests in `tests/it/compound_fk.rs`. Closes the open leg of `requirements.md` **T3**: a foreign key that *references* a parent's compound primary key. A 2026-06-09 audit found single-column FK woven through ~15–20 sites (metadata table, `RelationshipSchema`, `project.yaml` `RawEndpoint`, both grammar surfaces, executor FK-DDL emission, per-column type-compat, display) — earns an ADR, not an inline build. **Decision:** reference the parent's **full** compound PK, matched **positionally** to an equal-length child column list, per-pair `fk_target_type` compat (ADR-0011, element-wise); DSL `from

.(a, b) to .(x, y)` (single form unchanged), SQL `FOREIGN KEY (x, y) REFERENCES P(a, b)` (extend the existing one-cap lists; bare table-level FK auto-expands to the parent PK when arities match). **Storage — no migration (back-compat not required, user-confirmed 2026-06-09; no installed base):** the relationship endpoint joins the list convention `project.yaml` *already* uses — `columns: [a, b]` like `primary_key: [id]` and index `columns: [...]` (the endpoint was the lone scalar `column:` holdout); the metadata `TEXT` columns are unchanged and store the list **comma-joined** (`a,b`; the bare name for single — safe because identifiers are `[A-Za-z0-9_]+`). No F3 migrator, no version bump; accepted trade-off is that a pre-change `project.yaml` with relationships won't load (clean cutover). In-memory model goes list-based (`Vec`) through all six layers; the enforced FK is the rebuilt child-table DDL (`FOREIGN KEY (a,b) REFERENCES P(x,y)`), one relationship = one undo step (ADR-0013). Genuine forks escalated: matching policy (full-PK vs subset), storage (house-style uniform lists vs normalized table), DSL syntax (parenthesized vs repeated-dotted), bare-SQL-FK auto-expansion. OOS: subset/non-PK (UNIQUE-targeted) FK references; any single-column behaviour change +- [ADR-0044 — Relationship visualization (two-table connector diagrams)](0044-relationship-visualization.md) — **Accepted 2026-06-09** (revised post-`/runda` DA pass). Resolves **ADR-0016 OOS-1** and closes the open half of `requirements.md` **V1** ("a selected relationship as two tables joined by a line"). Renders a relationship as **Style A** (two structure boxes + connector). **Reach = "relationship-relevant"** (user-chosen over global / show-only): diagrams on the surfaces where the relationship is the *subject* — `show relationship ` (one full diagram), `show table ` (T's structure box then a **Relationships** section of **stacked compact** per-relationship diagrams — chosen over a focal-centred subgraph: no crossing lines, scales via scroll, two-boxes-wide fits any terminal), and relationship DDL echoes (`add`/`drop`/`modify relationship`); incidental DDL echoes (`add column`, `drop index`, `change column`, plain `create table`) keep the terse prose, via a `Diagram`|`Prose` render mode on `render_structure`. Reading convention **child(FK)-left / parent-right, arrow →, `n`…`1` cardinality**, applied uniformly; every box gets a **bold title row + rule** so the name can't read as a column. **Compound FKs** (ADR-0043) route one connector per positional pair + an explicit pairing line. **Width-aware** (first in the codebase) but **App-side**: `render_structure`/diagram rendering runs in `app.rs` (the worker only returns `TableDescription`s), a new `App::last_output_width` (set from `ui.rs`) drives side-by-side vs a **vertical-stack** fallback + last-resort "run `show relationship`" pointer; rendered once at command time, **no live reflow** (V4). `show relationship`'s worker path (`do_show_one`, prose-only) is restructured to return both endpoint `TableDescription`s. Styling reuses **ADR-0028** App-side styled runs (new classes: table-name/key/connector/cardinality/action) — no worker→UI contract change. **Partially supersedes ADR-0016 §5** (prose block replaced on relationship-subject surfaces, retained on incidental ones); extends §4 (layout width-awareness, still no cell truncation) and §6 (per-span theming). Tests: insta snapshots (single, compound, vertical fallback, helper line, self-referential, multi-rel `show table`) + width-threshold/routing unit tests + Tier-3 wiring; enumerated prose-fallout updates (`output_render.rs:121/135/793`, the relationships snapshot, `walking_skeleton.rs:477/530`). A `/runda` DA pass corrected three inverted-architecture claims (App-side rendering, untracked width, prose-in-worker show-relationship) before acceptance. OOS: user-configurable display setting (OOS-7), live reflow (V4), whole-DB ER export (V3), m:n (C4), ASCII fallback (ADR-0016 OOS-5)