- README: shipped concurrency/HTTP/WebSockets sat in the roadmap as "not yet available"; "no package manager" contradicted [deps]; the deps example would not have compiled (the key IS the module name) - runtime/README: leads with wovm, wo-rt.c demoted to a historical section; dropped 2 nonexistent recipes, crates/rt, @gc refcounting, 13 suites -> 18 - employee + log-watcher READMEs claimed "does not compile"; both are gates - error catalog: +10 emitted codes incl WO-E250, the only diagnostic the shipped query surface raises; recorded why the sweep rotted - language-surface: group-by parses, then the typechecker refuses it - 00-code-review + 00-link-audit re-run; history kept, not rewritten - 48 dead Rust-era exploration links de-linked rather than re-pointed (their prose names the retired plan by number); successor map -> discarded.md - 08-project-structure: compiler/plan/ never existed; corpus has 9 dirs, 5 empty - releasing.md: dropped a --draft step the workflow never had - new docs/00-doc-audit.md: findings + disposition, incl one row where the audit was wrong and the doc it accused was right - status folders removed: 34 stories flat, status only in frontmatter; 252 links recomputed from resolved paths; board/board-views/structure retaught - story 24 -> in-progress, since frontmatter is now the only truth - new iteration 38: fs mutation verbs + net.connect, the two capability families no iteration owned - new iteration 39: gofiber/fiber v3.5.0 parity study. The ledger called CSRF/sessions unblocked by iteration 34's HMAC, but the runtime has no source of randomness at all - linkcheck skips .dev/.superpowers: 0 broken paths, 0 bad anchors Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 KiB
| iteration | status |
|---|---|
| 38 | refine |
Iteration 38 — content collaboration workload: the file-mutation and outbound-socket gaps
Format:
product/story-iteration-template. Part of Story — one language, one runtime, one database, one binary.Inserted 2026-08-26 (developer ask: "can the language create an application like Nextcloud, a content collaboration platform?"). The answer was no, and the reason was not missing
.woeffort: two whole capability families are unowned by any iteration. Read off the runtime contract, not off prose —runtime/src/wob.hallocates exactly sixfsbuiltins, ids 40–45 (exists,list,stat,read_all,read_at,append;append"creates the file if absent"), so a file can be created and grown and then never replaced, truncated, deleted or renamed. It allocates tennetbuiltins — 51–55 (listen/accept/read/write/close) and 91–95 (the three_dldeadline twins,listen_unix,peer) — and noconnect;grep -rn 'connect(' runtime/src/returns nothing, so a program cannot open an outbound connection to anything, ever. This iteration is the workload that forces both into the open, in the mould of iteration 28.
Goals
- A writeonce program shaped like a content platform.
docs/examples/vault: authenticated users, folders and documents as@tableclasses related byref/backlink, multipart upload intake, content-addressed blobs, share grants with expiry, a version chain per document, a trash bucket, an activity feed, and a server-rendered UI overwriteonce-serve+writeonce-view. Everything above the storage line is expressible on today's toolchain and is the part that ships. fsgrows the mutation verbs. The gap is not ergonomic. A blob store that can onlyappendcan never reclaim a byte: a cancelled upload, a deleted document and a superseded version all leak their file forever, and there is no atomic-publish primitive (write-temp-then-rename) so every write is torn-visible to a concurrent reader. Name the minimum set the workload actually needs and no more. The syscalls are not the work —sysio.c:669already callsunlink()on thelisten_unixpath to clear a stale socket, soremoveis a builtin-table row over a call the runtime already links.netgrows the client side. Every federation, identity and notification story in a collaboration platform is an outbound call — OIDC token exchange, SMTP for share mail, an object-store backend, a webhook, an antivirus or preview service. All of them are one missing builtin. Iteration 35 declared connect-side timeouts out of scope "no workload asks yet" — this is the workload asking.- Block honestly on the rest, in writing. WebDAV (the protocol every desktop and mobile sync client speaks), collaborative editing, previews and full-text search each get a named verdict in this story — owned elsewhere, deliberately rejected, or deferred with the reason — so the sample's README never implies a feature the stack cannot serve.
Acceptance Criteria
- Given the
vaultsample and today'sfs, when a user deletes a document and the acceptance script inspects the blob directory, then the blob is gone from disk and the disk footprint returns to its pre-upload size — the criterion that is impossible today and is the whole point of the fs half. - Given two concurrent readers of a document while a new version is being written, when the write completes, then every read returned either the whole old version or the whole new one and never a partial file — atomic publish, proven by a racing reader, not asserted.
- Given an upload that is abandoned mid-stream (client disconnect),
when the next request arrives, then no partial blob and no
orphan row survive — cleanup is reachable from
.wo. - Given a share notification configured against a local SMTP sink and
an identity provider stub, when a share is created, then the
program opened an outbound connection, spoke the protocol and recorded
delivery — the connect half proven end to end, offline, with a fixture
server the gate starts itself (the
deps-accept/web-appprecedent: no network in CI). - Given an outbound peer that accepts the connection and then never answers, when the configured deadline expires, then the call returns the expected-timeout shape, the fd is closed, and the fiber is not parked forever — the same cleanup contract iteration 35 set for the listen side.
- Given the full sample under the standing battery, when
oop-accept,web-app,siteand the new gate run, then all are green and ASan-clean at both shard counts.
Out Of Scope
- Raw stdin/stdout byte I/O, bounded/killable subprocesses, recursive directory walk, executable-bit and realpath confinement — every one of these is iteration 28's Blockers B and C and its gap list. This story must not re-own them. It does depend on 28's killable-subprocess work for previews, which is exactly why previews are deferred here rather than attempted.
- WAL checkpoint and disk reclamation — iteration 32. A metadata store whose boot replays every write ever made is a real ceiling for this workload, and naming it here is the point; fixing it is 32's. This story's gate should record the replay time it observes so 32 inherits a number.
- WebDAV. Sync clients speak PROPFIND/PROPPATCH/MKCOL/MOVE/LOCK over
XML bodies. The language has
jsonand no XML parser, andMKCOL/MOVEhave nothing to land on until the fs half exists. Verdict for the spec to confirm: out of this slice, revisited only once file mutation ships, and never as a compiler concern. - Collaborative editing (OT/CRDT). Wants ordered trees and cheap
lookups;
multiis an array,mapis a documented linear scan, there are no closures and no generics beyond those two. Deliberately not attempted in.woat this stage — and the external-document-server route Nextcloud itself takes is unreachable untilconnectlands. - Full-text search. The engine indexes equality probes on declared columns; there is no prefix scan or FTS. Query-grammar growth is iteration 27's.
- TLS — proxy-terminated, by doctrine, unchanged. The outbound half therefore speaks plaintext to a local sidecar or a trusted-network peer, and the story says so out loud rather than implying HTTPS clients.
- A plugin/app ecosystem. In-runtime recompile is iteration 26's; nothing here loads code at run time.
- Scale claims. RAM is authoritative (64 MiB arena by default,
WO_HEAP_MB), so this is a team-scale platform and the README states the row ceiling it was measured at.
Info
The gap ledger this story exists to resolve, with current ownership:
Every "today" cell below was read from the source named beside it, not from another document.
| Capability | Today | Read from | Owner |
|---|---|---|---|
| overwrite / write-at-offset a file | absent; append (id 45) creates-if-absent and grows, nothing more |
wob.h:353-358 |
this story |
| remove, rename, mkdir, truncate | absent from the builtin table | wob.h:353-358 |
this story |
| outbound TCP / unix connect | absent; no connect() call in the runtime at all |
wob.h:364-368,477-485; grep over runtime/src/ |
this story |
| outbound deadline + cleanup | _dl seam exists, listen side only (ids 91–93) |
wob.h:477-482 |
this story, on 35's contract |
| stdin/stdout bytes | no builtin; only <stdint.h> matches stdin |
sysio.c, wob.h |
28, Blocker B |
| bounded, killable subprocess | proc.run (id 56) takes (cmd, args, cls) — no deadline, no signal |
wob.h:369; sysio.c:718 |
28, Blocker C |
directory walk, X_OK, realpath |
absent | sysio.c |
28, gap list |
| WAL checkpoint / bounded replay | no checkpoint, snapshot or truncate anywhere in the WAL; fdatasync per commit |
wal.c:403, wal.h |
32 |
| io_uring on the WAL write path | rings exist in the fiber/net plane only; the WAL is plain fdatasync |
park.c:22-54 vs wal.c:403 |
23 |
WO_DATA as a file |
hardcoded "%s/shard-0.wal" directory form |
main.c:202 |
33 |
| prefix scan / FTS / aggregates | equality probe only; group-by rejected in the typechecker | types.ml:2220,2241 |
27, parked drain |
Builtin id allocation, so this story's work does not collide: WO_B_MAX
is 95u and ids 89 and 90 are reserved holes — iteration 31's
monitor and time.after, named in the active slice's marker and absent
from both wob.h and types.ml. New builtins here start at 96. No
.wob version bump is implied: WOB_VERSION is 6u and moved last for
iteration 36's opcodes 42–46; stdlib builtins are a table row, exactly as
iteration 35's 91–95 were.
What already exists and needs no new capability, verified against the tree:
http/multipart and http/auth in writeonce-serve; sha1/sha256/
hmac_sha256 (builtin ids 85–87) for content addressing, ETags and session
tokens; base64_encode/decode and the Bytes carrier; ws_accept plus
the pure-.wo RFC 6455 frame codec for the activity feed; Component/
Layout with {{ }} escaping for the UI; @table with @unique, ref,
backlink and FK-restrict for the whole permission and version model.
Forks the spec must settle:
- How small is the fs set? Two candidate shapes. Minimal-atomic:
fs.write(create-or-replace whole file),fs.remove,fs.rename,fs.mkdir— four verbs, no offsets, atomic publish by write-temp + rename, and a blob store is expressible. Streaming: addfs.write_at/fs.truncateso a large upload can be resumed and a sparse file assembled. Leaning minimal-atomic: it closes the deletion hole, it is the one shape that composes with 32's own rename-swap, and offsets can be argued later by a workload that measures the need. - What does a handle look like — or is there one? Today every
fsmember takes a path and does the whole operation. Awritethat takes a path keeps that property and keeps handles out of the ownership model; a resumable upload wants an open fd, which means a droppablefs.Filescalar in the shape ofnet.Conn. This fork decides fork 1. - Does
connectreturnnet.Connand nothing more? Cheapest honest answer is yes:net.connect(host, port)andnet.connect_unix(path)yielding the same fd-scalaracceptalready yields, so every existingread/write/close/_dlmember composes unchanged and the client side costs two builtins, not a subsystem. - Path confinement. A content platform maps user-supplied names onto
paths. Without realpath (28's gap) the sample must confine by
construction — content-addressed filenames the user never names — or the
fs half hands
.wocode a traversal footgun the day it lands. This is the security fork and it gates the fs half, not the sample. - Where does the workload stop? The sample is the acceptance test, so
its edge is the story's edge. Leaning: upload / download / delete /
rename / version / share / trash / activity feed, one share
notification over
connect, and no sync-client protocol at all.
Proposed Solution
Two capability halves and a workload, and the workload is written first so
the capability set is argued from a program that needs it rather than from
a wish list. Order: brainstorm the five forks (fork 1 and 2 together, fork
4 before any fs code), then the vault sample against today's toolchain to
establish exactly where it blocks, then the fs verbs, then connect, each
with corpus fixtures and its own gate leg. Big enough to want a spec and a
plan document — this is not a bounded slice like
33.
Precedence: independent of the concurrency chain (stage 3 → 22 → 31 → 24 → 23 → 32) and startable beside it, with one caveat — the honest disk story needs 32, so if this lands first its gate records the replay number rather than claiming the platform is operationally done.