docs: four directories, and a rule for when a thing leaves the roadmap

docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.

A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.

So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.

schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.

Three corrections found on the way, all load-bearing:

  * projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
    sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
  * The root README said "No TCP/IP" months after the stack landed.
  * history/kernel.md sec.7 was marked "in progress" on a problem that process
    groups solved.

Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
Nexoniarz 2026-09-12 17:13:35 +02:00
commit 57aff40228
59 changed files with 3981 additions and 1124 deletions

267
docs/plan/1.0.md Normal file
View file

@ -0,0 +1,267 @@
# SchOS 1.0
A finish line, so that the list of open work stops being infinite.
---
## Why this document exists
[schapi.md](schapi.md) §3 says the stability freeze starts at 1.0 and covers
whatever is marked `stable` at that moment. **1.0 was never defined**, which
means there has been no finish line — and with no finish line every gap is
equally mandatory, the list only grows, and the project feels larger every week
regardless of how much gets done.
That feeling is structural, not a judgement about progress. This document fixes
the structure by drawing a line and putting most of the open work on the far
side of it.
> **Anything not named in this document is not part of 1.0.** It is not
> cancelled, not forgotten, and not up for reconsideration until 1.0 ships.
---
## What 1.0 is
> **A machine one person can sit down at and use for an afternoon without
> falling off an edge.**
Concretely — boots on real hardware, roots on SchFS, runs a shell and the real
toybox userland, is on the network, and puts a windowed terminal on the screen.
It is deliberately a **small complete system** rather than a rich incomplete
one. SchOS 1.0 is not Linux, does not run third-party applications, and has no
package manager. It is a system that works, end to end, with nothing in it that
lies about what it did.
### The acceptance test
One unattended run, asserted from the host, the way everything else here is
tested. When this passes, it is 1.0:
```
boot on real hardware from a USB stick
→ SchFS root mounts, generation counter advances
→ schinit starts the compositor
→ a terminal window appears and is asserted by pixel
→ in it: ls, cat, a file written, a network fetch
→ reboot
→ the file is still there and the checksum matches
```
Nothing in that sentence is a feature list. Every clause is something that is
currently either broken, missing, or unproven.
---
## The four gates
In order. **Do not start a gate before the one before it closes**, and closing
means a description exists in [../architecture/](../architecture/) — see
[the rule](../README.md#the-rule-that-keeps-this-from-rotting).
### Gate 0 — Fix what is already wrong
**This comes first, before any new capability.** The desktop work is the
temptation and it is the wrong thing to do next: a compositor built on a kernel
with an unexplained hang and a socket layer that reports success for things it
did not do will fail in ways indistinguishable from its own bugs. That is the
same argument that put `netdev_selftest()` before the TCP/IP stack, and it was
right then.
**Things that are actively wrong:**
- [x] ~~**The `ls -lR` hang.**~~ **Not reproducible.** Instrumented and hunted;
see below. Left here rather than deleted because the absence of a result
is the result.
- [x] ~~**`setsockopt` returns 0 without setting anything.**~~ Six options now
genuinely reach lwIP's pcb and everything else is `ENOPROTOOPT`. Round-trip
checks in `cmd/socktest`, which a permissive stub cannot pass: it has to
read back a value it was set *and* refuse an unknown option.
- [x] ~~**`accept4` silently drops its flags.**~~ It is its own table entry now
rather than an alias for `accept` — an alias would have had to read a
fourth argument that `accept` never passes, inventing a `SOCK_NONBLOCK`
out of whatever the caller last left in that register.
- [x] ~~**Timestamps on everything created after boot read 1970.**~~ Both
`know()` (SchFS) and `now_sec()` (v1) returned `uptime()` under comments
saying the kernel had no wall clock. It has had one since `time_init()`
started reading the RTC; the comments outlived the condition. Two errors,
not one — `uptime()` counts 10 ms ticks and they were used as seconds, so
the clocks also ran 100× fast.
- [ ] **`getrusage` does not distinguish `RUSAGE_CHILDREN`** from `RUSAGE_SELF`.
- [ ] **`/proc/<pid>/exe` is a regular file, not a symlink**, so `readlink()`
fails and `ps -o comm` prints `[init]`. Needs symlink support in procfs.
- [ ] **OVMF memory reporting**, if the region-list fix did not fully close it.
**Work already done and not connected:**
These are the galling ones — the replacement exists, is tested, and is unused.
- [ ] **Port the console onto the text layer and delete the cell grid.**
`text.c` and `ttf.c` both exist and work. The console still indexes
`font_data[256][16]`.
- [ ] **Load a font from disk** and point the console at it.
- [ ] **Interrupt-driven receive.** The network thread polls the NIC every timer
tick, so a frame waits up to 10 ms. Waking it from the e1000 interrupt was
deliberately not the first version; it is time.
**Structural, because everything after this compounds on it:**
- [ ] **A slab or pool allocator.** `kalloc` hands out one page at a time, and
every subsystem written since has invented its own fixed-size table to
work around it — `NPROC`, `NSOCK`, `NDEVICE`, `NSHM`, `MAXJOBS`. This is
the single change that most improves everything else in the kernel, and
it gets more expensive with every table added.
- [x] ~~**SchFS on-disk format version policy.**~~ Done. `SCHFS_VERSION` for
frame changes, plus `compat`/`ro_compat`/`incompat` feature flags in what
was `reserved0` — so existing images keep every field offset and read
unchanged. `uber_check()` now distinguishes *damaged* (step over it; that
is what the ring is for) from *too new* (stop), because falling back to an
older uberblock mounts a stale filesystem and reports success. Proven on a
forged image: the old code mounted the stale root and exited 0.
Still owed: an actual upgrade step, which is only writable once there are
two formats. See [../architecture/schfs.md](../architecture/schfs.md).
**Found while doing the above, and fixed:**
- [x] ~~Every `AF_INET` socket failed with `ENFILE` until DHCP bound.~~
`inet_new()` required an *address* before it would hand out a socket, and
reported the refusal as "file table overflow". Creating a socket is not
the same as having somewhere to send; a bind or connect with no route
fails on its own with an error that says so. `net_is_ready()` (lwIP up) is
now distinct from `net_is_up()` (DHCP bound).
- [x] ~~`socktest` asserted "AF_INET is refused, not faked".~~ True when written,
false since AF_INET landed — and **green the whole time**, because the
suite runs before DHCP binds and the `ENFILE` above made the refusal look
real. A test asserting a failure has to say *which* failure.
**The identity item:**
- [ ] **Boot from a USB disk.** The xHCI stack, bulk-only transport, SCSI and
the block device all work; a superblock reads back through `bread()`. What
is missing is that the root device is compiled in rather than selectable.
Without this the acceptance test above cannot run at all.
#### The `ls -lR` hang, and why it is closed
Hunted with the instrumentation it asked for, under a workload heavier than the
one that originally triggered it — ten iterations of `ls -lR /bin`, interleaved
with writes and `cd`, 33 execs and 13378 tree descents. Measured across it:
```
inode_peak 9 / 200 unchanged
file_peak 9 / 512 unchanged
buf_pinned_peak 1 / 256 unchanged
schfs0 locked 0 tx_owner_pid 0 tx_depth 0
```
Nothing climbed, nothing wedged, and the repaired `holdingsleep` assertion never
fired. **That eliminates this file's own stated hypothesis** — "a lock or
reference-count leak somewhere in the `*at()` paths" — and it is very likely the
bug was fixed in passing by the `*at()` dirfd rewrite, the getdents cookie
rewrite, or SchFS becoming the root, all of which landed after it was reported.
It is closed rather than chased further, and that is a deliberate application of
the two-things-in-flight rule. The instrumentation is permanent: if it ever
happens again, `/proc/fsstat` names the table, or shows `locked 1` with
`tx_owner_pid 0` and the volume lock is orphaned. That is a much better position
than the one it was reported from.
Two real defects were found on the way and fixed, neither proven to be the
cause: `holdingsleep()` never compared the owning pid, which disarmed all seven
`panic()` assertions built on it; and SchFS's `tx_mine()` compared a bare
`struct proc *`, which a recycled ptable slot makes ambiguous.
### Gate 1 — The foundations
[foundations.md](foundations.md), items 1–4. Descriptor passing, a shareable
memory object, `AF_UNIX` in the filesystem namespace.
Nothing above needs these; everything below stops without them.
### Gate 2 — Init as a role
[init.md](init.md), milestones I0–I3.
Before the desktop rather than after, because a compositor being brought up for
the first time crashes constantly, and something that restarts it and says why is
worth far more during that work than at the end of it.
### Gate 3 — The desktop
[wayland.md](wayland.md), W1–W3, and the terminal.
**W4 and W5 are not in 1.0.** Multi-window focus policy and a panel are
post-1.0. One terminal window that works is the bar.
---
## Not in 1.0
The point of this list is that it is *written down*, so it can stop occupying
attention. Nothing here is cancelled.
| | Why it waits |
|---|---|
| **The hybrid kernel** ([hybrid.md](hybrid.md)) | An architecture improvement, not a completeness requirement. It is also the work that most wants a demanding consumer first, and 1.0's compositor is exactly that consumer. |
| **Dynamic linking, Mesa, SDL3** | Everything at 1.0 is statically linked. This is the single biggest post-1.0 unlock and it is a whole project. |
| **A page cache** | `memfd` covers the desktop's needs. General writable `MAP_SHARED` of a file is a real project with no 1.0 consumer. |
| **`.sip` and the installer** | 1.0 installs by writing a USB stick. That is how most systems start and it is honest. |
| **SchFS snapshots, FUSE, Linux module, sparse files, online fsck** | The format version (Gate 0) is the prerequisite for all of them, and it is the only part on a clock. |
| **The interactive crash screen** | PSOGM renders, prints a full report and emits a QR code. That is enough. |
| **CFF/OTF fonts, kerning, text layout** | TrueType and fixed advances are enough for a terminal. |
| **Routing, `getsockname`/`getpeername`, IPv6** | One interface, one default route, works. |
| **Multiple outputs, damage tracking, subsurfaces, xkbcommon** | One screen, full repaints, one keymap. |
| **Audio** | Does not exist in any form and is not required to use a machine. |
| **Panel, notifications, settings, file manager** | A terminal is a desktop. |
| **SchAPI itself** | Deliberately. It should be designed *from* the compositor's experience, not before it. |
---
## The rule for this list
**Adding to Gate 0–3 requires deleting something else from it.**
Not a productivity trick — a forcing function. The reason this project feels
overwhelming is that ~20 fronts have been open simultaneously, and no amount of
discipline makes that workable. The fix is structural.
And one working limit, worth holding to literally:
> **Two things in flight. Never three.**
One in Gate 0 and one being written up. Everything else is closed — in the
document, and out of your head.
---
## How you will know it is going well
Not by velocity. By this:
**Every closed item leaves a description behind.** When the `ls -lR` hang is
fixed, `history/kernel.md` gains an entry saying what it actually was. When the
console moves onto the text layer, `architecture/graphics.md` loses a line from
its *What it does not do* section.
Those "does not do" sections shrinking is the real progress bar, and it is the
only one that does not lie.
---
## A note worth keeping
This document was written because the work started to feel overwhelming, and
that is worth recording rather than tidying away — because the feeling was
about the *shape* of the work and not its quality, and the two are easy to
confuse when you are inside it.
The evidence at the time: SMP working on real hardware after five bugs stacked
behind each other; a copy-on-write filesystem whose crash test cuts the power one
write later, 119 times, and demands a clean mount after each; a TCP/IP stack
asserted from both ends; an xHCI driver written from the specification; and a
documentation tree that records why things are strange so they do not get
un-fixed by accident.
None of that is the output of a project in trouble. It was a project with too
many tabs open.