Compare commits
5 Commits
dff78412dd
...
ef99e6c676
| Author | SHA1 | Date | |
|---|---|---|---|
| ef99e6c676 | |||
| c30a6114b9 | |||
| fe9d58e037 | |||
| 628b250db6 | |||
| 784373a254 |
@@ -5,11 +5,15 @@
|
|||||||
# Matrix (D1, cross-built from Linux x86_64 via cargo-zigbuild):
|
# Matrix (D1, cross-built from Linux x86_64 via cargo-zigbuild):
|
||||||
# x86_64-unknown-linux-musl aarch64-unknown-linux-musl (static, D2)
|
# x86_64-unknown-linux-musl aarch64-unknown-linux-musl (static, D2)
|
||||||
# x86_64-pc-windows-gnu aarch64-pc-windows-gnullvm (standalone .exe)
|
# x86_64-pc-windows-gnu aarch64-pc-windows-gnullvm (standalone .exe)
|
||||||
# macOS is deferred — its arboard/AppKit link needs Apple's SDK (see ADR-ci-001).
|
# The two macOS targets are built separately by the dispatched
|
||||||
# D3 package-manager manifests layer on later.
|
# release-macos.yaml (native Tart runner; ADR-ci-003 amendment), uploading to
|
||||||
|
# the same release. D3 package-manager manifests layer on later.
|
||||||
#
|
#
|
||||||
# Tests run once (host) before the matrix, so a tag can never publish untested
|
# Tests run once (host) before the matrix, so a tag can never publish untested
|
||||||
# code, even one pointing at a commit that was never gated on a branch.
|
# code, even one pointing at a commit that was never gated on a branch. The
|
||||||
|
# version guard (ADR-0054) refuses to publish a tag whose vX.Y.Z disagrees with
|
||||||
|
# Cargo.toml's version, keeping `--version`, the release name, and the asset in
|
||||||
|
# lockstep.
|
||||||
name: release
|
name: release
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -23,6 +27,22 @@ jobs:
|
|||||||
image: git.lazyeval.net/oli/rdbms-playground-ci:latest
|
image: git.lazyeval.net/oli/rdbms-playground-ci:latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- name: version guard — tag must equal Cargo.toml version (ADR-0054)
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# CARGO_PKG_VERSION is the single source of truth; the binary reports
|
||||||
|
# it via --version / the `version` command. Parse it from cargo
|
||||||
|
# metadata (node is in the CI image; avoids assuming jq).
|
||||||
|
VER=$(nix develop -c cargo metadata --no-deps --format-version 1 \
|
||||||
|
| node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>process.stdout.write(JSON.parse(s).packages[0].version))')
|
||||||
|
echo "tag=$TAG cargo=$VER"
|
||||||
|
if [ "$TAG" != "v$VER" ]; then
|
||||||
|
echo "ERROR: release tag '$TAG' != 'v$VER' (Cargo.toml). Bump Cargo.toml to the release version, commit, then retag (ADR-0054)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- name: test
|
- name: test
|
||||||
run: nix develop -c cargo test --no-fail-fast
|
run: nix develop -c cargo test --no-fail-fast
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,13 @@
|
|||||||
*.snap.new
|
*.snap.new
|
||||||
*.pending-snap
|
*.pending-snap
|
||||||
|
|
||||||
|
# Website tooling — Cloudflare Wrangler local cache/state (regenerable;
|
||||||
|
# CI deploys from website/, this dir only appears on a local wrangler run)
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
# Editor / OS
|
# Editor / OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
# Astro/template-seeded editor configs we don't track (e.g. website/.vscode)
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -108,10 +108,11 @@ Current decisions at a glance (each backed by an ADR):
|
|||||||
SQL `select` / `with` / `insert` / `update` / `delete`
|
SQL `select` / `with` / `insert` / `update` / `delete`
|
||||||
(ADR-0039). `EXPLAIN QUERY PLAN` never executes, so
|
(ADR-0039). `EXPLAIN QUERY PLAN` never executes, so
|
||||||
explaining a destructive command is safe.
|
explaining a destructive command is safe.
|
||||||
- **Continuous integration & release** (built on the `ci` branch,
|
- **Continuous integration & release** (developed on the `ci` branch,
|
||||||
2026-06-15; decisions in `docs/ci/adr/` — **ADR-ci-001/002/003**,
|
**merged to `main` 2026-06-15**; decisions in `docs/ci/adr/` —
|
||||||
a namespace kept separate from the main ADR sequence to avoid
|
**ADR-ci-001/002/003**, a namespace kept separate from the main ADR
|
||||||
cross-branch number collisions, like the website's): a self-hosted
|
sequence to avoid cross-branch number collisions, like the website's):
|
||||||
|
a self-hosted
|
||||||
**Gitea Actions** pipeline built on a **nix flake** (pinned Rust
|
**Gitea Actions** pipeline built on a **nix flake** (pinned Rust
|
||||||
`1.95.0` — one source of toolchain for dev *and* CI) plus a
|
`1.95.0` — one source of toolchain for dev *and* CI) plus a
|
||||||
prebuilt CI image. **Gate** (`ci.yaml`): `clippy -D warnings` +
|
prebuilt CI image. **Gate** (`ci.yaml`): `clippy -D warnings` +
|
||||||
@@ -122,44 +123,72 @@ Current decisions at a glance (each backed by an ADR):
|
|||||||
`release-macos.yaml` on a Tart Apple-Silicon runner (de-nix the
|
`release-macos.yaml` on a Tart Apple-Silicon runner (de-nix the
|
||||||
`libiconv` load path + ad-hoc re-sign). All published to a Gitea
|
`libiconv` load path + ad-hoc re-sign). All published to a Gitea
|
||||||
release with `.sha256`s. **`fmt` is intentionally not gated yet**
|
release with `.sha256`s. **`fmt` is intentionally not gated yet**
|
||||||
(the tree isn't stock-`rustfmt`-clean). `workflow_dispatch` is
|
(the tree isn't stock-`rustfmt`-clean). Now that this is on `main`,
|
||||||
Gitea-default-branch-only, so `release-macos` is dispatchable once
|
`release-macos` is dispatchable (`workflow_dispatch` is
|
||||||
this lands on `main`.
|
Gitea-default-branch-only) — **dispatched and verified working**: the
|
||||||
|
macOS build + de-nix/re-sign + upload runs end-to-end and the binaries
|
||||||
|
launch.
|
||||||
|
- **Website & docs site** (developed on the `website` branch, **merged
|
||||||
|
to `main`**; the branch **stays open** for future staging deploys;
|
||||||
|
decisions in `docs/website/adr/` — **ADR-website-001**, its own
|
||||||
|
namespace like CI's): an **Astro + Starlight + Tailwind** marketing
|
||||||
|
landing page plus the **canonical** user docs, under `website/`.
|
||||||
|
Showcase demos are **asciinema casts** (a scripted-input driver, paced
|
||||||
|
+ re-recordable — *not* `history.log` replay; the `--demo` overlay,
|
||||||
|
ADR-0047, dresses them). **Deployed to Cloudflare Pages via Gitea
|
||||||
|
Actions** (`.gitea/workflows/website.yaml`; the crate gate is skipped
|
||||||
|
for website-only changes). Two copy rules bind user-facing text: **no
|
||||||
|
engine name** (continues ADR-0002) and **no "DSL"** (say "simple mode"
|
||||||
|
/ "advanced mode"). Install docs are still partial — package-manager
|
||||||
|
manifests + some installation instructions are pending (`requirements.md`
|
||||||
|
D3 / DOC1).
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── Cargo.toml # dependencies, lints (nursery)
|
├── Cargo.toml / Cargo.lock # dependencies, lints (nursery)
|
||||||
├── CLAUDE.md # this file
|
├── CLAUDE.md # this file
|
||||||
|
├── flake.nix / flake.lock # pinned Rust toolchain, one source for dev + CI (ADR-ci-002)
|
||||||
|
├── rust-toolchain.toml # toolchain pin
|
||||||
|
├── .gitea/ # Gitea Actions workflows + prebuilt CI image (ADR-ci-001..003, website)
|
||||||
|
├── ci/ # CI build helpers (e.g. winstub/ — Windows link stub)
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── adr/ # all decision records (read 0000 first)
|
│ ├── adr/ # project-wide decision records (read 0000 first)
|
||||||
│ ├── handoff/ # session-handover notes
|
│ ├── ci/{adr,handoff}/ # CI subproject ADRs (ci-001..003) + handoffs
|
||||||
│ └── requirements.md # the Phase-1 checklist with progress
|
│ ├── website/{adr,plans}/ # website subproject ADRs (website-001) + plan
|
||||||
|
│ ├── handoff/ # session-handover notes
|
||||||
|
│ ├── plans/ # working plans
|
||||||
|
│ └── requirements.md # the Phase-1 checklist with progress
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── action.rs # Action enum (Quit / ExecuteDsl)
|
│ ├── action.rs # Action enum (Quit / ExecuteDsl / …)
|
||||||
│ ├── app.rs # App state + pure update() + Tier-1 tests
|
│ ├── app.rs # App state + pure update() + Tier-1 tests
|
||||||
│ ├── cli.rs # CLI args (--theme, --log-file)
|
│ ├── cli.rs # CLI args (--theme, --log-file, --demo, --no-undo, --resume, …)
|
||||||
│ ├── db.rs # rusqlite worker, all DDL/DML, metadata tables
|
│ ├── clipboard.rs # copy output to the system clipboard
|
||||||
|
│ ├── completion.rs # Tab completion + schema cache
|
||||||
|
│ ├── db.rs # rusqlite worker, all DDL/DML, metadata tables
|
||||||
│ ├── dsl/
|
│ ├── dsl/
|
||||||
│ │ ├── action.rs # ReferentialAction enum + parsing
|
│ │ ├── grammar/ # hand-rolled unified grammar nodes (DSL + SQL)
|
||||||
│ │ ├── command.rs # Command AST + RelationshipSelector + RowFilter
|
│ │ ├── walker/ # grammar walker (driver / context / highlight / outcome)
|
||||||
│ │ ├── mod.rs # re-exports
|
│ │ ├── command.rs parser.rs types.rs value.rs action.rs shortid.rs sql_functions.rs
|
||||||
│ │ ├── parser.rs # parse entry point → unified-grammar walker
|
│ ├── echo.rs # command echo / SQL rendering
|
||||||
│ │ ├── shortid.rs # base58 generator + validator
|
│ ├── event.rs # AppEvent (input + DSL outcomes)
|
||||||
│ │ ├── types.rs # user-facing Type enum + fk_target_type
|
│ ├── friendly/ # friendly-error layer + string catalog (strings/en-US.yaml) + keys
|
||||||
│ │ └── value.rs # Value/Bound + per-type validation
|
│ ├── input_render.rs # input-field render + ambient hint classification
|
||||||
│ ├── event.rs # AppEvent (input + DSL outcomes)
|
│ ├── output_render.rs # output-panel render helpers (incl. relationship diagrams)
|
||||||
│ ├── lib.rs # module re-exports for tests
|
│ ├── logging.rs main.rs mode.rs runtime.rs # tracing / entry / mode enum / Tokio loop
|
||||||
│ ├── logging.rs # tracing setup, file-backed
|
│ ├── persistence/ # csv + yaml + history IO + migrations
|
||||||
│ ├── main.rs # binary entry; thin
|
│ ├── project/ # open/create, lock, naming, prettifier
|
||||||
│ ├── mode.rs # Simple/Advanced mode enum
|
│ ├── seed/ # seed generators / heuristics / vocabulary (ADR-0048)
|
||||||
│ ├── runtime.rs # Tokio loop, terminal setup, dispatch
|
│ ├── snapshots/ # insta snapshots for Tier-2 tests
|
||||||
│ ├── snapshots/ # insta snapshots for Tier-2 tests
|
│ ├── theme.rs type_change.rs ui.rs undo.rs # themes / column type-change / render / undo ring
|
||||||
│ ├── theme.rs # light/dark themes
|
│ └── lib.rs # module re-exports for tests
|
||||||
│ └── ui.rs # ratatui rendering
|
├── tests/
|
||||||
└── tests/
|
│ ├── it/ # Tier-3 integration tests (consolidated into one binary)
|
||||||
└── walking_skeleton.rs # Tier-3 integration tests
|
│ └── typing_surface_matrix.rs # typing-surface matrix (separate Tier-3 target)
|
||||||
|
└── website/ # Astro + Starlight docs/marketing site (ADR-website-001)
|
||||||
|
├── src/ public/ casts-src/ # pages + assets + asciinema cast sources
|
||||||
|
└── astro.config.mjs package.json … # deploys to Cloudflare Pages via Gitea Actions
|
||||||
```
|
```
|
||||||
|
|
||||||
Key invariants in the code:
|
Key invariants in the code:
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# ADR-0054: Release versioning policy + version surfaces (`--version` / `version`)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted — **implemented 2026-06-16** (plan:
|
||||||
|
`docs/plans/20260616-public-availability.md`, step 1). First step on the
|
||||||
|
road to public availability. Adds a `--version` / `-V` CLI flag and an
|
||||||
|
in-app `version` command, both reporting `CARGO_PKG_VERSION`, plus a
|
||||||
|
release-CI guard that the `v*` tag equals that version. No prior issue or
|
||||||
|
`requirements.md` item existed for this — it was an untracked gap.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Before this, `Cargo.toml` carried `version = "0.1.0"`, the binary exposed
|
||||||
|
**no** way to report its version, and `release.yaml` named assets from the
|
||||||
|
**git tag** (`rdbms-playground-<tag>-<target>`) while building from
|
||||||
|
`Cargo.toml`. Tag and crate version were **decoupled**: tagging `v0.2.0`
|
||||||
|
would publish an asset named `…-v0.2.0-…` containing a binary that (had it
|
||||||
|
been able to say) reported `0.1.0`. On the way to public availability —
|
||||||
|
where users download a versioned artifact and file bug reports against "the
|
||||||
|
version I'm running" — that drift is a correctness problem.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **`Cargo.toml` `version` is the single source of truth.** This is the
|
||||||
|
idiomatic Rust position and avoids making `Cargo.toml` lie. The version
|
||||||
|
is read at compile time via `env!("CARGO_PKG_VERSION")`; no build-time
|
||||||
|
injection of the tag into the crate.
|
||||||
|
|
||||||
|
2. **Two user-facing surfaces, one source:**
|
||||||
|
- **`--version` / `-V`** — CLI flag (hand-rolled parser, mirrors
|
||||||
|
`--help`): prints and exits before any other work (`main.rs`).
|
||||||
|
- **`version`** — an in-app app command (REGISTRY node `app::VERSION`,
|
||||||
|
`AppCommand::Version`), emitting the same line into the output panel.
|
||||||
|
Both go through `cli::version_text()` → the catalog key
|
||||||
|
`cli.version_line` (`"rdbms-playground {version}"`), so there is exactly
|
||||||
|
one rendered string and one version source.
|
||||||
|
|
||||||
|
3. **Release-CI discipline.** `release.yaml`'s pre-build `test` job gains a
|
||||||
|
**version guard**: it parses `CARGO_PKG_VERSION` from `cargo metadata`
|
||||||
|
and **fails the release** unless the pushed tag equals `v<that version>`.
|
||||||
|
So `--version`, the release name, and the downloaded asset are always in
|
||||||
|
lockstep — enforced by the machine, not by memory.
|
||||||
|
|
||||||
|
4. **The release ritual:** bump `Cargo.toml` → commit → tag `v<that
|
||||||
|
number>` → push the tag. The guard rejects any deviation.
|
||||||
|
|
||||||
|
### Rejected / deferred
|
||||||
|
- **Inject the tag into the build** (tag as source of truth): fiddly with
|
||||||
|
cargo and makes `Cargo.toml` a placeholder/lie. Rejected.
|
||||||
|
- **Git-hash + build-date enrichment** (a `build.rs` so dev builds read
|
||||||
|
`0.2.0 (a1b2c3d)`): useful for bug reports, but not needed for the
|
||||||
|
tag↔release↔`--version` consistency this ADR is about. Deferred; can be
|
||||||
|
added behind the same `version_text()` seam without changing the policy.
|
||||||
|
- **UI placement beyond the `version` command** (status-bar string, etc.):
|
||||||
|
the command + `help` listing is enough for now (user decision).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A release can no longer ship a binary whose self-reported version
|
||||||
|
disagrees with its tag/filename.
|
||||||
|
- Cutting a release now *requires* a `Cargo.toml` bump commit — a small,
|
||||||
|
deliberate step (and a natural place to update a changelog later).
|
||||||
|
- New keys: `cli.version_line` (+ `help.app.version`, `parse.usage.version`,
|
||||||
|
`hint.cmd.version.what`/`.example`); a new REGISTRY command means the
|
||||||
|
comprehensiveness coverage test now also requires `hint.cmd.version`,
|
||||||
|
which is supplied. Tested: CLI flag parse (`--version`/`-V`/default-off),
|
||||||
|
`version_text()` carries `CARGO_PKG_VERSION`, the in-app command parses to
|
||||||
|
`AppCommand::Version` and emits the version.
|
||||||
|
- This is step 1 of `docs/plans/20260616-public-availability.md`; the
|
||||||
|
installer (`install.sh`) and package-manager work (D3) build on top.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# ADR-0055: `curl | sh` install script (`scripts/install.sh`)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted — **implemented 2026-06-17** (plan:
|
||||||
|
`docs/plans/20260616-public-availability.md`, step 2). Step 2 on the road
|
||||||
|
to public availability, building on ADR-0054 (versioned releases) and
|
||||||
|
ADR-ci-001/003 (the Gitea releases it downloads from). Tracked by the
|
||||||
|
plan + this ADR (no Gitea issue — user decision, 2026-06-17).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Until now, installing meant: find the releases page, work out which of
|
||||||
|
the six assets matches your machine, download it, `chmod +x`, move it
|
||||||
|
onto `PATH`, and (on macOS) wonder about Gatekeeper. That is too much
|
||||||
|
friction for a teaching tool aimed at beginners. The Gitea releases are
|
||||||
|
**publicly downloadable** (confirmed), with deterministic asset names
|
||||||
|
(`rdbms-playground-<tag>-<target>[.exe]`) and `.sha256` sidecars
|
||||||
|
(ADR-ci-003), and a `releases/latest` API — enough to script a one-liner
|
||||||
|
install.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Ship **`scripts/install.sh`**, run as
|
||||||
|
`curl -fsSL <gitea-raw>/scripts/install.sh | sh`:
|
||||||
|
|
||||||
|
- **POSIX `sh`** (no bashisms) — it runs under the `sh` of `curl | sh`;
|
||||||
|
kept **shellcheck-clean** (`-s sh`).
|
||||||
|
- **Platform detection** from `uname` → target triple: Linux →
|
||||||
|
`<arch>-unknown-linux-musl` (the fully-static build — one universal
|
||||||
|
Linux artifact, no glibc/version coupling), macOS → `<arch>-apple-darwin`;
|
||||||
|
`x86_64`/`amd64` and `aarch64`/`arm64` both map. **Windows is rejected**
|
||||||
|
with a pointer to Scoop/winget/the releases page (the binary is a `.exe`,
|
||||||
|
not a `curl|sh` target).
|
||||||
|
- **Version:** the `releases/latest` API tag by default; `RDBMS_VERSION`
|
||||||
|
pins a specific tag.
|
||||||
|
- **Integrity:** always download the `.sha256` sidecar and **verify**
|
||||||
|
(`sha256sum`/`shasum -a 256`); a mismatch aborts the install. HTTPS only.
|
||||||
|
- **Install location:** `~/.local/bin` by default (user-writable, no
|
||||||
|
sudo), overridable via `RDBMS_INSTALL_DIR`; prints a PATH hint if the
|
||||||
|
dir isn't on `PATH`.
|
||||||
|
- **macOS note:** a `curl` download is **not** Gatekeeper-quarantined, so
|
||||||
|
the binary runs as-is even while it is only ad-hoc-signed; proper
|
||||||
|
Developer-ID signing + notarization (for *browser* downloads) is a
|
||||||
|
separate, postponed task (see the plan's signing item).
|
||||||
|
- **Testing seams:** `RDBMS_OS`/`RDBMS_ARCH` force detection and
|
||||||
|
`--print-target` prints the resolved triple and exits — so the mapping
|
||||||
|
is checkable without a download.
|
||||||
|
|
||||||
|
### Rejected / deferred
|
||||||
|
- **Hosting the script on the website domain** (Cloudflare): nicer URL,
|
||||||
|
but adds a moving part; the **Gitea repo raw URL** is simplest and the
|
||||||
|
binaries live there anyway (user decision). The website may later
|
||||||
|
*reference* the same command.
|
||||||
|
- **`install.ps1` (Windows):** deferred — Windows users go via Scoop /
|
||||||
|
winget (D3, §3).
|
||||||
|
- **Uploading `install.sh` as a release asset** for a stable link:
|
||||||
|
optional; the branch raw URL is fine for now.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A first-time user runs one line and gets a checksum-verified binary on
|
||||||
|
`PATH`. The website's install copy (website branch, separate agent) can
|
||||||
|
point at this command.
|
||||||
|
- **Verified end-to-end** (2026-06-17) against the live public `v0.1.0`:
|
||||||
|
all four Linux/macOS platform mappings + Windows/unknown-arch rejection;
|
||||||
|
pinned and latest paths; checksum verification incl. a tamper-rejection
|
||||||
|
check; install + run on Linux x86_64. (The installed `v0.1.0` predates
|
||||||
|
`--version`, ADR-0054 — a non-issue, and the reason to cut a new
|
||||||
|
release.)
|
||||||
|
- **No automated regression guard in CI yet:** shellcheck isn't in the
|
||||||
|
flake, and there's no shell-test harness here (no bats). Recommended
|
||||||
|
follow-up: add a `shellcheck scripts/*.sh` gate (touches ADR-ci-002 —
|
||||||
|
needs shellcheck in the devShell). For now the guard is local
|
||||||
|
shellcheck + the documented end-to-end verification.
|
||||||
@@ -66,3 +66,5 @@ This directory contains the project's ADRs, recorded per
|
|||||||
- [ADR-0051 — Bottom keybinding strip: context- and state-aware](0051-context-state-aware-keybinding-strip.md) — **Accepted + implemented 2026-06-13 (issue #27)**, closes Gitea **#27**. Repurposes the bottom status line into a **keystrokes-only, state-selected** strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure `status_bar_bindings(app) -> Vec<(key,label)>` chooses the strip by **priority, first match wins**: (1) **sidebar focus** → `Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input`; (2) **completion memo live** (`last_completion`) → `Tab/Shift-Tab cycle · Esc cancel · Enter run`; (3) **history navigation** (new `App::is_browsing_history()` exposing the private `history_cursor`) → `↑↓ browse · Esc clear · Enter run`; (4) **editing** (input non-empty) → `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) **default** (empty) → `Ctrl-O sidebar · Tab complete · ↑ history · Enter run`. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. **Typed-command words leave the strip** (`mode advanced`/`mode simple` switch, `:` one-shot) and **mode discovery moves to the empty-input hint** (`resolve_hint_lines`), **simple mode only**: `\`mode advanced\` for SQL` (the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there and `help` covers the way back). The one-shot's old `Backspace cancel one-shot` label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **`Ctrl-C quit` omitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 new `shortcut.*` labels + the `panel.hint_mode_advanced` string added to `en-US.yaml`+`keys.rs` (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (`status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). **2467 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget)
|
- [ADR-0051 — Bottom keybinding strip: context- and state-aware](0051-context-state-aware-keybinding-strip.md) — **Accepted + implemented 2026-06-13 (issue #27)**, closes Gitea **#27**. Repurposes the bottom status line into a **keystrokes-only, state-selected** strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure `status_bar_bindings(app) -> Vec<(key,label)>` chooses the strip by **priority, first match wins**: (1) **sidebar focus** → `Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input`; (2) **completion memo live** (`last_completion`) → `Tab/Shift-Tab cycle · Esc cancel · Enter run`; (3) **history navigation** (new `App::is_browsing_history()` exposing the private `history_cursor`) → `↑↓ browse · Esc clear · Enter run`; (4) **editing** (input non-empty) → `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) **default** (empty) → `Ctrl-O sidebar · Tab complete · ↑ history · Enter run`. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. **Typed-command words leave the strip** (`mode advanced`/`mode simple` switch, `:` one-shot) and **mode discovery moves to the empty-input hint** (`resolve_hint_lines`), **simple mode only**: `\`mode advanced\` for SQL` (the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there and `help` covers the way back). The one-shot's old `Backspace cancel one-shot` label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **`Ctrl-C quit` omitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 new `shortcut.*` labels + the `panel.hint_mode_advanced` string added to `en-US.yaml`+`keys.rs` (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (`status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). **2467 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget)
|
||||||
- [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec<String>`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). **Follow-up done 2026-06-14:** the vestigial worker `source` plumbing was fully unwound (compiler-guided, no behaviour change) — `_source` removed from `finalize_persistence`/`do_rebuild_from_text`, the three `*_request` wrappers inlined+deleted, the dead `source` param dropped from the ~30 forwarding worker handlers, and the `source` field removed from the `DescribeTable`/`QueryData`/`RunSelect` requests + their `DatabaseHandle` methods (~164 mostly-test call sites); the only worker `source` left is the snapshot/undo label (see ADR-0052 *Consequences*)
|
- [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec<String>`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). **Follow-up done 2026-06-14:** the vestigial worker `source` plumbing was fully unwound (compiler-guided, no behaviour change) — `_source` removed from `finalize_persistence`/`do_rebuild_from_text`, the three `*_request` wrappers inlined+deleted, the dead `source` param dropped from the ~30 forwarding worker handlers, and the `source` field removed from the `DescribeTable`/`QueryData`/`RunSelect` requests + their `DatabaseHandle` methods (~164 mostly-test call sites); the only worker `source` left is the snapshot/undo label (see ADR-0052 *Consequences*)
|
||||||
- [ADR-0053 — Contextual `hint` command and keybinding](0053-contextual-hint-command-and-keybinding.md) — **Accepted, implemented 2026-06-15** (Phases A–D; closes **A1** + requirements **H2**). Settles the `hint` slot ADR-0003 left "ADR pending"; closes the last open piece of **A1** and tracks requirements **H2**. **Two surfaces:** an **F1 keybinding** that renders a deep hint for the *live* partial input without submitting (the primary path — a submitted `hint` command can't see the buffer it would help with, since Enter empties it), and a submitted **`hint` command** that expands on the *most recent error*. **No topic argument** (contextual only — `help <topic>` already owns explicit reference). Introduces a **tier-3 teaching layer**, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error `hint:`, which is shown **by default** since `Verbosity::Verbose` is the default — `messages short` is the opt-*out*); without it `hint` would just duplicate what's already on screen. Tier-3 content lives in the catalogue under `hint.cmd.<hint_id>` (per command form) and `hint.err.<class>` (per error/diagnostic class), each a structured `what`/`example`/`concept` block rendered via a new `note_hint*` family with `OutputStyleClass::Hint`. **Keyed per-form via a new `hint_ids: &[&str]` field on `CommandNode` mirroring `usage_ids`** (revised in Phase B): a per-*node* key proved too coarse — `add`/`drop`/`show`/`create` are each one node spanning many forms, and a live-input hint for `add 1:n relationship` must be specific to relationships; `hint_key_for_input_in_mode` reuses `usage_key_for_input_in_mode`'s form-word disambiguation, and covers the advanced-SQL forms whose `usage_ids` are empty. Not keyed off `help_id` (it is `None` on the advanced-SQL nodes purely to dedup the `help` list; that parallel gap is issue **#36**). **Clause-concept hints** (`on delete` actions, constraint slots, `with pk`, cardinality) are a recorded **deferred extension** (`hint.concept.<topic>`, issue **#37**) — per-form is the right tier-3 granularity, with position-awareness owned by tier-2 + the live `Next:` line. Runtime `translate_error` classes resolve via stored `last_error_hint_key` (`hint` command / empty-F1). (The second route — pre-submit `diagnostic.*` read live from the walker on the F1 path — is **deferred**, issue **#38**: `Diagnostic` carries no class key.) Adds `AppCommand::Hint`, a `HINT` grammar node + REGISTRY entry, the `hint_ids` field, and `last_error_hint_key`; F1 is a read-only overlay (buffer + completion memo untouched). **Content is the bulk of the work** (the mechanism is ~a day): v1 scope = ~37 command forms + 9 runtime error classes (comprehensive for those, ~57 blocks), authored **exemplars-first** (voice approved in this ADR's `/runda` review, then mass-authored in batches), enforced by a **comprehensiveness coverage test**, with graceful fall-back to tier-2 if a key is ever missing. The **pre-submit-diagnostic route + ~33 `diagnostic.*` blocks were deferred** (issue **#38**) — `Diagnostic` carries no class key, so the route needs a broad change for marginal value (tier-2 already surfaces diagnostics; many duplicate runtime classes). Forks user-chosen: two-surface model; **F1** (vs `?` / a chord); no-arg; comprehensive-for-commands-and-errors scope; exemplars-first; diagnostics deferred. OOS: per-topic `hint <topic>` (rejected — overlaps `help`); always-on tier-3 (rejected — keeps ambient terse); non-`en-US` locales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); the `help`-side advanced-SQL gap (issue #36)
|
- [ADR-0053 — Contextual `hint` command and keybinding](0053-contextual-hint-command-and-keybinding.md) — **Accepted, implemented 2026-06-15** (Phases A–D; closes **A1** + requirements **H2**). Settles the `hint` slot ADR-0003 left "ADR pending"; closes the last open piece of **A1** and tracks requirements **H2**. **Two surfaces:** an **F1 keybinding** that renders a deep hint for the *live* partial input without submitting (the primary path — a submitted `hint` command can't see the buffer it would help with, since Enter empties it), and a submitted **`hint` command** that expands on the *most recent error*. **No topic argument** (contextual only — `help <topic>` already owns explicit reference). Introduces a **tier-3 teaching layer**, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error `hint:`, which is shown **by default** since `Verbosity::Verbose` is the default — `messages short` is the opt-*out*); without it `hint` would just duplicate what's already on screen. Tier-3 content lives in the catalogue under `hint.cmd.<hint_id>` (per command form) and `hint.err.<class>` (per error/diagnostic class), each a structured `what`/`example`/`concept` block rendered via a new `note_hint*` family with `OutputStyleClass::Hint`. **Keyed per-form via a new `hint_ids: &[&str]` field on `CommandNode` mirroring `usage_ids`** (revised in Phase B): a per-*node* key proved too coarse — `add`/`drop`/`show`/`create` are each one node spanning many forms, and a live-input hint for `add 1:n relationship` must be specific to relationships; `hint_key_for_input_in_mode` reuses `usage_key_for_input_in_mode`'s form-word disambiguation, and covers the advanced-SQL forms whose `usage_ids` are empty. Not keyed off `help_id` (it is `None` on the advanced-SQL nodes purely to dedup the `help` list; that parallel gap is issue **#36**). **Clause-concept hints** (`on delete` actions, constraint slots, `with pk`, cardinality) are a recorded **deferred extension** (`hint.concept.<topic>`, issue **#37**) — per-form is the right tier-3 granularity, with position-awareness owned by tier-2 + the live `Next:` line. Runtime `translate_error` classes resolve via stored `last_error_hint_key` (`hint` command / empty-F1). (The second route — pre-submit `diagnostic.*` read live from the walker on the F1 path — is **deferred**, issue **#38**: `Diagnostic` carries no class key.) Adds `AppCommand::Hint`, a `HINT` grammar node + REGISTRY entry, the `hint_ids` field, and `last_error_hint_key`; F1 is a read-only overlay (buffer + completion memo untouched). **Content is the bulk of the work** (the mechanism is ~a day): v1 scope = ~37 command forms + 9 runtime error classes (comprehensive for those, ~57 blocks), authored **exemplars-first** (voice approved in this ADR's `/runda` review, then mass-authored in batches), enforced by a **comprehensiveness coverage test**, with graceful fall-back to tier-2 if a key is ever missing. The **pre-submit-diagnostic route + ~33 `diagnostic.*` blocks were deferred** (issue **#38**) — `Diagnostic` carries no class key, so the route needs a broad change for marginal value (tier-2 already surfaces diagnostics; many duplicate runtime classes). Forks user-chosen: two-surface model; **F1** (vs `?` / a chord); no-arg; comprehensive-for-commands-and-errors scope; exemplars-first; diagnostics deferred. OOS: per-topic `hint <topic>` (rejected — overlaps `help`); always-on tier-3 (rejected — keeps ambient terse); non-`en-US` locales + success-command teaching (deferred); clause-concept hints (issue #37); the diagnostic route (issue #38); the `help`-side advanced-SQL gap (issue #36)
|
||||||
|
- [ADR-0054 — Release versioning policy + version surfaces (`--version` / `version`)](0054-release-versioning-and-version-surfaces.md) — **Accepted + implemented 2026-06-16** (plan: `docs/plans/20260616-public-availability.md`, step 1 on the road to public availability; no prior issue/`requirements.md` item — an untracked gap). Fixes the **tag↔crate-version decoupling**: `Cargo.toml` built `0.1.0` while `release.yaml` named assets from the git tag, so a binary could report a version different from the asset it shipped in. **Decision:** `Cargo.toml` `version` is the **single source of truth** (read via `env!("CARGO_PKG_VERSION")`, no tag-injection); two surfaces report it through one `cli::version_text()` → catalog `cli.version_line` — a **`--version` / `-V`** CLI flag (mirrors `--help`, prints+exits in `main.rs`) and an in-app **`version`** command (REGISTRY node `app::VERSION`, `AppCommand::Version`, emits via `note_system`); and a **release-CI version guard** (`release.yaml` `test` job parses `cargo metadata` and **fails the release** unless the `v*` tag equals `v<CARGO_PKG_VERSION>`). Release ritual: bump `Cargo.toml` → commit → tag → push. New keys `cli.version_line` + `help.app.version` + `parse.usage.version` + `hint.cmd.version.{what,example}` (the new REGISTRY command pulls in the comprehensiveness coverage gate). Rejected: tag-as-source (makes Cargo.toml lie). Deferred: git-hash/build-date enrichment (behind the same `version_text()` seam); UI placement beyond the command. Tested test-first: CLI parse (`--version`/`-V`/default-off), `version_text()` carries `CARGO_PKG_VERSION`, the in-app command parses + emits. Also corrected a stale `release.yaml` header comment ("macOS is deferred" → built by the dispatched `release-macos.yaml`).
|
||||||
|
- [ADR-0055 — `curl | sh` install script (`scripts/install.sh`)](0055-curl-sh-install-script.md) — **Accepted + implemented 2026-06-17** (plan: `docs/plans/20260616-public-availability.md`, step 2; tracked by plan + ADR, no Gitea issue — user decision). A one-line installer (`curl -fsSL <gitea-raw>/scripts/install.sh | sh`) so beginners don't hand-pick an asset + `chmod +x`. **POSIX `sh`** (shellcheck-clean), detects `uname` OS/arch → target triple (**Linux → the fully-static `*-musl`** build, macOS → `*-apple-darwin`; `amd64`/`arm64` aliased; **Windows rejected** → Scoop/winget/releases page), resolves the version from the **`releases/latest`** API (or `RDBMS_VERSION` to pin), downloads the asset **and its `.sha256` and verifies it** (mismatch aborts), installs to `~/.local/bin` (`RDBMS_INSTALL_DIR` override) with a PATH hint. Testing seams: `RDBMS_OS`/`RDBMS_ARCH` + `--print-target`. macOS note: `curl` downloads aren't Gatekeeper-quarantined so the ad-hoc binary runs as-is (Developer-ID + notarization is the postponed signing task). **Verified end-to-end against the live public `v0.1.0`** (all platform mappings, pinned + latest, checksum incl. tamper-rejection, install + run). Rejected: website-domain hosting (extra moving part; Gitea raw is simplest); deferred: `install.ps1`, uploading the script as a release asset, and a **shellcheck CI gate** (shellcheck isn't in the flake — touches ADR-ci-002).
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ This ADR records the **cross-platform build strategy**; it sits on top of
|
|||||||
**ADR-ci-002** (the nix flake, which now carries the cross toolchain) and
|
**ADR-ci-002** (the nix flake, which now carries the cross toolchain) and
|
||||||
**ADR-ci-001** (the pipeline, whose release job this fills in).
|
**ADR-ci-001** (the pipeline, whose release job this fills in).
|
||||||
|
|
||||||
|
## Amendment 2 — 2026-06-16: CI on `main`; `release-macos` dispatched + verified
|
||||||
|
|
||||||
|
The CI branch is **merged to `main`**, so `release-macos.yaml`
|
||||||
|
(`workflow_dispatch`, Gitea-default-branch-only) is now triggerable. It
|
||||||
|
has been **dispatched and verified end-to-end**: both `*-apple-darwin`
|
||||||
|
targets build on the Tart runner, the de-nix/re-sign step runs, the
|
||||||
|
assets upload to the tagged release, and the binaries launch. macOS is
|
||||||
|
therefore runtime-verified (alongside the original Linux x86_64 +
|
||||||
|
Windows aarch64); only **Linux aarch64** and **Windows x86_64** remain
|
||||||
|
link-clean / valid-format without a runtime smoke-test.
|
||||||
|
|
||||||
## Amendment — 2026-06-14: macOS implemented (closes D1)
|
## Amendment — 2026-06-14: macOS implemented (closes D1)
|
||||||
|
|
||||||
macOS is no longer deferred. The two `*-apple-darwin` targets now build on a
|
macOS is no longer deferred. The two `*-apple-darwin` targets now build on a
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ here too).
|
|||||||
|
|
||||||
- [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:** the original release job was Linux x86_64 only; it's now the **four non-macOS D1 targets** (Linux + Windows × x86_64/aarch64) cross-built via cargo-zigbuild — see **ADR-ci-003**. macOS, D3 package-manager manifests, CI-speed dependency caching, and the website's static→Cloudflare deploy remain deferred, 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-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:** the original release job was Linux x86_64 only; it's now the **four non-macOS D1 targets** (Linux + Windows × x86_64/aarch64) cross-built via cargo-zigbuild — see **ADR-ci-003**. macOS, D3 package-manager manifests, CI-speed dependency caching, and the website's static→Cloudflare deploy remain deferred, 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).
|
- [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).
|
||||||
- [ADR-ci-003 — Cross-platform release builds (the D1 matrix)](20260613-adr-ci-003.md) — **Accepted 2026-06-13** (implemented + a real matrix release verified the same day — tag `v.0.0.0-citest3` published 8 assets). Cross-compiles the **four non-macOS D1 targets** from the Linux x86_64 runner with **`cargo-zigbuild`** (Zig's bundled clang + libc as one universal cross cc/linker, incl. rusqlite's bundled SQLite C; added to the flake devShell, replacing the single-target musl cc): **`x86_64`/`aarch64-unknown-linux-musl`** (musl + crt-static → fully static, **D2**) and **`x86_64-pc-windows-gnu`** / **`aarch64-pc-windows-gnullvm`** (Zig statically links libc → standalone `.exe`). **Windows `synchronization` stub:** Rust std links `-lsynchronization` (WaitOnAddress thread-parking), an import lib rust-overlay's toolchain doesn't ship and Zig's mingw lacks; the symbols are forwarded by `kernel32`, so an **empty 8-byte stub** `libsynchronization.a` (`ci/winstub/`, wired via `.cargo/config.toml` for the Windows targets only) satisfies the linker. **Workflow:** `release.yaml` = **`test` once (host) → `build` matrix** over the four targets (`needs: test`, `fail-fast: false`); each job packages binary (`.exe` for Windows) + `.sha256` and uploads to the **shared release** via idempotent create-or-get. **macOS** (2026-06-14 amendment) — built natively on a **Tart (Apple-Silicon) runner** (`runs-on: macos`), which makes the SDK fully licensed and dissolves the grey-area/public-image problem; `release-macos.yaml` is **dispatch-only** (intermittent runner; becomes triggerable once CI is on `main`), de-nixes the binary's libiconv load path (`install_name_tool` → `/usr/lib`) + re-signs ad-hoc, and uploads to the tagged release. **D1 complete (all six targets).** Alternatives rejected: `cross` (no macOS, needs DinD), per-target nix cross (Windows-aarch64 unpackaged, macOS-from-Linux unsupported), a real `libsynchronization.a` (more machinery, doesn't cover Windows-aarch64). Runtime-verified by the user (2026-06-13): Linux x86_64 + Windows aarch64 run correctly; Linux aarch64 + Windows x86_64 are the outstanding runtime checks. Builds on ADR-ci-002 (flake) and fills in ADR-ci-001 §3 (Release).
|
- [ADR-ci-003 — Cross-platform release builds (the D1 matrix)](20260613-adr-ci-003.md) — **Accepted 2026-06-13** (implemented + a real matrix release verified the same day — tag `v.0.0.0-citest3` published 8 assets). Cross-compiles the **four non-macOS D1 targets** from the Linux x86_64 runner with **`cargo-zigbuild`** (Zig's bundled clang + libc as one universal cross cc/linker, incl. rusqlite's bundled SQLite C; added to the flake devShell, replacing the single-target musl cc): **`x86_64`/`aarch64-unknown-linux-musl`** (musl + crt-static → fully static, **D2**) and **`x86_64-pc-windows-gnu`** / **`aarch64-pc-windows-gnullvm`** (Zig statically links libc → standalone `.exe`). **Windows `synchronization` stub:** Rust std links `-lsynchronization` (WaitOnAddress thread-parking), an import lib rust-overlay's toolchain doesn't ship and Zig's mingw lacks; the symbols are forwarded by `kernel32`, so an **empty 8-byte stub** `libsynchronization.a` (`ci/winstub/`, wired via `.cargo/config.toml` for the Windows targets only) satisfies the linker. **Workflow:** `release.yaml` = **`test` once (host) → `build` matrix** over the four targets (`needs: test`, `fail-fast: false`); each job packages binary (`.exe` for Windows) + `.sha256` and uploads to the **shared release** via idempotent create-or-get. **macOS** (2026-06-14 amendment) — built natively on a **Tart (Apple-Silicon) runner** (`runs-on: macos`), which makes the SDK fully licensed and dissolves the grey-area/public-image problem; `release-macos.yaml` is **dispatch-only** (intermittent runner), de-nixes the binary's libiconv load path (`install_name_tool` → `/usr/lib`) + re-signs ad-hoc, and uploads to the tagged release. **D1 complete (all six targets).** Alternatives rejected: `cross` (no macOS, needs DinD), per-target nix cross (Windows-aarch64 unpackaged, macOS-from-Linux unsupported), a real `libsynchronization.a` (more machinery, doesn't cover Windows-aarch64). **Amendment 2 (2026-06-16):** CI is **merged to `main`**, so `release-macos` is now triggerable (`workflow_dispatch` is default-branch-only) and has been **dispatched + verified end-to-end** (build → de-nix/re-sign → upload, binaries launch). Runtime-verified by the user: Linux x86_64, Windows aarch64, **and both macOS targets**; Linux aarch64 + Windows x86_64 are the outstanding runtime checks. Builds on ADR-ci-002 (flake) and fills in ADR-ci-001 §3 (Release).
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Session handoff — 2026-06-16 (73)
|
||||||
|
|
||||||
|
Short, focused handover. Continues from handoff-72 (which completed the
|
||||||
|
H2 hint-corpus verification pass). This session shipped one small
|
||||||
|
feature — a **`Ctrl-G` demo-mode alias for F1** — plus follow-on doc
|
||||||
|
hygiene. Commit `4016c3e`.
|
||||||
|
|
||||||
|
## §1. State
|
||||||
|
|
||||||
|
**Branch:** `main`, clean, all committed (local; **push pending** — your
|
||||||
|
step; the backlog now spans the CI merge, H2, the hint-corpus fixes,
|
||||||
|
handoffs 71/72/73, and this Ctrl-G commit). **2503 pass / 0 fail / 1
|
||||||
|
ignored** (the long-standing `friendly` doctest), **clippy clean**
|
||||||
|
(nursery, all targets). Open Gitea issues unchanged: **#35–#38**.
|
||||||
|
|
||||||
|
## §2. Why Ctrl-G (the problem)
|
||||||
|
|
||||||
|
Casts are recorded with **`autocast`** (ADR-0047 demo mode: `--demo` /
|
||||||
|
`RDBMS_PLAYGROUND_DEMO`). The contextual hint overlay (ADR-0053 / H2)
|
||||||
|
opens on **F1** — but F1 reaches the app only as a terminal **escape
|
||||||
|
sequence** (`\eOP` / `\e[11~`), and **autocast cannot emit escape
|
||||||
|
sequences**. So the single most teaching-relevant overlay was
|
||||||
|
unreachable in recordings (and in presenter/teacher sessions, which also
|
||||||
|
run `--demo`). Same wall that pushed step-captions onto `Ctrl+]` (a
|
||||||
|
single control byte) rather than `Ctrl+!`.
|
||||||
|
|
||||||
|
### Chord choice — why Ctrl-G, why not Ctrl-1
|
||||||
|
|
||||||
|
The user's first instinct was `Ctrl-1` (mnemonic, near F1). **Not
|
||||||
|
possible:** in a legacy terminal `Ctrl`+digit has no control byte —
|
||||||
|
`Ctrl-1` arrives as a bare `1` (would type "1" into the buffer). The
|
||||||
|
kitty keyboard protocol *would* encode it, but only as an escape
|
||||||
|
sequence (the very thing autocast can't send), and this app deliberately
|
||||||
|
does **not** push `KeyboardEnhancementFlags` (`runtime.rs` does only
|
||||||
|
`enable_raw_mode` + `EnterAlternateScreen`). So the usable space is
|
||||||
|
exactly **`Ctrl`+letter** (single legacy control bytes). After excluding
|
||||||
|
taken chords (`Ctrl-C` quit, `Ctrl-O` nav, `Ctrl+]` caption,
|
||||||
|
`Ctrl-A/E/W/K/U` readline per ADR-0049), byte-collisions
|
||||||
|
(`Ctrl-H/I/J/M/[`), flow-control (`Ctrl-S/Q`), and likely-future
|
||||||
|
(`Ctrl-R/P/N/Y/L/V`), **`Ctrl-G`** is the clean survivor (BEL/"abort" in
|
||||||
|
line editors — nothing destructive to shadow).
|
||||||
|
|
||||||
|
## §3. What shipped (commit `4016c3e`)
|
||||||
|
|
||||||
|
ADR-0047 **Amendment 1**. **In demo mode only**, `Ctrl-G` aliases F1:
|
||||||
|
|
||||||
|
- Runs the *exact* F1 hint logic (`hint_key` guard in
|
||||||
|
`App::handle_key`, `src/app.rs` ~line 1226 — `key.code == F(1) ||
|
||||||
|
(self.demo_mode && Ctrl-G)`), so live-input → form hint, empty input →
|
||||||
|
recent-error / getting-started.
|
||||||
|
- **Badges as `[F1]`** (not `[CTRL-G]`): `demo_badge_label` maps
|
||||||
|
`Ctrl-G → Some("[F1]")` (consulted only in demo mode — the caller
|
||||||
|
gates). So a recorded cast is **visually identical to a real F1
|
||||||
|
press**.
|
||||||
|
- **Demo-gated:** the shipped keymap stays F1-only. Outside demo mode
|
||||||
|
`Ctrl-G` falls through to the inert catch-all (the `Char(c)` insert arm
|
||||||
|
excludes CONTROL, so no `g` is typed).
|
||||||
|
- The keybinding strip (ADR-0051) is **not** changed — F1 stays the
|
||||||
|
advertised key; `Ctrl-G` is a recorder aid and the badge already reads
|
||||||
|
`[F1]`.
|
||||||
|
|
||||||
|
**Tests (test-first, `src/app.rs` Tier-1):** `ctrl_g_in_demo_mode_-
|
||||||
|
aliases_f1_on_input`, `…_on_empty_input`, `ctrl_g_outside_demo_mode_-
|
||||||
|
is_inert`, plus a `Ctrl-G → [F1]` assertion added to
|
||||||
|
`demo_badge_label_maps_the_invisible_keys`. All confirmed red→green; the
|
||||||
|
"inert" test passed on the pre-change code, proving the demo-gate.
|
||||||
|
|
||||||
|
### Using it in a cast
|
||||||
|
With `--demo` active, send **`Ctrl-G`** in the autocast script wherever
|
||||||
|
you want the hint overlay to appear; the viewer sees the `[F1]` badge.
|
||||||
|
|
||||||
|
## §4. Doc hygiene done alongside (same commit)
|
||||||
|
|
||||||
|
- **ADR-0047 file:** removed two stray `</content>` / `</invoke>` lines
|
||||||
|
(tool-call residue accidentally committed when the ADR was authored).
|
||||||
|
- **CLAUDE.md "Things deliberately deferred":** dropped three **stale**
|
||||||
|
entries — **I1b** readline shortcuts (done, ADR-0049), **I3** tab
|
||||||
|
completion, and **I4** syntax highlighting (both done; requirements.md
|
||||||
|
even carries a 2026-06-07 reconciliation note that I3/I4 were
|
||||||
|
"shipped but marked not yet"). Only **I1** (multi-line input) remains —
|
||||||
|
it is genuinely still open (`[ ]` in requirements.md). *(This follows
|
||||||
|
the handoff-72 cleanup that removed the equally-stale m:n/C4 +
|
||||||
|
project-storage entries.)*
|
||||||
|
|
||||||
|
## §5. Open / next (unchanged from handoff-72 §5)
|
||||||
|
|
||||||
|
The hint corpus is trustworthy and the cast-F1 gap is closed. Roadmap:
|
||||||
|
|
||||||
|
1. **Push** (your step).
|
||||||
|
2. **#35 (cargo fmt gate)** — precondition (CI merged) is met; the user
|
||||||
|
wants it done once, before first publication. Needs a `rustfmt.toml`-
|
||||||
|
vs-defaults decision first; tree is ~1800 hunks dirty.
|
||||||
|
3. Other open `requirements.md` items: **I1** multi-line input, **I5/B3**
|
||||||
|
in-flight cancellation, **TT4** PTY tier-4 (unwired), **DOC1**/**E2**
|
||||||
|
user docs (partial), **TT5** Windows-execution + Tier-4-in-CI, **D3**
|
||||||
|
packaging manifests. Design-first (`[~]`): **V4** session journal,
|
||||||
|
**TU1** tutorial, **C3a**, **V3**.
|
||||||
|
4. Hint follow-ups if wanted: **#36** `help` advanced-SQL, **#37** hint
|
||||||
|
clause-concepts, **#38** hint diagnostic route.
|
||||||
|
|
||||||
|
## §6. How to take over
|
||||||
|
|
||||||
|
1. Read handoffs 71 → 72 → 73, `CLAUDE.md`, `docs/requirements.md`.
|
||||||
|
2. Confirm green: `cargo test` (**2503 / 1 ignored**) + `cargo clippy
|
||||||
|
--all-targets` (clean).
|
||||||
|
3. For demo-mode / casting, read **ADR-0047** (+ its Amendment 1); for
|
||||||
|
the hint overlay it aliases, **ADR-0053**.
|
||||||
|
4. Workflow unchanged: phased, test-first, `/runda` + DA before commits,
|
||||||
|
ADR amendment + README index-upkeep for decided-area changes, confirm
|
||||||
|
commit messages with the user.
|
||||||
|
5. Consider a `cargo sweep` at this milestone (`target/` grows; see
|
||||||
|
CLAUDE.md "Build hygiene").
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Plan — road to public availability (versioning + install + packaging)
|
||||||
|
|
||||||
|
Status: **planning** (2026-06-16). Captures the decisions taken in
|
||||||
|
discussion plus the open questions, so the work can proceed in steps.
|
||||||
|
The versioning piece is being implemented first and is recorded as a
|
||||||
|
decision in **ADR-0054**; the install-script and package-manager pieces
|
||||||
|
are roadmap items here until each is built.
|
||||||
|
|
||||||
|
Scope note: this repo lives on the self-hosted Gitea at
|
||||||
|
`git.lazyeval.net` (`oli/rdbms-playground`); releases publish there
|
||||||
|
(ADR-ci-001/003). Publication branding uses the company name **Lazy
|
||||||
|
Evaluation Ltd → `lazyeval`**, not the personal `oli` username, for
|
||||||
|
anything user-facing (taps, buckets).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Versioning + version surfaces — DECIDED (→ ADR-0054, building now)
|
||||||
|
|
||||||
|
- **`Cargo.toml` `version` is the single source of truth.** `--version`
|
||||||
|
reads `env!("CARGO_PKG_VERSION")` (compile-time, no deps).
|
||||||
|
- **Two surfaces:** a `--version` / `-V` CLI flag (prints + exits, like
|
||||||
|
`--help`), and an in-app **`version`** app command.
|
||||||
|
- **CI discipline:** `release.yaml` gains a guard that asserts the pushed
|
||||||
|
tag `vX.Y.Z` equals `CARGO_PKG_VERSION`, and **fails the release on
|
||||||
|
mismatch** — so the binary's self-reported version, the release name,
|
||||||
|
and the downloaded asset always agree.
|
||||||
|
- **Release ritual:** bump `Cargo.toml` → commit → tag `v<that number>`
|
||||||
|
→ push tag. (The guard enforces it.)
|
||||||
|
- Optional enrichment (not decided): a `build.rs` baking a git short-hash
|
||||||
|
+ build date so non-tagged dev builds read `0.2.0 (a1b2c3d)`. Good for
|
||||||
|
bug reports; can be added later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `install.sh` (curl | sh) — DONE 2026-06-17 (ADR-0055)
|
||||||
|
|
||||||
|
**Shipped** `scripts/install.sh` (POSIX sh, shellcheck-clean). Verified
|
||||||
|
end-to-end against the live public `v0.1.0` release: platform mappings
|
||||||
|
(Linux/macOS × x86_64/aarch64; Windows + unknown arch error cleanly),
|
||||||
|
pinned (`RDBMS_VERSION`) and latest (`releases/latest`) paths, SHA-256
|
||||||
|
verification (incl. a tamper-rejection check), install to
|
||||||
|
`~/.local/bin`, PATH hint. **`install.ps1` (Windows) deferred** — Windows
|
||||||
|
users go via Scoop/winget (§3). The website copy that references the
|
||||||
|
`curl` command is the **website branch's** job (separate agent), later.
|
||||||
|
A **shellcheck CI gate** for `scripts/` is a recommended follow-up (not
|
||||||
|
added — shellcheck isn't in the flake yet; touches ADR-ci-002).
|
||||||
|
|
||||||
|
Original decided shape (for reference):
|
||||||
|
|
||||||
|
- **Hosted from the Gitea repo URL** on `git.lazyeval.net` (simplest):
|
||||||
|
`curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh`
|
||||||
|
(exact raw-URL form to confirm against the Gitea version).
|
||||||
|
- **Behaviour:** POSIX `sh`; detect `uname` OS+arch → target triple
|
||||||
|
(Linux → the **musl static** build, our universal Linux artifact);
|
||||||
|
query the latest release via the Gitea API
|
||||||
|
(`/repos/oli/rdbms-playground/releases/latest`) → tag → deterministic
|
||||||
|
asset name `rdbms-playground-<tag>-<target>`; download + **verify the
|
||||||
|
`.sha256`**; install to `~/.local/bin` (fallback `/usr/local/bin` with
|
||||||
|
a sudo prompt); `chmod +x`; print a PATH hint if needed.
|
||||||
|
- **macOS:** binaries are signed (see §4 signing note); a `curl`
|
||||||
|
download does **not** apply the Gatekeeper quarantine xattr, so the
|
||||||
|
installed binary runs without `xattr` faff.
|
||||||
|
- **Windows:** not `curl | sh` — provide a PowerShell `install.ps1`
|
||||||
|
(`irm … | iex`) and/or steer to Scoop/winget (§3).
|
||||||
|
- **Security posture:** HTTPS only; in-script checksum verification;
|
||||||
|
document the download-inspect-run alternative (`curl|sh` is a trust
|
||||||
|
tradeoff).
|
||||||
|
- **Deliverables we own now:** `scripts/install.sh` (+ later
|
||||||
|
`install.ps1`); confirm releases are **publicly downloadable**; decide
|
||||||
|
whether to also upload `install.sh` as a release asset for a stable
|
||||||
|
link. Website copy referencing the command is the **website branch's**
|
||||||
|
job (separate agent), later.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. D3 — package managers (roadmap; each layers on the release assets)
|
||||||
|
|
||||||
|
Common thread: a manifest pointing at our checksummed assets + a
|
||||||
|
per-release step to bump it. Ordered cheapest → most gatekept.
|
||||||
|
|
||||||
|
### 3a. `cargo binstall`
|
||||||
|
- **Bootstrapping matters (user-flagged):** `binstall` is **not** a
|
||||||
|
built-in cargo subcommand — users must install **`cargo-binstall`**
|
||||||
|
first (its own `curl|sh`/PowerShell installer, or
|
||||||
|
`cargo install cargo-binstall`). **Our instructions must say this.**
|
||||||
|
- Add `[package.metadata.binstall]` to `Cargo.toml` (pkg-url template →
|
||||||
|
our Gitea release assets; our naming already fits).
|
||||||
|
- **DECIDED (2026-06-17): publish to crates.io** — so the frictionless
|
||||||
|
`cargo binstall rdbms-playground` resolves the crate, and the project
|
||||||
|
is discoverable there. (A crates.io publish is its own small task:
|
||||||
|
metadata completeness — description/license/repository/keywords/readme
|
||||||
|
— and `cargo publish`; the `[package.metadata.binstall]` URL template
|
||||||
|
points binstall at our Gitea release assets.) *(Verify current
|
||||||
|
cargo-binstall behaviour when wiring.)*
|
||||||
|
|
||||||
|
### 3b. Scoop (Windows)
|
||||||
|
- A **bucket** repo under `lazyeval` on Gitea with a JSON manifest
|
||||||
|
(`.exe` URL + hash + `autoupdate`). Release job commits the bump.
|
||||||
|
- Users: `scoop bucket add lazyeval <gitea-url>; scoop install rdbms-playground`.
|
||||||
|
|
||||||
|
### 3c. Homebrew (macOS/Linux)
|
||||||
|
- A **company-branded tap** — `lazyeval/homebrew-tap` (on Gitea) — with a
|
||||||
|
Ruby formula (per-arch `url` + `sha256`). Release job commits the bump.
|
||||||
|
- Users: `brew tap lazyeval/tap https://git.lazyeval.net/lazyeval/homebrew-tap`
|
||||||
|
then `brew install lazyeval/tap/rdbms-playground` (the explicit-URL tap
|
||||||
|
form, since the `user/repo` shorthand assumes GitHub).
|
||||||
|
- *"Notability bars"* = the acceptance criteria for the default
|
||||||
|
**homebrew-core** tap (must be sufficiently popular/maintained). Our
|
||||||
|
own `lazyeval` tap sidesteps that entirely — no review gate.
|
||||||
|
|
||||||
|
### 3d. winget (Windows)
|
||||||
|
- Manifests are YAML PR'd to `microsoft/winget-pkgs` (reviewed by MS).
|
||||||
|
- **`wingetcreate` is Windows-only** (.NET) — no good without a Windows
|
||||||
|
runner. **Automatic path to evaluate first: `komac`** — a
|
||||||
|
cross-platform (Rust) winget manifest creator/submitter that runs on
|
||||||
|
our **Linux** CI. *(Verify komac's current capabilities/auth model.)*
|
||||||
|
- **Fallback:** a manual YAML PR per release — acceptable given releases
|
||||||
|
are infrequent (user-confirmed).
|
||||||
|
|
||||||
|
### Cross-cutting (3a–3d)
|
||||||
|
- Two extra repos (tap + bucket) under `lazyeval`, with CI push
|
||||||
|
credentials — setup TBD (user: "we'll figure that out").
|
||||||
|
- **`cargo-dist`/"dist"** automates installers + Homebrew + CI, but is
|
||||||
|
**GitHub-Actions/Releases-centric**; on self-hosted Gitea it won't drop
|
||||||
|
in cleanly (installer-script generation might be reusable). Likely
|
||||||
|
hand-roll the manifests + a small "update on release" job instead.
|
||||||
|
*(Verify cargo-dist's current Gitea support before fully ruling out.)*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open decisions
|
||||||
|
|
||||||
|
1. **crates.io:** **RESOLVED 2026-06-17 — yes, publish.** (See §3a.)
|
||||||
|
2. **Tracking:** **RESOLVED 2026-06-17 — doc + ADR only, no Gitea
|
||||||
|
issues.**
|
||||||
|
3. **Release downloads public:** **CONFIRMED 2026-06-17** — the Gitea
|
||||||
|
releases are publicly downloadable (no auth); `install.sh` relies on
|
||||||
|
it and was verified against the live `v0.1.0`.
|
||||||
|
|
||||||
|
### Still open / postponed
|
||||||
|
|
||||||
|
- **macOS signing — CONFIRMED BUG (2026-06-16), POSTPONED by the user
|
||||||
|
(2026-06-17)** pending the correct signing ID. Details:
|
||||||
|
- `release-macos.yaml` does `codesign --force --sign -` (ad-hoc) and has
|
||||||
|
**no signing scaffolding at all** (no keychain import, no secrets) —
|
||||||
|
so a downloaded binary is *not* properly signed (user-verified).
|
||||||
|
- **The credential the user has is the wrong type:** `Apple Development:
|
||||||
|
Oliver Sturm (W687M898E4)` is a *development* cert (Gatekeeper won't
|
||||||
|
trust it for distribution). Distribution needs a **`Developer ID
|
||||||
|
Application`** cert (same format, different type). Signing under the
|
||||||
|
company name *"Lazy Evaluation Ltd"* would need an **Organization**
|
||||||
|
Apple Developer account; a personal account signs as "Oliver Sturm".
|
||||||
|
- **Notarization** (required with Developer ID for non-quarantined trust
|
||||||
|
on browser downloads): after signing, `xcrun notarytool submit`. Creds
|
||||||
|
= an **App Store Connect API key** (Issuer ID + Key ID + `.p8`,
|
||||||
|
recommended for CI) *or* Apple ID + app-specific password + Team ID.
|
||||||
|
A bare CLI binary can't be *stapled* (only bundles/dmg/pkg) — Gatekeeper
|
||||||
|
does an online check instead.
|
||||||
|
- **Urgency caveat:** the `curl|sh` path doesn't need any of this (curl
|
||||||
|
downloads aren't quarantined); signing matters for browser downloads
|
||||||
|
from the releases page. Fix when the right cert + creds exist; corrects
|
||||||
|
the ad-hoc docs once landed.
|
||||||
|
|
||||||
|
## Sequencing
|
||||||
|
|
||||||
|
1. ✅ **Version discipline** (ADR-0054) — `--version`/`-V` + `version`
|
||||||
|
command + CI tag-match guard + tests.
|
||||||
|
2. ✅ **`scripts/install.sh`** (ADR-0055) — built + verified against the
|
||||||
|
live public release.
|
||||||
|
3. **← next:** package managers, cheapest first: `cargo binstall`
|
||||||
|
(+ crates.io publish) + Scoop → Homebrew (`lazyeval` tap) → winget
|
||||||
|
(komac / manual). Two `lazyeval` repos (tap + bucket) + CI push creds
|
||||||
|
to set up.
|
||||||
|
4. **Cut a release at a new version** — bump `Cargo.toml` (0.1.0 →
|
||||||
|
0.1.1/0.2.0; the ADR-0054 guard checks the tag), tag, push; the four
|
||||||
|
Linux/Windows targets build immediately. (macOS leg awaits signing.)
|
||||||
@@ -70,8 +70,11 @@ since ADR-0027.)
|
|||||||
natively on a Tart Apple-Silicon runner via the dispatched
|
natively on a Tart Apple-Silicon runner via the dispatched
|
||||||
`release-macos.yaml`. All uploaded to the Gitea release with a
|
`release-macos.yaml`. All uploaded to the Gitea release with a
|
||||||
`.sha256` each. Decisions in `docs/ci/adr/` (ADR-ci-001/002/003).
|
`.sha256` each. Decisions in `docs/ci/adr/` (ADR-ci-001/002/003).
|
||||||
Runtime-verified by the user: Linux x86_64 + Windows aarch64; the
|
Runtime-verified by the user: Linux x86_64, Windows aarch64, and both
|
||||||
others are link-clean / valid format.)*
|
macOS targets (the `release-macos.yaml` dispatch — now triggerable
|
||||||
|
since CI is on `main` — was run end-to-end and the binaries launch);
|
||||||
|
Linux aarch64 + Windows x86_64 remain link-clean / valid-format
|
||||||
|
only.)*
|
||||||
- [x] **D2** Single static binary, no runtime dependencies.
|
- [x] **D2** Single static binary, no runtime dependencies.
|
||||||
*(Done 2026-06-15, per platform: **Linux** is fully static (musl +
|
*(Done 2026-06-15, per platform: **Linux** is fully static (musl +
|
||||||
`crt-static`); **Windows** is a standalone `.exe` (Zig statically
|
`crt-static`); **Windows** is a standalone `.exe` (Zig statically
|
||||||
@@ -888,7 +891,10 @@ since ADR-0027.)
|
|||||||
exists (~55 lines, covers the WHERE-expression and
|
exists (~55 lines, covers the WHERE-expression and
|
||||||
table-creation boundaries). **Missing:** a DSL
|
table-creation boundaries). **Missing:** a DSL
|
||||||
command-surface reference and a standalone type-system
|
command-surface reference and a standalone type-system
|
||||||
reference under `docs/`.)*
|
reference under `docs/`. **Note (2026-06-16):** the **canonical**
|
||||||
|
user docs now live on the **website** (ADR-website-001, deployed) —
|
||||||
|
it covers the full feature set; the in-repo `docs/` reference pieces
|
||||||
|
named here remain the outstanding part of DOC1.)*
|
||||||
|
|
||||||
## Testing (per ADR-0008)
|
## Testing (per ADR-0008)
|
||||||
|
|
||||||
|
|||||||
@@ -16,4 +16,4 @@ applies here too).
|
|||||||
|
|
||||||
## Index
|
## Index
|
||||||
|
|
||||||
- [ADR-website-001 — Public website and documentation site](20260604-adr-website-001.md) — **Accepted 2026-06-04** (formerly ADR-0044 in the main index; moved here 2026-06-10 to end recurring cross-branch number collisions). The first public website: a marketing landing page plus the **canonical** user docs. Stack **Astro 6 + Starlight + Tailwind v4** (chosen over SvelteKit + Tailwind for a docs-heavy + marketing site; interactive bits as Astro islands). Showcase demos are **asciinema** `.cast` recordings (scripted-input driver for paced, re-recordable sessions — *not* `history.log` replay), reused inline in docs. The **in-page WASM playground is deferred** (OOS: deferred) behind a stable `Demo` seam, with the portable-core vs native-edge boundary recorded for a future ADR + iteration plan. Portable **static build** (**Cloudflare** target via **Gitea Actions**, host-agnostic); **no CI yet**; **monorepo** (`website/`). Docs cover the **full supported feature set** with "planned" callouts for the unshipped minority; two wording rules bind user-facing copy — **no engine name** (continues ADR-0002) and **no "DSL"** ("simple mode" / "advanced mode"). Install docs cover **prebuilt binaries + package managers** (D1–D3 track the release tooling). Plan: [`docs/website/plans/20260604-website-implementation-plan.md`](../plans/20260604-website-implementation-plan.md)
|
- [ADR-website-001 — Public website and documentation site](20260604-adr-website-001.md) — **Accepted 2026-06-04** (formerly ADR-0044 in the main index; moved here 2026-06-10 to end recurring cross-branch number collisions). The first public website: a marketing landing page plus the **canonical** user docs. Stack **Astro 6 + Starlight + Tailwind v4** (chosen over SvelteKit + Tailwind for a docs-heavy + marketing site; interactive bits as Astro islands). Showcase demos are **asciinema** `.cast` recordings (scripted-input driver for paced, re-recordable sessions — *not* `history.log` replay), reused inline in docs. The **in-page WASM playground is deferred** (OOS: deferred) behind a stable `Demo` seam, with the portable-core vs native-edge boundary recorded for a future ADR + iteration plan. Portable **static build** (**Cloudflare** target via **Gitea Actions**, host-agnostic); **monorepo** (`website/`). **§4 update (CI implemented):** the static→Cloudflare Pages deploy now runs via Gitea Actions (`.gitea/workflows/website.yaml`; the crate gate is skipped for website-only changes); both `website` and `main` are merged and the site is **deployed**. Docs cover the **full supported feature set** with "planned" callouts for the unshipped minority; two wording rules bind user-facing copy — **no engine name** (continues ADR-0002) and **no "DSL"** ("simple mode" / "advanced mode"). Install docs cover **prebuilt binaries + package managers** (D1–D3 track the release tooling). Plan: [`docs/website/plans/20260604-website-implementation-plan.md`](../plans/20260604-website-implementation-plan.md)
|
||||||
|
|||||||
Executable
+166
@@ -0,0 +1,166 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# install.sh — download and install a prebuilt rdbms-playground binary.
|
||||||
|
#
|
||||||
|
# Quick start (Linux / macOS):
|
||||||
|
# curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh
|
||||||
|
#
|
||||||
|
# What it does: detects your OS + CPU, downloads the matching release binary
|
||||||
|
# from the Gitea releases (Linux uses the fully-static musl build), verifies
|
||||||
|
# its SHA-256 checksum, and installs it to ~/.local/bin.
|
||||||
|
#
|
||||||
|
# Environment overrides:
|
||||||
|
# RDBMS_VERSION install a specific tag (e.g. v0.2.0) instead of the latest
|
||||||
|
# RDBMS_INSTALL_DIR install directory (default: $HOME/.local/bin)
|
||||||
|
# RDBMS_OS force the OS (testing): Linux | Darwin
|
||||||
|
# RDBMS_ARCH force the CPU (testing): x86_64 | aarch64
|
||||||
|
#
|
||||||
|
# Flags:
|
||||||
|
# --print-target print the resolved target triple and exit (no download)
|
||||||
|
# -h, --help print this help and exit
|
||||||
|
#
|
||||||
|
# Notes:
|
||||||
|
# * Windows is not installable via this script (the binary is a .exe) —
|
||||||
|
# use Scoop/winget (planned) or download the .exe from the releases page.
|
||||||
|
# * macOS: a curl download is not quarantined by Gatekeeper, so the binary
|
||||||
|
# runs without extra steps. (Developer-ID signing + notarization is a
|
||||||
|
# separate, planned improvement for browser downloads.)
|
||||||
|
#
|
||||||
|
# POSIX sh — no bashisms, so it runs under the `sh` of `curl | sh`.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
REPO_BASE="https://git.lazyeval.net/oli/rdbms-playground"
|
||||||
|
API_BASE="https://git.lazyeval.net/api/v1/repos/oli/rdbms-playground"
|
||||||
|
BIN_NAME="rdbms-playground"
|
||||||
|
PRINT_TARGET=0
|
||||||
|
|
||||||
|
err() {
|
||||||
|
printf 'install: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
info() { printf '%s\n' "$1" >&2; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
# Lines 2..(first blank) of this file are the human-readable header.
|
||||||
|
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve the Rust target triple for the current (or forced) platform.
|
||||||
|
# Linux -> <arch>-unknown-linux-musl (the fully-static build)
|
||||||
|
# macOS -> <arch>-apple-darwin
|
||||||
|
detect_target() {
|
||||||
|
os="${RDBMS_OS:-$(uname -s)}"
|
||||||
|
arch="${RDBMS_ARCH:-$(uname -m)}"
|
||||||
|
case "$os" in
|
||||||
|
Linux | linux) os_part="unknown-linux-musl" ;;
|
||||||
|
Darwin | darwin | macos | macOS) os_part="apple-darwin" ;;
|
||||||
|
MINGW* | MSYS* | CYGWIN* | *Windows* | *windows*)
|
||||||
|
err "Windows is not supported by this installer — use Scoop/winget (planned) or download the .exe from $REPO_BASE/releases" ;;
|
||||||
|
*) err "unsupported operating system: $os" ;;
|
||||||
|
esac
|
||||||
|
case "$arch" in
|
||||||
|
x86_64 | amd64) arch_part="x86_64" ;;
|
||||||
|
aarch64 | arm64) arch_part="aarch64" ;;
|
||||||
|
*) err "unsupported CPU architecture: $arch" ;;
|
||||||
|
esac
|
||||||
|
printf '%s-%s' "$arch_part" "$os_part"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download $1 to file $2 (curl or wget).
|
||||||
|
download() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$1" -o "$2"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -qO "$2" "$1"
|
||||||
|
else
|
||||||
|
err "need either curl or wget on PATH"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch $1 to stdout (curl or wget).
|
||||||
|
fetch() {
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$1"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -qO- "$1"
|
||||||
|
else
|
||||||
|
err "need either curl or wget on PATH"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# The release tag to install: $RDBMS_VERSION if set, else the latest release.
|
||||||
|
resolve_version() {
|
||||||
|
if [ -n "${RDBMS_VERSION:-}" ]; then
|
||||||
|
printf '%s' "$RDBMS_VERSION"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
json=$(fetch "$API_BASE/releases/latest") ||
|
||||||
|
err "could not query the latest release from $API_BASE"
|
||||||
|
# Portable JSON scrape (no jq): the latest-release object carries exactly
|
||||||
|
# one "tag_name": "<tag>" field.
|
||||||
|
tag=$(printf '%s' "$json" |
|
||||||
|
grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' |
|
||||||
|
head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
||||||
|
[ -n "$tag" ] || err "could not parse the latest release tag"
|
||||||
|
printf '%s' "$tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify file $1 against sha256 sidecar $2 (format: "<hash> <name>").
|
||||||
|
verify_checksum() {
|
||||||
|
expected=$(awk '{print $1; exit}' "$2")
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
actual=$(sha256sum "$1" | awk '{print $1}')
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
actual=$(shasum -a 256 "$1" | awk '{print $1}')
|
||||||
|
else
|
||||||
|
info "warning: no sha256 tool found — skipping checksum verification"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
[ "$expected" = "$actual" ] ||
|
||||||
|
err "checksum mismatch (expected $expected, got $actual) — refusing to install"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--print-target) PRINT_TARGET=1 ;;
|
||||||
|
-h | --help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) err "unknown argument: $1 (try --help)" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
target=$(detect_target)
|
||||||
|
if [ "$PRINT_TARGET" = "1" ]; then
|
||||||
|
printf '%s\n' "$target"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=$(resolve_version)
|
||||||
|
asset="$BIN_NAME-$version-$target"
|
||||||
|
url="$REPO_BASE/releases/download/$version/$asset"
|
||||||
|
dir="${RDBMS_INSTALL_DIR:-$HOME/.local/bin}"
|
||||||
|
|
||||||
|
tmp=$(mktemp -d 2>/dev/null) || err "could not create a temporary directory"
|
||||||
|
trap 'rm -rf "$tmp"' EXIT INT TERM
|
||||||
|
|
||||||
|
info "downloading $asset ..."
|
||||||
|
download "$url" "$tmp/$BIN_NAME" || err "download failed: $url"
|
||||||
|
download "$url.sha256" "$tmp/$BIN_NAME.sha256" || err "checksum download failed: $url.sha256"
|
||||||
|
verify_checksum "$tmp/$BIN_NAME" "$tmp/$BIN_NAME.sha256"
|
||||||
|
|
||||||
|
mkdir -p "$dir" || err "could not create install directory: $dir"
|
||||||
|
chmod +x "$tmp/$BIN_NAME"
|
||||||
|
mv "$tmp/$BIN_NAME" "$dir/$BIN_NAME" || err "could not install to $dir"
|
||||||
|
info "installed $BIN_NAME $version -> $dir/$BIN_NAME"
|
||||||
|
|
||||||
|
case ":${PATH:-}:" in
|
||||||
|
*":$dir:"*) ;;
|
||||||
|
*) info "note: $dir is not on your PATH. Add it, e.g.: export PATH=\"$dir:\$PATH\"" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
+29
@@ -1868,6 +1868,12 @@ impl App {
|
|||||||
self.note_hint_for_recent_error();
|
self.note_hint_for_recent_error();
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
// ADR-0054: the in-app twin of `--version`. Reports the same
|
||||||
|
// single source of truth (`CARGO_PKG_VERSION`, via cli::version_text).
|
||||||
|
AppCommand::Version => {
|
||||||
|
self.note_system(crate::cli::version_text());
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
AppCommand::Rebuild => vec![Action::PrepareRebuild],
|
AppCommand::Rebuild => vec![Action::PrepareRebuild],
|
||||||
AppCommand::Save => self.handle_save_command(false),
|
AppCommand::Save => self.handle_save_command(false),
|
||||||
AppCommand::SaveAs => self.handle_save_command(true),
|
AppCommand::SaveAs => self.handle_save_command(true),
|
||||||
@@ -5737,6 +5743,29 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── ADR-0054: in-app `version` command ──────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_command_parses_to_app_version() {
|
||||||
|
use crate::dsl::{parse_command, AppCommand, Command};
|
||||||
|
assert!(matches!(
|
||||||
|
parse_command("version"),
|
||||||
|
Ok(Command::App(AppCommand::Version))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_command_emits_the_cargo_version() {
|
||||||
|
let mut app = App::new();
|
||||||
|
type_str(&mut app, "version");
|
||||||
|
submit(&mut app);
|
||||||
|
assert!(
|
||||||
|
output_contains(&app, env!("CARGO_PKG_VERSION")),
|
||||||
|
"in-app `version` should print CARGO_PKG_VERSION: {}",
|
||||||
|
error_lines(&app),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hint_command_with_no_recent_error_shows_getting_started() {
|
fn hint_command_with_no_recent_error_shows_getting_started() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
+49
@@ -30,6 +30,10 @@ pub struct Args {
|
|||||||
/// `--help` / `-h`: print usage to stdout and exit. The
|
/// `--help` / `-h`: print usage to stdout and exit. The
|
||||||
/// runtime checks this flag before doing any other work.
|
/// runtime checks this flag before doing any other work.
|
||||||
pub help: bool,
|
pub help: bool,
|
||||||
|
/// `--version` / `-V`: print the version (`CARGO_PKG_VERSION`,
|
||||||
|
/// the single source of truth — ADR-0054) and exit. Checked
|
||||||
|
/// alongside `--help` before any other work.
|
||||||
|
pub version: bool,
|
||||||
/// `--no-undo`: disable the auto-snapshot / undo machinery for
|
/// `--no-undo`: disable the auto-snapshot / undo machinery for
|
||||||
/// this run (ADR-0006 Amendment 1). When set, no snapshots are
|
/// this run (ADR-0006 Amendment 1). When set, no snapshots are
|
||||||
/// taken — zero per-command overhead — and `undo` / `redo`
|
/// taken — zero per-command overhead — and `undo` / `redo`
|
||||||
@@ -62,6 +66,17 @@ pub fn help_text() -> String {
|
|||||||
crate::t!("help.cli_banner")
|
crate::t!("help.cli_banner")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Version line printed by `--version` / `-V` and the in-app `version`
|
||||||
|
/// command (ADR-0054).
|
||||||
|
///
|
||||||
|
/// `CARGO_PKG_VERSION` is the single source of truth — it equals the `v*`
|
||||||
|
/// release tag (the release CI guards that), so what the binary reports
|
||||||
|
/// always matches the downloaded artifact.
|
||||||
|
#[must_use]
|
||||||
|
pub fn version_text() -> String {
|
||||||
|
crate::t!("cli.version_line", version = env!("CARGO_PKG_VERSION"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ArgsError {
|
pub enum ArgsError {
|
||||||
MissingValue(&'static str),
|
MissingValue(&'static str),
|
||||||
@@ -129,6 +144,7 @@ impl Args {
|
|||||||
let mut project_path: Option<PathBuf> = None;
|
let mut project_path: Option<PathBuf> = None;
|
||||||
let mut resume = false;
|
let mut resume = false;
|
||||||
let mut help = false;
|
let mut help = false;
|
||||||
|
let mut version = false;
|
||||||
let mut no_undo = false;
|
let mut no_undo = false;
|
||||||
let mut mode: Option<Mode> = None;
|
let mut mode: Option<Mode> = None;
|
||||||
// Demonstration mode (ADR-0047): the env var is the default,
|
// Demonstration mode (ADR-0047): the env var is the default,
|
||||||
@@ -143,6 +159,9 @@ impl Args {
|
|||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
help = true;
|
help = true;
|
||||||
}
|
}
|
||||||
|
"--version" | "-V" => {
|
||||||
|
version = true;
|
||||||
|
}
|
||||||
"--resume" => {
|
"--resume" => {
|
||||||
resume = true;
|
resume = true;
|
||||||
}
|
}
|
||||||
@@ -208,6 +227,7 @@ impl Args {
|
|||||||
project_path,
|
project_path,
|
||||||
resume,
|
resume,
|
||||||
help,
|
help,
|
||||||
|
version,
|
||||||
no_undo,
|
no_undo,
|
||||||
mode,
|
mode,
|
||||||
demo,
|
demo,
|
||||||
@@ -475,4 +495,33 @@ mod tests {
|
|||||||
let err = Args::parse(["--bogus", "/some/path"]).unwrap_err();
|
let err = Args::parse(["--bogus", "/some/path"]).unwrap_err();
|
||||||
assert!(matches!(&err, ArgsError::Unknown(s) if s == "--bogus"), "got: {err:?}");
|
assert!(matches!(&err, ArgsError::Unknown(s) if s == "--bogus"), "got: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- ADR-0054: --version / -V ----
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_long_flag_parses() {
|
||||||
|
assert!(Args::parse(["--version"]).unwrap().version);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_short_flag_parses() {
|
||||||
|
assert!(Args::parse(["-V"]).unwrap().version);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_defaults_off() {
|
||||||
|
assert!(!Args::parse(std::iter::empty::<&str>()).unwrap().version);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn version_text_carries_the_cargo_version() {
|
||||||
|
// The binary's self-reported version IS Cargo.toml's (the
|
||||||
|
// single source of truth, ADR-0054) — and the release CI guards
|
||||||
|
// that the `v*` tag equals it.
|
||||||
|
let text = version_text();
|
||||||
|
assert!(
|
||||||
|
text.contains(env!("CARGO_PKG_VERSION")),
|
||||||
|
"version line should embed CARGO_PKG_VERSION; got {text:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -557,6 +557,10 @@ pub enum AppCommand {
|
|||||||
/// (the buffer is empty post-submit). The live-input surface is
|
/// (the buffer is empty post-submit). The live-input surface is
|
||||||
/// the F1 keybinding, handled in `App::handle_key`, not here.
|
/// the F1 keybinding, handled in `App::handle_key`, not here.
|
||||||
Hint,
|
Hint,
|
||||||
|
/// Print the application version (ADR-0054): the in-app twin of the
|
||||||
|
/// `--version` / `-V` CLI flag. Emits `CARGO_PKG_VERSION` — the same
|
||||||
|
/// single source of truth — into the output panel.
|
||||||
|
Version,
|
||||||
/// Rebuild `playground.db` from `project.yaml` + data/, with
|
/// Rebuild `playground.db` from `project.yaml` + data/, with
|
||||||
/// confirmation modal.
|
/// confirmation modal.
|
||||||
Rebuild,
|
Rebuild,
|
||||||
@@ -1019,6 +1023,7 @@ impl Command {
|
|||||||
AppCommand::Quit => "quit",
|
AppCommand::Quit => "quit",
|
||||||
AppCommand::Help { .. } => "help",
|
AppCommand::Help { .. } => "help",
|
||||||
AppCommand::Hint => "hint",
|
AppCommand::Hint => "hint",
|
||||||
|
AppCommand::Version => "version",
|
||||||
AppCommand::Rebuild => "rebuild",
|
AppCommand::Rebuild => "rebuild",
|
||||||
AppCommand::Save => "save",
|
AppCommand::Save => "save",
|
||||||
AppCommand::SaveAs => "save as",
|
AppCommand::SaveAs => "save as",
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ const fn build_rebuild(_path: &MatchedPath, _source: &str) -> Result<Command, Va
|
|||||||
Ok(Command::App(AppCommand::Rebuild))
|
Ok(Command::App(AppCommand::Rebuild))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn build_version(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||||
|
Ok(Command::App(AppCommand::Version))
|
||||||
|
}
|
||||||
|
|
||||||
const fn build_undo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
const fn build_undo(_path: &MatchedPath, _source: &str) -> Result<Command, ValidationError> {
|
||||||
Ok(Command::App(AppCommand::Undo))
|
Ok(Command::App(AppCommand::Undo))
|
||||||
}
|
}
|
||||||
@@ -294,6 +298,14 @@ pub static REBUILD: CommandNode = CommandNode {
|
|||||||
hint_ids: &["rebuild"],
|
hint_ids: &["rebuild"],
|
||||||
usage_ids: &["parse.usage.rebuild"],};
|
usage_ids: &["parse.usage.rebuild"],};
|
||||||
|
|
||||||
|
pub static VERSION: CommandNode = CommandNode {
|
||||||
|
entry: Word::keyword("version"),
|
||||||
|
shape: EMPTY_SEQ,
|
||||||
|
ast_builder: build_version,
|
||||||
|
help_id: Some("app.version"),
|
||||||
|
hint_ids: &["version"],
|
||||||
|
usage_ids: &["parse.usage.version"],};
|
||||||
|
|
||||||
pub static SAVE: CommandNode = CommandNode {
|
pub static SAVE: CommandNode = CommandNode {
|
||||||
entry: Word::keyword("save"),
|
entry: Word::keyword("save"),
|
||||||
shape: SAVE_AS_OPT,
|
shape: SAVE_AS_OPT,
|
||||||
|
|||||||
@@ -791,6 +791,7 @@ pub static REGISTRY: &[(&CommandNode, CommandCategory)] = &[
|
|||||||
(&app::QUIT, CommandCategory::Simple),
|
(&app::QUIT, CommandCategory::Simple),
|
||||||
(&app::HELP, CommandCategory::Simple),
|
(&app::HELP, CommandCategory::Simple),
|
||||||
(&app::HINT, CommandCategory::Simple),
|
(&app::HINT, CommandCategory::Simple),
|
||||||
|
(&app::VERSION, CommandCategory::Simple),
|
||||||
(&app::REBUILD, CommandCategory::Simple),
|
(&app::REBUILD, CommandCategory::Simple),
|
||||||
(&app::SAVE, CommandCategory::Simple),
|
(&app::SAVE, CommandCategory::Simple),
|
||||||
(&app::NEW, CommandCategory::Simple),
|
(&app::NEW, CommandCategory::Simple),
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("help.app.quit", &[]),
|
("help.app.quit", &[]),
|
||||||
("help.app.help", &[]),
|
("help.app.help", &[]),
|
||||||
("help.app.hint", &[]),
|
("help.app.hint", &[]),
|
||||||
|
("help.app.version", &[]),
|
||||||
("help.app.rebuild", &[]),
|
("help.app.rebuild", &[]),
|
||||||
("help.app.save", &[]),
|
("help.app.save", &[]),
|
||||||
("help.app.new", &[]),
|
("help.app.new", &[]),
|
||||||
@@ -272,6 +273,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("hint.cmd.help.concept", &[]),
|
("hint.cmd.help.concept", &[]),
|
||||||
("hint.cmd.hint.what", &[]),
|
("hint.cmd.hint.what", &[]),
|
||||||
("hint.cmd.hint.example", &[]),
|
("hint.cmd.hint.example", &[]),
|
||||||
|
("hint.cmd.version.what", &[]),
|
||||||
|
("hint.cmd.version.example", &[]),
|
||||||
("hint.cmd.rebuild.what", &[]),
|
("hint.cmd.rebuild.what", &[]),
|
||||||
("hint.cmd.rebuild.example", &[]),
|
("hint.cmd.rebuild.example", &[]),
|
||||||
("hint.cmd.rebuild.concept", &[]),
|
("hint.cmd.rebuild.concept", &[]),
|
||||||
@@ -486,6 +489,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("parse.usage.mode", &[]),
|
("parse.usage.mode", &[]),
|
||||||
("parse.usage.new", &[]),
|
("parse.usage.new", &[]),
|
||||||
("parse.usage.quit", &[]),
|
("parse.usage.quit", &[]),
|
||||||
|
("parse.usage.version", &[]),
|
||||||
("parse.usage.rebuild", &[]),
|
("parse.usage.rebuild", &[]),
|
||||||
("parse.usage.redo", &[]),
|
("parse.usage.redo", &[]),
|
||||||
("parse.usage.replay", &[]),
|
("parse.usage.replay", &[]),
|
||||||
@@ -580,6 +584,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|||||||
("cli.multiple_paths", &["first", "second"]),
|
("cli.multiple_paths", &["first", "second"]),
|
||||||
("cli.resume_with_path", &[]),
|
("cli.resume_with_path", &[]),
|
||||||
("cli.unknown_argument", &["arg"]),
|
("cli.unknown_argument", &["arg"]),
|
||||||
|
("cli.version_line", &["version"]),
|
||||||
(
|
(
|
||||||
"archive.export_sequence_exhausted",
|
"archive.export_sequence_exhausted",
|
||||||
&["project", "target_dir", "limit"],
|
&["project", "target_dir", "limit"],
|
||||||
|
|||||||
@@ -162,6 +162,10 @@ error:
|
|||||||
# ---- Help text (CLI banner + in-app `help` command) ------------------
|
# ---- Help text (CLI banner + in-app `help` command) ------------------
|
||||||
# ---- CLI argument-parsing errors (stderr before TUI starts) ---------
|
# ---- CLI argument-parsing errors (stderr before TUI starts) ---------
|
||||||
cli:
|
cli:
|
||||||
|
# Version line for `--version` / `-V` and the in-app `version` command
|
||||||
|
# (ADR-0054). `{version}` is `CARGO_PKG_VERSION` — the single source of
|
||||||
|
# truth, equal to the `v*` release tag (release CI guards the match).
|
||||||
|
version_line: "rdbms-playground {version}"
|
||||||
missing_value: "missing value for --{flag}"
|
missing_value: "missing value for --{flag}"
|
||||||
invalid_value: "invalid value for --{flag}: {value} (expected one of: {expected})"
|
invalid_value: "invalid value for --{flag}: {value} (expected one of: {expected})"
|
||||||
unknown_argument: "unknown argument: {arg}"
|
unknown_argument: "unknown argument: {arg}"
|
||||||
@@ -186,6 +190,7 @@ help:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Print this help and exit.
|
-h, --help Print this help and exit.
|
||||||
|
-V, --version Print the version and exit.
|
||||||
--theme <light|dark> Override theme (default: auto-detect).
|
--theme <light|dark> Override theme (default: auto-detect).
|
||||||
--data-dir <PATH> Use PATH as the data root instead of
|
--data-dir <PATH> Use PATH as the data root instead of
|
||||||
the OS-standard location for this run.
|
the OS-standard location for this run.
|
||||||
@@ -210,6 +215,7 @@ help:
|
|||||||
|
|
||||||
App-level commands (typed inside the app, available in both modes):
|
App-level commands (typed inside the app, available in both modes):
|
||||||
quit Exit cleanly.
|
quit Exit cleanly.
|
||||||
|
version Print the application version.
|
||||||
mode simple|advanced Switch input mode.
|
mode simple|advanced Switch input mode.
|
||||||
help Show this list of commands in-app.
|
help Show this list of commands in-app.
|
||||||
save Save the current temp project under a
|
save Save the current temp project under a
|
||||||
@@ -258,6 +264,8 @@ help:
|
|||||||
help <command> — detailed help for one command (e.g. `help insert`)
|
help <command> — detailed help for one command (e.g. `help insert`)
|
||||||
hint: |-
|
hint: |-
|
||||||
hint — explain the most recent error (press F1 for a hint on what you're typing)
|
hint — explain the most recent error (press F1 for a hint on what you're typing)
|
||||||
|
version: |-
|
||||||
|
version — print the application version (same as the `--version` command-line flag)
|
||||||
rebuild: |-
|
rebuild: |-
|
||||||
rebuild — rebuild the project database from project.yaml + data/ (with confirmation)
|
rebuild — rebuild the project database from project.yaml + data/ (with confirmation)
|
||||||
save: |-
|
save: |-
|
||||||
@@ -425,6 +433,9 @@ hint:
|
|||||||
hint:
|
hint:
|
||||||
what: "Explain the most recent error — or, pressing F1 while typing, the command you're building."
|
what: "Explain the most recent error — or, pressing F1 while typing, the command you're building."
|
||||||
example: "hint"
|
example: "hint"
|
||||||
|
version:
|
||||||
|
what: "Print the application version."
|
||||||
|
example: "version"
|
||||||
rebuild:
|
rebuild:
|
||||||
what: "Rebuild the project database from its saved text files."
|
what: "Rebuild the project database from its saved text files."
|
||||||
example: "rebuild"
|
example: "rebuild"
|
||||||
@@ -874,6 +885,7 @@ parse:
|
|||||||
quit: "quit"
|
quit: "quit"
|
||||||
help: "help [<command>]"
|
help: "help [<command>]"
|
||||||
hint: "hint"
|
hint: "hint"
|
||||||
|
version: "version"
|
||||||
rebuild: "rebuild"
|
rebuild: "rebuild"
|
||||||
save: "save | save as"
|
save: "save | save as"
|
||||||
new: "new"
|
new: "new"
|
||||||
|
|||||||
+6
-1
@@ -1,6 +1,6 @@
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use rdbms_playground::cli::{help_text, Args};
|
use rdbms_playground::cli::{help_text, version_text, Args};
|
||||||
use rdbms_playground::{logging, runtime};
|
use rdbms_playground::{logging, runtime};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -22,6 +22,11 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if args.version {
|
||||||
|
println!("{}", version_text());
|
||||||
|
return ExitCode::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
if args.help {
|
if args.help {
|
||||||
print!("{}", help_text());
|
print!("{}", help_text());
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
|
|||||||
@@ -251,6 +251,7 @@ fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String {
|
|||||||
AppCommand::Quit => "App(Quit)".into(),
|
AppCommand::Quit => "App(Quit)".into(),
|
||||||
AppCommand::Help { .. } => "App(Help)".into(),
|
AppCommand::Help { .. } => "App(Help)".into(),
|
||||||
AppCommand::Hint => "App(Hint)".into(),
|
AppCommand::Hint => "App(Hint)".into(),
|
||||||
|
AppCommand::Version => "App(Version)".into(),
|
||||||
AppCommand::Rebuild => "App(Rebuild)".into(),
|
AppCommand::Rebuild => "App(Rebuild)".into(),
|
||||||
AppCommand::Save => "App(Save)".into(),
|
AppCommand::Save => "App(Save)".into(),
|
||||||
AppCommand::SaveAs => "App(SaveAs)".into(),
|
AppCommand::SaveAs => "App(SaveAs)".into(),
|
||||||
|
|||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
||||||
Vendored
-11
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user