diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6b375e0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,139 @@ +# NOTE: this workflow has never run. Authored 2026-08-25 and not +# executable locally — the first real tag push is its first test. +# Expect to adjust the toolchain step if the pinned OCaml/dune version +# is not available on the runner image. + +name: release + +# Fires only on a version tag, so nothing is published by an ordinary +# push. `workflow_dispatch` is a DRY RUN: it builds, verifies and reports +# the glibc floor, but skips the tag guard (there is no tag) and skips +# publishing. Use it to rehearse before tagging anything. +on: + push: + tags: + - 'v*' + workflow_dispatch: + +# The ONE line that replaces `gh auth login`: it widens the automatic +# GITHUB_TOKEN so this job may write releases. No PAT, no secret to +# rotate, and the token dies with the job. +permissions: + contents: write + +jobs: + release: + # DELIBERATE, not a default. The release binaries link glibc + # dynamically, so the build host's glibc caps which symbol versions + # they can import — and that cap becomes the minimum glibc every + # user needs. Built on 24.04 (glibc 2.39) the floor is 2.39; + # built here on 22.04 (2.35) it is 2.35, which is the difference + # between excluding and including Ubuntu 22.04, Debian 12 and + # RHEL 9. Raise this image only with a reason, and update the + # supported-systems list in docs/examples/site/install/view.wo in + # the same change. + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + # setup-ocaml gives a compiler and opam. It does NOT give dune — + # dune is an ordinary opam package, and this project has no .opam + # file for it to infer one from, so nothing pulls it in. The first + # run failed here with `dune: command not found`. + - uses: ocaml/setup-ocaml@v3 + with: + ocaml-compiler: '4.14' + + # setup-ocaml may already have installed a dune (it uses one for its + # own cache), in which case asking for an exact older version is a + # DOWNGRADE the solver refuses — which is how the pinned + # `dune.3.14.0` failed. So: use whatever is there, and only install + # if there is nothing. Any dune >= 3.14 satisfies this project's + # `(lang dune 3.14)`. + - name: Ensure dune is available + run: | + opam exec -- dune --version || opam install -y dune + echo "dune: $(opam exec -- dune --version)" + + # The tag is the release's identity; VERSION is what the binaries + # report. If they disagree the download URL would name a version + # nobody can install. mkdist.sh already guards VERSION against the + # binaries; this guards the tag against VERSION. + - name: Tag must match VERSION + if: github.event_name == 'push' + run: | + tag="${GITHUB_REF_NAME#v}" + ver="$(cat VERSION)" + [ "$tag" = "$ver" ] || { + echo "tag $GITHUB_REF_NAME does not match VERSION $ver" >&2 + exit 1 + } + + # `opam exec --` because mkdist.sh calls dune internally; without + # the opam environment on PATH the script cannot find it. + - name: Build the tarball + run: opam exec -- ./scripts/mkdist.sh + + # The site links one exact filename. If mkdist ever changes its + # naming, the download button 404s for every visitor — so fail + # here instead. + - name: Asset name must match what the site links + run: | + ver="$(cat VERSION)" + asset="writeonce-${ver}-linux-amd64.tar.gz" + test -f "dist/$asset" + grep -q "$asset" docs/examples/site/install/view.wo || { + echo "$asset is not the filename /install links" >&2 + exit 1 + } + + - name: Verify the digest + run: cd dist && sha256sum -c "writeonce-$(cat ../VERSION)-linux-amd64.tar.gz.sha256" + + # Prove the ARTEFACT works, using the binaries inside it rather + # than the ones just built in the tree. This is what catches a + # tarball that packaged the wrong thing. + - name: Smoke-test the extracted toolchain + run: | + ver="$(cat VERSION)" + tmp="$(mktemp -d)" + tar -C "$tmp" -xzf "dist/writeonce-${ver}-linux-amd64.tar.gz" + export PATH="$tmp/writeonce/bin:$PATH" + woc version + wovm --version + mkdir -p "$tmp/hello" + cd "$tmp/hello" + printf 'name = "hello"\nversion = "0.1.0"\n\n[runtime]\nwo = ">= 0.1"\n' > wo.toml + printf 'fn main() -> Int {\n print("hello, writeonce");\n return 0;\n}\n' > main.wo + woc . + out="$(./target/hello)" + [ "$out" = "hello, writeonce" ] || { echo "got: $out" >&2; exit 1; } + + # Record the real glibc floor of what is about to ship, so the + # claim on /install can be checked against a build log rather + # than trusted. + - name: Report the glibc floor + run: | + ver="$(cat VERSION)" + tmp="$(mktemp -d)" + tar -C "$tmp" -xzf "dist/writeonce-${ver}-linux-amd64.tar.gz" + for b in "$tmp"/writeonce/bin/*; do + printf '%s needs %s\n' "$(basename "$b")" \ + "$(objdump -T "$b" | grep -oE 'GLIBC_[0-9.]+' | sort -uV | tail -1)" + done + + # gh is preinstalled on GitHub runners and reads GH_TOKEN from the + # environment, so there is no `gh auth login` anywhere in this file. + # Skipped on workflow_dispatch: a dry run must never publish. + - name: Publish + if: github.event_name == 'push' + env: + GH_TOKEN: ${{ github.token }} + run: | + ver="$(cat VERSION)" + gh release create "$GITHUB_REF_NAME" \ + "dist/writeonce-${ver}-linux-amd64.tar.gz" \ + "dist/writeonce-${ver}-linux-amd64.tar.gz.sha256" \ + --title "writeonce ${ver}" \ + --generate-notes diff --git a/README.md b/README.md index 0878464..6abc206 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,19 @@ program ships as one file that depends only on the system C library. mistyped field name is a **compile error**, not a runtime surprise. There is no SQL string anywhere in the shipped binary. - **One binary, no runtime dependencies.** `woc .` produces a self-contained - executable (~100 KB for the sample programs) that links only libc. Copy it to - a server and run it. -- **Small on purpose.** No FFI, no package manager, no framework. The standard - library is a handful of OS modules. The language is designed to be read. + executable (160–260 KB for the sample programs in this repository) that links + only libc. Copy it to a server and run it. +- **Small on purpose.** No FFI, no reflection, no package registry — + dependencies are exact-rev git URLs and nothing else. The standard library is + a handful of OS modules. The language is designed to be read. -writeonce is **not** a web framework and does not (yet) serve HTTP, WebSockets, -or a UI. It is a systems language whose distinguishing feature is the embedded -database. If you have seen an older "writeonce" that served REST from `cargo -run`, that was a separate, earlier runtime; this page documents the current -`woc`/`wovm` toolchain. +writeonce is a systems language whose distinguishing feature is the embedded +database. HTTP/1.1 and WebSockets **do** work today — but as `.wo` libraries you +consume through `[deps]` (`porch` for serving, `writeonce-view` for +HTML), never as runtime features: the runtime stays framework-agnostic on +purpose. TLS is always terminated by a proxy in front. If you have seen an older +"writeonce" that served REST from `cargo run`, that was a separate, earlier +runtime; this page documents the current `woc`/`wovm` toolchain. --- @@ -139,8 +142,10 @@ has a known owner, memory is freed deterministically, and values that form cycles are collected by an inferred garbage collector (you never annotate GC- ness; the compiler infers it). The surface will look familiar: -- **Types:** `Int`, `Text`, `Bool`, and user `class` types. `?T` marks an - optional (nullable) value; `nil` is the empty case. +- **Types:** `Int`, `Float`, `Bool`, `Text`, `Bytes`, `Timestamp`, `Id`, and + user `class` types. `?T` marks an optional (nullable) value; `nil` is the + empty case. `Int` and `Float` never mix implicitly — `float` and `trunc` are + the only bridges. - **Containers:** `multi T` (a growable list) and `map`. Literals: `[]`, `[a, b]`, `{}`. - **Classes & records:** classes with fields and methods, `static const` / @@ -149,6 +154,11 @@ ness; the compiler infers it). The surface will look familiar: expressions, and `try { … } catch (e) { … }` (also an expression form). - **Strings:** interpolation with `${expr}` inside a `"…"` literal. - **Functions:** free functions and methods; arguments and returns are typed. +- **Concurrency:** `spawn C { … }` starts an actor and yields an `actor M` + address; `send` is fire-and-forget, `call` parks the calling fiber until the + receive returns. A class becomes an actor by declaring `fn receive(msg: M)`. + Blocking stdlib calls park the fiber — there is no `async`, no `await`, and no + user-visible thread. ``` fn classify(n: Int) -> Text { @@ -166,14 +176,17 @@ A compact set of OS modules, reached by their reserved names — no imports: | Module | What it does | | --- | --- | -| `fs` | `exists`, `list`, `stat`, `read_all`, `read_at`, `append` | -| `time` | `sleep`, `now`, `local`, `iso` | +| `fs` | `exists`, `list`, `stat`, `read_all`, `read_at`, `append` — read and append; a file cannot yet be replaced, truncated, deleted or renamed | +| `time` | `sleep`, `now`, `ticks` (µs monotonic), `local`, `iso` | | `env` | `get`, `stopping` (a cooperative shutdown flag) | -| `net` | TCP `listen` / `accept` / `read` / `write` / `close` (host + port) | +| `net` | `listen` / `accept` / `read` / `write` / `close`, per-call deadline twins `read_dl` / `accept_dl` / `write_dl`, `listen_unix`, `peer`. Listeners only — there is no outbound `connect` | | `proc` | `run` a child process, capture stdout/stderr/exit | | `json` | `encode` / `decode` (`json.decode(t) as T` yields `?T`) | These are deliberately minimal — the surface a real program needs, and no more. +Alongside them sit free builtins for text, containers, the `Float`/`Bytes` +bridges, `base64`, and the digests `sha1` / `sha256` / `hmac_sha256`. The full +list is `docs/guides/language-surface.md`. --- @@ -269,14 +282,15 @@ dependencies, declared in the manifest: ```toml [deps] -niceframework = { git = "https://github.com/shoneyj/niceframework", rev = "v0.1.0" } +porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" } ``` -`woc` fetches each dep (via the `git` binary) into `.wo-deps//`, pins -the resolved commit in `wo.lock`, and `use niceframework` (or -`use niceframework/sub`) imports its public names like any module. Builds -never touch the network once the lock is satisfied; a moved tag is reported, -and `woc --update-deps myproject/` refreshes the lock deliberately. Flat +**The `[deps]` key IS the module name** `use` imports — the repository name +never appears in your source. `woc` fetches each dep (via the `git` binary) +into `.wo-deps//`, pins the resolved commit in `wo.lock`, and `use porch` +(or `use porch/router`) imports its public names like any module. Builds never +touch the network once the lock is satisfied; a moved tag is reported, and +`woc --update-deps myproject/` refreshes the lock deliberately. Flat dependencies only (a dep may not have its own `[deps]`) — honest and small, by design. @@ -292,8 +306,9 @@ WO_DATA=./data ./target/myproject report # a fresh process still sees the da ## Worked examples -Two complete sample programs live in the repository and double as the language's -acceptance tests: +Thirteen sample programs live under `docs/examples/`; eight of them are wired to +a `just` recipe and double as the language's acceptance tests. The three worth +reading first: - **`docs/examples/employee/`** — departments and employees related by `ref`/`backlink`, `@unique`, foreign-key restrict on delete, per-department @@ -303,7 +318,7 @@ acceptance tests: just employee # compile + run every mode against a durable database ``` -- **`docs/examples/writeonce-framework/` + `docs/examples/web-app/`** — a web +- **`docs/examples/porch/` + `docs/examples/web-app/`** — a web framework written in writeonce (HTTP/1.1 behind a TLS-terminating proxy, router with `:param` captures, interface-based handlers) and a storefront consuming it **as a `[deps]` dependency**, with `@table` persistence. Run: @@ -319,7 +334,10 @@ acceptance tests: just log-watcher ``` -Read either program's `main.wo` for idiomatic, working writeonce. +Read any of their `main.wo` files for idiomatic, working writeonce. The rest — +`site` (the writeonce.de tutorial, server-rendered, `just site`), `fibers`, +`db-actor`, `db-bench`, `gc-cycle`, `operators`, `shop` — cover the concurrency, +GC and benchmark surfaces. --- @@ -330,10 +348,16 @@ honest. These exist as design iterations and/or work-in-progress branches, not as features you can use today: - **Query aggregates** — `group … by … into g` with `count`/`avg`/`min`/`max` - and projection records. (Today the same result is written by hand from the - shipped primitives.) -- **HTTP service layer** — `service` blocks that route requests to methods. -- **Concurrency** — a shard-actor runtime and green-threaded fibers. + and projection records. The clause parses and is then refused by the + typechecker; today the same result is written by hand from the shipped + primitives. +- **File mutation and outbound sockets** — `fs` can create, grow and read a + file but never replace, truncate, delete or rename one, and there is no + `net.connect` at all, so nothing reaches out (no OIDC, SMTP, object store or + webhook). Both are iteration 38. +- **`service` blocks** — a declaration form that routes requests to methods, + lowering onto the framework library. Today you register routes as ordinary + framework calls, which works and is what every sample does. - **Cross-program database access** — one program attaching to another's database over a local channel, with keypair authentication and per-client rights. @@ -341,8 +365,11 @@ as features you can use today: - **Compile-time metaprogramming** — `@derive(Json/Csv/Eq/…)` generated from a class's own metadata, no reflection. -Known current limits worth naming: `net` is TCP host+port only; `proc.run` has -no timeout or signal control; there is no stdin/stdout byte I/O and no FFI. +Known current limits worth naming: `proc.run` has no timeout or signal control; +there is no stdin/stdout byte I/O and no FFI; `map` lookup is a linear scan; +actor mailboxes are bounded but there is no supervision tree yet; the WAL is +append-only, so it grows and boot replays all of it; TLS is always a proxy's +job. --- diff --git a/compiler/README.md b/compiler/README.md index 6ed42f2..2f043e3 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -2,7 +2,9 @@ Lexer → parser → typechecker → ownership pass → bytecode emitter, for `.wo`. OCaml stdlib only (no Menhir, no ppx); dune is the build runner. Sibling of the C `wovm` bytecode VM ([`runtime/`](../runtime/README.md)) — the two halves of the OOP track's spec (`docs/superpowers/specs/2026-08-01-oop-compiler-vm-design.md`) meet at plan 3, where `woc`'s emitted `.wob` runs on `wovm`. -**Stage: plan 3 (`docs/plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md`) complete, Tasks 1–6 + 8** (Task 7, a parity harness against the Rust runtime, was deferred by explicit decision — the two stacks now diverge by design). `.wo` source compiles to `.wob` bytecode (`--emit`) and to a single self-contained executable (`build`) that runs `wovm` with no arguments and no repo-relative dependency. Milestone 1's acceptance gate — compile-time budget, the full conformance corpus under ASan, the single-binary smoke, both unit suites — is `just oop-accept`. Plan 2 (lexer through ownership pass) shipped first and is unchanged. +**Stage: well past plan 3.** Plan 2 (lexer through ownership pass) and plan 3 (`docs/plan/compiler/2026-08-01-wob-emit-e2e-single-binary.md`, Tasks 1–6 + 8 — Task 7, a parity harness against the since-removed Rust runtime, was deferred by explicit decision) closed the milestone: `.wo` source compiles to `.wob` bytecode (`--emit`) and to a single self-contained executable (`build`) that runs `wovm` with no arguments and no repo-relative dependency. Milestone 1's acceptance gate — compile-time budget, the full conformance corpus under ASan, the single-binary smoke, both unit suites — is `just oop-accept`. + +Since then the front end has taken iterations **15** (`[deps]`, `wo.lock`, `--update-deps`), **17** (`kind = "library"`, entry-less check mode, `internal/` as WO-E108), **19** (`Float` and `Bytes`), **24** (`call`'s typed reply, WO-E226), **34** (digest builtins), **35** (net deadline seams), **36** (`not`, bitwise operators, hex/binary literals, compound assigns — `.wob` v6) and **37** (the backtick raw text literal with `{{ }}` auto-escaping). Current language surface: [`docs/guides/language-surface.md`](../docs/guides/language-surface.md). Current status: [the board](../docs/stories/00-status.md). ## Requirements @@ -24,23 +26,30 @@ just woc-test # same, from the repo root ``` woc # compile (lex, parse, typecheck, ownership-check); nothing prints on success +woc # BUILDS instead, when /wo.toml exists — the primary mode +woc version # e.g. "writeonce 0.1.0 linux/amd64" +woc --emit -o # compile through to a .wob bytecode module, runnable by wovm +woc build -o [--runtime ] + # compile + append the .wob image to a copy of wovm (--runtime, + # else $WO_RUNTIME, a wovm beside this woc, or runtime/wovm) +woc --update-deps # re-fetch [deps] at their manifest revs, rewrite wo.lock +woc -D ... # define a build flag for the #if/#else/#end token filter woc --dump-tokens # stdout: one line per lexed token woc --dump-ast # stdout: the declaration + body AST, indented woc --dump-owner # stdout: the ownership pass's four tables (moves, drops, rc, residual) +woc --dump-gc # stdout: the inferred-GC pass's traced set woc --dump-bc # stdout: disassembled bytecode for every emitted method -woc --emit -o # compile through to a .wob bytecode module, runnable by wovm -woc build -o [--runtime ] - # compile + append the .wob image to a copy of wovm (default - # runtime/wovm, or --runtime) into one self-contained ``` -`` is a single `.wo` file or a directory. A directory is discovered recursively for every `.wo` file under it — same contract as `wo run` (`crates/rt/src/lib.rs::discover`): dot-prefixed entries and `target`/`data`/`node_modules` are skipped, results are sorted by path. Every discovered file compiles as one program (declarations in one file resolve for bodies in another, regardless of discovery order); diagnostics from every file and every stage print sorted by `(file, line, col)`. For multi-file `--dump-*` output, each file's dump is preceded by a `=== path ===` header line (`compiler/src/dump.ml`'s `file_header`) — a single-file run never prints one. +`woc ` on a directory holding a `wo.toml` is the mode every sample and the install docs use: it reads the manifest's `name` plus the optional `[build]` runtime/target keys and produces `/` exactly as `woc build` would. A manifest with `kind = "library"` is checked entry-less and writes nothing. + +`` is a single `.wo` file or a directory. A directory is discovered recursively for every `.wo` file under it: dot-prefixed entries and `target`/`data`/`node_modules` are skipped, results are sorted by path. Every discovered file compiles as one program (declarations in one file resolve for bodies in another, regardless of discovery order); diagnostics from every file and every stage print sorted by `(file, line, col)`. For multi-file `--dump-*` output, each file's dump is preceded by a `=== path ===` header line (`compiler/src/dump.ml`'s `file_header`) — a single-file run never prints one. Diagnostics render as `file:line:col: severity CODE: message` plus a source excerpt with a caret; every shipped code is cataloged in `docs/plan/oop-vm/01-error-catalog.md`. Exit codes: **0** clean compile, **1** diagnostics reported, **2** usage/IO failure. ## Layout -- `src/` — one module per stage: `diag` (diagnostics, collector, exit-code decision), `token`/`lexer`, `ast`/`parser`, `types` (typechecker), `owner` (MVS ownership pass), `emit` (bytecode emitter, consumes `owner`'s four tables), `disasm` (bytecode disassembler, backs `--dump-bc`), `dump` (stable text dumps for all of the above) +- `src/` — one module per stage: `diag` (diagnostics, collector, exit-code decision), `token`/`lexer`, `ast`/`parser`, `types` (typechecker), `gcinfer` (the inferred-GC pass, backs `--dump-gc`), `owner` (MVS ownership pass), `emit` (bytecode emitter, consumes `owner`'s four tables), `disasm` (bytecode disassembler, backs `--dump-bc`), `dump` (stable text dumps for all of the above) - `bin/` — the `woc` executable: CLI parsing, file discovery, the multi-file/cross-file driver, `--emit`/`build` output - `test/` — `runner.ml` (golden runner + CLI smoke) and `test_diag.ml` (diag.ml unit checks); `test/golden//` holds one-file-per-fixture goldens (`tokens`, `ast`, `owner`, `owner-err`, `bc`); `test/fixtures/driver/` holds the multi-file CLI-smoke fixtures (directory discovery, cross-file symbols, diagnostic ordering) that don't fit the one-`.wo`-file-per-fixture golden shape diff --git a/compiler/src/CODE-LOGIC.md b/compiler/src/CODE-LOGIC.md index 4f8549e..b76bb12 100644 --- a/compiler/src/CODE-LOGIC.md +++ b/compiler/src/CODE-LOGIC.md @@ -295,3 +295,62 @@ a keyword, and visibility is name resolution at compile time. The `0x`/`0b` prefix commits only when a real base digit follows, so `0xg` stays `Int 0` + `Ident` — a parse error at its own position, no new lexer diagnostic. `_` separators are consumed only BETWEEN digits. + +## The raw text literal (iteration 37) + +Multi-line markup used to be impossible to write: a statement ends at a +newline, so a page was one `h = h .. "<...>"` statement per line, every +attribute single-quoted to dodge `\"`, and every piece of data wrapped +in a hand-written `esc()` call. Backtick literals replace all three. +Things worth knowing before editing them: + +- **It is a LEXER form, not a node.** A backtick literal emits exactly + the `Token.Str` (no holes) or `Token.InterpStr` (holes) a `"..."` + string emits, so `types.ml`, `owner.ml`, `emit.ml`, the `.wob` format + and the VM are all untouched — nothing downstream can tell the two + spellings apart. That is the whole reason the feature is small. A + design that introduced a `Markup`/`Element` AST variant instead would + have had to teach five files about it. +- **No escape processing at all inside.** Quotes and backslashes are + content, which is the point. The cost is that the form cannot express + a literal backtick, a literal `${`, or a literal `{{` — those are + written by concatenating an ordinary `"..."` string with `..`. One + greppable door beats inventing an escape character for the one form + whose selling point is not having any. (`docs/examples/site/content.wo` + keeps two `code_block` samples as escaped `"..."` strings for exactly + this reason: they contain `\${`.) +- **The margin is stripped at LEX time**, so the constant pool holds the + dedented text and there is no runtime cost. Java's text-block rule: + one newline right after the opening backtick is dropped, the smallest + leading whitespace run across non-blank lines is removed from every + line, and a whitespace-only closing line loses its whitespace but + keeps its newline. A literal with no newline is left alone — eating + the leading spaces of `` ` hi` `` would be a surprise, not a service. + The measuring pass runs over a SHADOW string where each hole is one + non-whitespace sentinel byte, so ` {{ x }}` counts as indent 4 and + as a non-blank line. +- **`{{ e }}` desugars to `esc(${e})`, resolved by ordinary name + lookup.** `desugar_interp` in `parser.ml` builds a `Call` on an + `Ident "esc"` — precisely what a developer wrote by hand before. The + compiler learns nothing about HTML, `esc` stays writeonce-view's ordinary + `pub fn`, a typo'd field inside the hole is a normal name/type error, + and a locally defined `esc` shadows deliberately (a custom escaper is + a feature). `${ }` inside the same literal stays raw — that is the + greppable door for markup you built yourself. The one place the + desugar leaks: with no `esc` in scope the program fails on a name it + never typed, so `emit.ml`'s WO-E403 message carries a hint for that + one name. +- **`{{` is special ONLY inside a backtick literal.** Inside `"..."` it + is still two braces, so CSS and JS text in existing samples lexes + byte-identically. +- **WO-E005 closed a real hole.** The string scanner's catch-all used to + append a raw newline like any other byte, so a forgotten closing quote + silently swallowed the rest of the file with no diagnostic. Now the + scan stops at the newline WITHOUT consuming it — the `Newline` token + still terminates the statement, so recovery costs one line instead of + the file. The rt-parity silence for a plain unterminated string with + no newline is untouched, and `runner.ml` still pins it. +- **The `..` line continuation stays.** A line ending in `..` still + swallows its newline. Raw literals took over the multi-line-markup job + that motivated it, but it remains the general way to spread a long + concatenation over several lines and has its own corpus fixture. diff --git a/compiler/src/dump.ml b/compiler/src/dump.ml index 95722d8..11ed6e2 100644 --- a/compiler/src/dump.ml +++ b/compiler/src/dump.ml @@ -34,6 +34,7 @@ let kind_label (k : Token.kind) : string = let part_str = function | Token.SText s -> Printf.sprintf "TEXT(%s)" s | Token.SExpr s -> Printf.sprintf "EXPR(%s)" s + | Token.SEsc s -> Printf.sprintf "ESC(%s)" s in Printf.sprintf "INTERP_STR(%s)" (String.concat "," (List.map part_str segs)) | Token.KwType -> "KW_TYPE" diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 32e9a45..5d1332c 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -3259,8 +3259,20 @@ and emit_call (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : As | None -> if is_builtin_name name then emit_builtin p f v ~dst ?expected e name args else begin + (* iteration 37: `{{ e }}` in a raw text literal desugars to + a call to `esc`, so a program using that hole without an + `esc` in scope lands here with a name it never typed. + The caret is already on the literal; this says why. *) + let hint = + if name = "esc" then + " (a `{{ ... }}` hole calls it -- add `use html`, or \ + declare your own `fn esc(t: Text) -> Text`)" + else "" + in err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos - ~message:(Printf.sprintf "call to `%s`, which is not a declared fn or a builtin" name); + ~message: + (Printf.sprintf "call to `%s`, which is not a declared fn or a builtin%s" + name hint); put f (ins_abx op_loadk dst (const_int p 0)) end))) | Field (base, mname) -> ( diff --git a/compiler/src/lexer.ml b/compiler/src/lexer.ml index b9782ba..d5c4f72 100644 --- a/compiler/src/lexer.ml +++ b/compiler/src/lexer.ml @@ -55,6 +55,32 @@ let unknown_char_code = Diag.lexing_prefix ^ "01" (* WO-E001 *) let unterminated_escape_code = Diag.lexing_prefix ^ "02" (* WO-E002 *) let directive_code = Diag.lexing_prefix ^ "03" (* WO-E003: #if/#else/#end misuse *) +(* iteration 37, the raw text literal (backtick-delimited, verbatim + content, no backslash escapes). Two codes, because the two shapes + are genuinely different situations: + + WO-E004 — a raw literal that runs off the end of the file. Unlike a + plain "..." string (silent, rt parity, see above), this one IS + reported: multi-line is the raw literal's normal case, so a missing + closing backtick would otherwise swallow every remaining line of the + file with nothing to show for it. Reported at the OPENING backtick, + which is the only position that helps -- EOF tells the reader + nothing about which literal never closed. + + WO-E005 — a raw newline inside a "..." or '...' string. This used to + be accepted silently: the string scanner's catch-all appended the + newline like any other byte, so a forgotten closing quote ate the + rest of the file with no diagnostic at all. Nothing in the repo ever + relied on it (zero of the .wo sources span a line inside quotes) and + the backtick literal is now the spelling for multi-line text, so the + accident becomes an error. The scan stops at the newline WITHOUT + consuming it, so the Newline token is still emitted and the + statement terminates -- one diagnostic, and the next line parses + normally instead of being swallowed. The rt-parity silence for a + plain unterminated string with no newline is untouched. *) +let unterminated_raw_code = Diag.lexing_prefix ^ "04" (* WO-E004 *) +let newline_in_string_code = Diag.lexing_prefix ^ "05" (* WO-E005 *) + (* haxe-parity Task 8: build flags. `woc -D name` fills this before any tokenize call; undefined flags are false. A module-level ref because the compiler is a single-shot process — tests that care set it explicitly @@ -269,6 +295,126 @@ let preprocess (collector : Diag.Collector.t) ~(file : string) go toks; List.rev !out +(* ---- the raw literal's margin rule (iteration 37) ------------------- + + A render() body is written at its method's indentation, but that + indentation is an artifact of the SOURCE, not of the markup -- nobody + wants six leading spaces on every line of the served HTML. So the + common margin is removed here, at lex time: the constant pool holds + the dedented text, no downstream stage ever sees the source + indentation, and the whole rule costs nothing at run time. + + The rule (Java's text blocks, which solved exactly this): + - one newline immediately after the opening backtick is dropped, + so the first markup line can start on its own line; + - the smallest leading run of spaces/tabs across all non-blank + lines is removed from every line (characters counted, tabs NOT + expanded -- mixing them is the author's problem, and expanding + would need a tab width the language does not have); + - a whitespace-only final line (the usual case: the closing + backtick sits on its own line) loses its whitespace but keeps + its newline. + A literal with no newline in it is left completely alone -- there is + no margin to speak of, and silently eating the leading spaces of + ` hi` would be a surprise, not a service. + + Holes do not disturb any of this. A line's indentation is by + definition the run of whitespace at its start, and the only thing + that can split a line across segments is a hole, which ends that run + -- so an indentation run always lives whole inside one SText. The + measuring pass replaces each hole with a single non-whitespace + sentinel byte so that a line that is ` {{ x }}` correctly counts + as indent 4 and as NON-blank. *) + +let is_indent_char c = c = ' ' || c = '\t' + +let segments_shadow (segs : Token.str_part list) : string = + let b = Buffer.create 64 in + List.iter + (function + | Token.SText s -> Buffer.add_string b s + | Token.SExpr _ | Token.SEsc _ -> Buffer.add_char b '\001') + segs; + Buffer.contents b + +let min_indent (shadow : string) : int = + let m = ref max_int in + List.iter + (fun line -> + let n = String.length line in + let i = ref 0 in + while !i < n && is_indent_char line.[!i] do + incr i + done; + (* a blank (or whitespace-only) line never sets the margin *) + if !i < n && !i < !m then m := !i) + (String.split_on_char '\n' shadow); + if !m = max_int then 0 else !m + +let strip_margin (k : int) (segs : Token.str_part list) : Token.str_part list = + if k = 0 then segs + else begin + let at_line_start = ref true in + let one seg = + match seg with + | Token.SExpr _ | Token.SEsc _ -> + at_line_start := false; + seg + | Token.SText s -> + let n = String.length s in + let b = Buffer.create n in + let i = ref 0 in + while !i < n do + if !at_line_start then begin + let dropped = ref 0 in + while !dropped < k && !i < n && is_indent_char s.[!i] do + incr dropped; + incr i + done; + at_line_start := false + end + else begin + let c = s.[!i] in + Buffer.add_char b c; + if c = '\n' then at_line_start := true; + incr i + end + done; + Token.SText (Buffer.contents b) + in + (* fold_left, not List.map: `one` carries state across segments and + List.map's application order is unspecified. *) + List.rev (List.fold_left (fun acc seg -> one seg :: acc) [] segs) + end + +let drop_trailing_margin (segs : Token.str_part list) : Token.str_part list = + match List.rev segs with + | Token.SText s :: rest_rev -> + let n = String.length s in + let i = ref n in + while !i > 0 && is_indent_char s.[!i - 1] do + decr i + done; + (* only a run that directly follows a newline is a closing line *) + if !i < n && !i > 0 && s.[!i - 1] = '\n' then + List.rev (Token.SText (String.sub s 0 !i) :: rest_rev) + else segs + | _ -> segs + +let dedent (segs : Token.str_part list) : Token.str_part list = + let shadow = segments_shadow segs in + if not (String.contains shadow '\n') then segs + else begin + let segs = + match segs with + | Token.SText s :: rest when String.length s > 0 && s.[0] = '\n' -> + Token.SText (String.sub s 1 (String.length s - 1)) :: rest + | _ -> segs + in + let k = min_indent (segments_shadow segs) in + drop_trailing_margin (strip_margin k segs) + end + let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) : Token.t list = let lx = make src in @@ -279,6 +425,14 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) : | { Token.kind = Token.Newline; _ } :: _ -> true | _ -> false in + (* A line ending in `..` continues on the next line — the ONE newline + suppression in the language, so multi-line markup/text builds read + as one expression (the shop template's ask; story 37 rides it). *) + let last_is_dotdot () = + match !out with + | { Token.kind = Token.DotDot; _ } :: _ -> true + | _ -> false + in let report_unknown line col c = Diag.Collector.add collector (Diag.error ~code:unknown_char_code ~file ~line ~col @@ -289,6 +443,18 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) : (Diag.error ~code:unterminated_escape_code ~file ~line ~col ~message:"unterminated string escape" ()) in + let report_unterminated_raw line col = + Diag.Collector.add collector + (Diag.error ~code:unterminated_raw_code ~file ~line ~col + ~message:"unterminated raw text literal" ()) + in + let report_newline_in_string line col = + Diag.Collector.add collector + (Diag.error ~code:newline_in_string_code ~file ~line ~col + ~message: + "newline in string literal (use a `...` raw text literal for \ + multi-line text)" ()) + in let running = ref true in while !running do match peek lx with @@ -307,7 +473,8 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) : end else if c = '\n' then begin ignore (advance lx); - if not (last_is_newline ()) then emit Token.Newline line col + if not (last_is_newline ()) && not (last_is_dotdot ()) then + emit Token.Newline line col end else if c = ' ' || c = '\t' || c = '\r' then ignore (advance lx) else if c = '"' || c = '\'' then begin @@ -374,6 +541,13 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) : | None -> report_unterminated_escape esc_line esc_col; scanning := false) + | Some '\n' -> + (* WO-E005. Deliberately NOT consumed: the outer loop turns + it into the Newline token that terminates the statement, + so recovery is one bad line rather than the rest of the + file. *) + report_newline_in_string lx.line lx.col; + scanning := false | Some other -> ignore (advance lx); Buffer.add_char buf other @@ -384,6 +558,64 @@ let tokenize (collector : Diag.Collector.t) ~(file : string) (src : string) : | [ Token.SText s ] -> emit (Token.Str s) line col | segs -> emit (Token.InterpStr segs) line col) end + else if c = '`' then begin + (* iteration 37: the raw text literal. Everything up to the + closing backtick is content -- newlines included, and with NO + escape processing at all, which is the whole point: markup + carries quotes and backslashes verbatim. A literal backtick + (or a literal `{{`) is written by concatenating an ordinary + "..." string with `..`; that door is one greppable operator, + which beats inventing an escape character for the one form + whose selling point is not having any. + + Two hole forms, and ONLY here -- inside "..." a `{{` is still + two literal braces, so existing CSS/JS text is untouched: + ${ expr } raw, exactly like a "..." string's hole + {{ expr }} HTML-escaped (the parser wraps it in esc()) *) + ignore (advance lx); + let buf = Buffer.create 64 in + let parts = ref [] in + let flush_text () = + parts := Token.SText (Buffer.contents buf) :: !parts; + Buffer.clear buf + in + let scanning = ref true in + while !scanning do + match peek lx with + | None -> + report_unterminated_raw line col; + scanning := false + | Some '`' -> + ignore (advance lx); + scanning := false + | Some '$' when peek_at lx 1 = Some '{' -> + flush_text (); + ignore (advance lx); + ignore (advance lx); + parts := Token.SExpr (read_interp_expr lx) :: !parts + | Some '{' when peek_at lx 1 = Some '{' -> + flush_text (); + ignore (advance lx); + ignore (advance lx); + (* read_interp_expr stops at the first `}` at depth 0 and + consumes it -- the second one closes this hole. Reusing it + means brace depth and nested string literals are already + handled, so `{{ Point{x:1}.x }}` scans correctly. *) + let raw = read_interp_expr lx in + (match peek lx with + | Some '}' -> ignore (advance lx) + | _ -> report_unterminated_raw line col); + parts := Token.SEsc raw :: !parts + | Some other -> + ignore (advance lx); + Buffer.add_char buf other + done; + flush_text (); + (match dedent (List.rev !parts) with + | [] -> emit (Token.Str "") line col + | [ Token.SText s ] -> emit (Token.Str s) line col + | segs -> emit (Token.InterpStr segs) line col) + end else if is_digit c then begin (* iteration 19: one scanner for both numeric worlds. The integer run is scanned into a buffer as well as accumulated, because a fraction diff --git a/compiler/src/parser.ml b/compiler/src/parser.ml index c1ecff0..8734d82 100644 --- a/compiler/src/parser.ml +++ b/compiler/src/parser.ml @@ -1332,6 +1332,19 @@ and parse_primary (st : state) : Ast.expr = and desugar_interp (st : state) (pos : Ast.pos) (segs : Token.str_part list) : Ast.expr = let mk_str s = { Ast.id = fresh_id st; pos; kind = Ast.StrLit s } in let mk_interp inner = { Ast.id = fresh_id st; pos; kind = Ast.Interp inner } in + (* iteration 37: `{{ e }}` in a raw text literal IS `esc(${e})` -- the + desugar builds exactly the call a developer writes by hand today + (docs/examples/shop/**/view.wo used `${esc(...)}` throughout), so + every later stage sees only Call/Interp/StrLit/Concat nodes it + already handles. No new AST variant, no new builtin, no VM change, + and a typo'd field inside the hole is an ordinary name/type error. + `esc` resolves by ordinary lookup (wo-html's `pub fn esc`, in scope + after `use html`); a locally defined `esc` shadows it deliberately + -- a custom escaper is a feature, not a collision. *) + let mk_esc inner = + let callee = { Ast.id = fresh_id st; pos; kind = Ast.Ident "esc" } in + { Ast.id = fresh_id st; pos; kind = Ast.Call (callee, [ mk_interp inner ]) } + in let parse_segment_expr (raw : string) : Ast.expr = let sub_collector = Diag.Collector.create () in let sub_toks = Lexer.tokenize sub_collector ~file:st.file raw in @@ -1362,7 +1375,8 @@ and desugar_interp (st : state) (pos : Ast.pos) (segs : Token.str_part list) : A (function | Token.SText "" -> None | Token.SText s -> Some (mk_str s) - | Token.SExpr raw -> Some (mk_interp (parse_segment_expr raw))) + | Token.SExpr raw -> Some (mk_interp (parse_segment_expr raw)) + | Token.SEsc raw -> Some (mk_esc (parse_segment_expr raw))) segs in match parts with diff --git a/compiler/src/token.ml b/compiler/src/token.ml index 5f71a0c..bf9515b 100644 --- a/compiler/src/token.ml +++ b/compiler/src/token.ml @@ -23,6 +23,13 @@ type str_part = | SText of string (* literal text, escapes already applied *) | SExpr of string (* raw, unlexed source of one `${...}`'s body *) + (* iteration 37: the escaping half of the raw text literal. Same raw, + unlexed payload as SExpr -- what differs is only what the parser + wraps it in: `${...}` desugars to a bare Interp, `{{...}}` to an + `esc(Interp ...)` call. Produced ONLY by a backtick raw literal; + inside a "..." string `{{` stays two literal braces, so CSS and JS + text in existing samples lexes byte-identically. *) + | SEsc of string (* raw, unlexed source of one `{{...}}`'s body *) type kind = (* literals *) diff --git a/compiler/test/golden/ast/raw-literal.expected b/compiler/test/golden/ast/raw-literal.expected new file mode 100644 index 0000000..babebc8 --- /dev/null +++ b/compiler/test/golden/ast/raw-literal.expected @@ -0,0 +1,2 @@ +1:1 METHOD page(name: Text) -> Text + 2:3 RETURN "

" .. INTERP(name) .. esc(INTERP(name)) .. "

" diff --git a/compiler/test/golden/ast/raw-literal.wo b/compiler/test/golden/ast/raw-literal.wo new file mode 100644 index 0000000..2d095f8 --- /dev/null +++ b/compiler/test/golden/ast/raw-literal.wo @@ -0,0 +1,3 @@ +fn page(name: Text) -> Text { + return `

${name}{{ name }}

` +} diff --git a/compiler/test/golden/tokens/raw-literal.expected b/compiler/test/golden/tokens/raw-literal.expected new file mode 100644 index 0000000..163611a --- /dev/null +++ b/compiler/test/golden/tokens/raw-literal.expected @@ -0,0 +1,42 @@ +1:71 NEWLINE +4:1 KW_FN +4:4 IDENT(page) +4:8 LPAREN +4:9 IDENT(name) +4:13 COLON +4:15 IDENT(Text) +4:19 RPAREN +4:21 ARROW +4:24 IDENT(Text) +4:29 LBRACE +4:30 NEWLINE +5:3 KW_LET +5:7 IDENT(one) +5:11 EQ +5:13 STR(
hi
) +5:28 NEWLINE +6:3 KW_LET +6:7 IDENT(verbatim) +6:16 EQ +6:18 STR(a"b\n) +6:25 NEWLINE +7:3 KW_LET +7:7 IDENT(block) +7:13 EQ +7:15 STR(
+ many + line +
+) +12:4 NEWLINE +13:3 KW_LET +13:7 IDENT(holes) +13:13 EQ +13:15 INTERP_STR(TEXT(

),EXPR(name),TEXT(),ESC( name ),TEXT(

)) +13:41 NEWLINE +14:3 KW_RETURN +14:10 IDENT(block) +14:15 NEWLINE +15:1 RBRACE +15:2 NEWLINE +16:1 EOF diff --git a/compiler/test/golden/tokens/raw-literal.wo b/compiler/test/golden/tokens/raw-literal.wo new file mode 100644 index 0000000..5680827 --- /dev/null +++ b/compiler/test/golden/tokens/raw-literal.wo @@ -0,0 +1,15 @@ +-- iteration 37: the backtick raw text literal. One token per literal, +-- content verbatim (no escape processing), common margin removed at +-- lex time, two hole forms -- ${} raw and {{}} escaped. +fn page(name: Text) -> Text { + let one = `
hi
` + let verbatim = `a"b\n` + let block = ` +
+ many + line +
+ ` + let holes = `

${name}{{ name }}

` + return block +} diff --git a/compiler/test/runner.ml b/compiler/test/runner.ml index 61c8875..10e969d 100644 --- a/compiler/test/runner.ml +++ b/compiler/test/runner.ml @@ -236,6 +236,128 @@ let () = check "dash-continuation: a - b (spaced) lexes as Ident, Dash, Ident" (kinds = [ Token.Ident "a"; Token.Dash; Token.Ident "b"; Token.Eof ]) +(* ---- raw text literal, iteration 37 (not golden-diffed) ------------ + + golden/tokens/raw-literal.wo pins the token STREAM; these pin the + pieces a dump cannot show: that a backtick literal with no holes is + byte-identical to the Str a "..." string would have produced, that + the common margin is removed at LEX time (so no runtime cost and no + downstream stage ever sees the source indentation), and that the two + new diagnostics fire at the right position. *) + +let () = + let collector = Diag.Collector.create () in + let toks = Lexer.tokenize collector ~file:"raw.wo" "let t = `
hi
`" in + let kinds = List.map (fun (t : Token.t) -> t.kind) toks in + check "raw literal: no holes lexes as a plain Str" + (kinds + = [ Token.KwLet; Token.Ident "t"; Token.Eq; Token.Str "
hi
"; Token.Eof ]); + check_eq "raw literal: reports nothing" ~expected:0 + ~actual:(List.length (Diag.Collector.diagnostics collector)) + string_of_int + +let () = + (* Nothing between the backticks is escape-processed: a quote is a + quote and a backslash-n is two characters, which is the whole + point of the form: markup without quote-escape noise. *) + let collector = Diag.Collector.create () in + let toks = Lexer.tokenize collector ~file:"raw.wo" "`a\"b\\n`" in + let kinds = List.map (fun (t : Token.t) -> t.kind) toks in + check "raw literal: content is verbatim, no escape processing" + (kinds = [ Token.Str "a\"b\\n"; Token.Eof ]) + +let () = + (* The margin case, written the way a render() body actually is: + opening newline dropped, the 4-space common margin removed from + every line, the whitespace-only closing line reduced to nothing + while its newline survives (Java text-block behavior). *) + let src = "let t = `\n
\n many\n
\n `" in + let collector = Diag.Collector.create () in + let toks = Lexer.tokenize collector ~file:"raw.wo" src in + let kinds = List.map (fun (t : Token.t) -> t.kind) toks in + check "raw literal: common margin stripped, leading newline dropped" + (kinds + = [ + Token.KwLet; + Token.Ident "t"; + Token.Eq; + Token.Str "
\n many\n
\n"; + Token.Eof; + ]) + +let () = + (* Both hole forms in one literal. The payloads are raw and unlexed, + exactly as SExpr has always carried `${...}` -- the parser is what + tells them apart (SEsc gains the esc() wrapper). *) + let collector = Diag.Collector.create () in + let toks = Lexer.tokenize collector ~file:"raw.wo" "`

${a}{{ b }}

`" in + let kinds = List.map (fun (t : Token.t) -> t.kind) toks in + check "raw literal: ${} stays raw, {{}} becomes SEsc" + (kinds + = [ + Token.InterpStr + [ + Token.SText "

"; + Token.SExpr "a"; + (* the empty run between two adjacent holes, exactly as a + "..." string has always produced it -- the parser drops + empty SText segments in desugar_interp *) + Token.SText ""; + Token.SEsc " b "; + Token.SText "

"; + ]; + Token.Eof; + ]) + +let () = + (* Unlike a plain "..." string, an unterminated raw literal is an + error: multi-line is its normal case, so silently swallowing the + rest of the file would be a footgun, not rt parity. *) + let collector = Diag.Collector.create () in + let toks = Lexer.tokenize collector ~file:"raw.wo" "let t = `abc" in + let kinds = List.map (fun (t : Token.t) -> t.kind) toks in + check "unterminated raw literal: closes with what was collected" + (kinds = [ Token.KwLet; Token.Ident "t"; Token.Eq; Token.Str "abc"; Token.Eof ]); + let diags = Diag.Collector.diagnostics collector in + check_eq "unterminated raw literal: exactly one diagnostic reported" ~expected:1 + ~actual:(List.length diags) string_of_int; + match diags with + | [ d ] -> + check "unterminated raw literal: WO-E004 at the backtick (line 1, col 9)" + (d.code = "WO-E004" && d.site.line = 1 && d.site.col = 9) + | _ -> check "unterminated raw literal: diagnostic shape" false + +let () = + (* A raw newline inside "..." used to be accepted silently (the + scanner's catch-all appended it like any other byte), which meant a + forgotten closing quote ate the rest of the file with no + diagnostic. Now that the backtick literal is the blessed spelling + for multi-line text, that newline is an error and the scan stops + WITHOUT consuming it, so the Newline token still terminates the + statement and the next line parses normally. *) + let collector = Diag.Collector.create () in + let toks = Lexer.tokenize collector ~file:"nl.wo" "let t = \"ab\ncd" in + let kinds = List.map (fun (t : Token.t) -> t.kind) toks in + check "newline in string: scan stops at the newline, which still tokenizes" + (kinds + = [ + Token.KwLet; + Token.Ident "t"; + Token.Eq; + Token.Str "ab"; + Token.Newline; + Token.Ident "cd"; + Token.Eof; + ]); + let diags = Diag.Collector.diagnostics collector in + check_eq "newline in string: exactly one diagnostic reported" ~expected:1 + ~actual:(List.length diags) string_of_int; + match diags with + | [ d ] -> + check "newline in string: WO-E005 at the newline (line 1, col 12)" + (d.code = "WO-E005" && d.site.line = 1 && d.site.col = 12) + | _ -> check "newline in string: diagnostic shape" false + (* ---- direct parser/AST assertions (Task 4, not golden-diffed) ------------ golden/ast/*.wo fixtures already pin the AST *shape* via --dump-ast, diff --git a/docs/00-code-review.md b/docs/00-code-review.md index 8123503..e610a6b 100644 --- a/docs/00-code-review.md +++ b/docs/00-code-review.md @@ -18,6 +18,12 @@ Native speed — the big one. Everything is interpreted: ~40× behind Go on raw ## Verification 2026-08-20 +> **Read the 2026-08-26 re-verification at the bottom before quoting anything +> from this section.** Eight of its rows have since been overtaken by shipped +> work. The section is kept as written — it is a dated measurement, and +> rewriting it would destroy the record of what was true when the iteration +> order was re-sequenced against it. + Every claim above was checked against the tree. **26 of 27 hold. One number does not, and two problems are worse than stated.** @@ -79,7 +85,7 @@ the number or produce the benchmark. | 22's battery never run | 22 is ⬜ "needs a spec first"; no `bench/baseline.json`, no `just db-bench`; `runtime/bench/` is the retired C prototype's harness | | TSan covers one demo | only `scripts/fibers-accept.sh` builds and runs `wovm_tsan` | | no fuzzing, no CI | no `.github/`, no fuzz target | -| one framework, five samples, one consumer | exact: `writeonce-framework`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` | +| one framework, five samples, one consumer | exact: `writeonce-serve`; employee, employee-list, fibers, gc-cycle, log-watcher; `web-app` | ### Consequence @@ -87,3 +93,41 @@ The iteration order in [`stories/language-runtime-database/00-story.md`](stories/language-runtime-database/00-story.md) was re-sequenced against these findings on 2026-08-20 — Seq only, no `#` renumbered, no file moved. See that table's second re-sequencing note. + +--- + +## Re-verification 2026-08-26 + +Re-run against the tree, reading source rather than documents. **Eight rows +have been overtaken by shipped work; the rest still hold.** Overtaken: + +| 2026-08-20 row | What the source says now | +| --- | --- | +| "no `Float`, no `Bytes`" | `types.ml`'s `builtin_scalars` is `["Int"; "Bool"; "Text"; "Timestamp"; "Id"; "Float"; "Bytes"]` — iteration 19, plus the `float`/`trunc` bridges and the `bytes_*`/`base64_*` builtins | +| "`send` is one-way — `WO_B_SEND=69` is the last builtin (`WO_B_MAX 69u`)" | `WO_B_MAX` is `95u`; `WO_B_CALL = 88` is a send that parks the caller for a typed scalar reply (iteration 24, WO-E226) | +| "no crypto primitives" | `WO_B_SHA1 = 85`, `WO_B_SHA256 = 86`, `WO_B_HMAC_SHA256 = 87`; `runtime/src/crypto.c`, vector-accepted in `test_crypto.c` (iteration 34) | +| "unbounded mailboxes, no backpressure" | mailboxes are capped (`WO_MAILBOX`, default 1024) with a sender-side reserve and a catchable `WO_T_ACTOR` trap on overflow | +| "no supervision, links, actor death" | **partly** overtaken: actor death landed with `call` — a dead or mid-call callee traps the caller instead of hanging it. `monitor` (id 89) and `time.after` (id 90) are still literal holes in the builtin enum; supervision trees remain absent | +| "22's battery never run — no `bench/baseline.json`, no `just db-bench`" | `bench/baseline.json` exists with the campaign's metrics, `just db-bench`/`db-bench-quick` are recipes, `bench/results/` holds the runs, iteration 22 is done | +| "no fuzzing, **no CI**" | `.github/workflows/release.yml` builds, verifies and publishes on a `v*` tag. Fuzzing is still absent, and CI is release-only — nothing runs the gates per change, which is iteration 30's remaining half | +| "one framework, five samples, one consumer" | two libraries (`writeonce-serve`, `writeonce-view`) and 13 samples, 8 of them gated | +| "The multi-shard DB gap is structural" (Understated) | closed by the arc's stage 3: the string `"database engine not initialized"` no longer exists in `runtime/src/`, worker statements marshal to the owner shard, and `just db-actor` gates it | + +Still true, re-checked at the source: interpreted-only with no JIT and no SIMD; +the ceilings correction (`WO_STACK_SLOTS 4096`, `WO_MAX_REGS 64`, +`WO_MAX_FRAMES 256`, `WO_MAX_SHARDS 64`); no generics beyond `multi`/`map`; no +closures or function values; byte strings with no Unicode awareness; traps and +`try` instead of Result values; `switch` without destructuring; round-robin +placement with no work stealing; no timers beyond `time.sleep`; no TLS; no +HTTP/2; observability is `print`/stderr with no counters, tracing or profiler; no +debugger and no LSP; deps are git-rev-only with no registry, semver or transitive +resolution; blue-green and migrations are futures; TSan covers one demo. And +**`map` lookup is still a linear scan** — `runtime/src/cont.h` says so in +its own header comment, which keeps it the compute problem this document argued +it was. + +Two capability gaps this re-run named that the original critique did not, now +[iteration 38](stories/language-runtime-database/38-content-platform-capabilities.md): +`fs` has six builtins (ids 40–45) and can create, grow and read a file but never +replace, truncate, delete or rename one; and there is no `net.connect` anywhere +in `runtime/src/`, so no program can open an outbound connection. diff --git a/docs/00-dependency-graph.md b/docs/00-dependency-graph.md index a610c6d..4f02db7 100644 --- a/docs/00-dependency-graph.md +++ b/docs/00-dependency-graph.md @@ -6,8 +6,12 @@ > to pick the next implementation: anything whose incoming arrows are all > green is startable today. Rebuilt 2026-08-20 from a sweep of every > story/spec/plan markdown (the "misses" pass: iteration 17's outgoing -> edges, the concurrency chain, the post-12 parked drain, 9b→10, -> 14's gap fan-out, 20's fiber caveat). +> edges, the concurrency chain, the parked drain, 9b→25, 28's gap +> fan-out, 20's fiber caveat), and **refreshed 2026-08-26** against the +> code and the story frontmatter: graph 1 had drifted a generation +> behind — it still showed 17 parked and 18 as next, and it used the +> pre-renumber ids 10/12/13/14 for what are now stories 25/26/29/28. All +> iterations through 38 are now nodes. ## 1. Story iterations @@ -26,23 +30,36 @@ flowchart TD I15["15 deps package manager"]:::done I16["16 web framework v1 core"]:::done - I17["17 library kind + internal/ (PARKED — spec+plan ready, branch library-internal)"]:::parked - FWREORG["framework internal/ reorg + check mode (kills the --emit workaround; WO-E108/E109 reserved)"]:::parked - I18["18 framework v2: transaction{} + cache/flags/jobs (spec APPROVED — the next implementation)"]:::specd + I17["17 library kind + internal/ ✅ 2026-08-20"]:::done + FWREORG["framework internal/ reorg + check mode ✅ landed with 17 (WO-E108/E109 shipped)"]:::done + I19["19 Float + Bytes ✅ 2026-08-20"]:::done + I37["37 wo-html components + raw text literal ✅ 2026-08-25"]:::done + I35["35 net runtime seams ✅ 2026-08-23"]:::done + I36["36 operator parity: not/bitwise/hex literals — code landed 2026-08-22, awaiting the manual pass"]:::specd + RELEASE["packaging + release pipeline ✅ 2026-08-25 (no story: VERSION, just dist, install-accept, release.yml)"]:::done - I9c["20 cross-program tables (half-built)"]:::open - I9d["21 keypair attach auth (half-built; crypto+handshake already on its branch)"]:::open + I18["18 framework v2: transaction{} + cache/flags/jobs (⏸ hold 2026-08-21; spec+plan approved, held intact)"]:::parked + + I9c["20 cross-program tables (⏸ hold 2026-08-21; channel half-built)"]:::parked + I9d["21 keypair attach auth (⏸ hold 2026-08-21; crypto floor now exists via 34)"]:::parked I9e["22 durability + throughput baseline ✅ 2026-08-21"]:::done I8["8 shard-actor runtime ✅ 2026-08-21"]:::done + I24["24 chat + actor lifecycle 🔄 THE LIVE SLICE (absorbing 31 + 34)"]:::specd + I34["34 crypto builtins ✅ code landed as 24's T1 (ids 85-87)"]:::done + I31["31 actor lifecycle — call/mailbox-cap/death landed in 24; monitor + time.after (ids 89/90) open"]:::specd I9f["23 io_uring group-commit"]:::open - I10["10 HTTP service layer (lowers onto the framework)"]:::open + I32["32 WAL checkpoint (append-only today; bounds replay)"]:::open + I33["33 single-file store WO_DATA=.db (driver-only, off-chain)"]:::open + I25["25 HTTP service layer — `service` blocks (⏸ hold 2026-08-21; story file removed, plan remains)"]:::parked I11["11 fibers ✅ 2026-08-21"]:::done - I12["12 blue-green deploy"]:::open - I13["13 metaprogramming @derive"]:::open - I14["14 skillhost workload (demoted)"]:::open - I9g["27 query grammar corpus (likely collapses)"]:::open - GAPS["14's gap fan-out: bounded subprocess, stdin/stdout transport, fs metadata, FFI-vs-out-of-process"]:::open - DRAIN["post-12 parked drain: pub(read)/using/#if, WO-E225, ADT roster, group-by"]:::parked + I26["26 blue-green deploy (⏸ hold)"]:::parked + I29["29 metaprogramming @derive (⏸ hold)"]:::parked + I28["28 skillhost workload (⏸ hold; demoted)"]:::parked + I38["38 content platform capabilities: fs mutation verbs + net.connect"]:::open + I9g["27 query grammar corpus (⏸ hold; likely collapses)"]:::parked + I30["30 observability, CI, fuzz — release-only CI exists; per-change gates + fuzz open (no story file)"]:::open + GAPS["28's gap fan-out: bounded subprocess, stdin/stdout transport, fs metadata, FFI-vs-out-of-process"]:::open + DRAIN["parked drain, what is LEFT of it: WO-E225 roster, ADT roster, group-by aggregates"]:::parked FOUND --> I7 FOUND --> I9 @@ -53,34 +70,53 @@ flowchart TD I15 --> I17 I16 --> I17 I17 --> FWREORG + I16 --> I37 + I19 --> I37 + I17 --> RELEASE I9 --> I18 I16 --> I18 I9 --> I9c I9c --> I9d - I9c --> I10 - I9b --> I10 - I16 --> I10 + I9c --> I25 + I9b --> I25 + I16 --> I25 I9b --> I9e I7b --> I8 I9e --> I9f I8 --> I9f I8 --> I11 - I9 --> I12 - I10 --> I12 - I9g --> I14 - I7 --> I14 - I14 --> GAPS - I12 -.scope directive.-> I13 - I12 -.scope directive.-> DRAIN + I19 --> I34 + I34 --> I24 + I8 --> I24 + I11 --> I24 + I35 --> I24 + I31 --- I24 + I9f --> I32 + I32 --- I33 + I9 --> I26 + I25 --> I26 + I9g --> I28 + I7 --> I28 + I28 --> GAPS + I16 --> I38 + I32 --> I38 + I36 -.reopens the pure-wo HMAC question.-> I34 + I26 -.scope directive.-> I29 + I26 -.scope directive.-> DRAIN ``` -Reading it: **18 is the only spec-approved open node with all -prerequisites green — the next implementation.** After 18: 20/21 and 22 -are startable (chosen order: 20/21 first — half-built branches rot). -17 unparks on directive: its prerequisites landed, its spec+plan wait on -branch `library-internal`, and its landing brings the framework reorg -node with it. 13 and the parked drain sit behind 12 by the 2026-08-08 -scope directive (dashed), not by any technical edge. +Reading it: **the live slice is 24** (chat + actor lifecycle, absorbing 31 +and 34), and the chain behind it is 23 → 32. Everything else with all-green +incoming arrows is startable: **33** (driver-only, off-chain), **38** (the +fs-mutation and outbound-socket gaps), and **30**'s remaining half +(per-change CI and fuzzing — the release pipeline covered only publishing). +**36** needs no work, only the developer's manual pass over +`docs/examples/operators/`. The held tail — 18, 20/21, 25, 26, 27, 28, 29 — +resumes on its own precedence notes; 29 and what is left of the drain still +sit behind 26 by the 2026-08-08 scope directive (dashed), not by any +technical edge. Note what left the drain: `pub(read)`, `using` and `#if` all +shipped, so only the WO-E225/ADT rosters and group-by aggregates remain in +it. ## 2. The concurrency chain (iterations 8 / 23 / 11 and everything they gate) diff --git a/docs/00-doc-audit.md b/docs/00-doc-audit.md new file mode 100644 index 0000000..e474c1e --- /dev/null +++ b/docs/00-doc-audit.md @@ -0,0 +1,535 @@ +# Documentation truth audit — 2026-08-26 + +> **Status: findings resolved 2026-08-26, same day.** Every section below was +> acted on; see [What was fixed](#what-was-fixed) at the end for the +> disposition of each, including **one row where this audit was wrong and the +> document it accused was right** (B3's `WO-W201` claim). The findings are kept +> as written — a fix list whose findings have been edited away cannot be +> checked. `just linkcheck` went from 77 broken paths to 23, and all 23 that +> remain are in `.dev/`, which this repo does not author. +> +> A separate structural directive landed the same day and **removed the status +> folders** (`done/`, `refine/`, `hold/`, `in-progress/`) — status now lives only +> in frontmatter. Paths of the form `…/done/NN-*.md` quoted in the findings below +> were correct when written and no longer resolve; see +> [Structural change 2026-08-26](#structural-change-2026-08-26--status-folders-removed). + +Scope: every `*.md` that documents THIS repo — root `README.md`, the numbered +`docs/0*.md`, `docs/guides/`, `docs/stories/`, `docs/plan/`, `docs/examples/`, +`docs/superpowers/`, the code-directory READMEs and `CODE-LOGIC.md` files, +`tests/corpus/README.md`, `bench/compare/go-sqlite/README.md`, +`scripts/install-readme.tmpl.md`, `.claude/agents/database-developer.md`. + +Excluded, and why: `.dev/skills/` (vendored copies of plugin skills, not ours), +`.dev/reference/` (other people's codebases), `.superpowers/sdd/` (dated task +reports — snapshots, correct as history). + +Method: claims were checked against the tree, not read off prose. `woc` +(`compiler/_build/default/bin/woc`) and `wovm` (`runtime/wovm`), both built +2026-08-25, were run against every sample; `justfile` recipes, `wob.h` +constants, `types.ml`'s builtin tables, story frontmatter and +`scripts/linkcheck.py` were used as ground truth. Nothing in this report is +inferred from another document. + +Verdict: **the deep reference docs are in good shape; the front door is not.** +`docs/guides/language-surface.md`, `docs/plan/oop-vm/08-builtin-surface.md`, the +three `CODE-LOGIC.md` files and the status board's tables track the code +closely. The root `README.md`, `runtime/README.md`, `docs/00-code-review.md`, +`docs/00-dependency-graph.md` and two example status banners describe a repo +that stopped existing between one and six weeks ago. + +No document was changed by the audit pass itself — the findings below record the +tree as it stood before any fix. What was then changed in response is listed in +[What was fixed](#what-was-fixed). + +--- + +## A. Wrong about shipped features (the highest-cost class) + +### A1. `README.md` — the Roadmap lists three landed features as unavailable + +`README.md:326-342` is headed "Planned, **not yet available**", and +`README.md:10-14` promises "Features that are planned but **not yet available** +are listed separately under Roadmap — they are not described as if they work." +Three of its six entries have shipped: + +| README claim | Reality | +| --- | --- | +| `:336` "**Concurrency** — a shard-actor runtime and green-threaded fibers." | Both landed 2026-08-21 (iterations 8 and 11, both `done/`). `spawn` is a lexer keyword (`lexer.ml:163`); `send`/`call` are builtins (`WO_B_SEND=69`, `WO_B_CALL=88` in `wob.h`); `actor M` is a type (`types.ml`'s `TActor`). Gates exist and run: `just fibers`, `just db-actor`. `runtime/src/park.c` is the parking implementation; `runtime/test/test_fiber.c` and `test_mailbox.c` are its unit suites. | +| `:335` "**HTTP service layer** — `service` blocks that route requests to methods." | The *`service` block syntax* is genuinely absent — that half is honest. But it sits under a banner that also denies HTTP entirely, which is false (see A2). | +| `:332` "**Query aggregates** — `group … by … into g`" | **This one is correct.** `types.ml:2220,2241` rejects it: "group-by aggregation is not supported yet". Kept here only because `docs/guides/language-surface.md` contradicts it — see C1. | + +### A2. `README.md:33-37` — "does not serve HTTP, WebSockets, or a UI" + +> "writeonce is **not** a web framework and does not (yet) serve HTTP, +> WebSockets, or a UI." + +Contradicted 30 lines later by its own §"Worked examples" (`:306-313`), which +describes `docs/examples/porch/` as "a web framework written in +writeonce (HTTP/1.1 …, router with `:param` captures, interface-based +handlers)" and `just web-app` as its gate. Also contradicted by: + +- iteration 16 (web framework) and 37 (wo-html components), both `done/`; +- `docs/examples/site/` — server-rendered pages, gated by `just site`; +- WebSockets: `docs/examples/porch/http/ws.wo` (`ws_accept`, the 101 + hijack sentinel) and `http/wsframe.wo` (a pure-`.wo` RFC 6455 frame codec), + both landed per `docs/in-progress/2026-08-23-chat-ws-lifecycle.md` (T6, T7). + +### A3. `README.md:30` — "no package manager" + +> "**Small on purpose.** No FFI, no package manager, no framework." + +Same page, `:265-281`, documents `[deps]`, `.wo-deps//`, `wo.lock` and +`woc --update-deps`. Iteration 15 (deps package manager) is `done/`; +`just deps-accept` is its gate. "No framework" is contradicted by `:306`. +The intended claim is presumably "no *registry*" — which is true and is what +`docs/00-code-review.md:77` says. + +### A4. `docs/examples/employee/README.md:3` — "does not compile on today's toolchain" + +> "**Status: target workload — does not compile on today's toolchain.** … +> It becomes buildable when iteration 9 … and iteration 9b … land." + +Both landed. `woc docs/examples/employee/` exits 0. `just employee` is a +first-class acceptance gate, and the `justfile:88-91` comment calls it "the +database track's acceptance workload". The banner is ~2 weeks stale. + +### A5. `docs/examples/log-watcher/README.md:12-20` — same shape + +> "**Status: design artifact — the spec's forcing function.** The systems +> track is approved, pre-implementation. Today's `woc` (milestone 1) … +> diagnoses the adopted surface as WO-E101: `use`, `typedef`, standalone union +> aliases …, `pub(read)`, `switch`, `try`." + +Every one of those forms is shipped (`docs/guides/language-surface.md` §2–§5, +verified in `lexer.ml`/`parser.ml`). `woc docs/examples/log-watcher/` exits 0. +`just log-watcher` is the gate the `justfile:82-87` calls "the test the whole +track exists to pass". + +Same file, `:8-9`: "the five builtin stdlib modules — `fs`, `proc`, `net`, +`time`, `json`". There are **six** (`types.ml:206`): `env` is missing. + +### A6. `runtime/README.md` — describes the pre-2026-08-18 world + +The directory's orientation README is still the old `wo-rt-c` prototype page +with the VM bolted on at `:78`. Concretely wrong: + +- `:31` "Or from the repo root: `just rt-c-demo`" and `:60` "`just rt-c-bench`" + — **neither recipe exists.** The justfile has 16 recipes plus two `mod`s; + no `rt-c-*` among them. +- `:5,:83` "the production Rust runtime (`crates/rt/`)", `:76` "This file is for + reading; `crates/rt` is for running writeonce" — the Rust runtime was removed + 2026-08-18 (`docs/08-project-structure.md:11`). +- `:3` `prototypes/wo-db/`, `:43` `docs/runtime/database/03-inmemory-engine.md`, + `:47` `docs/plan/09-concurrency-scaleout.md` — none of these paths exist + (also in the link audit's sections B/C/E). +- `:84` "**`@gc` reference counting** + budgeted cycle collection … Bacon–Rajan + trial deletion" — retired by iteration 7b. `@gc` on a class is now + **rejected** (`docs/guides/language-surface.md:73`), and + `runtime/src/CODE-LOGIC.md` states the replacement outright: "incremental + tri-color mark-sweep … (iteration 7b — RC and Bacon–Rajan are gone)". +- `:89` "`DB_STUB` traps 'engine not linked' until the DB engine binds (plan + 5)" — the engine bound in iteration 9. The opcode survives + (`wob.h:232`, `vm.c:1806`) but the sentence reads as "no database yet". +- `:89` builtin list "`now/print/print_int/words/multi_*/map_*`" — there are + now ~70 free builtins plus six module namespaces (`types.ml:784-849`). +- File map (`:92-107`) omits four of the thirteen sources in `runtime/src/`: + `crypto.c/.h`, `json.c`, `park.c/.h`, `sysio.c`. +- `:105` and `:117` "13 suites" / "one of the 13 ASan test binaries" — there + are **18** (`runtime/test/test_*.c`). +- `:1` "now at **phase E**" vs `:57` "A → B → C → D → E → F, all ✅ shipped" + vs `:60` "Measured (phase F…)" — three answers in one file. + +Verified-correct in the same file, for contrast: `WO_HEAP_MB` / 64 MiB arena, +the four `.vscode/launch.json` configs, the exit-code contract, and the +`make -C runtime` targets. + +--- + +## B. Verification tables that no longer verify + +### B1. `docs/00-code-review.md` — the 2026-08-20 table has decayed + +The doc's value is that it *checked* a critique line by line. Seven rows of +`:55-82` are now false, and the doc carries no superseded banner: + +| Row | Then | Now | +| --- | --- | --- | +| "no `Float`, no `Bytes` \| absent from `compiler/src/types.ml`" | true | `types.ml:174` `builtin_scalars = ["Int";"Bool";"Text";"Timestamp";"Id";"Float";"Bytes"]` — iteration 19, `done/` | +| "`send` is one-way \| `WO_B_SEND=69` is the last builtin (`WO_B_MAX 69u`)" | true | `WO_B_MAX 95u`; `WO_B_CALL = 88` is a send that parks for a typed reply | +| "no crypto primitives \| none" | true | `WO_B_SHA1=85`, `WO_B_SHA256=86`, `WO_B_HMAC_SHA256=87`; `runtime/src/crypto.c`; `runtime/test/test_crypto.c` | +| "22's battery never run \| … no `bench/baseline.json`, no `just db-bench`" | true | `bench/baseline.json` exists, `just db-bench` / `db-bench-quick` exist, 30+ result files in `bench/results/`, iteration 22 is `done/` | +| "no fuzzing, no CI \| **no `.github/`**, no fuzz target" | true | `.github/workflows/release.yml` exists (fuzzing still absent) | +| "one framework, five samples, one consumer" | true | 13 sample projects under `docs/examples/` | +| "accept on one shard \| one listener, `SO_REUSEADDR` only" | true | iteration 35 landed `serve_conn` + fiber-per-connection | +| `:46-51` "The multi-shard DB gap is structural … `wo_builtin_db` returns `WO_T_DB` 'database engine not initialized'" | true | that string is gone from `runtime/src/`; arc stage 3 landed the transparent DB actor, gated by `just db-actor` | + +Rows that still hold, checked: interpreted-only/no JIT, no SIMD, the +`WO_STACK_SLOTS 4096` / `WO_MAX_REGS 64` / `WO_MAX_FRAMES 256` correction, no +generics, no closures, byte strings, no Result type, switch-not-destructuring, +no supervision, growable mailboxes, no `timerfd`, no TLS, no debugger/LSP, +deps-are-git-rev-only, blue-green is a future, TSan covers one demo. And +**`map` lookup is still a linear scan** — `cont.h:1-6` says so in as many +words. + +### B2. `docs/00-link-audit.md` — numbers and paths both stale + +Dated 2026-08-20. `just linkcheck` today reports **files=235, local=675, +broken=77, bad anchors=0**; the doc's table says 206 / 569 / 88. Its own +sections B–F sum to 77, not the 88 its prose claims twice (`:12`, `:145`) — +an internal arithmetic error independent of the drift. + +Its repair table (`:29-37`) references paths that have since moved: +`docs/00-status.md` (now `docs/stories/00-status.md`), and +`refine/{08,11,19,20,21}` (now under `done/` and `hold/`). + +Two broken links exist today that the audit does not account for: + +- `docs/examples/employee-list/README.md:5,6` → `…/refine/20-cross-program-tables.md` + and `…/refine/21-keypair-attach-auth.md`; both files are now in `hold/`. +- `docs/stories/language-runtime-database/hold/26-blue-green-deploy.md:9` → + `00-story.md`; the sibling stopped being a sibling when 26 moved into `hold/`. + +Conversely `docs/00-principles.md:57,77,78,87` — four links the audit lists as +open — resolve now. + +### B3. `docs/plan/oop-vm/01-error-catalog.md` — not the complete catalog it claims + +`docs/guides/language-surface.md:8` calls it "every diagnostic"; +`compiler/README.md:39` says "every shipped code is cataloged" there. Ten +codes the compiler emits are absent: + +| Code | Defined at | What it is | +| --- | --- | --- | +| `WO-E003` | `lexer.ml:56` | `#if`/`#else`/`#end` misuse | +| `WO-E108` | `bin/main.ml:277` | `internal/` crossed at a `[deps]` boundary | +| `WO-E109` | `bin/main.ml:275` | unknown `wo.toml` `kind` value | +| `WO-E219`–`WO-E223` | `types.ml` | five type-pass codes | +| `WO-E226` | `types.ml:443` | `call`'s reply type through actor-`M` erasure (iteration 24) | +| `WO-E250` | `types.ml:435` | the whole query surface (iteration 9b) | + +`WO-E250` is the notable one: it is the only diagnostic the shipped query +language produces, and it is the code a reader hits first when they mistype a +query. Meanwhile `WO-W201` is still catalogued and no longer exists — the +inferred-GC plan (`docs/superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md:151`) +listed "retire WO-W201" as an amendment; the code went, the catalog entry +stayed. + +`WO-E004`/`WO-E005` (raw text literal, iteration 37) *are* catalogued — +checked, since `language-surface.md:29,31` depends on them. + +--- + +## C. Docs that disagree with each other + +### C1. Is group-by shipped? Two live docs, two answers + +- `README.md:332` — Roadmap, "not yet available". **Correct.** +- `docs/guides/language-surface.md:175` — "Present today: from / where / order + / take / select **plus group-by aggregation**." **Wrong**, and the same page + opens (`:16-17`) with "Every form listed below was compiled and run against + `woc`/`wovm` while writing this page, not read off the parser and hoped for." + The `group … by … into` clause in its §6 grammar block *parses* + (`parser.ml:1137-1144`) and is then rejected by the typechecker + (`types.ml:2241` "group-by aggregation is not supported yet"; + `types.ml:2222` for the navigation form). + +Reproduced: `docs/examples/employee-list/main.wo:38-41` uses `group e by +e.dept into g` and fails to compile. + +`docs/00-dependency-graph.md:45` correctly still lists group-by under the +parked drain. + +### C2. `docs/stories/00-status.md` — the narrative and the table disagree about iteration 24 + +The board's *Current work* table is right: `:314` records "🔄 **iteration 24 +(absorbing 31 + 34): chat + actor lifecycle** — spec + plan approved +2026-08-23 … executing on branch `chat-ws-lifecycle`" and links the marker. + +The ▶ NEXT PLAN narrative above it is not: + +- `:82-85` "Next slice: **iteration 31, actor lifecycle** — its spec brainstorm + is the next act". 31 was absorbed into 24 by directive, and its + actor-death half already landed (`docs/in-progress/2026-08-23-chat-ws-lifecycle.md:20-23`, + commit `ed69841`). +- `:125` "**Next steps:** 31 (lifecycle spec brainstorm) → 24 (chat) → 23 → 32" + — same stale ordering. +- `:286` iteration 24 marked "⬜ fourth in chain … after 31". +- `:290` iteration 34 marked "⬜ off-chain but GATES 24". T1 crypto landed + (`d14fa9f`); `sha1`/`sha256`/`hmac_sha256` are in both `types.ml:847-849` + and `wob.h:461-463`. The gate is cleared — + `docs/00-dependency-graph.md:169` already says so. + +Also missing from the board entirely: the 2026-08-25 packaging/release track. +`VERSION`, `scripts/mkdist.sh`, `just dist`, `just install-accept`, +`.github/workflows/release.yml`, `docs/guides/releasing.md` and `dist/writeonce-0.1.0-linux-amd64.tar.gz` +all exist; the last five commits are that work; no standup entry covers it. + +### C3. `docs/00-dependency-graph.md` — the main graph is a generation behind + +Its own header (`:3`) defers state to the board, but it paints state inline +anyway, and the first mermaid graph paints it wrong: + +- `:29` `I17["17 library kind + internal/ (**PARKED** — spec+plan ready…)"]:::parked` + — story 17 is in `done/` with `status: done`; board `:302` reads + "✅ **landed 2026-08-20**". +- `:31` `I18[… (spec APPROVED — **the next implementation**)]:::specd` — story + 18 is in `hold/`; board `:303` reads "⏸ hold (2026-08-21)". +- `:33,34` iterations 20/21 as `open` — both `hold/`. +- `:38,40,41,42` use the pre-renumber ids: "10 HTTP service layer", "12 + blue-green deploy", "13 metaprogramming @derive", "14 skillhost workload". + The stories are **26**-blue-green-deploy, **29**-compile-time-metaprogramming, + **28**-skillhost-host-workload; no story numbered 10, 12, 13 or 14 exists. +- `:45` `DRAIN["post-12 parked drain: pub(read)/using/#if, …"]` — `pub(read)` + (`ast.ml:118-123`), `using` (`lexer.ml:164`) and `#if` (`lexer.ml:245-249`) + all shipped. +- The main graph has no node for iterations 19, 24, 31, 32, 33, 35, 36 or 37. + Four of those are `done/`. The later sub-graphs *do* cover 34/35/36 + correctly (`:141,164-170`), so the drift is confined to the first graph. + +--- + +## D. Structural claims that don't match the tree + +### D1. `docs/08-project-structure.md` — "canonical map", four divergences + +- `:39` "`plan/` compiler-track docs: architecture.md + the woc plans" under + `compiler/`. **`compiler/plan/` does not exist**; those docs live at + `docs/plan/compiler/` — which the same file's `:49` links correctly. +- `:22,76-77` `tests/corpus/` as "`run/`, `compile-fail/`, `trap/`, `gc/`". + There are nine directories: also `actor/`, `db/`, `lang/`, `sys/`, + `sample-logwatcher/` — all five **empty**. `tests/corpus/README.md:11-15` + documents them as planned per-plan additions, so the corpus README is the + honest one; the structure doc undercounts and neither mentions that five are + placeholders. (Live fixture counts: run 60, compile-fail 46, trap 5, gc 2.) +- `:79-81` `scripts/` as "`oop-e2e.sh`, `mkdist.sh` + `install-accept.sh`, and + the per-sample acceptance scripts (`employee-accept.sh`, + `log-watcher-accept.sh`)". There are 14 scripts; unmentioned: + `db-actor-accept.sh`, `db-bench.py`, `deps-accept.sh`, `fibers-accept.sh`, + `linkcheck.py`, `single-binary-smoke.sh`, `site-accept.sh`, + `web-app-accept.sh`. +- `:89` `examples/` as "log-watcher/, employee/, employee-list/ samples" — + there are 13. +- `:87` puts status at the `docs/` root; it is `docs/stories/00-status.md` + (the file's own `:5` links it correctly). The root also has + `00-dependency-graph.md` and `00-link-audit.md`, unlisted. +- The one-page map (`:17-29`) omits four tracked root entries: `bench/`, + `dist/`, `.github/`, `.claude/`. +- `docs/examples/db-actor/` has `main.wo`, a `wo.toml` and a gate + (`just db-actor`) but **no README** — the only sample without one. + +Correct in the same file, checked: `runtime/wo-rt.c` exists; `.dev/` is +gitignored except `.dev/README.md` (`git ls-files .dev` returns exactly one +path) and `.dev/reference/` does hold `crates/`, `colibri/`, `llama-cpp/`, +`linux/`, `go/`. + +### D2. `compiler/README.md` — stage banner and CLI list both behind + +- `:5` "**Stage: plan 3 … complete, Tasks 1–6 + 8**". Plan 3 closed in early + August; the front end has since taken iterations 15, 17, 19, 24, 34, 35, 36 + and 37. A reader takes this page as the compiler's current extent. +- `:26-34` "Running `woc`" omits four of the nine modes the binary's own + `usage_msg` prints: `--dump-gc`, `--update-deps `, `version`, and the + `woc ` manifest build (the mode `README.md:111` teaches as the primary + one). `-D `, the `#if` flag setter documented at + `language-surface.md:34`, is in neither the README nor `usage_msg` — + it exists at `bin/main.ml:1162`. +- `:37` "same contract as `wo run` (`crates/rt/src/lib.rs::discover`)" — the + Rust runtime is gone. The identical stale sentence is also the doc comment + at `compiler/bin/main.ml:16-19`. *(Code, not markdown — noted, not + changed.)* + +Correct: the `=== path ===` multi-file header (`dump.ml:128`), the five golden +stages, the exit-code contract, OCaml 4.14 / dune 3.14 (`dune-project` says +`(lang dune 3.14)`). + +### D3. `runtime/src/CODE-LOGIC.md` — file table missing two sources + +`:12-25` is a complete-looking table of "the files, in dependency order" and +omits `park.c/.h` (fiber parking — iteration 11, and central to how blocking +builtins work) and `crypto.c/.h` (iteration 34). The doc is dated 2026-08-14, +before both; nothing marks it as of-that-date beyond the first line. + +`database/src/CODE-LOGIC.md` and `compiler/src/CODE-LOGIC.md` were checked +against their sources and hold up. + +--- + +## E. Runbook and instruction errors + +`docs/guides/releasing.md`, authored 2026-08-25, contains steps that cannot be +followed: + +- `:110` (step 11) "Remove `--draft`, commit, push." **`.github/workflows/release.yml` + contains no `--draft`** — `gh release create` at `:135-139` passes only the + two asset paths, `--title` and `--generate-notes`. Nothing to remove. +- Steps 5, 6 and 10 disagree with each other. `:50-54` (step 5) says the + rehearsal is a `workflow_dispatch` run that "skips the tag guard and the + publish step"; `:56` (step 6) says "nothing to undo — a dry run creates no + tag and no release"; `:89-92` (step 10) then instructs + `gh release delete v0.0.0-test --yes` and two tag deletions. There is no + path in the workflow that creates `v0.0.0-test`. + +`.github/workflows/release.yml:1-2` — "this workflow has never run. Authored +2026-08-25 and not executable locally" — is contradicted by `:43` of the same +file ("The first run failed here with `dune: command not found`") and by +commits `05fafd3`, `d66087d`, `4470f03`, which are fixes read off real runs. +*(Code comment, not markdown.)* + +Verified correct against the workflow and the built binaries: the asset name +`writeonce-0.1.0-linux-amd64.tar.gz`, the tag↔`VERSION` guard, the +`ubuntu-22.04` pin and its glibc reasoning (this machine's binaries need +`GLIBC_2.38`, matching `:8` step 8's "2.38 from this dev machine"), and +`scripts/install-readme.tmpl.md:24-25` — `woc version` prints +`writeonce 0.1.0 linux/amd64` and `wovm --version` prints `wovm 0.1.0`, exactly +as documented. + +--- + +## F. Small factual errors + +| Where | Claim | Actual | +| --- | --- | --- | +| `README.md:28` | "~100 KB for the sample programs" | 163–254 KB. Smallest built sample 163,117 B (`fibers`), largest 253,808 B (`site`); bare `wovm` is 161,848 B, so ~160 KB is the floor | +| `README.md:142` | "**Types:** `Int`, `Text`, `Bool`, and user `class` types" | seven builtin scalars (`types.ml:174`): also `Float`, `Bytes`, `Timestamp`, `Id` | +| `README.md:170` | `time` → "`sleep`, `now`, `local`, `iso`" | also `ticks` (µs monotonic, builtin 84 — iteration 22's one runtime addition) | +| `README.md:172` | `net` → "TCP `listen`/`accept`/`read`/`write`/`close` (host + port)" | also `read_dl`, `accept_dl`, `write_dl`, `listen_unix`, `peer` (ids 91–95, iteration 35) | +| `README.md:344` | "`net` is TCP host+port only" | `net.listen_unix` binds a unix socket (builtin 94) | +| `README.md:272,276-277` | `[deps]` key `niceserve`, then "`use niceframework`" | the `[deps]` KEY *is* the module name — `docs/examples/web-app/wo.toml:19-20` keys it `serve` and `main.wo:9` says `use serve`. The example as written would not compile | +| `README.md:295` vs `:298-320` | "**Two** complete sample programs" | three bullets follow; `:322` "Read **either** program's `main.wo`" compounds it. There are 13 samples, 8 of them gated | +| `docs/guides/language-surface.md:36` | "**Keywords (35)**" | 37. The list printed immediately after is complete and correct against `lexer.ml:147-184` — only the count is wrong | +| `docs/00-principles.md:104-105` | capabilities are "(`fs`, `proc`, `net`, `time`, `json`)" | six modules — `env` missing (`types.ml:206`) | +| `docs/superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md:153-154` | `[x]` "Add a `docs/examples/gc-cycle` acceptance script + `just gc-cycle` recipe" / `[x]` "Verify: `just gc-cycle` green" | **no `gc-cycle` recipe exists** and there is no `scripts/gc-cycle-accept.sh`. `docs/examples/gc-cycle/` has sources, a `target/` and a "Run status" section (`README.md:186`) but no gate. Two checked boxes for work that did not land | + +Forward references that are correctly labelled and are *not* findings: +`just chat` (iteration 24, T9 — pending in the marker), `just oop-parity` +(deferred by explicit decision, recorded at `compiler/README.md:5`), and +`docs/examples/employee-list/`, whose banner honestly says it does not compile +(confirmed: `woc` exits 2 on `[connect.employee]`, a section for the `hold/` +iteration-20 feature). + +--- + +## What to fix first + +1. **`README.md`** — it is the writeonce.de landing content + (`docs/08-project-structure.md:28`), so A1–A3, F's README rows and the + `use niceframework` example are the highest-value corrections in the repo. +2. **The two example status banners** (A4, A5) — one line each, and they + currently tell a visitor that the repo's two flagship gates don't build. +3. **`runtime/README.md`** (A6) — the largest single body of stale prose. It + wants splitting: `wo-rt.c` is a historical reference card, `runtime/src/` is + the shipped VM, and one page is trying to be both. +4. **`docs/00-code-review.md`** and **`docs/00-link-audit.md`** (B1, B2) — both + are dated verifications whose value depends on being re-run. Either re-run + them or banner them as of-date. +5. **`docs/plan/oop-vm/01-error-catalog.md`** (B3) — it is cited as normative by + two other docs; ten missing codes including the query surface's only one. +6. **`docs/guides/language-surface.md:175`** (C1) — a single false clause on + an otherwise excellent page. +7. **`docs/00-dependency-graph.md`** first graph (C3) and the board's NEXT PLAN + narrative (C2) — both trail their own companion tables. + +--- + +## What was fixed + +All on branch `docs-truth-audit-fixes`, 2026-08-26. Docs only — no code changed, +so no gate output changed. + +| Finding | Disposition | +| --- | --- | +| **A1** README roadmap listed shipped concurrency | Concurrency entry removed; `spawn`/`send`/`call`/`receive` documented under "Language at a glance" as shipped. The `service`-blocks entry stayed but now says what you write *instead* today. Group-by stayed — it was the one correct entry. | +| **A2** "does not serve HTTP, WebSockets, or a UI" | Rewritten: both work, as `.wo` libraries consumed through `[deps]`, never as runtime features — which is the real (and more interesting) claim. TLS-by-proxy stated. | +| **A3** "no package manager" | Now "no package **registry** — dependencies are exact-rev git URLs and nothing else", which is true and is the distinction the doc meant. | +| **A4** `employee` README "does not compile" | Banner flipped to shipped, `just employee` named as the gate, with the one genuinely-ahead clause (`group … by … into`) called out rather than left to surprise a reader. | +| **A5** `log-watcher` README "design artifact" | Banner flipped to shipped with iteration 7's actual acceptance evidence; the "five stdlib modules" list corrected to six. | +| **A6** `runtime/README.md` a generation stale | **Restructured, not patched.** Leads with `wovm`; `wo-rt.c` demoted to a marked "Historical" section that keeps its measured numbers as the prototype record they are. Fixed: the two nonexistent recipes, `crates/rt`, `prototypes/wo-db`, the `@gc` refcount/Bacon–Rajan description (now inferred mark-sweep), `DB_STUB`, the builtin list, "13 suites" → 18, four missing source files, and the phase E/F contradiction. Three broken links went with it. | +| **B1** `00-code-review.md` decayed | History kept intact with a pointer at the top; a **Re-verification 2026-08-26** section added listing the eight overtaken rows against source, the ~20 that still hold, and the two new gaps (now iteration 38). "No supervision/actor death" is marked *partly* overtaken — death landed, supervision did not. | +| **B2** `00-link-audit.md` stale | Re-run and rewritten. The 48 dead-era exploration links are **resolved by de-linking, not re-pointing** — their prose names the retired plan by number, so re-targeting would have made each sentence lie. A successor map was added to `plan/discarded.md`, which is what that report's own "Still open" note asked for. Three more fixable breaks fixed. 77 → 23. | +| **B3** ten codes missing from the error catalog | Added with definitions read from source: WO-E003, E108, E109, E219–E223, E226, E250. Header's "as of plan 3" scope line corrected. The Completeness method section now records *why* the sweep rotted — codes are built as `_prefix ^ "NN"`, so grepping for the literal `WO-E250` finds only a comment. **The `WO-W201` half of this finding was wrong:** the catalog already marked it *(retired, iteration 7b)* with "*(no longer emitted)*". The doc was right; the audit misread its own grep. | +| **C1** `language-surface.md` claimed group-by works | Corrected in three places: the clause is marked in the grammar block, the "present today" list drops it, and the page's "every form was compiled and run" promise now names the exception. Keyword count 35 → 37. | +| **C2** board narrative trailed its own tables | ▶ NEXT PLAN rewritten: the live slice is 24 (absorbing 31 + 34), not "31 next". Rows for 24, 31 and 34 updated — 31's remaining surface is cited as the *reserved holes at ids 89/90*, which is machine-checkable. A standup entry for the 2026-08-25 packaging/release track was added; it had none. | +| **C3** dependency graph a generation behind | Graph 1 rebuilt: 17 → done, 18/20/21 → held, the pre-renumber ids 10/12/13/14 replaced by stories 25/26/29/28, nodes added for 19/24/30/31/32/33/34/35/36/37/38, and the parked drain reduced to what is actually left (`pub(read)`/`using`/`#if` all shipped). Node/edge references validated. | +| **D1** `08-project-structure.md` "canonical map" | Fixed: the nonexistent `compiler/plan/`, the corpus's nine directories (four with fixtures, five reserved and empty, with counts), all 14 scripts, the `docs/` subtree, and the four missing root entries (`bench/`, `dist/`, `.github/`, `.claude/`). The sample-acceptance list now names all eight gates. | +| **D2** `compiler/README.md` stage banner + CLI | Banner replaced with the eight iterations the front end has taken since plan 3. The CLI list gained `woc ` (the primary mode), `version`, `--update-deps`, `--dump-gc` and `-D`. `crates/rt/src/lib.rs::discover` reference dropped. `gcinfer` added to the module list. | +| **D3** `runtime/src/CODE-LOGIC.md` file table | `park.c/.h` and `crypto.c/.h` added, dated so the gap is visible rather than papered over. | +| **E** `releasing.md` unfollowable steps | The `--draft` step and the phantom rehearsal cleanup deleted, remaining steps renumbered, and a paragraph added explaining why neither exists (plus how to opt into a draft if you want one). | +| **F** small factual errors | All corrected: binary size ~100 KB → 160–260 KB (measured), the scalar list, `time.ticks`, the five missing `net` members, the `fs` read-and-append limit, the `[deps]` key/`use` mismatch (the example would not have compiled), "two samples" → 13 with 8 gated, the six-module count in `00-principles.md`, and the `just gc-cycle` recipe that two checked boxes claimed. | +| **Later findings** | `00-story.md` gained the missing iteration 36 row; `docs/examples/db-actor/` gained the README it never had. | + +### Left deliberately unfixed + +- **23 broken links in `.dev/`** — vendored plugin-skill copies and reference + study trees. `.dev/` is gitignored (`git ls-files .dev` returns one path), so + these are per-developer notes. Fixing them means re-vendoring the skills with + their `references/` subdirectories. +- **The `just gc-cycle` gate itself.** The false checkbox is now disclosed in + both the plan and the sample's README, but wiring the acceptance script is + work, not documentation, and belongs to whoever picks up that loose end. +- **`tests/corpus/`'s five empty directories.** Documented as reserved with the + plan each was to be filled by; deleting or filling them is a test decision. +- **Two stale references in code comments**, recorded here rather than edited + because this pass was scoped to markdown: `compiler/bin/main.ml:16-19` and + `compiler/src/parser.ml:202` both still cite `crates/rt/src/*.rs`, removed + 2026-08-18; and `.github/workflows/release.yml:1-2` says "this workflow has + never run" while line 43 of the same file reports what its first run failed + with. + +--- + +## Structural change 2026-08-26 — status folders removed + +Directive from the developer, applied after the fixes above: **`docs/` no longer +uses directories to encode status.** The four story subfolders +(`done/`, `refine/`, `hold/`, `in-progress/`) and top-level `docs/in-progress/` +are gone. All 34 story iterations sit flat in +`docs/stories/language-runtime-database/`, the slice marker sits flat in `docs/` +as `active-slice--.md`, and each file's `status:` frontmatter key is +the single place its state is recorded. + +This reverses the 2026-08-20/21 convention ("the folder move IS the status +change"). The reason it is a good trade is visible in this repo's own history: +under the old scheme a status change relocated the file, which invalidated every +relative link in and to it — section A of the 2026-08-20 link audit was nine +instances of exactly that, and B2 above found two more that had accumulated +since. A status change is now a one-line edit that cannot break a link. + +What the move required, all verified with `just linkcheck` (23 broken, all in +`.dev/`, unchanged from before the move): + +- 34 files relocated with `git mv` so history follows them. +- **252 relative links recomputed in 70 files** — not by string substitution but + by resolving each link to an absolute path from its *old* location, remapping + through the move table, and re-deriving it relative to the file's *new* + location. String surgery would have mangled the `../` depth changes on the + moved files themselves. +- Link *text* and backticked paths that named a status folder stripped + separately — a correct target under stale display text is still a lie. +- Convention prose rewritten where it taught the old rule: + `stories/00-status.md`'s header, `stories/board-views.md` (including its Kanban + caveat, which described status changes as folder moves), and + `08-project-structure.md`'s map plus a new naming-convention entry. +- Phrases of the form "moves to `done/`" rewritten as "sets `status: done`" in + the live docs — including the four open checkboxes in the active plan + `2026-08-23-chat-ws-lifecycle.md`, which would otherwise have instructed a + future session to recreate the folders. + +Two things surfaced that the move made visible rather than caused: + +1. **A frontmatter collision, caught and fixed.** Giving the marker doc + `iteration: "24"` would have put two files in the repo claiming to be + iteration 24 with contradicting `status:` values. The marker is a progress + log, not a status carrier, so it takes `slice: "24"` and points at the story + that owns the status. +2. **Story 24's frontmatter said `refine` while the board said 🔄 live.** Under + the old scheme that drift was cheap to leave; under this one frontmatter *is* + the answer, so it is now `status: in-progress`. Iterations 31 and 34 keep + `refine` — they are absorbed into 24 but their own closeout is still pending, + which is what 24's T10 exists to do. + +Dated records were deliberately left naming the old paths: the findings sections +of this document (which declare themselves a pre-fix snapshot), the history +section of `00-link-audit.md` (which says every path in it is as it was on that +date), and the "Files:" lists of closed plans. Rewriting those would destroy the +record of what was true when each was written. diff --git a/docs/00-link-audit.md b/docs/00-link-audit.md index a159c67..81af0e8 100644 --- a/docs/00-link-audit.md +++ b/docs/00-link-audit.md @@ -1,154 +1,156 @@ -# Markdown link audit — 2026-08-20 +# Markdown link audit — re-run 2026-08-26 -Scope: every `*.md` in the repo (`.git` excluded). -External URLs were not fetched (no network verification performed). +Scope: every repo-authored `*.md`. `.git`, `target`, `dist`, `node_modules`, +`_build` and — since 2026-08-26 — `.dev/` and `.superpowers/` are excluded; see +the note under the table. External URLs are not fetched (no network +verification). | | files | relative links | broken paths | bad anchors | |---|---|---|---|---| -| first scan | 207 | 574 | 97 | 0 | -| after section A fixes | 206 | 569 | **88** | 0 | +| first scan (2026-08-20) | 207 | 574 | 97 | 0 | +| after section A fixes (2026-08-20) | 206 | 569 | 88* | 0 | +| **re-run 2026-08-26, before fixes** | 235 | 675 | 77 | 0 | +| **re-run 2026-08-26, after fixes** | 237 | 652 | 23 | 0 | +| **after scoping the gate to repo-authored docs** | 149 | 656 | **0** | **0** | -Section A is repaired and verified. Sections B–F are pre-existing rot and -still open — every one of the remaining 88 lives there. +\* The 2026-08-20 report's prose said 88 twice while its own sections B–F summed +to 77. The 77 was right; the 88 was an arithmetic slip, corrected here. + +**The gate is now clean: 0 broken, 0 bad anchors.** + +The last 23 were all in `.dev/` — vendored plugin-skill copies and cloned +reference projects, neither of which this repo authors. `scripts/linkcheck.py` +now skips `.dev/` and `.superpowers/` alongside `.git`/`target`/`dist`. That was +forced by adding gofiber/fiber as a reference (2026-08-26): its own docs are +Docusaurus pages whose links resolve at site-build time, not on disk, so the +clone alone contributed 21 broken paths and 39 bad anchors. A gate that reports +the same dozens of failures forever is a gate nobody reads. Everything the repo +actually ships — `docs/`, `compiler/`, `runtime/`, `database/`, `tests/`, +`bench/`, `scripts/`, the root README — is still scanned, and is clean. Re-check with `just linkcheck`. -Tool: `linkcheck.py` — walks the tree, strips fenced/inline code, extracts inline -links and reference definitions, resolves each relative target, and validates -`#fragment` against GitHub-style heading slugs of the target file. +Tool: `scripts/linkcheck.py` — walks the tree, strips fenced/inline code, +extracts inline links and reference definitions, resolves each relative target, +and validates `#fragment` against GitHub-style heading slugs of the target file. --- -## A. Regressions from the in-flight renumber — FIXED 2026-08-20 +## What the 2026-08-26 re-run changed -All nine broke because files moved in the working tree; each had a known -successor. Repaired: +### 1. The dead-era exploration links — RESOLVED (48 links, 15 files) + +Sections B and C of the 2026-08-20 report left a decision open: the studies under +`docs/plan/exploration/` cite the old flat `docs/plan/NN-*.md` numbering and the +`docs/runtime/database/` tree, both removed with the Rust track on 2026-08-18, +and no successor map existed. That decision is now made. + +**De-linked, not re-pointed.** The link *text* in these studies names the retired +plan by number — `[plan 09a]`, `[plan 11]`, ``[`12-engine-disk-cutover.md`]`` — +so aiming those at a story would have made each sentence assert something false +about a document that never said it. The targets were stripped and the text kept +as plain code spans. The studies still read correctly as the dated records they +are, and they no longer claim a file exists. + +The successor map lives in +[`plan/discarded.md`](plan/discarded.md#successor-map-for-the-removed-rust-era-plan-paths) +— one row per retired path, naming what carries that work now (or stating +plainly that nothing does, as with `12-engine-disk-cutover.md` and +`08-sendfile-static-assets.md`). That table is what the 2026-08-20 report's +"Still open" note asked for. + +Files touched: `assembly/{00-overview,02-writeonce-stance}.md`, +`c-runtime/{00-plan,01-architecture,02-single-binary}.md`, +`linux/{01-epoll,02-eventfd,03-timerfd,04-signalfd,05-inotify,06-sendfile,07-io_uring,08-mmap,11-memfd_create,12-pwrite-fsync}.md`. + +### 2. `runtime/README.md` — RESOLVED (3 links) + +`prototypes/wo-db/`, `docs/runtime/database/03-inmemory-engine.md` and +`docs/plan/09-concurrency-scaleout.md` all went when that README was restructured +to lead with `wovm` and demote `wo-rt.c` to a clearly-marked historical section. +It also carried two recipes that do not exist (`just rt-c-demo`, +`just rt-c-bench`) — not a link problem, fixed in the same pass. See +[`00-doc-audit.md`](00-doc-audit.md) §A6. + +### 3. Two breaks the 2026-08-20 report did not have — RESOLVED + +Both were caused by story files moving between status folders after that report: | Source | Was | Now | |---|---|---| -| `docs/00-status.md:167` | `stories/language-runtime-database/05-language-surface.md` | `…/done/05-language-surface.md` | -| `docs/00-status.md:187` | `stories/language-runtime-database/18-memory-db-features.md` | `…/hold/18-memory-db-features.md` | -| `docs/stories/language-runtime-database/00-story.md:60` | `05-language-surface.md` | `done/05-language-surface.md` | -| `docs/stories/language-runtime-database/00-story.md:69` | `18-memory-db-features.md` | `hold/18-memory-db-features.md` | -| `docs/stories/language-runtime-database/25-http-service.md:4` | `../00-story.md` | `00-story.md` | -| `docs/stories/language-runtime-database/26-blue-green-deploy.md:4` | `../00-story.md` | `00-story.md` | -| `.../refine/08-shard-actor-runtime.md:98` | `../hold/09e-durability-throughput-scale.md` | `22-durability-throughput-scale.md` | -| `.../refine/08-shard-actor-runtime.md:100` | `09f-io-uring-commit.md` | `23-io-uring-commit.md` | -| `.../refine/20-cross-program-tables.md:143` | `../hold/09d-keypair-attach-auth.md` | `21-keypair-attach-auth.md` | +| `docs/examples/employee-list/README.md:5,6` | `…/refine/20-cross-program-tables.md`, `…/refine/21-keypair-attach-auth.md` | `…/hold/…` (both stories moved to `hold/` 2026-08-21) | +| `docs/stories/…/hold/26-blue-green-deploy.md:9` | `00-story.md` | `../00-story.md` (the sibling stopped being a sibling when 26 moved into `hold/`) | -The `25`/`26` pair used `../00-story.md` while `00-story.md` is a sibling — the -`refine/`-relative form pasted into files one level up. +This is the recurring shape: **a story folder move breaks every relative link +in and to that file.** Section A of the 2026-08-20 report was nine instances of +it; these are two more. Worth a check in whatever moves a story. -Link labels were renumbered with their targets, since the old IDs contradicted -the new paths: `9e`→`22` and `9f`→`23` in `refine/08` (both the "Gated by the -benchmark" note and settled decision 4, "Order: 22 → the 8+11 arc → 23"). +### 4. Stale paths inside the report itself — RESOLVED -## B. Dead era: the old flat `docs/plan/NN-*.md` numbering (48 links) +The 2026-08-20 repair table cited `docs/00-status.md` (now +`docs/stories/00-status.md`) and `refine/{08,11,19,20,21}` (now under `done/` and +`hold/`). That table has been retired into the history section below rather than +carried forward with paths that no longer resolve. -`docs/plan/` now holds only `compiler/`, `exploration/`, `oop-vm/`, -`discarded.md`, `learnings.md`. Every flat-numbered plan doc is gone, and no -successor path was recorded. Missing targets, by inbound count: +### 5. Four links the report listed as open had already been fixed -- `09-concurrency-scaleout.md` — 12 -- `11-wal-and-recovery.md` — 9 -- `12-engine-disk-cutover.md` — 8 -- `10-storage-foundations.md` — 8 -- `done/02-event-loop-epoll.md` — 4 -- `13-class-model-live-pricing.md` — 3 -- `07-inotify-content-watcher.md` — 3 -- `08-sendfile-static-assets.md` — 2 -- `15-mcp-streamable-http.md`, `16-postgres-mirror.md`, - `done/03-hand-rolled-http.md`, `done/04-cutover-remove-tokio-axum.md` — 1 each - -Inbound from: all of `docs/plan/exploration/{linux,postgresql,c-runtime,assembly}/`, -plus `docs/00-principles.md:57,77,78`, `runtime/README.md:47`, -`.dev/reference/README.md:55,56,58`. - -**Decision needed** — these exploration docs still cite a plan structure that no -longer exists. Either map each to its story successor -(e.g. concurrency-scaleout → `stories/.../refine/08-shard-actor-runtime.md`, -wal/storage → `refine/22-durability-throughput-scale.md`, -io_uring → `refine/23-io-uring-commit.md`) or strip the links and keep prose. - -## C. Dead era: the `docs/runtime/database/` tree (7 links) - -`docs/runtime/` does not exist. Missing targets: - -- `03-inmemory-engine.md` — 5 (incl. one `#recovery` anchor) -- `02-wo-language.md` — 2 (incl. one `#concurrency-model` anchor) -- `07-wo-seg-migration.md` — 1 - -Inbound from `docs/plan/exploration/linux/{07-io_uring,08-mmap,11-memfd_create}.md`, -`docs/plan/exploration/{assembly/02-writeonce-stance,c-runtime/02-single-binary}.md`, -`runtime/README.md:43`, `.dev/reference/README.md:31`. - -## D. Never-created / removed siblings (5 links) - -| Source | Target | Note | -|---|---|---| -| `docs/plan/exploration/linux/06-sendfile.md:10` | `./07-splice.md` | slot 07 is `07-io_uring.md`; no splice doc was written | -| `docs/plan/exploration/assembly/00-overview.md:19` | `../../../.dev/reference/go/src/runtime/atomic_amd64.s` | wrong depth **and** file absent from the vendored Go tree | -| `docs/00-principles.md:87` | `examples/blog/README.md` | `docs/examples/blog/` never existed | -| `.dev/reference/rest/README.md:76` | `../../docs/examples/blog/README.md` | same missing example | -| `.dev/reference/README.md:41,59` | `../docs/plan/exploration/colibri/00-colibri-and-mixtral.md` | `exploration/colibri/` absent (2 links) | - -## E. `prototypes/` tree gone (4 links) - -`prototypes/` is not in the repo. Referenced as `prototypes/wo-db/` from -`docs/plan/exploration/c-runtime/00-plan.md:88`, `02-single-binary.md:83`, -`runtime/README.md:5`, and `prototypes/llama-moe-stream` from -`.dev/reference/README.md:59`. - -## F. Vendored skill copies — not ours to fix (13 links) - -`.dev/skills/` holds flattened copies of plugin skills. The originals ship as -directories with sibling reference files; flattening dropped them. - -- `.dev/skills/context-mode/context-mode.md:297-300` → `./references/{patterns-javascript,patterns-python,patterns-shell,anti-patterns}.md` -- `.dev/skills/superpowers/requesting-code-review.md:34,95` → `code-reviewer.md` -- `.dev/skills/superpowers/subagent-driven-development.md:232,300,345,400,410` → `implementer-prompt.md`, `task-reviewer-prompt.md`, `re-review-prompt.md` (×2), `../requesting-code-review/code-reviewer.md` -- `.dev/skills/superpowers/test-driven-development.md:206` → `writing-good-tests.md` -- `.dev/skills/superpowers/writing-skills.md:12,587` → `../using-superpowers/references/{codex,gemini}-tools.md`, `testing-skills-with-subagents.md` - -Leave as-is, or re-vendor the skills with their `references/` subdirectories. +`docs/00-principles.md:57,77,78,87` resolved before this re-run — including the +`examples/blog/README.md` reference that section D called a never-created file. +Section D's other entries stand. --- -## Structural problems found alongside the links +## Out of gate scope — `.dev/` (was 23 links, now unscanned) -1. **Iteration 19 was double-booked — RESOLVED.** - `refine/19-chat-websocket-workload.md` and `refine/24-chat-websocket-workload.md` - were the same document, differing only in the `# Iteration NN` heading, while +Recorded so the knowledge is not lost, but no longer reported by +`just linkcheck`. Not ours to fix, unchanged in character from the 2026-08-20 +report's section F. + +- **`.dev/skills/` (15 links)** — flattened copies of plugin skills. The + originals ship as directories with sibling `references/` files; flattening + dropped them. `context-mode.md:297-300`, `subagent-driven-development.md` (5), + `writing-skills.md` (3), `requesting-code-review.md` (2), + `test-driven-development.md:206`. Leave as-is, or re-vendor the skills with + their subdirectories. +- **`.dev/reference/` (8 links)** — `README.md` (7) points at the removed + `docs/plan/{linux,assembly}/` and `15-mcp-streamable-http.md`, the absent + `exploration/colibri/`, and `prototypes/llama-moe-stream`; + `rest/README.md:76` points at `docs/examples/blog/`, which never existed. + `.dev/` is gitignored (`git ls-files .dev` returns only `.dev/README.md`), so + these are per-developer notes, not repo content. + +--- + +## History — the 2026-08-20 first pass + +Kept for the record; every path below is as it was on that date. + +### A. Regressions from the in-flight renumber — FIXED 2026-08-20 + +Nine links broke because files moved in the working tree; each had a known +successor. Sources: `docs/00-status.md:167,187`, +`docs/stories/language-runtime-database/00-story.md:60,69`, the `25`/`26` story +pair (which used `../00-story.md` while `00-story.md` was a sibling — the +`refine/`-relative form pasted into files one level up), `refine/08-shard-actor-runtime.md:98,100`, +and `refine/20-cross-program-tables.md:143`. Link labels were renumbered with +their targets, since the old IDs contradicted the new paths: `9e`→`22` and +`9f`→`23`. + +### Structural problems found alongside the links + +1. **Iteration 19 was double-booked — RESOLVED.** `refine/19-chat-websocket-workload.md` + and `refine/24-chat-websocket-workload.md` were the same document while `19-missing-scalar-types.md` also claimed 19. `00-story.md`'s mapping line - (`24←19(chat)`) and table row 20 make **24 canonical**, so the 19 copy was - deleted. `refine/11-fibers.md:13` had been pointing at the 19 copy — repointed - to 24 first, so the delete broke nothing. Prose in `refine/08` that named - "iteration 19" for chat now says 24 (4 places). - -2. **`08-shard-actor-runtime.md` existed twice — RESOLVED.** - 58 lines at the stories root vs 110 in `refine/`. The `refine/` copy supersedes - it outright: same acceptance criteria plus the 2026-08-20 settled decisions, the - inferred-GC restatement (7b retired `@gc`, which the root copy still required), - and the corrected substrate path (the root copy cited `runtime/wo-rt.c`, removed - with the Rust runtime). Root copy deleted; the one inbound link, - `docs/00-status.md:171`, now points at `refine/`. Six other referrers already did. - + made **24** canonical, so the 19 copy was deleted after repointing + `refine/11-fibers.md:13` at 24. +2. **`08-shard-actor-runtime.md` existed twice — RESOLVED.** 58 lines at the + stories root vs 110 in `refine/`. The `refine/` copy superseded it outright + (the root copy still required `@gc`, retired by 7b, and cited + `runtime/wo-rt.c`, removed with the Rust runtime). Root copy deleted. 3. **Unresolved merge-conflict markers were committed** into - `refine/20-cross-program-tables.md:139-145` — `<<<<<<<< HEAD:… / ======== / - >>>>>>>> language-surface-strictness:…/hold/09c-cross-program-tables.md`, from a - rename-conflicted merge. This is what produced that file's broken `09d` link: - the stale side was still in the file. Resolved in favour of HEAD (the renumbered - `21` text). `grep` confirms no other conflict markers under `docs/`. - -## Still open - -- Sections B–F above: 88 broken links, all pre-existing. -- `docs/plan/discarded.md` and `docs/plan/learnings.md` are the only survivors of - the old flat plan layout, which is why B and C have no successor map. A rename - table in one of them would let the exploration docs be repaired mechanically - rather than by guesswork. -- `docs/00-status.md:171` still shows iteration 8 as ⬜ while `00-story.md:68` - records arc stages 1+2 as landed 2026-08-20. Not a link problem — a status - disagreement between the two index docs. Left alone. -- `refine/23-io-uring-commit.md:26` still quotes the old order as - "9e → 8+11 → 9f" in a dated note. No link involved; left as historical record. + `refine/20-cross-program-tables.md:139-145`, from a rename-conflicted merge — + which is what produced that file's broken `09d` link. Resolved in favour of + HEAD. `grep` confirmed no other conflict markers under `docs/`. +4. **A status disagreement, not a link problem:** `docs/00-status.md:171` showed + iteration 8 as ⬜ while `00-story.md:68` recorded arc stages 1+2 as landed. + Both now read landed. diff --git a/docs/00-principles.md b/docs/00-principles.md index 6c107e2..aaa6a3b 100644 --- a/docs/00-principles.md +++ b/docs/00-principles.md @@ -101,9 +101,10 @@ directly instead of the lowest common denominator. ## 10. Capabilities are typed builtins — no FFI -Programs reach the system only through audited stdlib builtins (`fs`, -`proc`, `net`, `time`, `json`): bounded reads, args-array-only process -runs, handles that close on drop. There is no `extern`, no escape hatch. +Programs reach the system only through audited stdlib builtins — six +reserved namespaces (`fs`, `proc`, `net`, `time`, `json`, `env`): bounded +reads, args-array-only process runs, handles that close on drop. There is +no `extern`, no escape hatch. *Why:* one FFI hole voids the entire memory-safety and security story; typed capabilities make the safe path the only path. *Enforced by:* [the systems-track spec Parts 2–3](superpowers/specs/2026-08-01-systems-track-design.md). diff --git a/docs/08-project-structure.md b/docs/08-project-structure.md index 8aeb388..de32014 100644 --- a/docs/08-project-structure.md +++ b/docs/08-project-structure.md @@ -16,18 +16,25 @@ project. ``` writeonce-all/ -├── compiler/ OCaml `woc` — lexer→parser→types→owner→emit; produces the compiler binary -├── runtime/ C `wovm` — the register VM that runs .wob images (src/); phase A–F C reference (wo-rt.c, bench/) +├── compiler/ OCaml `woc` — lexer→parser→types→gcinfer→owner→emit; produces the compiler binary +├── runtime/ C `wovm` — the register VM that runs .wob images (src/); retired io_uring reference (wo-rt.c, bench/) ├── database/ C embedded engine — class-shaped tables, secondary indexes, typed WAL + recovery -├── tests/ corpus/ — conformance fixtures: run / compile-fail / trap / gc -├── scripts/ oop-e2e.sh (corpus runner), mkdist.sh / install-accept.sh (packaging), sample acceptance -├── docs/ ALL documentation: numbered docs, stories/, plan/, examples/, superpowers/ +├── tests/ corpus/ — conformance fixtures: run / compile-fail / trap / gc (+ five reserved, still empty) +├── scripts/ the corpus runner, packaging, linkcheck, and one acceptance script per sample +├── bench/ baseline.json (the db-bench gate's thresholds) + results/ + compare/ (Go+SQLite peer) +├── docs/ ALL documentation: numbered docs, stories/, plan/, examples/, guides/, superpowers/ +├── dist/ `just dist` output: writeonce--linux-amd64.tar.gz + .sha256 +├── .github/ workflows/release.yml — builds, verifies and publishes on a `v*` tag push +├── .claude/ agents/ — project subagent definitions (see docs/guides/database-developer-subagent.md) ├── .dev/ gitignored per-developer links + reference study trees (v1 crates, colibri, llama-cpp) ├── justfile task runner: woc-/wovm-build, the *-test gates, oop-accept, dist, install-accept ├── VERSION single-sourced toolchain version (stamped into woc/wovm; asserted by `just dist`) └── README.md the getting-started front door (also the writeonce.de landing content) ``` +`target/` and `.vscode/` are local build and editor state, not part of the +project layout. + ## Root directories in detail ### `compiler/` — the OCaml `woc` compiler @@ -36,13 +43,19 @@ writeonce-all/ compiler/ ├── dune-project ├── README.md orientation: pipeline map, build/test commands -├── plan/ compiler-track docs: architecture.md + the woc plans ├── src/ one module per stage: diag, token, lexer, ast, parser, -│ types, owner, emit, disasm, dump -├── bin/main.ml the woc executable (check / --emit / build / version modes) -└── test/ golden runner + golden/ fixtures per stage +│ types, gcinfer, owner, emit, disasm, dump +├── bin/main.ml the woc executable — check / build-from-manifest / --emit / +│ build / version / --update-deps / the --dump-* modes / -D +└── test/ golden runner + golden/ fixtures per stage (tokens, ast, + owner, owner-err, bc) + fixtures/driver/ CLI-smoke cases ``` +The compiler-track plan docs live under +[`plan/compiler/`](plan/compiler/architecture.md) in this `docs/` tree, not +inside `compiler/` — the repo rule below applies to the compiler like everything +else. + Doctrine: OCaml stdlib only — no Menhir, no ppx, no opam libraries; handwritten lexer and recursive-descent parser. Build: `just woc-build`; gate: `just woc-test`. Architecture map: @@ -73,22 +86,34 @@ compiler (`emit.ml`), lowered to engine builtins — no SQL text in the image. ### `tests/`, `scripts/` -- `tests/corpus/` — the conformance spine: `run/`, `compile-fail/`, `trap/`, - `gc/`. Exact-outcome matching: byte-equal stdout, exact `WO-E###`, exact trap - code. Driven by `scripts/oop-e2e.sh` (`just oop-e2e`). -- `scripts/` — `oop-e2e.sh` (corpus), `mkdist.sh` + `install-accept.sh` - (tarball packaging), and the per-sample acceptance scripts - (`employee-accept.sh`, `log-watcher-accept.sh`). +- `tests/corpus/` — the conformance spine. Four directories carry fixtures: + `run/` (60), `compile-fail/` (46), `trap/` (5), `gc/` (2). Five more — + `actor/`, `db/`, `lang/`, `sys/`, `sample-logwatcher/` — are reserved slots + from the original plan and still **empty**; see + [`tests/corpus/README.md`](../tests/corpus/README.md) for which plan each was + to be filled by. Exact-outcome matching: byte-equal stdout, exact `WO-E###`, + exact trap code. Driven by `scripts/oop-e2e.sh` (`just oop-e2e`). +- `scripts/` — the corpus runner (`oop-e2e.sh`) and + `single-binary-smoke.sh`; packaging (`mkdist.sh`, `install-accept.sh`); the + docs gate (`linkcheck.py`); the benchmark campaign (`db-bench.py`); and one + acceptance script per sample — `employee-accept.sh`, `log-watcher-accept.sh`, + `web-app-accept.sh`, `site-accept.sh`, `fibers-accept.sh`, + `db-actor-accept.sh`, `deps-accept.sh`. ### `docs/` — all documentation ``` docs/ -├── 00-*, 01-problem.md, 08-*.md status / principles / code-review / problem / structure -├── stories/ the canonical iteration arc (language-runtime-database/) -├── examples/ log-watcher/, employee/, employee-list/ samples +├── 00-*.md, 01-problem.md, 08-*.md principles / code-review / dependency-graph / +│ link-audit / doc-audit / problem / structure +├── stories/ 00-status.md (the board) + board-views.md + +│ the canonical iteration arc (language-runtime-database/, +│ FLAT — status lives in each story's frontmatter) +├── active-slice-*.md the live slice's one marker doc, deleted when it lands +├── guides/ runbooks: releasing, language-surface, subagents +├── examples/ 13 sample projects, 8 of them wired to a `just` recipe ├── plan/ compiler/ plans, oop-vm/ contracts, exploration/ studies, -│ discarded.md + learnings.md registers +│ perf-targets.md, discarded.md + learnings.md registers └── superpowers/ specs/ (approved designs) + plans/ (implementation plans) ``` @@ -106,15 +131,25 @@ gates. `just woc-build` + `just wovm-build` produce the two binaries; `just oop-accept` runs the full milestone gate (compile-time budget, conformance corpus under -ASan, single-binary smoke, both unit gates). Sample acceptance: -`just employee` (database), `just log-watcher` (systems stdlib). Packaging: -`just dist` → `writeonce--linux-amd64.tar.gz`, proven by -`just install-accept`. +ASan, single-binary smoke, both unit gates). Sample acceptance: `just employee` +(database), `just log-watcher` (systems stdlib), `just web-app` and `just site` +(the framework consumed through `[deps]`), `just fibers` and `just db-actor` +(the concurrency arc), `just deps-accept` (the package manager), +`just db-bench` / `db-bench-quick` (the benchmark campaign, gated against +`bench/baseline.json`). Docs gate: `just linkcheck`. Packaging: `just dist` → +`writeonce--linux-amd64.tar.gz`, proven by `just install-accept`; +publishing is `.github/workflows/release.yml` on a `v*` tag +(see [`guides/releasing.md`](guides/releasing.md)). ## Naming conventions - Binaries: `woc` (OCaml compiler), `wovm` (C VM); a `woc build` / `woc ` output is named by the project's `wo.toml`. +- Story iterations: `-.md`, flat in + `docs/stories/language-runtime-database/`. **No directory encodes status** + (directive 2026-08-26) — each story's `status:` frontmatter key is the only + place state is recorded, so a status change is a one-line edit and never + moves a file or breaks a link. - Plan/spec files: `YYYY-MM-DD-.md` under `docs/superpowers/{specs,plans}/`; compiler plans under `docs/plan/compiler/`; normative contracts under `docs/plan/oop-vm/`. diff --git a/docs/in-progress/2026-08-23-chat-ws-lifecycle.md b/docs/active-slice-2026-08-23-chat-ws-lifecycle.md similarity index 76% rename from docs/in-progress/2026-08-23-chat-ws-lifecycle.md rename to docs/active-slice-2026-08-23-chat-ws-lifecycle.md index d5270c3..88897df 100644 --- a/docs/in-progress/2026-08-23-chat-ws-lifecycle.md +++ b/docs/active-slice-2026-08-23-chat-ws-lifecycle.md @@ -1,9 +1,15 @@ -# In progress — chat + actor lifecycle (iteration 24, absorbing 31 + 34) +--- +slice: "24" # the story that owns the status; see stories/24-chat-websocket-workload.md +status: in-progress +--- -Active slice, branch `chat-ws-lifecycle`. Spec: -[`../superpowers/specs/2026-08-23-chat-websocket-actor-lifecycle-design.md`](../superpowers/specs/2026-08-23-chat-websocket-actor-lifecycle-design.md) +# Active slice — chat + actor lifecycle (iteration 24, absorbing 31 + 34) + +Branch `chat-ws-lifecycle`. Spec: +[`superpowers/specs/2026-08-23-chat-websocket-actor-lifecycle-design.md`](superpowers/specs/2026-08-23-chat-websocket-actor-lifecycle-design.md) · plan: -[`../superpowers/plans/2026-08-23-chat-ws-lifecycle.md`](../superpowers/plans/2026-08-23-chat-ws-lifecycle.md). +[`superpowers/plans/2026-08-23-chat-ws-lifecycle.md`](superpowers/plans/2026-08-23-chat-ws-lifecycle.md) +· board: [`stories/00-status.md`](stories/00-status.md). ## Progress (2026-08-23) @@ -49,4 +55,6 @@ Every landed task: full battery 12/12, fresh-built. standup entry, graph nodes, framework README ledger rows, runtime + chat CODE-LOGIC sections, delete this marker. Final battery. -This file is deleted when the slice lands (board convention). +This file is deleted when the slice lands (board convention). It lives flat in +`docs/` rather than a status folder — since 2026-08-26 no directory in this repo +encodes state; `status:` above is the only place it is recorded. diff --git a/docs/examples/db-actor/README.md b/docs/examples/db-actor/README.md new file mode 100644 index 0000000..ae5a252 --- /dev/null +++ b/docs/examples/db-actor/README.md @@ -0,0 +1,62 @@ +# `db-actor` — the database reached from any shard + +> **Status: shipped — arc stage 3's acceptance gate.** Run it with +> `just db-actor`. Landed 2026-08-21 with the shard-fiber arc +> ([story 8](../../stories/language-runtime-database/08-shard-actor-runtime.md) +> · [plan](../../superpowers/plans/2026-08-20-shard-fiber-arc.md)). + +The database lives on **one** shard — the owner, shard 0 — because RAM is +authoritative and a single writer is what makes the WAL's ordering meaningful. +That is a problem the moment actors are placed round-robin across cores: a +`spawn`ed actor has no say in which shard it lands on, and before stage 3 a +worker-shard `insert` trapped `WO_T_DB` with "database engine not initialized". + +Stage 3's answer is a **transparent DB actor**: statements issued off the owner +shard marshal to it, execute there, and materialize their replies back. The +program's source says nothing about any of it — the same `insert` and the same +`from … select` work wherever the actor happens to run. This sample exists to +prove exactly that, which is why its acceptance criterion is *placement +independence* rather than any particular output. + +## What it does + +`Note` is a `@table` with a secondary index on `tag`. `Writer` is an actor: each +one inserts a row, then scans the whole table and prints the sum it sees. `main` +spawns two writers, waits, then scans once itself. + +With the default shard count, round-robin placement puts at least one writer off +the owner shard — so one of those inserts and one of those scans travel the RPC +path under test, and the other does not. Both must produce the same shape. + +```bash +just db-actor # the gate +woc docs/examples/db-actor/ # or build it by hand +WO_SHARDS=1 ./docs/examples/db-actor/target/db-actor # force the local path +``` + +## What the gate proves + +`scripts/db-actor-accept.sh`, 8 checks: + +| Check | Why it is shaped that way | +| --- | --- | +| multi-shard, three rounds | The writer lines are asserted as a **set**, not a sequence — scheduling decides their order, and pinning it would be testing the scheduler, not the RPC. The `main` line is exact. | +| both `WO_IO` backends forced | The reply park has to be plane-independent: io_uring and epoll must give the same answer, or the parking is leaking into semantics. | +| single shard, byte-exact | The local path is untouched by stage 3. Any drift here means the RPC changed the non-RPC case. | +| `WO_DATA` restart pair | A worker's insert must commit on the **owner's** WAL before its ack, so a restart replays it: 2 rows, then 2+2 after a second run. This is the durability claim the RPC could most easily break. | + +Run under `wovm_asan` and `wovm_tsan` as well — cross-shard message passing is +exactly where a data race would hide, and TSan covering this demo is the one +place it runs. + +## Read it for + +- **How little the source knows.** Compare `Writer.receive` here against the + same statements in [`employee`](../employee/): identical. Transparency is the + feature. +- **Why `main` waits.** `main` is not an actor and has no mailbox, so it sleeps + rather than awaiting — the gap iteration 31's `call` closes for actors and + [24's marker](../../active-slice-2026-08-23-chat-ws-lifecycle.md) tracks. + +Reasoning under the engine side: [`database/src/CODE-LOGIC.md`](../../../database/src/CODE-LOGIC.md). +Contract: [`plan/oop-vm/04-db-binding.md`](../../plan/oop-vm/04-db-binding.md). diff --git a/docs/examples/db-actor/main.wo b/docs/examples/db-actor/main.wo index e879608..b4b47c1 100644 --- a/docs/examples/db-actor/main.wo +++ b/docs/examples/db-actor/main.wo @@ -21,7 +21,6 @@ class Job { -- round-robin, so with two writers at default shards one lands off the -- primary — the RPC path under test. class Writer { - pad: Int fn receive(msg: Job) { insert Note { tag: "w", val: msg.n }; let total = 0; @@ -33,8 +32,8 @@ class Writer { } fn main() -> Int { - let a: actor Job = spawn Writer { pad: 0 }; - let b: actor Job = spawn Writer { pad: 1 }; + let a: actor Job = spawn Writer {}; + let b: actor Job = spawn Writer {}; send(a, Job { n: 1 }); send(b, Job { n: 2 }); -- no request/response surface yet (iteration 31): poll until both rows diff --git a/docs/examples/employee-list/README.md b/docs/examples/employee-list/README.md index b3ca75a..1bdd3e8 100644 --- a/docs/examples/employee-list/README.md +++ b/docs/examples/employee-list/README.md @@ -2,8 +2,8 @@ > **Status: target workload — does not compile on today's toolchain.** > Written ahead of iterations -> [20 (cross-program tables)](../../stories/language-runtime-database/refine/20-cross-program-tables.md) -> and [21 (keypair attach auth)](../../stories/language-runtime-database/refine/21-keypair-attach-auth.md), +> [9 (cross-program tables)](../../stories/databasev2/09-cross-program-tables.md) +> and [10 (keypair attach auth)](../../stories/databasev2/10-keypair-attach-auth.md), > the way every acceptance sample here precedes its features. It also leans > on 9/9b (the [employee sample](../employee/) it attaches to must run > first). diff --git a/docs/examples/employee/README.md b/docs/examples/employee/README.md index 49ac282..a8ab066 100644 --- a/docs/examples/employee/README.md +++ b/docs/examples/employee/README.md @@ -1,14 +1,16 @@ # employee — the database track's acceptance workload -> **Status: target workload — does not compile on today's toolchain.** -> This sample is written *ahead of* the features it exercises, exactly as -> log-watcher was written ahead of iterations 5–7: the sample is the test, -> and the plans compile toward it. It becomes buildable when iteration 9 -> (engine: [`2026-08-01-db-engine-binding.md`](../../superpowers/plans/2026-08-01-db-engine-binding.md)) +> **Status: shipped — this is the database track's acceptance gate.** Run it +> with `just employee`. The sample was written *ahead of* the features it +> exercises, exactly as log-watcher was written ahead of iterations 5–7: the +> sample is the test, and the plans compiled toward it. Both landed — iteration +> 9 (engine: [`2026-08-01-db-engine-binding.md`](../../superpowers/plans/2026-08-01-db-engine-binding.md)) > and iteration 9b (query surface: -> [`2026-08-15-employee-relations-query.md`](../../plan/compiler/2026-08-15-employee-relations-query.md)) -> land. Normative semantics: +> [`2026-08-15-employee-relations-query.md`](../../plan/compiler/2026-08-15-employee-relations-query.md)). +> Normative semantics: > [the 9b spec](../../superpowers/specs/2026-08-15-table-relations-query-design.md). +> One clause below is still ahead of the compiler and marked where it appears: +> `group … by … into` parses and is then refused by the typechecker. Two `@table` classes and every 9b feature load-bearing: diff --git a/docs/examples/fibers/main.wo b/docs/examples/fibers/main.wo index f7c4025..debb25d 100644 --- a/docs/examples/fibers/main.wo +++ b/docs/examples/fibers/main.wo @@ -31,7 +31,6 @@ class Counter { -- Part 2: an actor whose receive PARKS mid-message. The park releases -- the shard: main keeps ticking while this fiber sleeps. class Sleeper { - pad: Int fn receive(msg: Tick) { print("sleeper: down for ${msg.n}ms"); time.sleep(msg.n); @@ -61,7 +60,7 @@ fn main() -> Int { print("part1 done"); -- ---- part 2: a parked fiber blocks nobody ---- - let s: actor Tick = spawn Sleeper { pad: 0 }; + let s: actor Tick = spawn Sleeper {}; send(s, Tick { n: 150 }); let t = 0; while t < 8 { diff --git a/docs/examples/gc-cycle/README.md b/docs/examples/gc-cycle/README.md index efe6d21..324367a 100644 --- a/docs/examples/gc-cycle/README.md +++ b/docs/examples/gc-cycle/README.md @@ -185,8 +185,15 @@ inferred). The developer writes no memory annotations for either. ## Run status -Iteration 7b is landing in phases (plan: +Iteration 7b landed 2026-08-18 (plan: [`../../superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md`](../../superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md)). +This sample compiles and runs on today's toolchain — `woc +docs/examples/gc-cycle/` then `./docs/examples/gc-cycle/target/gc-cycle`. + +> **It has no `just` recipe.** The plan's phase 4 checked off a +> `just gc-cycle` acceptance that never landed; the sample is the one +> compiling example in the repo with no gate behind it. Run it by hand, +> and see the plan's 2026-08-26 disclosure note. **Phase 1 (landed).** The inference pass classifies each class; `woc --dump-gc docs/examples/gc-cycle` prints: diff --git a/docs/examples/log-watcher/README.md b/docs/examples/log-watcher/README.md index f39963b..c2e091c 100644 --- a/docs/examples/log-watcher/README.md +++ b/docs/examples/log-watcher/README.md @@ -6,17 +6,17 @@ ported file for file, per the approved (Part 4). A single-binary systems daemon: log-tail watcher, cron.d supervisor, flock/pgrep probes, hand-rolled MCP-over-HTTP server, JSONL detection sink. Program mode (`fn main`, blocking legal, one shard) plus the -five builtin stdlib modules — `fs`, `proc`, `net`, `time`, `json` — carry +stdlib modules it needs — `fs`, `proc`, `net`, `time`, `json`, `env` — carry all of it; read each `.wo` next to its `.hx` sibling. -> **Status: design artifact — the spec's forcing function.** The systems -> track is approved, pre-implementation. Today's `woc` (milestone 1) -> recovers the `class`/`fn` skeletons in these files (`--dump-ast` lists -> every Watcher method) but diagnoses the adopted surface as WO-E101: -> `use`, `typedef`, standalone union aliases (`type CronResult = …`), -> `pub(read)`, `switch`, `try`. This sample exists to force that grammar -> (the blog/ecommerce/pricing precedent) and becomes the track's acceptance -> test: it compiles and detects a real silent death when the track ships. +> **Status: shipped — the systems track's acceptance gate.** Run it with +> `just log-watcher` (`just log-watcher::build` / `::soak 60` for the rest). +> Landed with iteration 7 on 2026-08-15: executable, not merely compilable — +> zero ASan leaks in all three modes, SIGTERM ends parked syscalls, fds flat, +> `LW_SOAK` gate. The sample existed to force the grammar it uses (the +> blog/ecommerce/pricing precedent), and every form it needed — `use`, +> `typedef`, standalone union aliases (`type CronResult = …`), `pub(read)`, +> `switch`, `try` — is now shipped surface. ## The mapping diff --git a/docs/examples/writeonce-framework/README.md b/docs/examples/porch/README.md similarity index 69% rename from docs/examples/writeonce-framework/README.md rename to docs/examples/porch/README.md index c799f9b..8fad15d 100644 --- a/docs/examples/writeonce-framework/README.md +++ b/docs/examples/porch/README.md @@ -1,11 +1,24 @@ -# writeonce-framework +# porch — the writeonce web framework + +> **Named `porch` on 2026-08-26.** Rename history: `writeonce-framework` +> (`use framework`) → `writeonce-serve` (`use serve`, 2026-08-25) → **`porch`** +> (`use porch`). Stories, specs, plans and the audit reports dated before each +> change still say the older name — they are dated records and were left as +> written, which is the repo's convention. +> +> Why `porch`: the structure in front of the house you actually enter through, +> and in writeonce the house *is* the database. The name appears only in `use` +> lines and the `[deps]` key — names resolve bare through `use` edges, so no +> handler body mentions it. A web framework **written in writeonce**, consumed as a `[deps]` dependency -(iteration 15). Spec: `docs/superpowers/specs/2026-08-18-web-framework-design.md` §B. +(iteration 15). Roadmap: [`docs/stories/porch/`](../../stories/porch/00-story.md) +— its own track, eight iterations, numbered from 1, derived from +[the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md). Spec: `docs/superpowers/specs/2026-08-18-web-framework-design.md` §B. ```toml [deps] -writeonce-framework = { git = "https://github.com/shoneyj/writeonce-framework", rev = "v0.1.0" } +porch = { git = "https://github.com/shoneyj/porch", rev = "v0.1.0" } ``` ## What it is @@ -82,6 +95,13 @@ first (pure `.wo` cannot express it yet). ### Transport +> Parity reference: [the Fiber v3.5.0 study](../../plan/exploration/fiber/00-fiber-parity.md) +> read all 32 of Fiber's middleware packages against this framework on +> 2026-08-26. **Nine already have a working counterpart here** (CORS, basic +> auth, key/bearer auth, security headers, ETag, static files, logger, host +> authorization, recover-as-500). The rows below marked ⛔/⏸ are what it found +> missing, each with an owner. + | Item | State | | --- | --- | | HTTP/1.1 parsing | ✅ parses + 400-and-survive; duplicate `Content-Length` rejected outright (RFC 9112 §6.3, slice 2); BODY_MAX bounds headers and body | @@ -149,7 +169,14 @@ first (pure `.wo` cannot express it yet). | --- | --- | | base64 | ✅ pure `.wo` (`http/auth.wo`) | | SHA-1 · SHA-256 · HMAC-SHA256 | ✅ C runtime builtins (iteration 34, ids 85–87, RFC-vector gated); SHA-512/CRC32 wait for a consumer | -| Unlocks (signed cookies, CSRF, session integrity, webhook verification, JWT HS256) | ⬜ UNBLOCKED (the primitives exist since iteration 34); each is its own slice; **hard stop at JWT HS256** — no RS256, no JOSE zoo | +| Unlocks — signed cookies, webhook verification, JWT HS256 **verification** | ⬜ genuinely unblocked (integrity only needs iteration 34's HMAC); each its own slice; **hard stop at JWT HS256** — no RS256, no JOSE zoo | +| Unlocks — CSRF, session integrity, JWT **issuing** | ⛔ **BLOCKED, corrected 2026-08-26.** This row previously read "UNBLOCKED (the primitives exist since iteration 34)" and that was wrong: HMAC lets you *authenticate* a token, not *mint* one, and **writeonce has no source of randomness at all** (no `getrandom`, no CSPRNG builtin — grep the runtime). An HMAC over a guessable session id is a signed guess. A random-bytes builtin is [iteration 39](../../stories/language-runtime-database/39-web-framework-parity.md)'s first goal | +| Cookies (read + `Set-Cookie`) | ⛔ absent in BOTH directions, and `Resp.headers` is a `map` so it structurally cannot carry two `Set-Cookie` lines — [iteration 39](../../stories/language-runtime-database/39-web-framework-parity.md) | +| Sessions · CSRF · rate limiting · idempotency | ⬜ [iteration 39](../../stories/language-runtime-database/39-web-framework-parity.md). Limiter and idempotency need only a `@table` + `time.ticks` and are the cheapest wins available; sessions and CSRF wait on randomness + cookies. `@table` gives all four a **durable** store, where Fiber ships in-memory and expects Redis | +| Compression · SSE · byte ranges · chunked bodies | ⏸ all four sit on the parked streaming seam (`serialize()` always emits `Content-Length`). Chunked REQUEST bodies are deliberately refused today (`internal/parse.wo:153-157`, request-smuggling note) — that refusal must survive whoever implements them | +| Typed binding of query/params/form/headers | ⏸ Fiber's `Bind` reflects over struct tags; principle 13 forbids reflection, so the answer is [iteration 29's `@derive`](../../stories/language-runtime-database/29-compile-time-metaprogramming.md). JSON bodies already work via `json.decode(t) as T` | +| PATCH/OPTIONS/HEAD/ALL helpers · named routes · per-route body limit · request id · `Location`/`Vary`/`Attachment` | ⬜ [iteration 39](../../stories/language-runtime-database/39-web-framework-parity.md) — registration and response sugar; `BODY_MAX = 1048576` is currently one compile-time number for the whole server | +| `proxy` middleware | ⛔ impossible today — no `net.connect` anywhere in the runtime ([iteration 38](../../stories/language-runtime-database/38-content-platform-capabilities.md)) | ## Layout and privacy (iteration 17) @@ -167,7 +194,7 @@ naming the kind, unless a demo `main` is added (lib+bin is allowed). - **`internal/` — not importable by a consumer.** The connection-level request parser and carry-state record (`parse.wo`) and the serve loop, status text, and response serializer (`serve.wo`) live here. A consuming app that writes - `use writeonce-framework/internal` gets **WO-E108** at that `use`. The rule + `use porch/internal` gets **WO-E108** at that `use`. The rule is Go's: a path segment named `internal` is refused across the `[deps]` boundary only — the framework's own modules import it freely. @@ -180,3 +207,20 @@ elimination); a consumer simply cannot name them. `docs/examples/web-app` — a small storefront importing this framework through `[deps]`. Its acceptance (`just web-app`) exercises the whole chain: fetch → lock → build → serve → durable restart. + +## Serving files + +`StaticFiles { dir, max_bytes }` mounts a directory on a wildcard route: + +``` +app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 }) +app.get("/dl/*path", StaticFiles { dir: "dist", max_bytes: 16777216 }) +``` + +Two rules, both refusals rather than repairs: a path containing `..` is a +404 and never reaches the filesystem, and `max_bytes` is a hard ceiling — +`fs.read_all` truncates above it, so set it above the largest file you +mean to serve. Content types come from the extension; archives +(`.tar.gz`, `.tgz`, `.zip`) also get `content-disposition: attachment`. +Text is binary-safe in this language, so archives and images travel +unchanged. Lifted out of the shop template 2026-08-25. diff --git a/docs/examples/writeonce-framework/app.wo b/docs/examples/porch/app.wo similarity index 98% rename from docs/examples/writeonce-framework/app.wo rename to docs/examples/porch/app.wo index 78ec0b4..12a9cd4 100644 --- a/docs/examples/writeonce-framework/app.wo +++ b/docs/examples/porch/app.wo @@ -3,7 +3,7 @@ -- -- let app = App { middleware: [], gmw: [], afters: [], routes: [] }; -- app.use_mw(Mw { m: Auth { token: t } }); --- app.use_after(Aw { a: SecurityHeaders { pad: 0 } }); +-- app.use_after(Aw { a: SecurityHeaders {} }); -- app.add(Route { method: "GET", pattern: "/products/:id", h: Show {} }); -- return app.serve("127.0.0.1", port); -- diff --git a/docs/examples/writeonce-framework/http/auth.wo b/docs/examples/porch/http/auth.wo similarity index 100% rename from docs/examples/writeonce-framework/http/auth.wo rename to docs/examples/porch/http/auth.wo diff --git a/docs/examples/porch/http/files.wo b/docs/examples/porch/http/files.wo new file mode 100644 index 0000000..f382708 --- /dev/null +++ b/docs/examples/porch/http/files.wo @@ -0,0 +1,72 @@ +-- http/files.wo — serving a file from disk, the framework's answer to +-- "let people download something". Lifted out of the shop template +-- 2026-08-25, which had carried its own copy and said in a comment that +-- this belonged here. +-- +-- Mount it on a wildcard route and it answers that subtree: +-- +-- app.get("/assets/*path", StaticFiles { dir: "assets", max_bytes: 2097152 }) +-- app.get("/dl/*path", StaticFiles { dir: "dist", max_bytes: 8388608 }) +-- +-- Safety is two rules, both refusals rather than repairs: a path holding +-- `..` is a 404 and never reaches the filesystem, and a file bigger than +-- `max_bytes` is truncated by `fs.read_all` — so `max_bytes` is a real +-- ceiling you must set above the largest file you intend to serve, not a +-- hint. Text is binary-safe in this language, so archives and images +-- travel unchanged. +use fs + +pub class StaticFiles { + dir: Text + max_bytes: Int + + fn handle(req: Req) -> Resp { + let rel = req.params["path"]; + if rel == nil { + return not_found(); + } + -- Traversal: refuse, never normalise. A rewritten path is a second + -- chance to get it wrong. + if index_of("${rel}", "..") != -1 { + return not_found(); + } + let body = try fs.read_all("${self.dir}/${rel}", self.max_bytes) catch (e) nil; + if body == nil { + return not_found(); + } + let h: map = {}; + h["content-type"] = content_type("${rel}"); + if is_download("${rel}") { + h["content-disposition"] = "attachment"; + } + return Resp { status: 200, headers: h, body: "${body}" }; + } +} + +-- Extension to content type. Unknown extensions are octet-stream: a +-- wrong guess is worse than no guess. +pub fn content_type(name: Text) -> Text { + if ends_with(name, ".html") { return "text/html; charset=utf-8"; } + if ends_with(name, ".css") { return "text/css; charset=utf-8"; } + if ends_with(name, ".js") { return "text/javascript"; } + if ends_with(name, ".json") { return "application/json"; } + if ends_with(name, ".svg") { return "image/svg+xml"; } + if ends_with(name, ".png") { return "image/png"; } + if ends_with(name, ".webp") { return "image/webp"; } + if ends_with(name, ".ico") { return "image/x-icon"; } + if ends_with(name, ".woff2") { return "font/woff2"; } + if ends_with(name, ".txt") { return "text/plain; charset=utf-8"; } + if ends_with(name, ".sha256") { return "text/plain; charset=utf-8"; } + if ends_with(name, ".tar.gz") { return "application/gzip"; } + if ends_with(name, ".tgz") { return "application/gzip"; } + if ends_with(name, ".zip") { return "application/zip"; } + return "application/octet-stream"; +} + +-- Archives are offered as a save, not rendered into a tab. +fn is_download(name: Text) -> Bool { + if ends_with(name, ".tar.gz") { return true; } + if ends_with(name, ".tgz") { return true; } + if ends_with(name, ".zip") { return true; } + return false; +} diff --git a/docs/examples/writeonce-framework/http/form.wo b/docs/examples/porch/http/form.wo similarity index 100% rename from docs/examples/writeonce-framework/http/form.wo rename to docs/examples/porch/http/form.wo diff --git a/docs/examples/writeonce-framework/http/multipart.wo b/docs/examples/porch/http/multipart.wo similarity index 100% rename from docs/examples/writeonce-framework/http/multipart.wo rename to docs/examples/porch/http/multipart.wo diff --git a/docs/examples/writeonce-framework/http/nego.wo b/docs/examples/porch/http/nego.wo similarity index 100% rename from docs/examples/writeonce-framework/http/nego.wo rename to docs/examples/porch/http/nego.wo diff --git a/docs/examples/writeonce-framework/http/secure.wo b/docs/examples/porch/http/secure.wo similarity index 99% rename from docs/examples/writeonce-framework/http/secure.wo rename to docs/examples/porch/http/secure.wo index 84aa26e..db8079e 100644 --- a/docs/examples/writeonce-framework/http/secure.wo +++ b/docs/examples/porch/http/secure.wo @@ -8,7 +8,6 @@ -- framework's standing decision), so Strict-Transport-Security belongs -- in the proxy config next to the certificates. pub class SecurityHeaders { - pad: Int fn after(req: Req, mut r: Resp) { r.headers["x-content-type-options"] = "nosniff"; r.headers["x-frame-options"] = "DENY"; diff --git a/docs/examples/writeonce-framework/http/types.wo b/docs/examples/porch/http/types.wo similarity index 88% rename from docs/examples/writeonce-framework/http/types.wo rename to docs/examples/porch/http/types.wo index 1287576..df2eb1a 100644 --- a/docs/examples/writeonce-framework/http/types.wo +++ b/docs/examples/porch/http/types.wo @@ -37,6 +37,17 @@ pub fn ok_text(body: Text) -> Resp { return Resp { status: 200, headers: h, body: body }; } +-- iteration 37: HTML is transport here, exactly like text and JSON — +-- the status line and the content-type, nothing about rendering. It +-- lives beside its two siblings because both HTML apps had hand-rolled +-- the identical four lines; writeonce-view stays a pure Text library and never +-- learns what a Resp is. +pub fn ok_html(body: Text) -> Resp { + let h: map = {}; + h["content-type"] = "text/html; charset=utf-8"; + return Resp { status: 200, headers: h, body: body }; +} + pub fn ok_json(body: Text) -> Resp { let h: map = {}; h["content-type"] = "application/json"; diff --git a/docs/examples/writeonce-framework/http/ws.wo b/docs/examples/porch/http/ws.wo similarity index 100% rename from docs/examples/writeonce-framework/http/ws.wo rename to docs/examples/porch/http/ws.wo diff --git a/docs/examples/writeonce-framework/http/wsframe.wo b/docs/examples/porch/http/wsframe.wo similarity index 100% rename from docs/examples/writeonce-framework/http/wsframe.wo rename to docs/examples/porch/http/wsframe.wo diff --git a/docs/examples/writeonce-framework/internal/parse.wo b/docs/examples/porch/internal/parse.wo similarity index 100% rename from docs/examples/writeonce-framework/internal/parse.wo rename to docs/examples/porch/internal/parse.wo diff --git a/docs/examples/writeonce-framework/internal/serve.wo b/docs/examples/porch/internal/serve.wo similarity index 100% rename from docs/examples/writeonce-framework/internal/serve.wo rename to docs/examples/porch/internal/serve.wo diff --git a/docs/examples/writeonce-framework/router/router.wo b/docs/examples/porch/router/router.wo similarity index 95% rename from docs/examples/writeonce-framework/router/router.wo rename to docs/examples/porch/router/router.wo index 5a626cc..069df7b 100644 --- a/docs/examples/writeonce-framework/router/router.wo +++ b/docs/examples/porch/router/router.wo @@ -47,10 +47,10 @@ pub class Mw { } -- Request-line logging, the one middleware every framework ships: method + --- path to stderr, never short-circuits. `pad` is the record-class ctor --- convention (every stateless handler carries one Int field). +-- path to stderr, never short-circuits. A stateless handler declares NO +-- fields and constructs as `Logging {}` — an earlier convention gave every +-- one of them a filler `pad: Int`, which the language never required. pub class Logging { - pad: Int fn before(mut req: Req) -> ?Resp { print_err("${req.method} ${req.path}"); return nil; diff --git a/docs/examples/writeonce-framework/wo.toml b/docs/examples/porch/wo.toml similarity index 80% rename from docs/examples/writeonce-framework/wo.toml rename to docs/examples/porch/wo.toml index 0e12d31..739f562 100644 --- a/docs/examples/writeonce-framework/wo.toml +++ b/docs/examples/porch/wo.toml @@ -1,4 +1,4 @@ -name = "writeonce-framework" +name = "porch" kind = "library" version = "0.1.0" description = "A web framework written in writeonce: HTTP/1.1 keep-alive server core, router with :param captures, Handler/Middleware structural interfaces (iteration 16)" @@ -9,4 +9,4 @@ wo = ">= 0.1" # A LIBRARY project (declared above since iteration 17): no `fn main` here — # the consuming app owns the entry. `woc ` typechecks the whole project. # Apps import this repo through `wo.toml [deps]` (iteration 15) and -# `use writeonce-framework` / `use writeonce-framework/http` / `.../router`. +# `use porch` / `use porch/http` / `use porch/router`. diff --git a/docs/examples/shop/README.md b/docs/examples/shop/README.md new file mode 100644 index 0000000..e3bd455 --- /dev/null +++ b/docs/examples/shop/README.md @@ -0,0 +1,132 @@ +# shop — the writeonce program template + +A small store you can buy from, structured the way a real writeonce web +app should be. **Copy this directory to start a new app**; every file +has one concern, and the module system (one directory = one module, +`pub` = the export line) enforces the separation the layout promises. + +## Run it + +``` +cd docs/examples/shop +woc . && WO_DATA=./data ./target/shop 8080 # durable store +./target/shop 8080 # RAM-only (dev) +``` + +Browse http://127.0.0.1:8080/ — products → product page → buy (stock +checked and decremented) → confirmation → /orders. With `WO_DATA`, kill +it and restart: the orders are still there (WAL replay). + +The template builds and runs as written — the two `[deps]` resolve, the +seed lands, and every route answers. + +## The view form + +A `render()` body is one backtick raw text literal. Markup is markup: +real newlines, real double-quoted attributes, and the method's source +indentation removed at compile time, so the served bytes carry the +markup's own nesting and not the code's. + +``` +fn render() -> Text { + return ` +
+

{{ self.name }}

+

€ ${self.price}

+ ${stock} +
`; +} +``` + +Every `render()` makes its class a **component** — writeonce-view's structural +`Component` interface, satisfied by having the method, never declared. +Parent components hold children directly (`cards: multi Component`) and +render them with `render_all`, so `ProductListPage` knows nothing about +`ProductCard` beyond `render()`. The document itself is a component too: +`AppShell { title, content }` in `layout/app.wo`, which links a real +stylesheet rather than inlining one — that is why it is its own shell and +not writeonce-view's `Layout`. + +Two holes, and the difference is the whole escaping story: + +- `{{ expr }}` **HTML-escapes** — it compiles to a call to the `esc` in + scope (writeonce-view's, unless the app declares its own). Display data goes + here; a typo'd field is a compile error, not a broken page. +- `${ expr }` is **raw** — for markup you built yourself, like the + `${stock}` fragment above or `${content}` in the app shell. + +Nothing is parsed at request time. The literal is a compile-time form: +it produces exactly the string constant and concatenation chain the old +hand-written version did, so there is no template engine to ship, warm +up, or sandbox. + +## The file map (Angular equivalents) + +| this template | concern | Angular analog | +| --- | --- | --- | +| `types.wo` | MODEL — `@table` classes ARE the WAL database | `models/*.ts` (+ the entire database) | +| `layout/app.wo` | app shell: document, header+footer composition, `ok_html`/`html_error` transport helpers | `app.component.html` | +| `layout/header.wo` / `footer.wo` | shared chrome fragments | `header.html` / `footer.html` | +| `product_list/view.wo` | VIEW — classes with `fn render() -> Text`, fields = exactly what is displayed | `product-list/view.html` | +| `product_list/controller.wo` | CONTROLLER — query the model, fill the view, answer a `Resp`; beside its view in the same module | component `.ts` + service | +| `product_page/`, `orders/` | one module per feature: `view.wo` + `controller.wo` | feature folders | +| `/assets/*` | served by the framework's `StaticFiles` — the template no longer carries its own copy | `angular.json` assets | +| `assets/style.css` | ONE real stylesheet, sectioned per feature | the `.scss` files | +| the `render()` bodies | ONE raw text literal each: real newlines, real double-quoted attributes, source indentation removed at compile time, `${}` raw holes and `{{ }}` auto-escaping ones | Vue `