- developer move; all inbound links repointed (root docs, plan/, plan/compiler/, exploration, superpowers plans+specs, in-progress marker), board's own links re-based one level deeper - prose mentions inside landed plans left as historical records Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
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.wowas SPLIT rather than moved whole (itsmedia_type/form_valuesare public surface the web-app calls), and the gate reads 26/0 rather than 17/0 (just web-appwas already at 23 before this iteration). Board: docs/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
(normative). Story:
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
kindmeans 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). internalmatches 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
- 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 — awo.tomlin a fixture dir would flip woc into manifest-build mode, and WO-E108 additionally needs a fetched dep. Both diagnostics are therefore gated inscripts/web-app-accept.sh(which already builds a file:// dep chain), not the corpus. Task 5. - The spec says check mode "stops before image emission". The cheapest
correct implementation reuses
compile_imagewhole — 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--emitprecedent). Behavior matches the spec; the pipeline boundary is one step later than the spec's wording. 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_parseknown-key table ~line 808;manifest_build~line 1023).
Interfaces:
-
Produces:
manifest_parseaccepts top-levelkind;manifest_buildexposes the validated kind ("program" when absent) to Task 2's branch. -
manifest_parse: addkindto the known TOP-LEVEL keys (the match arm that today allowsname,version,description). Nothing else in the parser changes — the value is a quoted string like every other key. -
manifest_build: after thenamecheck, readkind. 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 withkind = "program", and with nokindline, 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_modeno-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 akind = "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 runcompile_image ~deps dir, print diagnostics through the normalfinishpath on error (exit 1), and exit 0 silently on success. Notarget/directory is created, no file is written, no entry is required. The full pipeline runs — parse, typecheck, interface satisfaction, ownership, GC inference — becausecompile_imagealready runs it; the in-memory image is discarded (deviation 2). -
build_mode, the no-entry error: when<dir>/wo.tomlexists and nameskind = "library", append the library hint to the existing message — the project is a library; add amainfor a demo binary or check it withwoc <dir>. Read the manifest only if the file exists; malformed manifests keep failing as they do today. -
usage_msg: thewoc <dir>line notes "builds a program / checks a library, per the manifest'skind"; the long help gains two sentences onkindand check mode. No new flags. -
Verify by hand: scratch library project (
kind = "library", one class, nomain) —woc <dir>exits 0 with no output; plant a type error,woc <dir>exits 1 with the normal diagnostic;woc build <dir> -o xfails with the no-entry message plus the library hint; add amain,woc buildproduces a runnable binary (dual case) whilewoc <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.mapover parsed files: for a file NOT owned by any dep (theowner = Nonebranch, today untouched), inspect eachUsewhose FIRST segment names a dep indeps— when any LATER segment is exactlyinternal, add a diagnostic to the collector: code WO-E108, the use's file andpos, message naming the internal module path and the dependency it belongs to (theDiag.error+Collector.addpattern 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>/internalby 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-projectuse internalnever 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 theuse's line; the dep's own file importinginternalcompiles. -
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(addkind = "library"),app.wo(imports),README.md(verification note). - Move:
http/parse.wo→internal/parse.wo,http/serve.wo→internal/serve.wo(git mv; module becomesframework/internalunder a consumer,internalstandalone). - 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.woandinternal/parse.wonow needuse httpforReq/Resp(they shared thehttpmodule withtypes.wobefore);app.woaddsuse internalfor the serve loop andParsedseam. No declaration changes — imports only. -
wo.tomlgainskind = "library"(top-level, besidename). -
README: replace the
--emitverification workaround sentence with the check-mode invocation (woc <dir>— full pipeline, no entry), and one sentence oninternal/being unimportable by consumers. -
Verify:
compiler/_build/default/bin/woc docs/examples/writeonce-frameworkexits 0 silently — the workaround is dead. -
Verify the consumer:
just web-app— all 14 standing checks pass unchanged (the app imports onlyframework,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/fwframework copy and$W/appconsumer the script already builds; Tasks 1–4's behavior. -
Produces:
just web-appat 17 checks, the iteration's single gate. -
Check 15 — library check mode:
woc "$W/fw"(the framework copy, which now carrieskind = "library"and nomain) exits 0 with empty output. -
Check 16 — the boundary: copy the app to a second temp dir, append a
use framework/internalline to itsmain.wo, compile; assert exit 1 andWO-E108in stderr. -
Check 17 — kind validation: copy the app again, set
kind = "junk"in its manifest, compile; assert exit 2 andWO-E109in 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), story17-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 gainsinternal/), framework story16-web-framework.mdonly if it names the--emitworkaround. - Apply; every claim carries its measured gate result (17/0 etc.); no forward-looking "will".
just web-apponce more after the doc edits (nothing should move — honesty check).- Commit.
Success criteria (spec, restated as the gate reads them)
- Framework checks entry-less (
woc <dir>exit 0) andwoc buildon it fails naming the library kind — gate check 15 + Task 2's hand check. - A consumer import of
framework/internalis WO-E108 at theuse; the framework's own import stays legal — gate check 16 + Task 3. just web-app's original 14 checks pass unchanged after the reorg — Task 4/5.- A manifest without
kindbehaves 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 pathframework/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.