Spec 0003 handoff — CLI and vertical slice (2026-10-03, agent `slice3`)
Bound spec: docs/specs/0003-cli-and-vertical-slice.md (frozen). Re-read it before continuing.
1. Done and committed
Commit 7d63ecf (wip(cli): …, committed by pathspec on the shared tree):
| File | Content |
|---|---|
docs/specs/0003-cli-and-vertical-slice.md | the frozen spec (commands, output formats, manifest schema, example layout, e2e test plan, DoD) |
packages/cli/src/main.ts | composition root: effect/cli commands check/build/inspect/graph, BunServices.layer + TsSourceFrontend.layer, BunRuntime.runMain; one @effect-diagnostics unstableApiUsage:off with comment |
packages/cli/src/commands.ts | portable Effects check, build, inspectCommand, graphCommand, resolveProject; errors CheckFailed/UnknownName carry [Runtime.errorReported] = false so runMain exits 1 without re-printing the cause |
packages/cli/src/report.ts | pure diagnostics grouping/formatting (EFFX#### severity message file:line:col, groups by file, (no location) last, summary line) |
packages/cli/src/inspect.ts | pure resolveName (StableId → operation:<name> → unique node by name), renderOperation (research-report tree), schemaDisplay |
packages/cli/src/graph.ts | pure mermaid flowchart LR of the IR or of a node + IRGraph.reachable |
packages/cli/src/manifest.ts | Manifest Schema (effx-manifest v1: compiler versions, semanticHash, generated, diagnostics, locations), ManifestJson (Schema.fromJsonString(…, { space: 2 })), locationsOf (operation ids via Extensions.operationIdOf, model ids via the PersistentModel schema export) |
packages/cli/src/index.ts | re-exports |
packages/compiler/src/Collected.ts | Declaration.location: Schema.optionalKey(Location) (additive) |
packages/frontend-ts/src/collect.ts | fills location at the four sink.declarations.push sites with positionOf(node/member/declarator) |
packages/frontend-ts/src/TsSourceFrontend.ts | export const typescriptVersion = ts.version (manifest field) |
Verified: bun --bun node_modules/.bin/tsc --noEmit -p tsconfig.json → exit 0 after the commit's content.
Smoke run (not a test): cd packages/frontend-ts/test/fixtures/users && bun ../../../../cli/src/main.ts check → prints grouped diagnostics and 5 error(s), 3 warning(s), 0 info, exit 1 (expected: the fixture's include: src/**/*.ts pulls in broken.ts and both syntaxes → EFFX1001 duplicates; the CLI tests must use explicit files tsconfigs, see §5).
Not run: bun run lint, bun run fmt:check (oxfmt not run on the new files — expect formatting diffs in packages/cli/src/*), bun run typecheck, bun --bun vitest run. The lint agent is concurrently replacing the lint config; coordinate before formatting shared files.
Untracked, pre-existing, not mine: packages/cli/package.json (scaffold from Main; needs committing with the package), examples/users/package.json, docs/research/, the tsconfig.json exclude change.
2. In progress (uncommitted: nothing)
Nothing uncommitted. The next concrete step I was about to take: attach declaration.location to interpreter diagnostics in packages/compiler/src/pipeline.ts interpret (lines 39-41: wrap each contribution's diagnostics with location ?? declaration.location), so EFFX22xx/23xx show file:line:col in effx check instead of landing under (no location). Three-line change; message agent lint first (they own packages/compiler except generate/).
3. Decisions beyond the spec
- CLI output uses
Console.logfromeffect(theConsolereference service, asai-docs/src/70_cli/10_basics.tsdoes), notEffect.log:inspect/graphoutput is the product and must be plain text (mermaid, tree), andConsoleis swappable in tests. Globalconsoleis never used. effx checkexit 1 is modelled as a typed failure (CheckFailed) withRuntime.errorReported = false;Runtime.defaultTeardownmaps any failure to exit 1 andmakeRunMainskips the error log whengetErrorReportedis false (node_modules/effect/src/Runtime.ts:108-115, 204-214, 374-400). Noprocess.exit..effx/=dirname(tsconfig)/.effx, matching the frontend's defaultoutDirso generated import specifiers stay valid.- Manifest
locationscovers operation and model ids only (the only nodes with a declaring syntax); exposures/capabilities derive from the same declaration. - Versions:
effxfrompackages/cli/package.json(import cliPackage from "../package.json"),effectfromeffect/package.json(exported subpath, verified innode_modules/effect/package.json),typescriptfrom the frontend. - Spec revision of the
cligenerator (spec 0003 §"Generated CLI contract"): leaf commands must print the success encoded withSchema.fromJsonString(Success); the current generator discards the result. Not yet implemented (see §5).
4. Gotchas
Schema.TaggedErrorsubclasses: overriding[Runtime.errorReported]needsoverride readonly(TS4114) becauseYieldableErrordeclares the global marker.effect/clishapes that typecheck:Command.make(name, { flag }, handler),Flag.String("project").pipe(Flag.withDescription, Flag.withDefault("tsconfig.json")),Argument.String("name").pipe(Argument.withDescription, Argument.optional)→Option<string>in the handler config,Command.withSubcommands([...]),Command.run({ version })→ requiresEnvironment(FileSystem | Path | Terminal | ChildProcessSpawner | Stdio,cli/Command.ts:405) — all provided byBunServices.layer.- Bun 1.3.13 lowers TC39 standard decorators natively (verified with a scratch file), so
bun src/server-main.tsinexamples/userswill work without the vitest TS6 plugin. Schema.encodewith aStructignores excess properties (verified: encoding a fullUserthroughUser.Publicyields onlyid/displayName), so the generated CLI can print a handler'sUservia theUser.Publicsuccess schema.- HTTP server wiring (read, not yet exercised):
HttpApiBuilder.layer(Api).pipe(Layer.provide(ApiHandlers));RpcServer.layerHttp({ group: Operations, path: "/rpc", protocol: "http" })needsRpcSerialization.layerJson+HttpRouter+ handlers (rpc/RpcServer.ts:872-897);HttpRouter.serve(app).pipe(Layer.provide(BunHttpServer.layer({ port })))(http/HttpRouter.ts:1340,BunHttpServer.ts:330). For testsBunHttpServer.layerTestgives an ephemeral-port server plus anHttpClientalready prefixed with its origin (BunHttpServer.ts:350-357), which satisfies both the generatedClient.layer(HttpApiClient.make(Api)needsHttpClient) andRpcClient.layerProtocolHttp({ url: "/rpc" })(rpc/RpcClient.ts:1026, prepends the url). - Generated code imports schemas with extensionless relative specifiers (
../../src/schemas); fine for Bun andmoduleResolution: bundler. - Root
tsconfig.jsonincludesexamples/**/*.ts; the example's composition roots import.effx/generated/*.ts(gitignored), soeffx buildmust run before the roottscgate on a fresh clone. Record this in STATE.md. @PersistentModel({ views: { Public: User.Public } })would hit the class TDZ (decorator expressions evaluate before the class binding); defineUserPublic/UserSelfinschemas.tsand reference those, assign them tostatic Public/Self.- Interpreter diagnostics (EFFX22xx/23xx/1001) currently have no
location; see §2.
5. Remaining steps (ordered) with acceptance criteria
- Attach locations to interpreter diagnostics (
packages/compiler/src/pipeline.tsinterpret). Accept:effx checkon a project withsrc/broken.tsprintsEFFX2201 error … src/broken.ts:L:Cunder a file group. - Generator fix: CLI leaf prints result (
packages/compiler/src/generate/cli.tsleafLines): handler =({ input }) => Effect.flatMap(<handler>(input), (result) => Effect.flatMap(Schema.encodeEffect(Schema.fromJsonString(<Success>))(result), Console.log))(registerEffect,Schema,Consoleimports). Accept: frontend test "generated Effect code typechecks" still exit 0; running the example CLI prints one JSON line. - CLI tests
packages/cli/test/cli.test.ts(child process viaeffect/processChildProcessSpawner, pattern inpackages/frontend-ts/test/frontend.test.ts:308-321, cwd = fixture dir, cmdbun ../../../../cli/src/main.ts …). Write temp tsconfigs into the fixture root withEffect.addFinalizercleanup:tsconfig.effx-ok.json{ extends: "./tsconfig.json", files: ["src/operations.ts"] }andtsconfig.effx-broken.jsonwithfiles: ["src/broken.ts"]. Accept (spec laws): check ok → exit 0, output containsEFFX0001and0 error(s); check broken → exit 1, containsEFFX2201 error;buildtwice →.effx/ir.jsonbyte-identical, manifestgenerated= 4 paths, files exist;inspect User.Get→ containsKind,Query,GET /users/:id,RPC User.Get,CLI users get;graph→ startsflowchart LR, containsExposedAs;graph User.Get→ noUser.ChangeEmail. Add"test"nothing — root vitest pickspackages/*/test/**/*.test.ts. examples/userssources per spec table:tsconfig.json(copy of fixture tsconfig;include: ["src/**/*.ts"],pathsnot needed —examples/users/node_modules/@effx/runtimeis a workspace link),src/schemas.ts,src/user.ts,src/services.ts(Userswithstatic layeroverRef<HashMap>seeded with two users),src/capabilities.ts,src/operations.ts(decorator syntax,@Errors(UserNotFound, EmailTaken)+@Requirements(Users)onchangeEmail),src/server.ts(AppRoutes),src/server-main.ts,src/client-main.ts,src/cli-main.ts;package.jsonscriptseffx:build=bun ../../packages/cli/src/main.ts build,serve=bun src/server-main.ts,client=bun src/client-main.ts,cli=bun src/cli-main.ts. Accept:bun run effx:buildexit 0 writes.effx/{ir.json,manifest.json,generated/{cli,client,http,rpc}.ts}; roottsc --noEmitclean;bun run servelogsListening on;bun run clientlogs both results;bun run cli users get --input '{"id":"u1"}'prints JSON.- e2e test
examples/users/test/e2e.test.tsper spec §e2e (build as child process;BunHttpServer.layerTest+AppRoutes; generated clientUserGet/UserChangeEmail;Users.findto verify persistence;RpcClient.make(Operations)withRpcClient.layerProtocolHttp({ url: "/rpc" })+RpcSerialization.layerJson;EmailTakenas a typed failure). Accept:bun --bun vitest run examples/usersgreen. - Gates in this order, once:
bun run --cwd examples/users effx:build,bun --bun node_modules/.bin/tsc --noEmit -p tsconfig.json,bun run typecheck,bun run lint,bun run fmt(coordinate with agentlint),bun --bun vitest run. Accept: all clean. - Evidence + STATE.md:
wc -l examples/users/src/{operations,schemas,user}.tsvswc -l examples/users/.effx/generated/*.ts; update the lifecycle table (CLI + examples → build, spec 0003), the evidence table, note SQL persistence deferred (in-memoryUsers.layer), theeffx build-before-tscprerequisite, and the location gap if step 1 is skipped. Accept: STATE.md rows cite exact commands and results that were actually run. - Commits (conventional, by pathspec):
feat(compiler): …for steps 1–2,feat(cli): …for 3 (+packages/cli/package.json),feat(examples): …for 4–5,docs(state): …for 7.
Spec 0003 — CLI and vertical slice (`@effx/cli`, `examples/users`)
frozen 2026-10-03. Changes require editing this file explicitly. Depends on: spec 0001 (pipeline, diagnostics, generators), spec 0002 (TsSourceFrontend, module specifiers), ADR 0003 (manifest holds l…
Spec 0004 — mono-web migration gap analysis and plan
proposed migration contract. This document does not authorize a production cutover.