From 5941a092f783f63d889ac593034d8605eeb6b9e4 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Sat, 29 Aug 2026 21:05:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(query-corpus):=20iteration=209g=20corpus?= =?UTF-8?q?=20#1=20=E2=80=94=20skillhost=20needs=20no=20new=20query=20gram?= =?UTF-8?q?mar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - resolved 9g's forks EMPIRICALLY against the running compiler: - count() and len() already work (fork 2 collapses to zero code) - skillhost's correlated NOT EXISTS is a backlink emptiness in writeonce (`where len(x.children) == 0`), using only 9b machinery (fork 1) — verified on a self-referential ?ref/backlink table => corpus #1 forces NO new grammar; per the method ("add only what a corpus uses"), exists/not-exists was NOT built - docs/examples/skill-catalog: mirrors skillhost's `skills` table (name @unique, description/location/root, parent ?ref Skill, children backlink) and translates all five of its SQL statements 1:1 (insert+dup-trap, get-by-name, list, roots via backlink-emptiness, count); scripts/skill-catalog-accept.sh 7/0, WAL-durable, dup trap persists across restart - fixture run/db-query-corpus (count(query) + backlink NOT EXISTS); just skill-catalog module; target/ gitignored - general exists/not-exists left unbuilt and recorded as "enters when a corpus forces a non-relation correlation" - gates: oop-e2e 80/0, woc-test 566/0, skill-catalog 7/0; story + board record the finding Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 4c82461634d45f11eca1a252702031c03d999c7f) --- .gitignore | 1 + docs/examples/skill-catalog/README.md | 30 ++++++++ docs/examples/skill-catalog/justfile | 7 ++ docs/examples/skill-catalog/main.wo | 72 +++++++++++++++++++ docs/examples/skill-catalog/types.wo | 17 +++++ docs/examples/skill-catalog/wo.toml | 9 +++ .../databasev2/08-query-grammar-corpus.md | 15 ++++ justfile | 4 ++ scripts/skill-catalog-accept.sh | 34 +++++++++ tests/corpus/run/db-query-corpus/fixture.out | 3 + tests/corpus/run/db-query-corpus/fixture.wo | 17 +++++ 11 files changed, 209 insertions(+) create mode 100644 docs/examples/skill-catalog/README.md create mode 100644 docs/examples/skill-catalog/justfile create mode 100644 docs/examples/skill-catalog/main.wo create mode 100644 docs/examples/skill-catalog/types.wo create mode 100644 docs/examples/skill-catalog/wo.toml create mode 100755 scripts/skill-catalog-accept.sh create mode 100644 tests/corpus/run/db-query-corpus/fixture.out create mode 100644 tests/corpus/run/db-query-corpus/fixture.wo diff --git a/.gitignore b/.gitignore index 54dee67..bba9e2f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # `woc .` manifest builds (wo.toml [build] target) /docs/examples/log-watcher/target /docs/examples/employee/target +/docs/examples/skill-catalog/target # Rust runtime (crates/rt/): compiled binary + build artifacts /crates/rt/target diff --git a/docs/examples/skill-catalog/README.md b/docs/examples/skill-catalog/README.md new file mode 100644 index 0000000..c335d3f --- /dev/null +++ b/docs/examples/skill-catalog/README.md @@ -0,0 +1,30 @@ +# skill-catalog — the query grammar corpus (iteration 9g) + +> Corpus #1 for the query-grammar method (story +> [09g](../../stories/language-runtime-database/09g-query-grammar-corpus.md)): +> take a real application backed by an embedded SQL database, translate its +> every statement to the writeonce query surface, and add only the grammar it +> forces. The application is `~/projects/skillhost` (a C++ MCP host whose +> in-memory SQLite holds its skill catalog). + +**Result: skillhost forced no new grammar.** Its entire SQL footprint — one +table, a single-row `INSERT`, and four `SELECT`s (whose only non-trivial +features are `COUNT(*)` and a correlated `NOT EXISTS`) — is expressible on the +surface iteration 9b already shipped. The five statements, translated: + +| skillhost SQL (`src/catalog/catalog.cpp`) | writeonce | mode | +| --- | --- | --- | +| `INSERT INTO skills (…) VALUES (?,…)` + `SQLITE_CONSTRAINT` dup check | `insert Skill { … }` + `try…catch` on the `@unique` trap | `seed` | +| `SELECT … WHERE name = ?` | `from x in Skill where x.name == n take 1 select x` | `get ` | +| `SELECT … ORDER BY name` | `from x in Skill order by x.name select x` | `list` | +| `SELECT COUNT(*) FROM skills` | `count(from x in Skill select x)` | `count` | +| `SELECT … WHERE NOT EXISTS (SELECT 1 FROM skills c WHERE c.parent = s.name) ORDER BY name` | `from x in Skill where len(x.children) == 0 order by x.name select x` | `roots` | + +The one translation choice: skillhost's correlated `NOT EXISTS` (skills that +are nobody's parent) becomes a **backlink emptiness** — `Skill.children` is the +inverse of `parent`, and `len(x.children) == 0` is the childless test. No +subquery construct is needed; the general `exists`/`not exists` is deferred +until a corpus uses a correlation a backlink cannot express. + +`skill-catalog seed | list | roots | count | get `, WAL-durable under +`WO_DATA`. Acceptance: `scripts/skill-catalog-accept.sh`. diff --git a/docs/examples/skill-catalog/justfile b/docs/examples/skill-catalog/justfile new file mode 100644 index 0000000..8714c9a --- /dev/null +++ b/docs/examples/skill-catalog/justfile @@ -0,0 +1,7 @@ +ROOT := source_directory() / "../../.." +default: accept +build: + {{ROOT}}/compiler/_build/default/bin/woc . + @ls -la target/skill-catalog +accept: + {{ROOT}}/scripts/skill-catalog-accept.sh diff --git a/docs/examples/skill-catalog/main.wo b/docs/examples/skill-catalog/main.wo new file mode 100644 index 0000000..cf92dac --- /dev/null +++ b/docs/examples/skill-catalog/main.wo @@ -0,0 +1,72 @@ +-- skill-catalog — iteration 9g corpus proof. Each mode is a 1:1 translation +-- of one statement skillhost issues against its embedded SQLite; the point is +-- that ALL of them run on the query surface iteration 9b already shipped, with +-- NO new grammar. skillhost's SQL, translated: +-- +-- INSERT INTO skills (...) VALUES (?,...) -> insert Skill { ... } +-- SELECT ... WHERE name = ? -> where s.name == n +-- SELECT ... ORDER BY name -> order by s.name +-- SELECT COUNT(*) FROM skills -> count(from ... select ...) +-- SELECT ... WHERE NOT EXISTS (child) ORDER BY name -> where len(s.children) == 0 +-- +-- The NOT EXISTS correlated subquery becomes a backlink emptiness check — the +-- idiomatic writeonce form, needing no subquery construct. + +fn main(args: multi Text) -> Int { + if len(args) >= 1 and args[0] == "seed" { return seed(); } + if len(args) >= 1 and args[0] == "list" { return list(); } + if len(args) >= 1 and args[0] == "roots" { return roots(); } + if len(args) >= 1 and args[0] == "count" { return count_all(); } + if len(args) >= 2 and args[0] == "get" { return get(args[1]); } + print_err("usage: skill-catalog seed | list | roots | count | get "); + return 1; +} + +-- INSERT + the app's duplicate-name handling (skillhost checks SQLITE_CONSTRAINT; +-- writeonce catches the @unique trap). +fn seed() -> Int { + let git = try insert Skill { name: "git", description: "version control", + location: ".agents/git", root: ".agents/git", parent: nil } + catch (e) nil; + if git == nil { print("SEED-DUP catalog already seeded"); return 3; } + insert Skill { name: "git-commit", description: "make a commit", + location: ".agents/git/commit", root: ".agents/git", parent: git }; + insert Skill { name: "git-rebase", description: "rebase a branch", + location: ".agents/git/rebase", root: ".agents/git", parent: git }; + insert Skill { name: "docker", description: "containers", + location: ".agents/docker", root: ".agents/docker", parent: nil }; + print("SEEDED 4 skills"); + return 0; +} + +-- SELECT ... FROM skills ORDER BY name +fn list() -> Int { + for s in from x in Skill order by x.name select x { + print("${s.name}\t${s.description}"); + } + return 0; +} + +-- SELECT ... WHERE NOT EXISTS (SELECT 1 FROM skills c WHERE c.parent = s.name) +-- ORDER BY name -- the childless skills (skillhost's list-roots) +fn roots() -> Int { + for s in from x in Skill where len(x.children) == 0 order by x.name select x { + print(s.name); + } + return 0; +} + +-- SELECT COUNT(*) FROM skills +fn count_all() -> Int { + print_int(count(from x in Skill select x)); + return 0; +} + +-- SELECT ... FROM skills WHERE name = ? +fn get(name: Text) -> Int { + let hits = from x in Skill where x.name == name take 1 select x; + if len(hits) == 0 { print_err("no such skill: ${name}"); return 1; } + let s = hits[0]; + print("${s.name}\t${s.description}\t${s.location}\t${s.root}"); + return 0; +} diff --git a/docs/examples/skill-catalog/types.wo b/docs/examples/skill-catalog/types.wo new file mode 100644 index 0000000..804fc7c --- /dev/null +++ b/docs/examples/skill-catalog/types.wo @@ -0,0 +1,17 @@ +-- Mirrors ~/projects/skillhost's `skills` table (src/catalog/catalog.cpp): +-- CREATE TABLE skills (name TEXT PRIMARY KEY, description TEXT NOT NULL, +-- location TEXT NOT NULL, root TEXT NOT NULL, parent TEXT); +-- CREATE INDEX skills_parent ON skills(parent); +-- TEXT PRIMARY KEY -> @unique name; the parent self-reference is a ?ref, and +-- the `skills_parent` index is what the `children` backlink reads. This is +-- iteration 9g's corpus #1: proving the writeonce query surface already +-- covers a real embedded-SQLite app. +@table(name: "skills", index: [name], index: [parent]) +class Skill { + name: Text @unique + description: Text + location: Text + root: Text + parent: ?ref Skill -- SQLite's nullable `parent` column + children: backlink Skill.parent -- the inverse the skills_parent index serves +} diff --git a/docs/examples/skill-catalog/wo.toml b/docs/examples/skill-catalog/wo.toml new file mode 100644 index 0000000..edf408b --- /dev/null +++ b/docs/examples/skill-catalog/wo.toml @@ -0,0 +1,9 @@ +name = "skill-catalog" +version = "0.1.0" +description = "Iteration 9g corpus: skillhost's embedded-SQLite skill catalog, translated 1:1 to the writeonce query surface — proving it needs no new grammar" + +[runtime] +wo = ">= 0.1" + +[build] +runtime = "../../../runtime/wovm" diff --git a/docs/stories/databasev2/08-query-grammar-corpus.md b/docs/stories/databasev2/08-query-grammar-corpus.md index 3629367..bb73498 100644 --- a/docs/stories/databasev2/08-query-grammar-corpus.md +++ b/docs/stories/databasev2/08-query-grammar-corpus.md @@ -24,6 +24,21 @@ readiness: refine > applied to a whole application: **an embedded-SQLite app is a grammar > corpus; each one analysed drives a grammar increment.** > +> **Implemented 2026-08-16 (branch `query-grammar`).** The forks resolved +> empirically, and the result is the strongest possible one for the method: +> **corpus #1 (skillhost) forced NO new query grammar.** `count()` and +> `len()` already work (fork 2), and skillhost's correlated `NOT EXISTS` +> is a backlink emptiness — `where len(x.children) == 0` — using only 9b +> machinery (fork 1). So `exists`/`not exists` was **not** built: the method is +> "add only what a corpus uses," and this corpus uses nothing new. Deliverable +> is the parity sample (`docs/examples/skill-catalog`, all five skillhost +> statements translated 1:1, `scripts/skill-catalog-accept.sh` 7/0) and this +> recorded conclusion — not speculative subquery machinery. +> +> **Still open (no corpus yet):** the general `exists`/`not exists` construct, +> for a correlation a backlink cannot express (a correlation on a non-relation +> column). It enters when a corpus forces it, exactly as the method prescribes. + > **No spec exists yet.** The forks in *Info* are genuine decisions. ## Why this iteration exists diff --git a/justfile b/justfile index dd12490..86a5c15 100644 --- a/justfile +++ b/justfile @@ -116,6 +116,10 @@ mod log-watcher "docs/examples/log-watcher" # (scan/where/select/order/take, update, delete). `just employee` runs it. mod employee "docs/examples/employee" +# iteration 9g corpus: skillhost's embedded-SQLite catalog translated to the +# writeonce query surface (proves it needs no new grammar). `just skill-catalog` +mod skill-catalog "docs/examples/skill-catalog" + # conformance harness (plan 3): walks tests/corpus/{run,compile-fail,trap}, # exact outcome per fixture kind — see docs/plan/oop-vm/02-corpus.md. # Fails loudly (and names the recipe to run) if woc or wovm isn't built. diff --git a/scripts/skill-catalog-accept.sh b/scripts/skill-catalog-accept.sh new file mode 100755 index 0000000..7fb0023 --- /dev/null +++ b/scripts/skill-catalog-accept.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# scripts/skill-catalog-accept.sh — iteration 9g acceptance: every skillhost +# catalog operation, translated to writeonce, produces the expected result on +# the 9b query surface (no new grammar). WAL-durable; a second seed proves the +# @unique trap persists across a process restart. +set -uo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +WOC="$ROOT/compiler/_build/default/bin/woc" +WOVM="$ROOT/runtime/wovm" +pass=0; fail=0 +ok() { echo "ok $1"; pass=$((pass+1)); } +bad() { echo "FAIL $1 -- $2"; fail=$((fail+1)); } +[[ -x "$WOC" && -x "$WOVM" ]] || { echo "build woc+wovm first" >&2; exit 1; } +WORK="$(mktemp -d "${TMPDIR:-/tmp}/skillcat.XXXXXX")"; DATA="$WORK/d"; mkdir -p "$DATA" +IMG="$WORK/sc.wob" +trap '[[ -n "${SKILLCAT_KEEP:-}" ]] && echo "kept $WORK" || rm -rf "$WORK"' EXIT + +if "$WOC" --emit "$ROOT/docs/examples/skill-catalog" -o "$IMG" >"$WORK/c.out" 2>&1; then + ok "compile ($(stat -c%s "$IMG") bytes)" +else bad "compile" "$(head -1 "$WORK/c.out")"; echo; echo "skill-catalog-accept: $pass/$((pass+fail))"; exit 1; fi +run() { WO_DATA="$DATA" "$WOVM" "$IMG" "$@"; } + +run seed | grep -q "SEEDED 4 skills" && ok "seed (insert)" || bad "seed" "no SEEDED" +[[ "$(run count)" == "4" ]] && ok "count (COUNT(*) -> count(query))" || bad "count" "not 4" +[[ "$(run list | head -1)" == docker* ]] && ok "list (ORDER BY name)" || bad "list" "not name-ordered" +if [[ "$(run roots | tr '\n' ',')" == "docker,git-commit,git-rebase," ]]; then + ok "roots (NOT EXISTS -> backlink emptiness)" +else bad "roots" "got: $(run roots | tr '\n' ',')"; fi +run get git | grep -q "^git version control" && ok "get by name (WHERE name = ?)" || bad "get" "wrong row" +out="$(run seed 2>&1)"; rc=$? +[[ "$out" == *SEED-DUP* && $rc -eq 3 ]] && ok "unique trap persists across restart" || bad "dup" "rc=$rc" +echo +printf 'skill-catalog-accept: %d checks, %d failures\n' "$((pass+fail))" "$fail" +[[ $fail -eq 0 ]] diff --git a/tests/corpus/run/db-query-corpus/fixture.out b/tests/corpus/run/db-query-corpus/fixture.out new file mode 100644 index 0000000..cc031f7 --- /dev/null +++ b/tests/corpus/run/db-query-corpus/fixture.out @@ -0,0 +1,3 @@ +3 +docker +git-commit diff --git a/tests/corpus/run/db-query-corpus/fixture.wo b/tests/corpus/run/db-query-corpus/fixture.wo new file mode 100644 index 0000000..40e01d9 --- /dev/null +++ b/tests/corpus/run/db-query-corpus/fixture.wo @@ -0,0 +1,17 @@ +-- iteration 9g: skillhost's grammar on the 9b surface. count(query) for +-- COUNT(*), backlink-emptiness for a correlated NOT EXISTS. No new grammar. +@table(name: "skills", index: [name], index: [parent]) +class Skill { + name: Text + parent: ?ref Skill + children: backlink Skill.parent +} +fn main() { + let git = insert Skill { name: "git", parent: nil } + insert Skill { name: "git-commit", parent: git } + insert Skill { name: "docker", parent: nil } + print_int(count(from x in Skill select x)) + for s in from x in Skill where len(x.children) == 0 order by x.name select x { + print(s.name) + } +}