docs(ci): establish docs/ci/adr namespace (ci-001 pipeline, ci-002 flake)
ci / gate (push) Successful in 2m33s
ci / gate (push) Successful in 2m33s
Records the CI/release pipeline as ADR-ci-001 and relocates the nix-flake ADR from main's ADR-0049 to ADR-ci-002 (content unchanged, history note added). Both live in docs/ci/adr/ with a README index — a dated, ci-segmented namespace disjoint from main's integer ADR sequence, the same split the website subproject uses to avoid cross-branch number collisions. Drops the ADR-0049 entry from docs/adr/README. ci-001 covers the runner model, the baked nix CI image, the clippy+test gate, the static-musl release on tag, trigger hygiene, auth, and scope.
This commit is contained in:
@@ -54,4 +54,3 @@ This directory contains the project's ADRs, recorded per
|
|||||||
- [ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)](0046-sidebar-navigation-and-responsive-input-hint.md) — **Accepted + implemented 2026-06-10, phased A→B→C** (8 commits `9f5f76b`…`22bec61`; closes Gitea **#20** hint jumpiness, **#21** left-column improvements, **#23** long input — all forks user-confirmed, including the persistent show/hide toggle which is **deferred**: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on **`App`** not `SchemaCache` (DB2); the nav overlay clears **only the sidebar strip + a one-column gutter**, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. **Phase A (input & hint):** the hint panel's height becomes a function of **terminal geometry, fixed between resizes** (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets `H<40` compact (input 1 row + horizontal scroll / hint 2) vs `H≥40` comfortable (input 2 rows soft-wrap / hint 2), output `Min(5)` honoured first under degradation; input gains horizontal scroll (`input_scroll_offset`, single logical `String` — **not** I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. **Phase B (sidebar):** the 26-col Tables column is **kept but made optional and richer** (not deleted — pedagogy wins ties) — **width-derived session-only** visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a **relationships panel** rendered narrow with endpoints broken at the arrow, ellipsized — a **separate sibling panel** that **overrides S2**'s nested-list extension model (relationships are cross-table). the full records live on a new **`App.relationships`** field (revised from the ADR's original `SchemaCache.relationship_details` at implementation — `SchemaCache` is walker-facing and needs only the names, kept in `relationships: Vec<String>`; details are UI-only, so `App` mirrors `app.tables` and avoids ~23 fixture edits), delivered by `Database::read_all_relationships` + an `AppEvent::RelationshipsRefreshed`; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). **Phase C (navigation mode):** **`Ctrl-O`** enters a focus cycle (Input → Tables → Relationships → Input; `Esc` exits) orthogonal to the ADR-0003 input mode — **`Ctrl-B` was rejected on review as the default tmux prefix** (unreachable inside tmux); the focused panel **expands to ~40–50 cols as a `Clear` overlay** (right panels stay unchanging underneath) and scrolls via **Up/Down (line) + PageUp/PageDown (page)** (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); `Ctrl-O` (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; `H<40/≥40` thresholds; session-only persistence; Up/Down line-scroll; **separate relationships panel overriding S2**; **no hint-area toggle** (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build `/runda` DA pass drove these corrections: caught the `Ctrl-B`/tmux collision, the `SchemaCache` retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue **#22** (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output *narrower* when widened across the boundary (sidebar appears)
|
- [ADR-0046 — Schema sidebar focus/navigation mode and responsive input & hint layout (UI #20/#21/#23)](0046-sidebar-navigation-and-responsive-input-hint.md) — **Accepted + implemented 2026-06-10, phased A→B→C** (8 commits `9f5f76b`…`22bec61`; closes Gitea **#20** hint jumpiness, **#21** left-column improvements, **#23** long input — all forks user-confirmed, including the persistent show/hide toggle which is **deferred**: the Ctrl-O peek covers #21's "keystroke to show and hide"). Two decisions landed differently from the draft (recorded inline): relationship data on **`App`** not `SchemaCache` (DB2); the nav overlay clears **only the sidebar strip + a one-column gutter**, panels staying visible behind (DC2). Treats the three UI issues as one coupled decision because they share the terminal's width/height budget. **Phase A (input & hint):** the hint panel's height becomes a function of **terminal geometry, fixed between resizes** (not of hint content), eliminating the #20 jump at its source — measured catalog shows ≥ ~54-col right-column width never needs > 2 hint lines, so 3 lines is a rare narrow-terminal-only case; height buckets `H<40` compact (input 1 row + horizontal scroll / hint 2) vs `H≥40` comfortable (input 2 rows soft-wrap / hint 2), output `Min(5)` honoured first under degradation; input gains horizontal scroll (`input_scroll_offset`, single logical `String` — **not** I1 multi-line) and 2-row soft-wrap display when tall, preserving ADR-0027's 6-col indicator reserve. **Phase B (sidebar):** the 26-col Tables column is **kept but made optional and richer** (not deleted — pedagogy wins ties) — **width-derived session-only** visibility (visible iff width > 90 or a Ctrl-O peek is active — no stored field; hides at width ≤ 90 so the 90-col screencasts drop it; ADR-0015 format untouched), plus a **relationships panel** rendered narrow with endpoints broken at the arrow, ellipsized — a **separate sibling panel** that **overrides S2**'s nested-list extension model (relationships are cross-table). the full records live on a new **`App.relationships`** field (revised from the ADR's original `SchemaCache.relationship_details` at implementation — `SchemaCache` is walker-facing and needs only the names, kept in `relationships: Vec<String>`; details are UI-only, so `App` mirrors `app.tables` and avoids ~23 fixture edits), delivered by `Database::read_all_relationships` + an `AppEvent::RelationshipsRefreshed`; the two left panels split vertically with the relationships panel floored at 5 rows ("(none)" when empty) and capped at 50 % of the column (DB4). **Phase C (navigation mode):** **`Ctrl-O`** enters a focus cycle (Input → Tables → Relationships → Input; `Esc` exits) orthogonal to the ADR-0003 input mode — **`Ctrl-B` was rejected on review as the default tmux prefix** (unreachable inside tmux); the focused panel **expands to ~40–50 cols as a `Clear` overlay** (right panels stay unchanging underneath) and scrolls via **Up/Down (line) + PageUp/PageDown (page)** (context-rebind, reusing the output-scroll viewport mechanism), with an accent focus border; all non-nav keys inert in nav mode (and nav keys inert while a modal is open). Forks all user-chosen: keep-optional-richer (vs remove/narrow); navigation-mode (vs modeless modifier scroll); `Ctrl-O` (Ctrl-B rejected = tmux prefix); overlay (vs layout re-split); inert-non-nav-keys; geometry-fixed hint height; `H<40/≥40` thresholds; session-only persistence; Up/Down line-scroll; **separate relationships panel overriding S2**; **no hint-area toggle** (S4's stale "keyboard-toggleable" claim struck — never implemented, unwanted). A pre-build `/runda` DA pass drove these corrections: caught the `Ctrl-B`/tmux collision, the `SchemaCache` retype that would have broken completion, the 2-row-input/indicator placement, the missing nav-mode key disposition + modal gate, and three unreferenced requirements (S1 evolved, S2 overridden, S4 corrected); also cross-checked open issue **#22** (overlay/annotation layer — separate ADR, adjacent). OOS: true multi-line input (I1); readline shortcuts (I1b); cross-session sidebar persistence; output as a third nav focus; relationship search/edit from the panel; hint-area toggle; #22's annotation layer. Accepted consequence: the 90-col visibility threshold makes a terminal's output *narrower* when widened across the boundary (sidebar appears)
|
||||||
- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54`→`2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle
|
- [ADR-0047 — Demonstration overlay layer (keystroke badges + step captions)](0047-demonstration-overlay-layer.md) — **Accepted 2026-06-10; implemented 2026-06-11, phased A→B→C (closes Gitea #22)** (commits `f879d54`→`2d0f4b2`; no `requirements.md` item — tracked by issue + ADR per convention; all forks user-confirmed + a pre-build `/runda` pass that produced 10 tightening findings and a whole-implementation `/runda` pass that returned PASS, no blockers). An in-app **demonstration mode** (`--demo` flag / `RDBMS_PLAYGROUND_DEMO` env, **off by default, zero footprint when off**) that renders two transient overlays so `autocast` screencasts — and live teaching, and a future guided-lesson system — can show otherwise-invisible interactions. **Keystroke badges** (`[TAB]`, `[ENTER]`, `[UP]`, …): **automatic, app-detected** over a fixed set of glyph-less keys (the app already sees every key, so it re-records for free), label via a pure `demo_badge_label(&KeyEvent)`; the badge **auto-expires on a ~1.5 s timer** that extends the runtime's existing time-boxed-`recv` arm condition (`debounce.is_armed() || badge_pending`; expiry `Instant` in the runtime, `App.demo_badge` the render mirror — mirroring the `input` vs `input_indicator` split). **Step captions**: a **stealth, control-code-delimited input buffer** toggled by **`Ctrl+]`** (byte `0x1D` → arrives as `Char('5')+CONTROL`, verified against crossterm 0.29 `parse.rs:110-113`; chosen over `Ctrl+!`, which is **not a single ASCII byte so autocast cannot send it** — the same wall as arrow keys, R4) — typed characters accumulate **invisibly** (prompt untouched, no echo/history), `Backspace` edits, other keys inert, a second `Ctrl+]` **commits** to the caption box (empty commit dismisses); lives in pure-sync `App::update()`, **intercepted before the modal gate** so captions/badges work **over the load picker** (the `#24` projects cast). Both render as **floating flat black-on-yellow rectangles** (solid fill, **no border glyphs** — a one-cell text margin, deliberately unlike the app's bordered panels; user decision post-build, `2d0f4b2`) **at the output panel's inner bottom-right**, drawn **last over modals**, badge **stacked above** the caption, **no layout reflow**; caption **word-wraps to ≤ 3 lines** (3–5 rows), badge fixed 3 rows; clamp/skip guard for tiny terminals; a new **`App.last_output_area: Rect`** (set in `render_output_panel`) gives the top-level draw the anchor. Caption persists **until the next keystroke**; badge suppressed while capturing. Forks all user-chosen: `--demo` activation (vs hidden command / chord); automatic badges (vs scripted); stealth buffer (vs typed-command / preloaded-file); floating bottom-right boxes (vs HUD / banner / subtitle); `Ctrl+]` trigger; wrap-to-3-line captions; ~1.5 s badge / next-keystroke caption timing. Tested test-first across Tier 1 (label fn, capture state machine incl. over-modal + demo-off gate, nearest-deadline helper), Tier 2 (insta snapshots: badge/caption/both-stacked at 90×26 light+dark, short-terminal clamp), Tier 3 (`--demo` plumbing, badge set/suppressed, caption-without-input wiring), CLI (`--demo` parse + env fallback) — with an **honest limit** noted: the `tokio` timer wiring inside `run_loop` is exercised via the pure pieces + Tier-3 plumbing, not a standalone integration test of the timeout (same posture as the existing `IndicatorDebounce`). One intentional, user-acknowledged behaviour: `Ctrl-C` is inert while capturing (every non-`Ctrl+]` key is, by spec). Final tally **2290 passing / 0 failing / 0 skipped** (1 long-standing ignored doctest), clippy clean. OOS: scripted/manual badge push; badges for glyph keys; configurable styling/placement; the guided-lesson system itself (own ADR); cross-session/-switch persistence; localised caption content; arrow-only cast interactions (output-pane scroll); wiring the overlays into the website `casts.mjs` scripts (website-branch follow-up). Implementation phased **A** (`--demo` plumbing) → **B** (badges) → **C** (captions) + a flat-rectangle restyle
|
||||||
- [ADR-0048 — `seed` fake-data generation command](0048-seed-fake-data-generation.md) — **Accepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11** (Phase 1 commits `202e25a`→`fbd219b`; design settled with the user across an extended fork dialogue, hardened by a pre-build `/runda` pass (six blockers folded in), a post-implementation `/runda` pass (eight gaps closed — FK/shortid determinism so **D4 holds with no exceptions**, plus six untested ADR decisions), and a Phase-2 pre-build `/runda` pass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation `/runda` pass (which added a friendly error for a bounded override on a UNIQUE column — see D2); **2400 tests pass, clippy clean**). Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1**. **Phase 1 shipped:** whole-row `seed <table> [count] [--seed <n>]` with realistic name-aware generation (the `fake` crate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolled `product` generator, bounded dates), identifier + constraint uniqueness incl. junction distinct-combos, FK sampling from existing parent rows (empty-parent error), `IN`-CHECK derivation + complex-CHECK advisory, a required-column block guard, `--seed` reproducibility (serial/FK/shortid all deterministic), undo as one batch step, replay as a data write, a capped auto-show preview, the enum/CHECK advisory, and an O(N) single-transaction insert path. **Phase 2 shipped (2026-06-11):** the `set` override clause (D2 — fixed value / pick-list / `as <generator>` / `between` range, **quoted** dates per the D2 amendment, type-aware, override drops the column from the advisory) and the `<table>.<column>` column-fill form (D1 form 2 — an UPDATE over existing rows, refusing PK/autogen targets, empty-table no-op, FK/unique-respecting, one undo step), with the new `KNOWN_GENERATORS` vocabulary (D9), a range `Generator`, full completion/highlight (`HighlightClass::Function`)/validity (`IdentSource::Generators`)/help/pedagogy wiring, and the D13 advisory's Phase-2/3 wording. Further SD2 increments (custom generators, NULL injection, multi-locale, recursive auto-seed) out of scope. Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1** (the other being `hint`/**H2**). A dedicated `seed` command (own AST variant + `do_seed` executor, **both modes**) generating **realistic, name-aware** fake data. Two forms: **`seed <table> [count]`** (new rows, default **20**, capped) and **`seed <table>.<column>`** (fill a column on existing rows, an UPDATE). Generation adds the **`fake` crate** (v5, English) driven by a **type-gated, token-matched name-heuristic catalogue** (~30 patterns, documented false-positive guards), with **table-context** disambiguating the `name`/`title` family (`products.name`→product, `users.name`→person, `vendors.name`→company), a **hand-rolled `product` generator** (`fake` has no commerce module), **bounded dates** (`date`/`timestamp`/`dob`/`*_at` recognised, recent windows — never "all of history"), the **identifier family** (`id`/`code`/`ref`/`number`, non-FK/non-PK) → **unique sequential**, and **enum-ish names** (`role`/`status`/`type`/…) left generic + a **post-seed Hint advisory** pointing at `set … in (…)`. A **`set` override clause** — `= value` / `in (a,b,c)` / `as <generator>` / `between a and b` (numeric **and** date), reusing ADR-0026 operators — answers the heuristic-miss case. **`--seed <n>`** makes runs reproducible (and enables exact-value tests). **FK** columns sampled uniformly from existing parent rows (**empty parent → friendly error**, no recursion v1); **junction/compound-PK** tables seeded with **distinct combinations**, capped + noted (SD1). A **required-column block guard** refuses rather than NULL-violate a `NOT NULL` column it can't fill (e.g. `NOT NULL blob`). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted as `tok_function`, hints, `help seed`, ADR-0042 near-miss matrix, ADR-0027 validity); **no DSL→SQL teaching echo** (seed is a utility command, not a SQL twin). Honours **X5** — `do_seed` reuses insert/update *mechanics as helpers*, not by emitting `Command::Insert`. Implementation phased: (1) core whole-row seed → (2) `set` overrides → (3) column-fill. Deferred (future SD2): recursive auto-seed, NULL injection, multi-locale, user-defined custom generators, full per-column report
|
- [ADR-0048 — `seed` fake-data generation command](0048-seed-fake-data-generation.md) — **Accepted 2026-06-11; Phase 1 + Phase 2 implemented 2026-06-11** (Phase 1 commits `202e25a`→`fbd219b`; design settled with the user across an extended fork dialogue, hardened by a pre-build `/runda` pass (six blockers folded in), a post-implementation `/runda` pass (eight gaps closed — FK/shortid determinism so **D4 holds with no exceptions**, plus six untested ADR decisions), and a Phase-2 pre-build `/runda` pass (which caught the no-date-literal-token reality → the D2 quoted-dates amendment), and a post-implementation `/runda` pass (which added a friendly error for a bounded override on a UNIQUE column — see D2); **2400 tests pass, clippy clean**). Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1**. **Phase 1 shipped:** whole-row `seed <table> [count] [--seed <n>]` with realistic name-aware generation (the `fake` crate + a type-gated heuristic catalogue, table-context name disambiguation, hand-rolled `product` generator, bounded dates), identifier + constraint uniqueness incl. junction distinct-combos, FK sampling from existing parent rows (empty-parent error), `IN`-CHECK derivation + complex-CHECK advisory, a required-column block guard, `--seed` reproducibility (serial/FK/shortid all deterministic), undo as one batch step, replay as a data write, a capped auto-show preview, the enum/CHECK advisory, and an O(N) single-transaction insert path. **Phase 2 shipped (2026-06-11):** the `set` override clause (D2 — fixed value / pick-list / `as <generator>` / `between` range, **quoted** dates per the D2 amendment, type-aware, override drops the column from the advisory) and the `<table>.<column>` column-fill form (D1 form 2 — an UPDATE over existing rows, refusing PK/autogen targets, empty-table no-op, FK/unique-respecting, one undo step), with the new `KNOWN_GENERATORS` vocabulary (D9), a range `Generator`, full completion/highlight (`HighlightClass::Function`)/validity (`IdentSource::Generators`)/help/pedagogy wiring, and the D13 advisory's Phase-2/3 wording. Further SD2 increments (custom generators, NULL injection, multi-locale, recursive auto-seed) out of scope. Closes `requirements.md` **SD1** and the core of **SD2**; closes the `seed` half of **A1** (the other being `hint`/**H2**). A dedicated `seed` command (own AST variant + `do_seed` executor, **both modes**) generating **realistic, name-aware** fake data. Two forms: **`seed <table> [count]`** (new rows, default **20**, capped) and **`seed <table>.<column>`** (fill a column on existing rows, an UPDATE). Generation adds the **`fake` crate** (v5, English) driven by a **type-gated, token-matched name-heuristic catalogue** (~30 patterns, documented false-positive guards), with **table-context** disambiguating the `name`/`title` family (`products.name`→product, `users.name`→person, `vendors.name`→company), a **hand-rolled `product` generator** (`fake` has no commerce module), **bounded dates** (`date`/`timestamp`/`dob`/`*_at` recognised, recent windows — never "all of history"), the **identifier family** (`id`/`code`/`ref`/`number`, non-FK/non-PK) → **unique sequential**, and **enum-ish names** (`role`/`status`/`type`/…) left generic + a **post-seed Hint advisory** pointing at `set … in (…)`. A **`set` override clause** — `= value` / `in (a,b,c)` / `as <generator>` / `between a and b` (numeric **and** date), reusing ADR-0026 operators — answers the heuristic-miss case. **`--seed <n>`** makes runs reproducible (and enables exact-value tests). **FK** columns sampled uniformly from existing parent rows (**empty parent → friendly error**, no recursion v1); **junction/compound-PK** tables seeded with **distinct combinations**, capped + noted (SD1). A **required-column block guard** refuses rather than NULL-violate a `NOT NULL` column it can't fill (e.g. `NOT NULL blob`). Full ambient wiring (completion incl. a new generator-name vocabulary highlighted as `tok_function`, hints, `help seed`, ADR-0042 near-miss matrix, ADR-0027 validity); **no DSL→SQL teaching echo** (seed is a utility command, not a SQL twin). Honours **X5** — `do_seed` reuses insert/update *mechanics as helpers*, not by emitting `Command::Insert`. Implementation phased: (1) core whole-row seed → (2) `set` overrides → (3) column-fill. Deferred (future SD2): recursive auto-seed, NULL injection, multi-locale, user-defined custom generators, full per-column report
|
||||||
- [ADR-0049 — Nix flake for a reproducible dev + build environment](0049-nix-flake-dev-and-build-env.md) — **Accepted + implemented 2026-06-12** (`ci` branch; first step of the CI work toward `requirements.md` **TT5** + **D1/D2/D3**). Adopts a root **Nix flake** as the single, version-pinned declaration of the dev *and* build toolchain so CI never relies on whatever Rust is installed on the build machine — mirroring **datamage ADR 0046**, but far simpler (pure-Rust TUI: no Tauri/WebKit/Node/WASM). Two outputs (user-chosen over dev-shell-only): **`devShells.default`** (pinned toolchain via `rust-toolchain.toml` + `rust-overlay`, plus `cargo-sweep`) and **`packages.default`** (a `rustPlatform.buildRustPackage` building the binary reproducibly from the committed `Cargo.lock` via `importCargoLock`; `doCheck = false` — the suite runs as its own `nix develop -c cargo test` stage, not in the HOME/X-less build sandbox; version read from `Cargo.toml` via `fromTOML`). Toolchain pinned to **exact `1.95.0`** (not floating `stable`) so `nix flake update` can't surprise-bump clippy lints past the `-D warnings` gate; components `rustfmt` + `clippy`; **no** cross/WASM targets yet (added when the release matrix needs them). System inputs are nearly empty by design — `libsqlite3-sys` `bundled` needs only the stdenv C compiler; `arboard`→`x11rb` is pure-Rust (no C X11 libs, X server only needed at *runtime*, OSC 52 otherwise). `.envrc` (`use flake`) kept for direnv parity though direnv isn't on the current VM. **Verified before acceptance:** `nix develop` toolchain pinned, `nix build` yields a working binary, clippy clean, **2424 tests pass / 0 fail / 1 intentional ignored doctest** — all through the flake. Consequences: the `nix build` artifact is glibc-**dynamic** (a reproducible build/test artifact, **not** the D2 static release binary — release uses a static target like `x86_64-unknown-linux-musl`, deferred to the CI release work); the **`fmt` gate is deliberately left out for now** (user decision — the tree isn't clean under stock `rustfmt`, ~100 files would churn and conflict with the website/`main` work; revisit on `main`), so the gate is **`clippy` + `test`**. Alternatives rejected: dev-shell-only (no reproducible artifact); a standard `rust:1.95` CI image (a second toolchain definition = drift, the very thing this prevents); `rustup` on the build host (non-reproducible — the status quo being eliminated). The CI **pipeline** itself (runner wiring, release matrix) is decided separately as it settles.
|
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# ADR-ci-001: CI + release pipeline on Gitea Actions
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**Accepted (2026-06-12); implemented the same day on the `ci` branch.** Every
|
||||||
|
fork below was settled with the user as the pipeline was built, and each stage
|
||||||
|
was verified live before acceptance:
|
||||||
|
|
||||||
|
- a throwaway probe workflow established how the runner executes jobs;
|
||||||
|
- the CI image was built and checked locally (runner contract, warm devShell);
|
||||||
|
- the gate ran green (**clippy clean; 2424 tests pass / 0 fail / 1 intentional
|
||||||
|
ignored doctest**);
|
||||||
|
- the release was exercised end-to-end — tag `v0.0.0-citest2` published a Gitea
|
||||||
|
release carrying the static binary (~10 MB) and its `.sha256`.
|
||||||
|
|
||||||
|
This ADR records the **CI/release pipeline**. The **dev/build environment it
|
||||||
|
runs on** — the nix flake (devShell + reproducible build, pinned Rust 1.95.0)
|
||||||
|
— is **ADR-ci-002** (relocated here from main's ADR-0049); this ADR builds on
|
||||||
|
it rather than restating it.
|
||||||
|
|
||||||
|
> **Namespacing.** Kept in `docs/ci/adr/` (id `ADR-ci-001`), disjoint from
|
||||||
|
> `main`'s integer ADR sequence, mirroring the website subproject's
|
||||||
|
> `docs/website/adr/`. This avoids the cross-branch number collisions that
|
||||||
|
> previously forced website ADRs to be renumbered (see that namespace's
|
||||||
|
> history note and ADR-0000 "Numbering discipline").
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The project is near feature-complete and needs CI (`requirements.md` **TT5**;
|
||||||
|
the **CI** item in the deferred list) and a release path for its distributed
|
||||||
|
binaries (**D1**/**D2**/**D3**). The self-hosted Gitea instance
|
||||||
|
(`git.lazyeval.net`) has its Actions runner freshly set up — a first-time
|
||||||
|
in-anger use — with a DinD-capable setup and a reusable `docker-build`
|
||||||
|
template, exercised by a handful of sample workflows.
|
||||||
|
|
||||||
|
The starting constraints, and what the probe found:
|
||||||
|
|
||||||
|
- The runner label is **`ci-public`**. A throwaway probe
|
||||||
|
(`ci-probe.yaml`, since removed) established that **jobs run *inside* a
|
||||||
|
container** — `ghcr.io/catthehacker/ubuntu:act-22.04` by default, as **root**
|
||||||
|
— and therefore the runner *host's* nix is **not** on the steps' PATH
|
||||||
|
(`nix NOT on PATH`, `no /nix`). A custom job `container:` *can* be pulled
|
||||||
|
(it pulled `nixos/nix:latest`), but the runner keeps job containers alive
|
||||||
|
with `entrypoint: /bin/sleep` and runs JS actions (e.g. `actions/checkout`)
|
||||||
|
with `node`, so the container must provide **`sleep` + `bash` + `node`** —
|
||||||
|
a bare `nixos/nix` image has none and fails to start.
|
||||||
|
- The reusable template only does `docker build`; it neither runs a Rust gate
|
||||||
|
nor pushes images nor uploads release assets — so a Rust pipeline can't just
|
||||||
|
call it.
|
||||||
|
- The whole motivation (per the user) is for CI to use the project's **nix
|
||||||
|
flake** for its tools rather than relying on whatever the build machine has
|
||||||
|
— i.e. **one toolchain definition shared by dev and CI**.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. Toolchain delivery — a baked nix CI image
|
||||||
|
|
||||||
|
CI gets its toolchain from a **purpose-built job-container image**, not from
|
||||||
|
host nix and not by installing nix per-job:
|
||||||
|
|
||||||
|
- **Base `node:22-bookworm-slim`.** Debian slim already provides `bash` +
|
||||||
|
coreutils (`sleep`); the `node` tag adds the actions runtime. This satisfies
|
||||||
|
the act_runner job-container contract at a fraction of the size of the
|
||||||
|
catthehacker runner images (chosen on the user's prompt to avoid those
|
||||||
|
multi-GB images), and far more reliably than a bare `nixos/nix` (which can't
|
||||||
|
start). `.gitea/ci-image/Dockerfile`.
|
||||||
|
- **Single-user nix on top**, flakes enabled, with the **flake's devShell
|
||||||
|
pre-warmed** (`nix develop` realizes nixpkgs + the pinned Rust toolchain +
|
||||||
|
`cargo-sweep` + the musl cc into the store). CI then runs `nix develop -c …`
|
||||||
|
against a warm store — the *same* pinned toolchain as dev (ADR-ci-002),
|
||||||
|
reaching a ready toolchain in ~1.4 s.
|
||||||
|
- **Built + pushed by `build-ci-image.yaml`** via the DinD service to the
|
||||||
|
Gitea container registry as `git.lazyeval.net/<owner>/rdbms-playground-ci`,
|
||||||
|
a **public** package (anonymous pull, no gate-side credentials). It runs only
|
||||||
|
when an image input changes (Dockerfile / `flake.nix` / `flake.lock` /
|
||||||
|
`rust-toolchain.toml`) or on manual dispatch.
|
||||||
|
|
||||||
|
### 2. Gate — `ci.yaml`
|
||||||
|
|
||||||
|
On branch pushes and PRs, a single job runs **inside the CI image**:
|
||||||
|
`nix develop -c cargo clippy --all-targets -- -D warnings` then
|
||||||
|
`nix develop -c cargo test --no-fail-fast`.
|
||||||
|
|
||||||
|
**`fmt` is deliberately not gated.** The tree isn't clean under stock
|
||||||
|
`rustfmt` (~100 files would change; no `rustfmt.toml` is committed) and
|
||||||
|
reformatting would churn blame across the in-flight website branch and ongoing
|
||||||
|
`main` work — so, by user decision, the gate is **clippy + test** and fmt is
|
||||||
|
revisited on `main` (also recorded in ADR-ci-002).
|
||||||
|
|
||||||
|
### 3. Release — `release.yaml`
|
||||||
|
|
||||||
|
On a `v*` tag, one job in the CI image:
|
||||||
|
|
||||||
|
1. **tests** (`cargo test`) — so a tag can never publish untested code, even
|
||||||
|
one pointing at a never-gated commit (user choice over relying solely on the
|
||||||
|
branch gate);
|
||||||
|
2. **builds the static binary** for **`x86_64-unknown-linux-musl`** (D2:
|
||||||
|
single static binary, no runtime deps). The glibc/nix-store build is
|
||||||
|
non-portable; the musl target with `crt-static` is fully static. rusqlite's
|
||||||
|
`bundled` SQLite C is compiled by a **musl `cc`** (`pkgsCross.musl64`) wired
|
||||||
|
into the flake devShell via `CC_<target>` + `CARGO_TARGET_<TARGET>_LINKER`;
|
||||||
|
`[profile.release] strip = "symbols"` trims it (~13 MB → ~10 MB);
|
||||||
|
3. **publishes** the binary + a `.sha256` to a Gitea release via the API and
|
||||||
|
the auto-provided **`GITEA_TOKEN`** — no third-party action (just `curl` +
|
||||||
|
`node`, both in the image).
|
||||||
|
|
||||||
|
### 4. Triggers — branch vs tag hygiene
|
||||||
|
|
||||||
|
- Gate and image-build are scoped to **branch** pushes (`branches: ['**']`).
|
||||||
|
Tag pushes ignore `paths:` filters and would otherwise spuriously rebuild the
|
||||||
|
unchanged image and re-gate an already-gated commit; the branch filter
|
||||||
|
excludes tags. **`release.yaml` owns tags** (`tags: ['v*']`).
|
||||||
|
- Pushing commits + a tag together still gates the commits (via the branch
|
||||||
|
ref) and releases (via the tag ref) — no lost coverage, no duplicate runs.
|
||||||
|
|
||||||
|
### 5. Auth
|
||||||
|
|
||||||
|
- **Image push:** a dedicated PAT with `write:package`, supplied as the
|
||||||
|
`REGISTRY_USERNAME` / `REGISTRY_TOKEN` Actions secrets (the package owner
|
||||||
|
must match the token's user — an `oli`-namespace push with a different user
|
||||||
|
is refused with `reqPackageAccess`).
|
||||||
|
- **Release publish:** the auto `GITEA_TOKEN` (repo/release scope).
|
||||||
|
|
||||||
|
### 6. Scope this iteration — Linux x86_64, step by step
|
||||||
|
|
||||||
|
The user's target is the full **D1** matrix, approached incrementally. This
|
||||||
|
iteration ships **Linux x86_64 only**; the rest is deferred (below).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **One toolchain, dev and CI.** They build through the same flake and cannot
|
||||||
|
drift. New image rebuilds only when the flake/toolchain/Dockerfile change.
|
||||||
|
- **D2 is met on Linux.** The release artifact is a genuinely static,
|
||||||
|
stripped musl binary that runs with no runtime dependencies.
|
||||||
|
- **DinD is per-job (no layer cache across runs),** so every `build-ci-image`
|
||||||
|
run rebuilds from scratch (~6 min). Acceptable at its trigger frequency;
|
||||||
|
base-pull caching via the `dind-cached` proxy variant is a possible later
|
||||||
|
optimisation.
|
||||||
|
- **The CI image is ~5.5 GB+** (the Rust toolchain closure, now also musl).
|
||||||
|
Pulled once per runner and cached; slimming (multi-stage, prune) is optional.
|
||||||
|
- **Every gate run recompiles the full dependency graph** (warm *toolchain*,
|
||||||
|
cold *deps*; clippy and test don't share artifacts), ~2 min total. Fine for
|
||||||
|
now; dependency/`target` caching is a deferred speed item.
|
||||||
|
- **`GITEA_TOKEN` must retain release scope;** if an instance policy narrows
|
||||||
|
it, the release publish falls back to a repo-scoped PAT secret.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Run on the runner host's nix.** Rejected — the probe showed steps run in a
|
||||||
|
container where host nix is unreachable.
|
||||||
|
- **Install nix per-job in the default image.** Works but cold every run
|
||||||
|
(slow) and throwaway once the image exists; rejected in favour of the baked
|
||||||
|
image.
|
||||||
|
- **`catthehacker` or bare `nixos/nix` as the base.** catthehacker is a
|
||||||
|
multi-GB runner emulation we don't need; bare `nixos/nix` lacks
|
||||||
|
`sleep`/`bash`/`node` and won't start. `node:22-bookworm-slim` is the small,
|
||||||
|
contract-satisfying middle (user's suggestion).
|
||||||
|
- **A standard `rust:1.95` CI image instead of the flake.** Simpler in CI but a
|
||||||
|
*second* toolchain definition (drift) — counter to the unify-with-dev goal.
|
||||||
|
- **A third-party Gitea release action.** Avoided; the API + auto token keep
|
||||||
|
the release self-contained and debuggable.
|
||||||
|
|
||||||
|
## Deferred / out of scope (tracked, step by step)
|
||||||
|
|
||||||
|
- **D1 matrix:** aarch64, macOS, Windows builds (cross toolchains; macOS is the
|
||||||
|
hard part on a Linux runner).
|
||||||
|
- **D3 packaging:** Homebrew / Scoop / winget / `cargo-binstall` manifests
|
||||||
|
(and binstall-friendly asset naming/archives).
|
||||||
|
- **Tier 4 (PTY E2E):** still unwired (`requirements.md` **TT4**); the gate runs
|
||||||
|
tiers 1–3 only, so **TT5** ("CI runs all tiers on Linux/macOS/Windows") is
|
||||||
|
partially met — Linux, tiers 1–3.
|
||||||
|
- **CI speed:** dependency/`target` caching (cargo-chef into the image, or
|
||||||
|
`actions/cache`), and image slimming / `dind-cached` base-pull caching.
|
||||||
|
- **Website deploy:** the static site → Cloudflare via Gitea Actions (a
|
||||||
|
separate, simpler workflow on the website branch).
|
||||||
|
- **fmt gate:** revisit on `main` once a `rustfmt` style is chosen.
|
||||||
|
|
||||||
|
## Relationship to other decisions
|
||||||
|
|
||||||
|
- **Builds on ADR-ci-002** (nix flake dev + build env). This ADR adds the
|
||||||
|
musl-target/cc to that flake and consumes it from CI.
|
||||||
|
- **Advances `requirements.md`:** **TT5** (CI runs the tiers — Linux, 1–3),
|
||||||
|
**D2** (static binary — Linux, done), **D1**/**D3** (partial/deferred).
|
||||||
|
- **Mirrors the website subproject's** separate ADR namespace and its
|
||||||
|
static→Cloudflare-via-Gitea-Actions deployment posture (ADR-website-001).
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# ADR-0049: Nix flake for a reproducible dev + build environment
|
# ADR-ci-002: Nix flake for a reproducible dev + build environment
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
@@ -10,8 +10,15 @@ clippy --all-targets -- -D warnings` is clean and `cargo test` is
|
|||||||
**2424 passed / 0 failed / 1 ignored** (the ignored item is the
|
**2424 passed / 0 failed / 1 ignored** (the ignored item is the
|
||||||
intentional ```` ```ignore ```` doctest at `src/friendly/mod.rs:21`),
|
intentional ```` ```ignore ```` doctest at `src/friendly/mod.rs:21`),
|
||||||
all run *through the flake*. This ADR is the dev/build-environment
|
all run *through the flake*. This ADR is the dev/build-environment
|
||||||
half of the CI work; the CI **pipeline** itself (runner wiring,
|
foundation; the CI **pipeline** that consumes it (runner model, image,
|
||||||
release matrix) is decided separately as it settles.
|
gate, release) is **ADR-ci-001**.
|
||||||
|
|
||||||
|
> **History.** Created as **ADR-0049** in `main`'s integer ADR namespace
|
||||||
|
> (`docs/adr/`); moved here to **ADR-ci-002** on 2026-06-12 to keep the
|
||||||
|
> CI/dev-env decisions out of `main`'s sequence and end the cross-branch
|
||||||
|
> number collision (`main` independently reaches for the next integer too —
|
||||||
|
> the same problem the website subproject hit). Content is otherwise
|
||||||
|
> unchanged. See ADR-0000 "Numbering discipline".
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
@@ -122,6 +129,7 @@ declaration of the dev *and* build environment.
|
|||||||
- Mirrors **datamage ADR 0046** (nix flake dev env) and its build
|
- Mirrors **datamage ADR 0046** (nix flake dev env) and its build
|
||||||
hygiene companion. This is the rdbms-playground analogue, scoped to
|
hygiene companion. This is the rdbms-playground analogue, scoped to
|
||||||
a pure-Rust project.
|
a pure-Rust project.
|
||||||
- Feeds the CI pipeline work for `requirements.md` **TT5** (CI runs
|
- Feeds **ADR-ci-001** (the CI + release pipeline), which consumes this
|
||||||
the tiers) and the **D1/D2/D3** distribution items (the release
|
flake for `requirements.md` **TT5** (CI runs the tiers) and the
|
||||||
matrix consumes `nix build` / a static target).
|
**D1/D2/D3** distribution items (the release uses a static musl target
|
||||||
|
built through this flake).
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# CI / Build Architecture Decision Records
|
||||||
|
|
||||||
|
Decision records for the **continuous-integration + release pipeline**
|
||||||
|
subproject — the Gitea Actions workflows under `.gitea/`, the nix CI image,
|
||||||
|
and the release tooling. These are kept in their own namespace, separate
|
||||||
|
from the project-wide ADRs in [`docs/adr/`](../../adr/README.md), so CI
|
||||||
|
decisions never compete with the main global ADR sequence for numbers — the
|
||||||
|
same split the website subproject uses (`docs/website/adr/`, on the `website`
|
||||||
|
branch), and for the same reason (see
|
||||||
|
[ADR-0000 "Numbering discipline"](../../adr/0000-record-architecture-decisions.md)).
|
||||||
|
|
||||||
|
**Numbering.** Files are named `<date>-adr-ci-<NNN>.md` and referenced in
|
||||||
|
prose as `ADR-ci-NNN`. The `<date>` (the ADR's accepted/created day,
|
||||||
|
`YYYYMMDD`) plus the `ci` segment keeps the namespace disjoint from `main`'s
|
||||||
|
integers. Assign the next free `NNN` from this index. Every ADR change
|
||||||
|
updates this index in the same edit (the ADR-0000 index-upkeep rule applies
|
||||||
|
here too).
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
- [ADR-ci-001 — CI + release pipeline on Gitea Actions](20260612-adr-ci-001.md) — **Accepted 2026-06-12** (implemented the same day on the `ci` branch). Establishes the CI/release pipeline on the self-hosted Gitea instance's Actions runner (`ci-public`). **Runner model** (established by a throwaway probe): jobs execute *inside* a container (`catthehacker/ubuntu:act-22.04` by default), as root, so the runner host's nix is **not** reachable from steps. **Toolchain delivery:** a **baked CI image** — `node:22-bookworm-slim` (satisfies the act_runner job-container contract: `/bin/sleep` keep-alive, `bash`, `node` for JS actions; a bare `nixos/nix` image lacks these and won't start) **+ single-user nix + the flake's devShell pre-warmed** — built by `build-ci-image.yaml` via DinD and pushed to the Gitea container registry as a **public** package, so CI runs `nix develop -c …` against the **same pinned toolchain as dev** (the flake, ADR-ci-002) with a warm store (~1.4 s to a ready toolchain). **Gate** (`ci.yaml`): `clippy -D warnings` + `cargo test` inside that image on branch pushes + PRs; **fmt deliberately not gated** (the tree isn't stock-rustfmt-clean — user decision, revisit on `main`; see ADR-ci-002). **Release** (`release.yaml`): on a `v*` tag, runs the tests, builds the **static `x86_64-unknown-linux-musl` binary** (D2: single static binary, no runtime deps — the glibc/nix build is non-portable), strips it, and publishes it + a `.sha256` to a Gitea release via the API and the auto-provided `GITEA_TOKEN`. **Triggers:** gate + image-build are scoped to **branch** pushes (`branches: ['**']`) so a release tag doesn't spuriously re-run them; the image-build additionally path-filters to its inputs (Dockerfile/flake/toolchain); the release owns tags. **Auth:** a dedicated PAT (`REGISTRY_USERNAME`/`REGISTRY_TOKEN` secrets) pushes the image; the auto `GITEA_TOKEN` publishes releases. **Scope this iteration:** Linux x86_64 only — the rest of the D1 matrix (aarch64/macOS/Windows), D3 package-manager manifests, CI-speed dependency caching, and the website's static→Cloudflare deploy are deferred, to be added step by step. Verified live: probe → runner facts; image built + checked locally; gate green (**2424 tests**); release exercised end-to-end (`v0.0.0-citest2` published with binary + checksum). Builds on **ADR-ci-002** (the nix flake, relocated here from main's ADR-0049 to avoid exactly this cross-branch collision).
|
||||||
|
- [ADR-ci-002 — Nix flake for a reproducible dev + build environment](20260612-adr-ci-002.md) — **Accepted 2026-06-12** (relocated from main's **ADR-0049** on the same day — content unchanged — to keep CI/dev-env decisions out of `main`'s integer sequence). The single, version-pinned declaration of the **dev *and* build toolchain** so CI never relies on whatever Rust is on the build machine — mirroring **datamage ADR 0046**, but far simpler (pure-Rust TUI). Root **Nix flake** with two outputs: **`devShells.default`** (pinned **Rust 1.95.0** via `rust-toolchain.toml` + `rust-overlay`, `cargo-sweep`, and the musl cc for the static release build) and **`packages.default`** (`rustPlatform.buildRustPackage` from the committed `Cargo.lock`; `doCheck = false`). Exact-pin (not floating `stable`) so `nix flake update` can't surprise-bump clippy past the `-D warnings` gate. System inputs near-empty by design (`libsqlite3-sys bundled` → stdenv cc only; `arboard`→`x11rb` pure-Rust). `.envrc` (`use flake`) for direnv parity. Verified through the flake: `nix build` yields a working binary, clippy clean, **2424 tests pass / 0 fail / 1 intentional ignored doctest**. Consumed by **ADR-ci-001** (the pipeline). Alternatives rejected: dev-shell-only; a standard `rust:1.95` CI image (a second toolchain definition = drift); `rustup` on the build host (non-reproducible).
|
||||||
Reference in New Issue
Block a user