- 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>
271 lines
14 KiB
Markdown
271 lines
14 KiB
Markdown
# Iteration 17 — library kind + `internal/`: implementation plan
|
||
|
||
> **Status: ✅ LANDED 2026-08-20** — executed in full, all six tasks. Was
|
||
> parked the same day (developer directive: framework v1 work first), then
|
||
> unparked and run. Two disclosed deviations, both because the framework grew
|
||
> after this plan was written: `http/parse.wo` was SPLIT rather than moved
|
||
> whole (its `media_type`/`form_values` are public surface the web-app calls),
|
||
> and the gate reads 26/0 rather than 17/0 (`just web-app` was already at 23
|
||
> before this iteration). Board: [docs/00-status.md](../../stories/00-status.md).
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||
> superpowers:subagent-driven-development (recommended) or
|
||
> superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||
> checkbox (`- [ ]`) syntax for tracking.
|
||
>
|
||
> **Style rule (user convention):** this plan carries concept, reason, and
|
||
> required behavior in words plus verification commands only — no
|
||
> implementation or test code blocks; the executor writes the code.
|
||
|
||
**Goal:** `kind = "library"` in `wo.toml` makes a project checkable without
|
||
an entry, the `internal/` rule keeps a dependency's plumbing private
|
||
(WO-E108), and the framework adopts both — `just web-app` proves the public
|
||
surface unmoved.
|
||
|
||
**Architecture:** every change is compile-time and lives in the driver
|
||
(`compiler/bin/main.ml`): the manifest reader learns one key, the manifest
|
||
build path grows a check branch, and the dep-use resolution walk in
|
||
`compile_image` grows the boundary rule. No lexer, parser, typechecker, VM,
|
||
`.wob`, or GC change — the spec's impact analysis is normative.
|
||
|
||
**Tech Stack:** OCaml stdlib only (`woc`), bash acceptance scripts, `just`.
|
||
|
||
**Spec:** [`../specs/2026-08-20-library-kind-internal-design.md`](../specs/2026-08-20-library-kind-internal-design.md)
|
||
(normative). Story:
|
||
[`17-library-projects-internal.md`](../../stories/language-runtime-database/17-library-projects-internal.md).
|
||
|
||
## Global Constraints
|
||
|
||
- Branch `library-internal`; commits local only, never push.
|
||
- OCaml stdlib only; no new executables, no new build steps.
|
||
- Absent `kind` means program — every existing project's behavior is
|
||
byte-identical; the standing gates prove it each task.
|
||
- Exit-code doctrine stays: 0 clean, 1 diagnostics, 2 usage/manifest/IO.
|
||
WO-E108 is a diagnostic (exit 1, printed by the normal collector path);
|
||
WO-E109 is a manifest error (exit 2, `woc: <file>: error WO-E109: ...`,
|
||
the WO-E106 shape).
|
||
- `internal` matches a whole path SEGMENT of a dep-relative module path —
|
||
never a substring.
|
||
- Gates that must stay green after every task: `just woc-test`,
|
||
`just oop-e2e`, `just deps-accept`, `just web-app`, `just log-watcher`,
|
||
`just employee`.
|
||
|
||
## Spec deviations, disclosed up front
|
||
|
||
1. The spec put WO-E108/E109 fixtures in the compile-fail corpus. The
|
||
corpus harness (`scripts/oop-e2e.sh`) compiles a fixture DIRECTORY with
|
||
no manifest and no network — a `wo.toml` in a fixture dir would flip woc
|
||
into manifest-build mode, and WO-E108 additionally needs a fetched dep.
|
||
Both diagnostics are therefore gated in `scripts/web-app-accept.sh`
|
||
(which already builds a file:// dep chain), not the corpus. Task 5.
|
||
2. The spec says check mode "stops before image emission". The cheapest
|
||
correct implementation reuses `compile_image` whole — emission happens
|
||
in memory and the image is discarded; no artifact is written, no entry
|
||
is required (an entry-less image is already legal there, the `--emit`
|
||
precedent). Behavior matches the spec; the pipeline boundary is one
|
||
step later than the spec's wording.
|
||
3. `woc build <dir> -o <out>` never reads the manifest, so it resolves no
|
||
`[deps]` (pre-existing, iteration 15). The dual lib+bin case therefore
|
||
holds for libraries without `[deps]` — the framework qualifies. Recorded
|
||
here, not fixed; a dep-aware explicit build is future work.
|
||
|
||
---
|
||
|
||
## Task 1 — the manifest `kind` key + WO-E109
|
||
|
||
**Files:**
|
||
- Modify: `compiler/bin/main.ml` (`manifest_parse` known-key table
|
||
~line 808; `manifest_build` ~line 1023).
|
||
|
||
**Interfaces:**
|
||
- Produces: `manifest_parse` accepts top-level `kind`; `manifest_build`
|
||
exposes the validated kind ("program" when absent) to Task 2's branch.
|
||
|
||
- [ ] `manifest_parse`: add `kind` to the known TOP-LEVEL keys (the match
|
||
arm that today allows `name`, `version`, `description`). Nothing else in
|
||
the parser changes — the value is a quoted string like every other key.
|
||
- [ ] `manifest_build`: after the `name` check, read `kind`. Absent or
|
||
`"program"` continues to build. `"library"` is Task 2's branch (for this
|
||
task, temporarily fall through to build — the framework does not carry
|
||
the key until Task 4, so nothing observable changes). Any OTHER value
|
||
fails as a manifest error in the WO-E106 print shape with code WO-E109,
|
||
naming the given value and the two legal ones, exit 2.
|
||
- [ ] Verify by hand: a scratch project under the scratchpad with
|
||
`kind = "junk"` refuses with WO-E109 and exit 2; the same project with
|
||
`kind = "program"`, and with no `kind` line, builds as before.
|
||
- [ ] Verify nothing moved: `just woc-test && just deps-accept && just employee`.
|
||
- [ ] Commit.
|
||
|
||
## Task 2 — library check mode + the build-error hint
|
||
|
||
**Files:**
|
||
- Modify: `compiler/bin/main.ml` (`manifest_build`; `build_mode` no-entry
|
||
error ~line 639; `usage_msg` ~lines 42–52 and the long help's build
|
||
paragraph ~line 96).
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 1's validated kind.
|
||
- Produces: `woc <dir>` on a `kind = "library"` manifest runs the full
|
||
check; the behavior Tasks 4–5 verify against.
|
||
|
||
- [ ] `manifest_build`, kind `"library"`: resolve `[deps]` and enforce the
|
||
`[runtime]` constraint exactly as build does (a library must be
|
||
checkable offline once locked), then run `compile_image ~deps dir`,
|
||
print diagnostics through the normal `finish` path on error (exit 1),
|
||
and exit 0 silently on success. No `target/` directory is created, no
|
||
file is written, no entry is required. The full pipeline runs — parse,
|
||
typecheck, interface satisfaction, ownership, GC inference — because
|
||
`compile_image` already runs it; the in-memory image is discarded
|
||
(deviation 2).
|
||
- [ ] `build_mode`, the no-entry error: when `<dir>/wo.toml` exists and
|
||
names `kind = "library"`, append the library hint to the existing
|
||
message — the project is a library; add a `main` for a demo binary or
|
||
check it with `woc <dir>`. Read the manifest only if the file exists;
|
||
malformed manifests keep failing as they do today.
|
||
- [ ] `usage_msg`: the `woc <dir>` line notes "builds a program / checks a
|
||
library, per the manifest's `kind`"; the long help gains two sentences
|
||
on `kind` and check mode. No new flags.
|
||
- [ ] Verify by hand: scratch library project (`kind = "library"`, one
|
||
class, no `main`) — `woc <dir>` exits 0 with no output; plant a type
|
||
error, `woc <dir>` exits 1 with the normal diagnostic; `woc build <dir>
|
||
-o x` fails with the no-entry message plus the library hint; add a
|
||
`main`, `woc build` produces a runnable binary (dual case) while
|
||
`woc <dir>` still only checks.
|
||
- [ ] Verify nothing moved: `just woc-test && just oop-e2e && just deps-accept`.
|
||
- [ ] Commit.
|
||
|
||
## Task 3 — the `internal/` boundary rule (WO-E108)
|
||
|
||
**Files:**
|
||
- Modify: `compiler/bin/main.ml` (`compile_image`'s dep-use walk,
|
||
~lines 487–518).
|
||
|
||
**Interfaces:**
|
||
- Consumes: the existing walk that already knows, per file, whether a dep
|
||
owns it (`owner`) and prefixes dep-internal use paths.
|
||
- Produces: WO-E108 diagnostics in the collector; Task 5 gates on the code
|
||
string.
|
||
|
||
- [ ] In the same `List.map` over parsed files: for a file NOT owned by any
|
||
dep (the `owner = None` branch, today untouched), inspect each `Use`
|
||
whose FIRST segment names a dep in `deps` — when any LATER segment is
|
||
exactly `internal`, add a diagnostic to the collector: code WO-E108, the
|
||
use's file and `pos`, message naming the internal module path and the
|
||
dependency it belongs to (the `Diag.error` + `Collector.add` pattern at
|
||
~line 277). Do not drop the use — the collector's has-error path already
|
||
prevents emission, and later resolution errors on the same use are
|
||
harmless duplicates suppressed by exit-on-first-report ordering as
|
||
today.
|
||
- [ ] Dep-owned files stay untouched on this path — the boundary is
|
||
consumer-only (spec §3): the dep's own `use internal` (prefixed to
|
||
`<dep>/internal` by the existing arm) must keep compiling.
|
||
- [ ] The root project's own `internal/` directories are NOT matched: the
|
||
rule keys on the first segment being a DEP name, so a root-project
|
||
`use internal` never fires it. No code needed — assert it in the Task 5
|
||
gate instead.
|
||
- [ ] Verify by hand: temp dir pair — a dep with an `internal/` module and
|
||
a consumer importing `<dep>/internal` — compile fails exit 1 printing
|
||
WO-E108 at the `use`'s line; the dep's own file importing `internal`
|
||
compiles.
|
||
- [ ] Verify nothing moved: `just woc-test && just deps-accept && just web-app`.
|
||
- [ ] Commit.
|
||
|
||
## Task 4 — framework reorg: adopt `kind` + `internal/`
|
||
|
||
**Files:**
|
||
- Modify: `docs/examples/writeonce-framework/wo.toml` (add
|
||
`kind = "library"`), `app.wo` (imports), `README.md` (verification note).
|
||
- Move: `http/parse.wo` → `internal/parse.wo`,
|
||
`http/serve.wo` → `internal/serve.wo` (git mv; module becomes
|
||
`framework/internal` under a consumer, `internal` standalone).
|
||
- Not touched: `http/types.wo`, `router/router.wo`, the web-app.
|
||
|
||
**Interfaces:**
|
||
- Consumes: Task 2's check mode (standalone verification), Task 3's rule
|
||
(what the move protects).
|
||
- Produces: the reorganized layout Task 5's gate checks against.
|
||
|
||
- [ ] Move the two plumbing files. Same-module access dies with the move:
|
||
`internal/serve.wo` and `internal/parse.wo` now need `use http` for
|
||
`Req`/`Resp` (they shared the `http` module with `types.wo` before);
|
||
`app.wo` adds `use internal` for the serve loop and `Parsed` seam. No
|
||
declaration changes — imports only.
|
||
- [ ] `wo.toml` gains `kind = "library"` (top-level, beside `name`).
|
||
- [ ] README: replace the `--emit` verification workaround sentence with
|
||
the check-mode invocation (`woc <dir>` — full pipeline, no entry), and
|
||
one sentence on `internal/` being unimportable by consumers.
|
||
- [ ] Verify: `compiler/_build/default/bin/woc docs/examples/writeonce-framework`
|
||
exits 0 silently — the workaround is dead.
|
||
- [ ] Verify the consumer: `just web-app` — all 14 standing checks pass
|
||
unchanged (the app imports only `framework`, `framework/http`,
|
||
`framework/router`, so NOTHING in it changes).
|
||
- [ ] Commit.
|
||
|
||
## Task 5 — gate: three new checks in web-app-accept
|
||
|
||
**Files:**
|
||
- Modify: `scripts/web-app-accept.sh` (after the build check, before the
|
||
serve matrix).
|
||
|
||
**Interfaces:**
|
||
- Consumes: the `$W/fw` framework copy and `$W/app` consumer the script
|
||
already builds; Tasks 1–4's behavior.
|
||
- Produces: `just web-app` at 17 checks, the iteration's single gate.
|
||
|
||
- [ ] Check 15 — library check mode: `woc "$W/fw"` (the framework copy,
|
||
which now carries `kind = "library"` and no `main`) exits 0 with empty
|
||
output.
|
||
- [ ] Check 16 — the boundary: copy the app to a second temp dir, append a
|
||
`use framework/internal` line to its `main.wo`, compile; assert exit 1
|
||
and `WO-E108` in stderr.
|
||
- [ ] Check 17 — kind validation: copy the app again, set
|
||
`kind = "junk"` in its manifest, compile; assert exit 2 and `WO-E109`
|
||
in stderr.
|
||
- [ ] Renumber nothing — the script counts dynamically (`pass`/`fail`);
|
||
update only the header comment's check inventory.
|
||
- [ ] Run the full battery: `just web-app` (17/0) and the standing gates —
|
||
`just woc-test`, `just oop-e2e`, `just deps-accept`, `just log-watcher`,
|
||
`just employee`.
|
||
- [ ] Commit.
|
||
|
||
## Task 6 — docs closeout
|
||
|
||
**Files:**
|
||
- Modify: `docs/00-status.md` (NEXT PLAN advances; board row 17 → done
|
||
with what landed; in-progress row moves to the next order item),
|
||
story `17-library-projects-internal.md` (landing banner),
|
||
`compiler/src/CODE-LOGIC.md` (driver section: `kind`, check mode, the
|
||
boundary rule), `docs/08-project-structure.md` (framework layout gains
|
||
`internal/`), framework story `16-web-framework.md` only if it names the
|
||
`--emit` workaround.
|
||
- [ ] Apply; every claim carries its measured gate result (17/0 etc.); no
|
||
forward-looking "will".
|
||
- [ ] `just web-app` once more after the doc edits (nothing should move —
|
||
honesty check).
|
||
- [ ] Commit.
|
||
|
||
## Success criteria (spec, restated as the gate reads them)
|
||
|
||
1. Framework checks entry-less (`woc <dir>` exit 0) and `woc build` on it
|
||
fails naming the library kind — gate check 15 + Task 2's hand check.
|
||
2. A consumer import of `framework/internal` is WO-E108 at the `use`;
|
||
the framework's own import stays legal — gate check 16 + Task 3.
|
||
3. `just web-app`'s original 14 checks pass unchanged after the reorg —
|
||
Task 4/5.
|
||
4. A manifest without `kind` behaves byte-identically — every standing
|
||
gate, every task.
|
||
|
||
## Self-review notes
|
||
|
||
- Spec coverage: §1 kind key → Task 1; §2 driver modes → Task 2; §3 rule →
|
||
Task 3; §4 diagnostics → Tasks 1–3; §5 reorg → Task 4; §6 gate → Task 5;
|
||
out-of-scope list untouched by any task. Corpus-fixture clause replaced
|
||
by deviation 1; emission-boundary wording by deviation 2; dual-with-deps
|
||
limit by deviation 3.
|
||
- Type consistency: the only cross-task names are the two code strings
|
||
(WO-E108, WO-E109), the manifest key `kind`, and the module path
|
||
`framework/internal` — spelled identically in every task.
|
||
- Risk, disclosed: moving serve/parse breaks same-module visibility they
|
||
silently enjoyed beside `types.wo`; Task 4 names the exact import each
|
||
file must gain, and the standalone check catches any miss before the
|
||
gate runs.
|