8 Commits
Author SHA1 Message Date
claude@clouddev1 f2b4ed00f4 docs(website): add cookieless Umami analytics + a privacy page
website / deploy (push) Successful in 37s
Wire self-hosted Umami (umami.oliversturm.com) into the site head:
- data-domains=relplay.org so the preview/staging deploys don't pollute
  the production stats
- data-do-not-track so DNT visitors aren't counted
- the Website ID is public by design (ships in every page)

Add a short /privacy page (footer-linked, kept out of the sidebar)
explaining the no-cookies / no-personal-data / no-cross-site-tracking
posture and the DNT behaviour. No cookie banner is needed: the tracker
stores nothing on the device, and first-party statistical analytics is
exempt from PECR consent under the Data (Use and Access) Act 2025.
2026-06-22 13:27:10 +00:00
claude@clouddev1 c86df95a0f docs(website): tabbed per-OS install instructions with auto-detection
website / deploy (push) Successful in 33s
Split the install page into Linux / macOS / Windows tabs so each visitor
sees only the commands that apply to them. A small InstallOsDetect component
seeds Starlight's synced-tabs localStorage key with the detected OS before
the tabs render, so the matching tab is pre-selected with no flash — and
only when the visitor hasn't already picked a tab, so a manual choice wins.

- installation.md -> .mdx (needs the Tabs/TabItem components)
- detection covers navigator.userAgentData and the legacy platform/UA
  fallbacks; unknown platforms fall through to the default tab
- piggybacks on Starlight's own restore mechanism rather than re-implementing
  tab switching
2026-06-22 09:36:56 +00:00
claude@clouddev1 fb536b4245 docs(website): reconcile installation docs with v0.2.0 shipped reality
ci / gate (push) Successful in 1m55s
ci / manifests (push) Successful in 3s
website / deploy (push) Successful in 35s
installation.md predated the install/packaging work that landed on main:
- drop the "once published" placeholders now that v0.2.0 is live
- fix the Homebrew/Scoop commands (tap/bucket-add forms; the bare
  `brew install rdbms-playground` was wrong)
- add the curl|sh and PowerShell one-line installers (ADR-0055),
  `cargo install` / `cargo binstall` (ADR-0056), and prebuilt binaries
- present winget as coming-soon (PR awaiting the public catalogue)

command-line-options.md: add `-V`/`--version` and the in-app `version`
command (ADR-0054).
2026-06-21 22:21:26 +00:00
claude@clouddev1 1237aa59c1 Merge branch 'main' into website
Bring v0.2.0 + install/packaging surface (crates.io, curl|sh installer,
Windows install.ps1, Scoop/Homebrew/winget, --version/version command)
onto the website branch for documentation reconciliation.
2026-06-21 22:15:41 +00:00
claude@clouddev1 ba0872dbb9 docs: website-branch session handoff (website-3) 2026-06-21 22:12:52 +00:00
claude@clouddev1 56e3456cfc docs: handoff 75 — D3 package managers complete (Scoop/Homebrew/winget)
ci / gate (push) Successful in 1m58s
ci / manifests (push) Successful in 4s
Covers the session that finished the D3 rollout: install.ps1 Windows 5.1
fixes, Scoop + Homebrew wiring and validation, the Homebrew-403 presigned-URL
saga fixed via a server-side Caddy HEAD rule, and the winget job + komac
bootstrap (PR #391335). Records the infra created off-repo (Caddy rule, the
lazyeval Gitea/GitHub bots + tokens), the gotchas, and the parked signing
tracks.
2026-06-21 22:05:00 +00:00
claude@clouddev1 0208c67e59 ci(publish): wire winget job via komac (D3 §3d)
Add the 4th publish.yaml sibling job: opens a PR to microsoft/winget-pkgs
with komac (pinned 2.16.0 prebuilt) for LazyEvaluation.RdbmsPlayground
(portable, x64 + arm64). Unlike scoop/homebrew it's a PR into Microsoft's
central, human-gated catalog - async and validated on their side.

- Auth: a classic public_repo GitHub PAT on a dedicated bot account
  (lazyeval-ci; fine-grained tokens can't open the cross-fork PR - komac
  #310), as the WINGET_GITHUB_TOKEN secret, job-scoped and passed to the API
  guards via a 0600 curl config file (never argv).
- Idempotent via two guards before submitting (already-merged version +
  already-open PR) so a repeated publish dispatch can't open a duplicate.
- No actions/checkout (komac works off URLs + the GitHub API).

Docs: ADR-0056 Amendment 4 - the model, the one-time manual `komac new`
bootstrap recipe (flags verified vs komac 2.16.0), and first-run learnings:
the fork must pre-exist, and a direct single-exe portable takes its PATH
alias from Commands[0] (not PortableCommandAlias, which is nested-only).
Plus README index + requirements D3.

Wiring only; going live needs the bootstrap PR (#391335, submitted) to merge.
2026-06-21 21:58:47 +00:00
claude@clouddev1 6bb2288470 docs(adr-0056): Scoop+Homebrew validated; record the Caddy HEAD fix
Amendment 3: scoop install and brew install both verified end-to-end on
real Windows + macOS (install + run), resolving Amendment 2's unverified
caveats (incl. the HEAD:main push).

Records the root cause + fix for the Homebrew 403: Gitea (>=1.25)
303-redirects release downloads to a method-bound SigV4 presigned S3 URL;
brew resolves with HEAD, captures the HEAD-signed URL, then GETs it -> 403
(GET-only tools are unaffected). Fixed by a server-side Caddy rule that
answers HEAD on release-download paths directly (200, no redirect) so the
download GET re-runs the redirect fresh. That rule lives in the Gitea edge
config, NOT this repo - documented so a server rebuild doesn't silently
break brew. Includes the reference Caddyfile block.

Also notes the ad-hoc mac signature runs fine via brew; Developer-ID +
notarization stays parked on the Apple org conversion. Remaining D3: winget.
2026-06-20 08:15:17 +00:00
13 changed files with 782 additions and 77 deletions
+84 -3
View File
@@ -198,6 +198,87 @@ jobs:
git push origin HEAD:main
echo "homebrew: tap updated to rdbms-playground $VER."
# winget remains a future sibling job here (komac on Linux CI, or a manual PR
# to microsoft/winget-pkgs). No `needs:` between jobs — each is independent and
# idempotent, so one failing or being added never breaks another.
# Update the winget package (Windows) by opening a PR to microsoft/winget-pkgs
# with komac. Unlike scoop-bucket/homebrew-tap (which push to OUR repos and are
# live at once), winget is a PR into Microsoft's central, human-gated catalog —
# asynchronous, and re-submitting the same version would open a DUPLICATE PR. So
# this job guards on both already-merged versions AND already-open PRs before
# submitting, which keeps a repeated `publish` dispatch safe.
#
# Auth: komac needs a CLASSIC GitHub PAT with `public_repo` (fine-grained tokens
# cannot open the cross-fork PR — komac #310). It is held on a dedicated GitHub
# bot account (a leak can't reach other repos — same reasoning as lazyeval-ci)
# and referenced ONLY in this job's env (job-level secret scoping keeps it out
# of the other publish jobs).
#
# PREREQUISITE: the package `LazyEvaluation.RdbmsPlayground` must already exist
# in winget-pkgs via a one-time `komac new` (interactive — run manually once;
# see ADR-0056 Amendment 4). This job only does the per-release `komac update`.
winget:
runs-on: ci-public
container:
image: git.lazyeval.net/oli/rdbms-playground-ci:latest
steps:
- name: submit the winget update PR (idempotent)
shell: bash
env:
TAG: ${{ inputs.tag }}
# Classic public_repo PAT for the winget bot account. komac reads it
# from GITHUB_TOKEN; the API guards below read it via a curl config file
# so the token never appears in a command line / process list.
GITHUB_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }}
run: |
set -euo pipefail
VER="${TAG#v}"
PKG="LazyEvaluation.RdbmsPlayground"
echo "winget: targeting $PKG $VER ($TAG)"
# Auth header in a 0600 curl config (keeps the token out of argv/logs).
umask 077
printf 'header = "Authorization: Bearer %s"\nheader = "Accept: application/vnd.github+json"\n' \
"$GITHUB_TOKEN" > /tmp/gh-curlrc
api="https://api.github.com"
# Guard 1 — already merged into winget-pkgs?
# (manifests/<first-letter>/<Publisher>/<Package>/<version>)
merged=$(curl -sS -o /dev/null -w '%{http_code}' --config /tmp/gh-curlrc \
"$api/repos/microsoft/winget-pkgs/contents/manifests/l/LazyEvaluation/RdbmsPlayground/$VER" || echo 000)
if [ "$merged" = "200" ]; then
echo "winget: $PKG $VER already in winget-pkgs — nothing to do."
exit 0
fi
# Guard 2 — an open PR for this exact id+version already? (avoid a dup)
# curl -G --data-urlencode does the URL-encoding (no jq in the image).
curl -sS -G --config /tmp/gh-curlrc \
--data-urlencode "q=repo:microsoft/winget-pkgs type:pr state:open in:title \"$PKG\" \"$VER\"" \
"$api/search/issues" -o /tmp/winget-search.json
open=$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync("/tmp/winget-search.json","utf8")).total_count||0))')
if [ "$open" != "0" ]; then
echo "winget: an open PR for $PKG $VER already exists — skipping."
exit 0
fi
# Install pinned komac (prebuilt glibc binary; the CI image has no
# cargo/komac). Pinned for reproducibility — bump deliberately.
KOMAC_VER=2.16.0
curl -fsSL -o /tmp/komac.tgz \
"https://github.com/russellbanks/Komac/releases/download/v$KOMAC_VER/komac-$KOMAC_VER-x86_64-unknown-linux-gnu.tar.gz"
tar -xzf /tmp/komac.tgz -C /tmp
komac_bin=$(find /tmp -maxdepth 2 -type f -name komac | head -1)
[ -n "$komac_bin" ] || { echo "ERROR: komac binary not found after extract" >&2; exit 1; }
base="https://git.lazyeval.net/oli/rdbms-playground/releases/download/$TAG"
echo "winget: submitting update PR via komac $KOMAC_VER"
# NB: confirm flags against `komac update --help` on first run — komac
# evolves; --version/--urls/--submit are the stable core. komac infers
# architecture + the `portable` installer type from the binaries.
"$komac_bin" update "$PKG" \
--version "$VER" \
--urls "$base/rdbms-playground-$TAG-x86_64-pc-windows-gnu.exe" \
"$base/rdbms-playground-$TAG-aarch64-pc-windows-gnullvm.exe" \
--submit
echo "winget: update PR submitted for $PKG $VER (Microsoft review is async)."
# No `needs:` between jobs — each is independent and idempotent, so one failing
# or being added never breaks another.
@@ -176,3 +176,144 @@ whether macOS Gatekeeper accepts the **ad-hoc-signed** mac binary via
requirement and `brew`'s curl download sets no quarantine xattr, unlike a
browser download — but this rides on the still-parked Developer-ID signing
decision). **Remaining D3:** winget (komac on Linux CI, or a manual PR).
## Amendment 3 — 2026-06-19: validated end-to-end; a presigned-URL/HEAD gotcha + the Caddy fix it needs
**Scoop and Homebrew now install and run end-to-end** (`v0.2.0`,
user-verified on real Windows + macOS): `scoop install rdbms-playground`
and `brew install lazyeval/tap/rdbms-playground` both fetch, checksum,
install, and the installed binary launches. Amendment 2's "unverified"
caveats are resolved (the `HEAD:main` push populated both repos cleanly).
**Distribution depends on a server-side Caddy rule — RECORD THIS: it lives
in the Gitea edge config, NOT this repo, and if it is lost in a server
rebuild Homebrew silently 403s again.** Symptom: `brew install` failed with
`curl (22) … 403` on the asset URL. Root cause: Gitea (≥1.25; here 1.26.2)
serves release-asset downloads by **303-redirecting to a method-bound
AWS-SigV4 presigned S3 URL** (OVH), signed for the *incoming* request's HTTP
verb. Homebrew **resolves the URL with a HEAD, captures the returned
(HEAD-signed) presigned URL, then runs the download GET against that
captured URL** → GET-on-a-HEAD-signed-URL → 403. Reproduced precisely:
`GET on HEAD-resolved = 403`, `HEAD on HEAD-resolved = 200`, `GET on
GET-resolved = 200`, `HEAD on GET-resolved = 403`. GET-only tools
(`install.sh`, `install.ps1`, `cargo binstall`, curl) are unaffected — they
GET the original URL and let it redirect fresh to a GET-signed URL. This is
a **Homebrew defect** (it reuses an ephemeral, method-scoped credential as
if it were a durable resource locator); Gitea's per-request method-bound
signing is correct, and SigV4 *cannot* sign one URL for two verbs.
`SERVE_DIRECT=false` (Gitea proxies, no presign) would also fix it but was
declined — not reshaping storage for one client.
**Fix (deployed, "Tier A"): a Caddy rule that answers HEAD on
release-download paths directly** — 200, no redirect, no body — so brew's
resolve records the *original* URL (no presigned credential captured) and
its download GET runs through the redirect fresh → GET-signed → 200. Scoped
to `method HEAD` + the release-download path; **GET is untouched**, so every
working channel is unaffected, and a HEAD carries no payload so no client
can *break* (at most a HEAD-probing download manager loses a progress-bar
size — cosmetic). Reference Caddyfile (place before the Gitea
reverse_proxy):
```
@release_head {
method HEAD
path_regexp ^/[^/]+/[^/]+/releases/download/.+
}
handle @release_head {
header Accept-Ranges bytes
header Content-Type application/octet-stream
respond 200
}
```
A "Tier B" variant (a sidecar that fetches and returns the *real*
Content-Length so even HEAD-probing clients stay fully faithful) was specced
but proved unnecessary — brew is happy with the bare 200.
**macOS signing:** the brew-installed binary **runs** under the current
**ad-hoc** signature (`codesign --sign -`) — confirmed on Apple Silicon.
**Developer-ID signing + notarization remains parked** pending the user's
Apple account conversion to an Organization (GA plan / ADR-ci-003); it is
needed for *browser-download* trust, not for the package-manager paths,
which are all working now.
**Remaining D3:** winget only.
## Amendment 4 — 2026-06-20: winget (D3 §3d) — the last package manager
winget wired as the fourth `publish.yaml` sibling job, **completing the D3
package-manager set** (crates.io/binstall, Scoop, Homebrew, winget; plus the
install scripts + direct binaries).
**Model — fundamentally unlike Scoop/Homebrew.** winget has no
self-hosted-source equivalent: its default catalog is the central,
**human-gated** GitHub repo `microsoft/winget-pkgs`, and you get in by
**opening a PR** of manifests into it. So the job submits a PR (via
**komac**, pinned `2.16.0`, prebuilt glibc binary — the CI image has no
cargo) and **Microsoft's pipeline validates async** (schema, SHA256,
AV/SmartScreen scan, sandbox install) + moderator review. It is *not* live
on dispatch like the bucket/tap.
**PackageIdentifier `LazyEvaluation.RdbmsPlayground`** (publisher segment =
the publishing entity / `lazyeval` org / license holder — consistent with
everything else). Bare-exe → winget **`portable`** installer type; x64
(`-pc-windows-gnu`) + arm64 (`-pc-windows-gnullvm`), komac infers arch +
type from the binaries.
**Auth — a dedicated GitHub bot, classic token.** komac needs a **classic
`public_repo` PAT**; **fine-grained tokens cannot open the cross-fork PR**
(komac #310 — the PR is created on the *target* repo you don't administer,
which the fine-grained model can't express). A classic `public_repo` token
can't be scoped to one repo, so it lives on a **dedicated GitHub bot
account** (a leak can't reach other repos — the lazyeval-ci reasoning), as
the `WINGET_GITHUB_TOKEN` secret, referenced **only** in the `winget` job
(job-level scoping keeps it away from the crates.io / lazyeval tokens) and
passed to the API guards via a 0600 curl config file (never argv).
**Idempotency — stronger than the others need.** A re-submitted version
would open a *duplicate* PR, so before submitting the job guards on **both**
(1) already-merged versions (`contents` API on
`manifests/l/LazyEvaluation/RdbmsPlayground/<ver>`) and (2) an already-open
PR for the id+version (`search/issues`). Either → clean skip, so a repeated
`publish` dispatch is safe even mid-review.
**Signing:** none required to *submit* (only MSIX needs it; ours is
portable). The unsigned binary may earn an AV/SmartScreen **manual-review
label** on the first PR — usually clears; a nudge toward Trusted Signing
(the UK Ltd clears the 3-year-history bar) but not a blocker. Continues the
parked Developer-ID/notarization posture.
**One-time bootstrap (manual — NOT in CI, because `komac new` is
interactive).** Run once to create the package in winget-pkgs, then CI
`komac update` handles every release after:
```
komac token update # paste the bot's classic public_repo PAT at the prompt (not in argv/history)
komac new LazyEvaluation.RdbmsPlayground \
-v 0.2.0 \
-u https://git.lazyeval.net/oli/rdbms-playground/releases/download/v0.2.0/rdbms-playground-v0.2.0-x86_64-pc-windows-gnu.exe \
https://git.lazyeval.net/oli/rdbms-playground/releases/download/v0.2.0/rdbms-playground-v0.2.0-aarch64-pc-windows-gnullvm.exe \
--publisher "Lazy Evaluation Ltd" --package-name "RDBMS Playground" \
--moniker rdbms-playground --license "MIT OR Apache-2.0" \
--package-url https://relplay.org -s
```
komac downloads the two exes, detects the bare binary as **`portable`**, and
**prompts for the command alias — enter `rdbms-playground`** so users get
that on PATH (not the long versioned filename). `-s` opens the PR; fill any
remaining prompts. (Flags verified against komac 2.16.0: `-v/--version`,
`-u/--urls`, `-s/--submit`; the alias has no flag and is prompted.) Once
that first PR merges, the CI job's `komac update` takes over.
**Bootstrap learnings (2026-06-21, first real run).** GitHub bot account =
`lazyeval-ci`. Its fork **`lazyeval-ci/winget-pkgs` must exist *before*
`komac new`** — komac's auto-fork races on winget-pkgs (one of GitHub's
largest repos) and fails with *"Could not resolve to a Repository
'lazyeval-ci/winget-pkgs'"*; fork it manually (web **Fork**, or `gh repo
fork microsoft/winget-pkgs --clone=false`), wait for it to populate, then
re-run. **Alias:** for a **direct single-exe portable**, winget derives the
on-PATH command from **`Commands[0]`** (the interactive "Commands" prompt →
`rdbms-playground`) — **not** `PortableCommandAlias`, which is
**archive/nested-portable-only** and must NOT be added to our manifest
(verified against winget-cli's portable-install logic + the 1.6.0 installer
schema; komac correctly omits it). So komac's generated manifest needs no
hand-editing. First PR: **#391335**.
+1 -1
View File
File diff suppressed because one or more lines are too long
+145
View File
@@ -0,0 +1,145 @@
# Session handoff — 2026-06-21 (75)
Continues from handoff-74 (v0.2.0 live on crates.io). This session **finished
the D3 package-manager rollout** — Scoop, Homebrew, and winget — plus the
Windows `install.ps1` fixes, and included a deep **presigned-URL / Homebrew**
debugging saga that's fixed by a server-side Caddy rule. Five commits; one
new ADR amendment arc (ADR-0056 Amendments 2–4).
## §1. State
**Branch `main`.** **No crate (Rust) code changed this session** — only
`.gitea/workflows/`, `scripts/`, and `docs/`. So the `cargo test` baseline
from handoff-74 (**2509 pass / 0 fail / 1 ignored**, clippy + `fmt --check`
clean) stands unchanged by construction; not re-run (nothing in the build
graph moved). New coverage this session is **`scripts/test-package-renders.sh`**
(shellcheck-clean, green), now gated on every push by a new **`ci.yaml`
`manifests` job** (bash + node; ruby-absent in CI degrades gracefully).
**Commits this session** (`cabc813`→`0208c67`; the workflow-bearing ones were
pushed, since the live `publish` dispatches + raw-URL installs the user tested
needed them on `main`):
- `42b40bc` install.ps1 → Windows PowerShell 5.1 compat
- `c0531aa` install.ps1 → immediate-use PATH + honest messaging
- `6d54c1e` Scoop + Homebrew jobs (D3 §3b/§3c)
- `6bb2288` Scoop/Homebrew validated + Caddy fix recorded (Amendment 3)
- `0208c67` winget job via komac (D3 §3d, Amendment 4)
**External / infra state created this session (NOT all in the repo):**
- **Caddy Tier-A HEAD rule on the Gitea server** — *load-bearing for Homebrew*,
lives in the Gitea edge config, **not** this repo (see §4). If lost on a
server rebuild, brew 403s again.
- **Gitea:** `lazyeval` org with repos **`scoop-bucket`** + **`homebrew-tap`**;
**`lazyeval-ci`** Gitea bot user (org team, Write to those repos);
**`LAZYEVAL_PKG_TOKEN`** secret on `oli/rdbms-playground`.
- **GitHub:** **`lazyeval-ci`** bot account; classic `public_repo` PAT →
**`WINGET_GITHUB_TOKEN`** secret on `oli/rdbms-playground`; fork
**`lazyeval-ci/winget-pkgs`**.
- **winget bootstrap PR [#391335](https://github.com/microsoft/winget-pkgs/pull/391335)**
submitted, in Microsoft review.
- **komac 2.16.0** installed locally at `~/.local/bin/komac`.
## §2. D3 — the package-manager set (the session's throughline)
| Channel | State |
| --- | --- |
| crates.io / `cargo binstall` | **live** (handoff-74) |
| `install.sh` / `install.ps1` | **live** — PS1 validated on ARM Windows 11 (5.1 + 7.6) |
| Scoop (`lazyeval/scoop-bucket`) | **live + validated** (install + run) |
| Homebrew (`lazyeval/homebrew-tap`) | **live + validated** — needs the Caddy rule (§4) |
| winget (`LazyEvaluation.RdbmsPlayground`) | **wired**; PR #391335 awaiting Microsoft merge |
How Scoop/Homebrew/winget are wired: sibling jobs in
`.gitea/workflows/publish.yaml` (manual `workflow_dispatch`, `tag` input),
each idempotent + independent. Scoop/Homebrew render **dependency-free bash**
manifests (`scripts/render-{scoop-manifest,homebrew-formula}.sh`) from the
release `.sha256` sidecars and push to the org repos via `LAZYEVAL_PKG_TOKEN`.
winget runs `komac update --submit` to PR `microsoft/winget-pkgs`, guarded
against duplicate PRs. ADR-0056 **Amendments 2–4** carry the full design.
## §3. The Homebrew 403 saga + Caddy fix (don't lose this)
Homebrew `brew install` 403'd while every other tool worked. Root cause,
reproduced: Gitea (≥1.25; here 1.26.2) **303-redirects release downloads to a
method-bound AWS-SigV4 presigned S3 URL** (OVH), signed for the *incoming*
request's HTTP verb. Homebrew **resolves with a HEAD, captures the returned
HEAD-signed URL, then runs the download GET against it** → GET-on-a-HEAD-signed
URL → 403. (`GET on HEAD-resolved = 403`, `HEAD on HEAD-resolved = 200`,
`GET on GET-resolved = 200`.) GET-only tools (install.sh/ps1, binstall, curl)
are unaffected. This is a **Homebrew defect** (reusing an ephemeral
method-scoped credential as a durable locator); Gitea's per-request signing is
correct, and SigV4 *cannot* sign one URL for two verbs. `SERVE_DIRECT=false`
would fix it but was **declined** (don't reshape storage for one client).
**Fix (deployed, "Tier A"):** a Caddy rule answering **HEAD on
`…/releases/download/…` directly** (200, no redirect, no body) so brew's
resolve records the *original* URL and its download GET redirects fresh →
GET-signed → 200. GET untouched; a HEAD carries no payload so nothing can
break (a HEAD-probing download manager at most loses a progress-bar size).
Reference Caddyfile is in **ADR-0056 Amendment 3**.
## §4. Immediate next steps
1. **winget PR #391335:** watch validation — the one likely speed bump is an
**AV/SmartScreen manual-review label** (unsigned binary); usually clears.
Once a moderator merges, winget is live.
2. **Post-merge check (Windows):** `winget install LazyEvaluation.RdbmsPlayground`,
confirm the command is **`rdbms-playground`** (it is — a direct single-exe
portable takes its PATH alias from `Commands[0]`, which komac set; see §5).
3. After merge, future releases are hands-off: the CI `winget` job's
`komac update` carries each version (alias inherited from the merged
manifest). The release ritual is unchanged from ADR-0054, with the
`publish` dispatch now also doing Scoop/Homebrew/winget.
## §5. Gotchas learned (don't relearn the hard way)
- **The Caddy HEAD rule is server-side, not in this repo.** Homebrew (and any
HEAD-then-GET client) depends on it. Record/back it up.
- **GitHub tokens:** komac needs a **classic `public_repo`** PAT — **fine-grained
tokens can't open the cross-fork PR** (komac #310: the PR is created on the
*target* repo you don't administer, which the fine-grained model can't
express). A classic token can't be repo-scoped, so it lives on a **dedicated
bot** (`lazyeval-ci`) to bound the blast radius.
- **komac fork must pre-exist:** `komac new` auto-fork *races* on winget-pkgs
(huge repo) → *"Could not resolve to a Repository 'lazyeval-ci/winget-pkgs'"*.
Fork manually first (`gh repo fork microsoft/winget-pkgs --clone=false`),
wait, then re-run. Pre-fill `komac new` flags to skip the long interactive
prompt parade.
- **Direct portable alias = `Commands[0]`, NOT `PortableCommandAlias`.** The
latter is **archive/nested-portable-only**; adding it to a bare-exe portable
is wrong (komac correctly omits it). The "Commands" prompt → `rdbms-playground`
is the on-PATH command. Verified vs winget-cli logic + the 1.6.0 schema.
- **install.ps1 / Windows PowerShell 5.1** (the in-box shell — PS7 is opt-in):
arch via `PROCESSOR_ARCHITECTURE` env (not `RuntimeInformation::OSArchitecture`,
which is absent under 5.1's .NET-Framework facade + StrictMode); force TLS 1.2;
`-UseBasicParsing`. Also update `$env:Path` in-session (the persisted User PATH
only reaches *new* processes; "restart your shell" was wrong — sign-out/in or
the in-session update).
- **Gitea release downloads go through OVH S3 presigned URLs** — method-bound,
300 s expiry. Never assume HEAD-resolve-then-GET works against them.
## §6. Parked / deferred (user decisions)
- **macOS Developer-ID signing + notarization** — pending the Apple account →
Organization conversion. Ad-hoc signing covers all package-manager paths
(brew-installed binary *runs* on Apple Silicon); Developer-ID only matters for
browser-download Gatekeeper trust.
- **Windows code signing (Trusted Signing)** — not required to *ship* winget
(portable; only MSIX needs signing). Unsigned can earn an AV/SmartScreen
manual-review label + a user-run warning. **Lazy Evaluation Ltd (since 2012)
clears Trusted Signing's 3-year-org bar** when the user wants to remove the
warning (individual onboarding is currently paused; org path is open).
- **Release notes / CHANGELOG** — raised this session, not done. If wanted:
a `CHANGELOG.md` and/or populated Gitea release bodies, then the CI `winget`
job can pass komac `--release-notes-url …/releases/tag/$TAG`.
## §7. How to take over
1. Read handoffs 73 → 74 → 75, `CLAUDE.md`, `docs/requirements.md` (D1/D3),
and **ADR-0056 (esp. Amendments 1–4)** + the GA plan
`docs/plans/20260616-public-availability.md`.
2. Workflow unchanged: phased, test-first, `/runda` + DA before commits, ADR
amendment + README index-upkeep for decided-area changes, confirm commit
messages, never push.
3. If `cargo test` is needed, the baseline is **2509 / 1 ignored** (handoff-74).
4. Consider a `cargo sweep` at this milestone (`target/` grows).
+130
View File
@@ -0,0 +1,130 @@
# Website-branch handoff — 2026-06-21 (website-3)
Third handoff for the **website** work (separate sequence from `main`'s
`YYYYMMDD-handoff-NN.md`). Read **website-2** (2026-06-11) first for the
scaffolding, cast pipeline, and the no-advertising constraint; this note covers
everything since, and — importantly — the state has moved on (see §1).
## §1. State (read this first)
- **Branch:** `website`. **HEAD `dff7841`.** Working tree **clean**.
- **Pushed:** `origin/website` == `dff7841` (this session's work is pushed).
- **Already merged into `main`:** `dff7841` is an ancestor of `main` — every
website change below is in `main`. **`main` has since advanced ~20 commits**
(now `56e3456`, handoff 75) with **v0.2.0 + install/packaging** work. So
**`website` is ~20 commits behind `main`.**
- **Consequence for the next session:** start by **merging `main` into
`website` again** (or rebranching `website` off `main`). There is real new
user-facing surface to document — see §4.
- **Agent limitation:** this machine's SSH key for `git.lazyeval.net` was **not
available to the agent** — `git fetch`/`push` fail from inside the session.
**Push and fetch are the user's steps.** Local merges work fine.
## §2. What shipped this session (all in `main` now)
The website lives in **`website/`**; living style guide **`website/STYLE.md`**;
decisions **`docs/website/adr/20260604-adr-website-001.md`**.
**CI / deployment — the big one (ADR-website-001 §4):**
- **`.gitea/workflows/website.yaml`** — on a push to `main` or `website` that
touches `website/**`, build the Astro site with pnpm and `wrangler pages
deploy website/dist` to the Cloudflare **Pages project `relplay`** (Direct
Upload, no Git integration). `--branch` selects environment vs the project's
production branch (`main`): **`main` → production (`relplay.org`)**,
**`website` → preview (`website.relplay.pages.dev`)**.
- Pure-Node build (the `.cast` files are committed, so no cargo). Runs on the
bare `ci-public` runner (node present; pnpm via corepack, pinned by
`package.json`'s `packageManager: pnpm@10.30.3`).
- **Secrets (set in repo Actions settings):** `CLOUDFLARE_API_TOKEN` (Pages:
Edit) + `CLOUDFLARE_ACCOUNT_ID`. Pages project created with production branch
= `main`.
- **`ci.yaml` gate** now skips website-only pushes (`paths-ignore` += `website/**`,
`.gitea/workflows/website.yaml`).
- **`staging.relplay.org`** (optional): attach the custom domain to the `website`
branch alias — add it in the Pages dashboard, then point its **proxied**
CNAME at `website.relplay.pages.dev` (external/unproxied DNS falls through to
production). Cloudflare Pages maps branches to **subdomains, not sub-folders**.
**Content updates (from two `main` merges that landed seed/readline/hint/etc.):**
- **Seed page** (`reference/generating-sample-data.mdx`) refreshed for **#33/#34**:
year-as-int columns (`*_year`/`published` → plausible years), built-in value
sets (`priority`/`severity`/`rating`), advisory now **status-only**. All output
blocks re-captured; the seed cast reflects it.
- **`hint` feature** (ADR-0053) documented: `getting-help.md` → **`.mdx`** with a
"Hints" section (F1 = tier-3 live-input hint; `hint` command = last-error hint)
+ the real rendered block; `the-assistive-editor.mdx` points at F1.
- **Readline keys (#29)** + **cross-mode history recall (#30)** documented on
`the-assistive-editor.mdx`; "Planned" narrowed to multi-line entry only.
- **Landing** (`index.mdx`): the redundant plain **`<h1>` "RDBMS Playground"** is
hidden (kept for SEO/a11y via a **title-only hero**, landing-scoped in
`global.css`); the **Wordmark + tagline + buttons** render in the body so the
lockup sits where the title was and content rises above the fold (it was nearly
hidden on an iPad).
- **All 10 casts re-recorded** + a **new `hint` cast** (11 total) against the
current app.
**Casts — the F1 trick (important for future hint/keybinding casts):**
- autocast **cannot send F1** (or arrows/function keys — escape sequences split
by the per-key delay). Solution shipped on `main`: **ADR-0047 Amendment 1** —
in `--demo` mode **Ctrl-G aliases F1** and **badges AS `[F1]`**, so a cast looks
like a real F1 press. The cast generator gained `CtrlG: '^G'`.
- The **hint cast** uses it realistically: type `add column `, pause, **F1**
(Ctrl-G) mid-command to recall the syntax, finish the command from the example,
then later a failed command + `hint` for the error.
**Hint-vs-docs audit (user-requested):** one genuine conflict found —
`hint.cmd.create_table.example` declared a 3-column compound PK; **fixed upstream**
(`5a37437`, now `create table Customers with pk id(serial)`). Everything else
(export/import, indexes, constraints, DML, SQL forms, error hints, terminology)
was consistent.
## §3. Verification status
- `cd website && pnpm build` → **clean, 27 pages**. Forbidden-terms grep clean
(`DSL`/`SQLite`/`STRICT`/`rusqlite`/`PRAGMA`). Output-table box-drawing intact;
anchors resolve; hint cast content verified (`[F1]` badge + both hint blocks).
- **NOT done — needs a human visual pass:** the new **landing spacing/aesthetics**
(couldn't be eyeballed by the agent) and the **11 re-recorded casts** (verified
programmatically only). Do this in staging/production before relying on them.
## §4. NEXT WORK — priority order
1. **Re-merge `main` into `website`** (it's ~20 behind) — or rebranch off `main`.
2. **Reconcile the installation / getting-started docs with what `main` actually
shipped (v0.2.0):** the website branch's `installation.md` predates several
real install methods now live —
- **crates.io v0.2.0** (`cargo install rdbms-playground`, `cargo binstall`),
- the **curl | sh installer** (ADR-0055),
- **Windows `install.ps1`** (`fix(install)` commits),
- **package managers**: Scoop / Homebrew / **winget** (ADR-0056; winget via
komac). `installation.md` mentions brew/scoop but **not** winget, the
curl|sh installer, or binstall — reconcile against the shipped reality
(check ADR-0055/0056 + handoffs 74/75 + the `publish.yaml` workflow).
- There's also `--version`/`-V` + an in-app `version` command (ADR-0054) — a
line on the command-line-options page may be warranted.
3. **Visual sweep** (§3): landing + casts; final caption review (website-2 §2).
4. **Carryover from website-2:** light/dark player theme (deferred), open STYLE
decisions (#7/#8 mostly resolved; recheck), output-pane-scroll cast still
blocked (PgUp/PgDn unsendable — would need a typeable scroll key like #24).
## §5. Process pins
- **Commits:** user-confirmed (show the message first), **no AI attribution**,
**append-only** (no amend/rebase/force-push). **Push/fetch are the user's
steps** (and the agent can't reach the remote anyway — §1).
- **Ground every page in source** (`src/dsl/*`, `src/friendly/strings/en-US.yaml`,
the ADRs) — not `requirements.md` markers. No engine name, no "DSL" in copy.
- **Output blocks captured from the real app**, never hand-drawn (STYLE.md). The
capture recipe (throwaway in-crate/`it` test → render → paste → delete) and the
cast pipeline (`pnpm casts [name]`, needs `../target/debug`) are in website-2.
- **Cast stance (revised 2026-06-12):** default to a cast; justify its *absence*.
- **Issues** via `tea` (repo `oli/rdbms-playground` on `git.lazyeval.net`;
append `< /dev/null` + `timeout 30`). This session filed **#33** (year ints)
and **#34** (enum value sets) — both **implemented** on `main` now.
## §6. Footnote — a git oddity in this branch's history
`c84a640 "docs(website): document the hint feature with a cast"` is an **empty
rename commit** (a `git add` tooling slip staged only the `.md`→`.mdx` rename);
its actual content is in the **next** commit `028d324`. Harmless, left in place
per the append-only rule (user-approved). No action needed.
+17 -9
View File
@@ -86,15 +86,23 @@ since ADR-0027.)
- [ ] **D3** Released via prebuilt binaries plus Homebrew, Scoop,
`winget`, and `cargo binstall`.
*(Prebuilt binaries + checksums on Gitea releases (D1); **`cargo
binstall` + crates.io live** (ADR-0056); **Scoop + Homebrew wired**
(ADR-0056 Amendment 2) — `publish.yaml` `scoop-bucket` /
`homebrew-tap` jobs render dependency-free manifests from the release
`.sha256` sidecars and push them, via the scoped `lazyeval-ci` bot
token, to `lazyeval/scoop-bucket` and `lazyeval/homebrew-tap`;
rendering covered by `scripts/test-package-renders.sh`, end-to-end
install still to be user-verified. **Remaining: winget** (komac on
Linux CI, or a manual PR). Asset naming
`rdbms-playground-<tag>-<target>` is binstall-friendly.)*
binstall` + crates.io live** (ADR-0056); **Scoop + Homebrew wired and
validated end-to-end** on real Windows + macOS (ADR-0056 Amendments
2 & 3) — `publish.yaml` `scoop-bucket` / `homebrew-tap` jobs render
dependency-free manifests from the release `.sha256` sidecars and push
them, via the scoped `lazyeval-ci` bot token, to
`lazyeval/scoop-bucket` and `lazyeval/homebrew-tap`; rendering covered
by `scripts/test-package-renders.sh`. NB: the Homebrew path depends on
a **server-side Caddy rule** answering HEAD on release-download paths
directly (Gitea's presigned-S3 redirect is method-bound and brew
reuses a HEAD-signed URL for its GET) — see ADR-0056 Amendment 3;
that rule lives in the Gitea edge config, not this repo. **winget
wired** (ADR-0056 Amendment 4) — a `publish.yaml` `winget` job submits
a PR to `microsoft/winget-pkgs` via komac (`LazyEvaluation.RdbmsPlayground`,
portable, x64+arm64), guarded against duplicate PRs; pending the
**one-time manual `komac new` bootstrap** + Microsoft review before
it goes live. Asset naming `rdbms-playground-<tag>-<target>` is
binstall-friendly.)*
## TUI shell
+19
View File
@@ -58,6 +58,25 @@ export default defineConfig({
},
{ tag: 'meta', attrs: { name: 'twitter:card', content: 'summary_large_image' } },
{ tag: 'meta', attrs: { name: 'twitter:image', content: 'https://relplay.org/og-card.png' } },
// Privacy-friendly, cookieless analytics (self-hosted Umami). The
// Website ID is public by design (it ships in every page). The script
// loads from the Umami host, which also becomes the data destination,
// so no `data-host-url` is needed.
// - data-domains: only run on the production apex, so the
// `website.relplay.pages.dev` preview and `staging.relplay.org`
// don't pollute the stats.
// - data-do-not-track: honour the browser's Do-Not-Track signal
// (those visits are not counted at all). See /privacy.
{
tag: 'script',
attrs: {
defer: true,
src: 'https://umami.oliversturm.com/script.js',
'data-website-id': 'fd77cfa5-cffe-4fc8-addb-1c6d7b6d9939',
'data-domains': 'relplay.org',
'data-do-not-track': true,
},
},
],
// Register the simple-mode command grammar with Expressive Code (Shiki).
expressiveCode: { shiki: { langs: [rdbmsLang, rdbmsSyntax] } },
+2
View File
@@ -15,6 +15,8 @@ import Default from '@astrojs/starlight/components/Footer.astro';
Made by <a href="https://www.lazyevaluation.biz/">Lazy Evaluation Ltd</a>
<span class="sep">·</span>
<a href="https://git.lazyeval.net/oli/rdbms-playground">Source &amp; issues</a>
<span class="sep">·</span>
<a href="/privacy/">Privacy</a>
</p>
</div>
@@ -0,0 +1,39 @@
---
// Pre-selects the install-instructions tab that matches the visitor's OS.
//
// This piggybacks on Starlight's synced-tabs mechanism rather than fighting it.
// A `<Tabs syncKey="install-os">` restores its active tab from
// `localStorage["starlight-synced-tabs__install-os"]` via a script Starlight
// inlines *before* the tabs render (so there is no flash of the wrong tab). All
// we do here is seed that same key with the detected OS — and only when the
// visitor has not already chosen a tab themselves, so a manual choice always wins.
//
// Requirements (mirrors Starlight's own restore script):
// - Must be placed *before* the `<Tabs>` it targets, so this runs first.
// - The stored value must equal one of the TabItem labels exactly
// ("Linux" / "macOS" / "Windows").
// - Inlined, so it runs during parse before the restore script's
// connectedCallback and paint.
---
<script is:inline>
(() => {
const KEY = 'starlight-synced-tabs__install-os';
try {
// A previously stored value is a deliberate user choice — never override it.
if (localStorage.getItem(KEY)) return;
const data = navigator.userAgentData;
const platform = ((data && data.platform) || navigator.platform || '').toLowerCase();
const ua = (navigator.userAgent || '').toLowerCase();
let os = null;
if (platform.includes('win') || ua.includes('windows')) os = 'Windows';
else if (platform.includes('mac') || ua.includes('mac os x')) os = 'macOS';
else if (platform.includes('linux') || platform.includes('x11') || ua.includes('linux'))
os = 'Linux';
if (os) localStorage.setItem(KEY, os);
} catch (e) {
// localStorage unavailable (private mode, blocked cookies, …) — silently
// fall back to the default first tab. No detection, no harm.
}
})();
</script>
@@ -1,64 +0,0 @@
---
title: Installation
description: Install RDBMS Playground from a prebuilt binary or a package manager, and run it for the first time.
sidebar:
order: 1
---
RDBMS Playground is a single self-contained program. There is nothing to
configure and no separate database to install — everything it needs is
built in.
## Prebuilt binaries
Download the binary for your platform, make it executable if needed, and
put it somewhere on your `PATH`.
:::note
Download links are published with each release. They are added here when the
first public version ships.
:::
## Package managers
Once published, the playground will be installable through common package
managers:
```sh
# macOS / Linux (Homebrew)
brew install rdbms-playground
# Windows (Scoop)
scoop install rdbms-playground
```
:::note
Package-manager availability lands with the first public release; the exact
names are confirmed here at that time.
:::
## Run it
Start the playground with no arguments and it opens a fresh, automatically
named temporary project so you can start experimenting immediately:
```sh
rdbms-playground
```
To open an existing project, pass its path:
```sh
rdbms-playground path/to/project
```
Useful options (run `rdbms-playground --help` for the full list):
| Option | What it does |
|---|---|
| `--resume` | Reopen the most recently used project. |
| `--data-dir <PATH>` | Use a different location for stored projects. |
| `--theme <light\|dark>` | Force a theme instead of auto-detecting. |
| `--mode <simple\|advanced>` | Start in a specific input mode. |
Next: [create your first project](/getting-started/first-project/).
@@ -0,0 +1,148 @@
---
title: Installation
description: Install RDBMS Playground with a one-line installer, a package manager, cargo, or a prebuilt binary.
sidebar:
order: 1
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
import InstallOsDetect from '../../../components/InstallOsDetect.astro';
RDBMS Playground is a single self-contained program. There is nothing to
configure and no separate database to install — everything it needs is
built in.
Pick the tab for your platform — we try to select it automatically — then
use whichever method you prefer. The one-line installers are the quickest if
you just want to get going.
<InstallOsDetect />
<Tabs syncKey="install-os">
<TabItem label="Linux">
**One-line installer**
```sh
curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh
```
It detects your CPU, installs to `~/.local/bin`, and prints a hint if that
directory isn't on your `PATH`. Set `RDBMS_INSTALL_DIR` to install elsewhere,
or `RDBMS_VERSION=vX.Y.Z` to pin a version. Prefer to read a script before
running it? It lives at
[`scripts/install.sh`](https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh).
**Homebrew**
```sh
brew tap lazyeval/tap https://git.lazyeval.net/lazyeval/homebrew-tap
brew install lazyeval/tap/rdbms-playground
```
</TabItem>
<TabItem label="macOS">
**One-line installer**
```sh
curl -fsSL https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh | sh
```
It detects your CPU (Apple Silicon or Intel), installs to `~/.local/bin`, and
prints a hint if that directory isn't on your `PATH`. Set `RDBMS_INSTALL_DIR`
to install elsewhere, or `RDBMS_VERSION=vX.Y.Z` to pin a version. Prefer to
read a script before running it? It lives at
[`scripts/install.sh`](https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.sh).
**Homebrew**
```sh
brew tap lazyeval/tap https://git.lazyeval.net/lazyeval/homebrew-tap
brew install lazyeval/tap/rdbms-playground
```
</TabItem>
<TabItem label="Windows">
**One-line installer (PowerShell)**
```powershell
irm https://git.lazyeval.net/oli/rdbms-playground/raw/branch/main/scripts/install.ps1 | iex
```
It downloads the matching `.exe`, verifies its checksum, installs to
`%LOCALAPPDATA%\Programs\rdbms-playground`, and adds that folder to your user
`PATH`. Use `-InstallDir` to choose a different location or `-Version` to pin
a release.
**Scoop**
```sh
scoop bucket add lazyeval https://git.lazyeval.net/lazyeval/scoop-bucket
scoop install rdbms-playground
```
**winget**
A winget package (`winget install LazyEvaluation.RdbmsPlayground`) is on its
way and awaiting review in the public winget catalogue. Until it lands, use
Scoop or the PowerShell one-liner above.
</TabItem>
</Tabs>
## With cargo
These work on any platform with a Rust toolchain. Install from
[crates.io](https://crates.io/crates/rdbms-playground):
```sh
cargo install rdbms-playground
```
For a faster install that fetches a prebuilt binary instead of compiling, use
[`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall) (install it
first — it is not part of `cargo` itself):
```sh
cargo binstall rdbms-playground
```
## Prebuilt binaries
Every release publishes static Linux, standalone Windows, and macOS binaries
(x86_64 and aarch64), each with a `.sha256` checksum, on the
[releases page](https://git.lazyeval.net/oli/rdbms-playground/releases).
Download the one for your platform, make it executable if needed, and put it
somewhere on your `PATH`.
## Run it
Start the playground with no arguments and it opens a fresh, automatically
named temporary project so you can start experimenting immediately:
```sh
rdbms-playground
```
To open an existing project, pass its path:
```sh
rdbms-playground path/to/project
```
Useful options (run `rdbms-playground --help` for the full list):
| Option | What it does |
|---|---|
| `--resume` | Reopen the most recently used project. |
| `--data-dir <PATH>` | Use a different location for stored projects. |
| `--theme <light\|dark>` | Force a theme instead of auto-detecting. |
| `--mode <simple\|advanced>` | Start in a specific input mode. |
| `--version` | Print the version and exit. |
The full list, with examples, is on the
[command-line options](/using-the-playground/command-line-options/) page.
Next: [create your first project](/getting-started/first-project/).
+51
View File
@@ -0,0 +1,51 @@
---
title: Privacy
description: What we measure on this site, and what we don't. No cookies, no personal data.
tableOfContents: false
---
This site is run as a free teaching tool, not a business that profiles its
visitors. We keep the analytics deliberately minimal.
## What we collect
We measure aggregate usage with **[Umami](https://umami.is/)**, a
privacy-friendly analytics tool that we **host ourselves** (on
`umami.oliversturm.com`). Nothing is sent to a third-party advertising
network.
For each page view we record:
- the page you viewed and the site you arrived from (the referrer),
- your browser, operating system, and device type,
- an approximate **country**.
That's it. The numbers are aggregated into counts — how many people read a
page, which pages are popular — and are never combined into a profile of an
individual.
## What we don't do
- **No cookies, and nothing stored on your device.** The tracker sets no
cookies and writes nothing to local storage. You can confirm this in your
browser's developer tools — there are no entries to clear.
- **No personal data.** Your IP address is used for a moment on our own
server to derive the approximate country and to tell repeated page-loads
apart within a single day; it is **not stored**.
- **No cross-site tracking.** We cannot and do not follow you to other sites,
and we never sell or share what we measure.
## Your choices
If your browser sends a **Do Not Track** signal, we honour it: those visits
are not counted at all.
Because the analytics use no cookies and store nothing on your device, there
is no cookie banner to dismiss — there is simply nothing to consent to.
## Questions
This site and its analytics are operated by
[Lazy Evaluation Ltd](https://www.lazyevaluation.biz/). If you have any
questions about what we measure, get in touch through the contact details
there.
@@ -29,8 +29,13 @@ rdbms-playground path/to/project
| `--no-undo` | Disable the undo machinery for this run — no snapshot is taken before each change (see [Undo, redo & history](/using-the-playground/undo-and-history/)). |
| `--demo` | Turn on demonstration mode — a teaching aid that briefly shows an on-screen badge for keys that otherwise leave no visible mark (Tab, Enter, the arrows, and the like). Useful for screencasts and for demonstrating the playground to a class. Off by default. |
| `--log-file <PATH>` | Write diagnostic logging to `PATH`. |
| `-V`, `--version` | Print the version and exit. |
| `-h`, `--help` | Print the usage banner and exit. |
Once the playground is running, the `version` command prints the same
version line into the output panel — handy when you want to note which
version you're on without leaving the app.
## Examples
```sh