diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/codex-thread-ids.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/codex-thread-ids.md new file mode 100644 index 0000000000..b1ba8722a8 --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/codex-thread-ids.md @@ -0,0 +1,16 @@ +# typed-error — Codex implementation thread +- **Thread / session id:** `01a006f3-ae2d-7941-bd17-2ac71dd3d0f0` +- **Rollout:** `/home/codex/.codex/sessions/2026/08/15/rollout-2026-08-15T21-43-48-01a006f3-ae2d-7941-bd17-2ac71dd3d0f0.jsonl` +- **Worktree:** `/home/codex/repos/netscript-007-leaf-typed-error` +- **Branch:** `fix/sdk-typed-error-channel` @ `0ef48c2ec` (NO upstream by design). +- **Push rule:** explicit refspec only — `git push origin HEAD:refs/heads/fix/sdk-typed-error-channel`. +- **Requested route:** provider=openai · model=gpt-5.6-sol · effort=medium +- **Observed route:** provider=openai · model=gpt-5.6-sol · effort=medium +- **Route verdict:** matched +- **Runtime:** approval=never · sandbox=dangerFullAccess +- **Brief (staged):** `/home/codex/typed-error-brief.md` +## Steering (same thread — never a second send-message-v2 at this worktree) +```bash +codex exec resume 01a006f3-ae2d-7941-bd17-2ac71dd3d0f0 -- "" +``` +_Written by `.llm/tools/agentic/codex/launch-codex-slice.ts`._ \ No newline at end of file diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/context-pack.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/context-pack.md new file mode 100644 index 0000000000..d31ae5a6ca --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/context-pack.md @@ -0,0 +1,106 @@ +# Context Pack: sdk-typed-error-channel (#1350) + +## Run Metadata + +| Field | Value | +| -------------- | ---------------------------------------------------------- | +| Run ID | `fix-sdk-typed-error-channel--0.0.7-wave1` | +| Branch | `fix/sdk-typed-error-channel` | +| Current phase | `implementation` — S4 stopped; S4-R amended with finding→correction mapping, 1 finding unresolved | +| Archetype | `1 — Small Contract` slice | +| Scope overlays | `docs` | + +## Current state + +PLAN-EVAL is terminal PASS at evaluator commit `f76a3c45b`; S1-S3 each passed fresh Tier-A. The +delivered product preserves the exact six-key contract error union through the real `ServiceClient` +promise marker, `safe()`, and `isDefinedError()`, and the two published docs pages carry one +compile-accurate narrative. S4 root check/test, scoped lint/format, quality, architecture, docs, +surface attribution, and publish dry-runs completed. Raw doc lint then proved new leaf-owned +private-type-reference findings (Contracts 9 -> 11; SDK 3 -> 13), so S4 stopped without product +changes. #1466 still owns metadata definition/export; no metadata vocabulary or acceptance claim was +introduced. + +## Completed + +- Required skill/harness/doctrine/RFC/issue reading. +- `deno doc` public-surface inspection before source inspection. +- Focused source map and executed whole-repo consumer search. +- Exact RED and JSR/publish baseline inspection. +- Research, plan, design, risk/gate set, docs dispositions, and rescope report. +- Separate-session PLAN-EVAL PASS with advisories A1-A5 incorporated. +- S1 builder annotation and real-export regression fixture using contracts-exported schemas. +- S1 structured check/test/lint/format gates. +- S1 fresh Tier-A PASS at `dc034d680`. +- S2 uses upstream public `ClientPromiseResult`, `ErrorFromErrorMap`, and `ThrowableError` types to + carry the procedure error map without a local schema shim or ambient declaration. +- S2 literal failure arms, real positive assertions, runtime identity test, and focused consumer + compatibility checks/tests. + +## Next steps + +1. S4-R (separate plan-only Claude session, `worklog.md` § S4-R) mapped all 13 new findings to + corrections; 12 of 13 resolve cleanly (all 10 SDK; `BaseContractErrors` and `Schema` in contracts). + `baseContract → ContractBuilder` is unresolved and needs a coordinator ruling: authorize a narrow + `src/public/mod.ts` re-export of `ContractBuilder`/`Schema`, or accept it as permanent leaf-owned + known-red debt. +2. Once the coordinator rules on `ContractBuilder`, the correction is a fresh authorized product slice + over the same three files (`contract-primitives.ts`, `errors.ts`, `service-client.ts`); S4-R itself + remains run-artifact-only, same as S4. +3. Two items are flagged as "reasoned, not `deno check`-proven" in S4-R and need verification at + implementation time: (a) `oc.errors(commonErrorMap)` still type-checks once `commonErrorMap` is + built from the public `ContractSchema`-typed schema aliases instead of the private Zod-typed ones; + (b) `ContractBuilder`'s generic constraint accepts `ContractSchema` in the + pre-`.input()`/`.output()` position. +4. After the repair lands, rerun the incomplete final matrix (JSR audits, specifier/export guards) + at a newly locked immutable content head. + +## Key decisions + +- Exact six-key `typeof commonErrorMap`; never open `ErrorMap`. +- `SafeResult` gains literal defined/non-defined failure arms and failure `data: undefined`. +- No broad fallback error union; error identity must originate in the real client promise. +- Breaking published change, not patch-level. +- No new export. The empty fourth metadata generic remains explicit; all metadata vocabulary belongs + to #1466. +- The exact six-path ceiling is locked; any seventh product/test/docs path requires a fresh ruling. + +## Files changed + +- `packages/contracts/src/application/contract-primitives.ts` +- `packages/sdk/src/client/errors.ts` +- `packages/sdk/src/ports/service-client.ts` +- `packages/sdk/tests/readme-doctest_test.ts` +- existing files under `.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/` + +No docs/S3-S4 product file was modified. + +## Gates + +- PLAN-EVAL: PASS (`plan-eval.md`, `f76a3c45b`). +- S1 RED: expected FAIL with exactly TS18046 and TS2339, captured once. +- S1 focused check/test/lint/format: PASS; full JSON is in `worklog.md`. +- S2 focused check: PASS, 15 affected source/test/type-fixture files, 0 diagnostics. +- S2 consumer tests: PASS, 12/12 results across SDK doctest/query/desktop and Fresh extraction. +- S2 lint and format: PASS after correcting two type-only imports; the initial lint red is retained + in `worklog.md`. +- S4 root check/test, scoped lint/format, quality, architecture, docs, and publish dry-runs: PASS. +- S4 raw `surface:diff`: known base RED plus exactly 15 authorized signature changes; after + subtracting those changes the 972-finding base/head sets have identical SHA-256 digests. +- S4 raw doc lint: blocking new RED; Contracts 9 -> 11 and SDK 3 -> 13. +- JSR audits and selected specifier/export guards: NOT RUN after the mandated S4 stop. +- S4-R (plan-only): 12/13 findings mapped to type-safe corrections (SDK 10/10, contracts 2/3); + `baseContract -> ContractBuilder` unresolved pending coordinator ruling. No gate was (re-)executed; + this is a mapping, not a rerun. + +## Open questions + +- Live service integration tests requiring a runtime lease were not run because this slice forbids a + lease. Benchmark reference prose remains coordinator-owned follow-up debt. + +## Drift and debt + +- Drift: earlier scope/ownership conflicts and the README research correction are recorded in the + append-only `drift.md`. +- Debt: no new debt accepted. +- Commit trail: draft PR commit list plus phase comment; no `commits.md`. diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/drift.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/drift.md new file mode 100644 index 0000000000..9f77e55bed --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/drift.md @@ -0,0 +1,105 @@ +# Drift Log: sdk-typed-error-channel (#1350) + +## 2026-08-15 — Real client error loss is outside the declared surface + +- **What:** Exact procedure error identity is erased in `packages/sdk/src/ports/service-client.ts`, + which is not one of the five declared files. +- **Source:** Executed `deno doc`, source map, and TS2339 RED. +- **Expected:** The declared surface was sufficient for an end-to-end `safe()` repair. +- **Actual:** `ServiceClientMethod` returns plain `Promise` before `safe()` sees the value; + editing `errors.ts` alone cannot recover the missing type. +- **Severity:** significant +- **Action:** rescope; request topic-orchestrator authorization and stop product work. +- **Evidence:** `packages/sdk/src/ports/service-client.ts:165-196`; + `packages/sdk/src/client/service-client.ts:41-65`; `research.md` §§1,3. + +## 2026-08-15 — Procedure metadata ownership and barrel scope conflict + +- **What:** The lane brief assigns procedure-metadata preservation to #1350, while live #1466 owns + metadata initialization/export and #1350's existing maintainer comment explicitly excludes it. +- **Source:** RFC 0001, issues #1348/#1350/#1466 fetched on 2026-08-15, and declared file surface. +- **Expected:** Stage 0 had settled one unambiguous owner and the declared files could publish it. +- **Actual:** RFC 0001 says Stage 0 must choose; #1348 contains internally conflicting wording; + #1466 is open; and the required curated root barrel is outside scope. +- **Severity:** architectural +- **Action:** rescope; brief treated as latest planning instruction, but implementation blocked for + topic-orchestrator ruling. +- **Evidence:** `rfcs/0001-sdk-client-contributions.md:347-370,1267-1277`; + `packages/contracts/src/public/mod.ts:1-6`; `research.md` “Re-baseline and authority.” + +## 2026-08-15 — Out-of-scope published prose already describes the erasure + +- **What:** Contracts/benchmark prose outside the two authorized docs pages explicitly says the + public base contract is erased; a successful repair makes that prose false. +- **Source:** Executed whole-repo consumer search. +- **Expected:** The two declared docs pages were the complete affected narrative. +- **Actual:** Additional published/reference consumers exist. +- **Severity:** significant +- **Action:** request scope disposition; do not edit without authorization. +- **Evidence:** `packages/bench/tasks/t1-storefront-api/reference/README.md:45-47`; + `packages/bench/tasks/t1-storefront-api/reference/netscript/router.ts:7-8`; + `packages/contracts/README.md:13-76`. + +## 2026-08-15 — Coordinator resolution locks six paths and #1466 metadata ownership + +- **What:** The coordinator corrected the original brief after reconciling #1348's amendment and + #1350 comment 5227724542. +- **Resolution:** `packages/sdk/src/ports/service-client.ts` is authorized as the sixth and final + product path. Any seventh product/test/docs path requires a fresh ruling. +- **Metadata:** #1466 owns definition, initialization, export, and semantic proof. #1350 retains + only the explicit fourth generic slot as `Record` and introduces no vocabulary. +- **Denied:** The contracts public barrel and all metadata export work. +- **Deferred:** Stale contracts/benchmark prose remains tracked follow-up debt outside this leaf. +- **Phase effect:** The plan has no remaining must-resolve decision, but fresh Tier-A review and a + separate PLAN-EVAL `PASS` remain hard stops before implementation. + +## 2026-08-15 — PLAN-EVAL corrects the out-of-scope prose classification + +- **What:** Original research grouped `packages/contracts/README.md` with benchmark prose that + describes the old erasure. +- **Actual:** The contracts README says the common error map is applied, which remains true. Only + the benchmark reference README/router contain the stale erasure claim. +- **Action:** Corrected existing research/plan/context artifacts. No new file or debt entry was + created; the coordinator owns any later issue for the benchmark prose. +- **Scope:** Both locations remain outside the exact six-path ceiling; no product/docs edit here. + +## 2026-08-15 — S4 raw doc-lint findings exceed the pinned baselines + +- **What:** The plan-locked raw Contracts and SDK `deno doc --lint` gates are still red and now + contain leaf-owned private-type-reference findings beyond their base sets. +- **Source:** Executed raw lint at immutable head `c7cba6d9b`, then executed the same commands over + an archive of `main@0ef48c2e`; exact identity deltas are recorded in `worklog.md`. +- **Expected:** Pinned raw reds remain red with no new leaf-owned findings. +- **Actual:** Contracts changes from 9 to 11 (three additions, one removal); SDK changes from 3 to + 13 (ten additions). The additions name `baseContract`, `ServiceClientMethod`, + `ServiceClientShape`, `SafeFailure`, `SafeResult`, `isDefinedError`, and `safe`. +- **Severity:** blocking final-gate regression. +- **Action:** Stop S4. Do not edit S1/S2 product files in the run-artifact-only slice. Remaining JSR + and specifier/export guards are not run or claimed pending coordinator disposition. +- **Scope:** A correction necessarily returns to already-landed product paths and therefore needs a + fresh authorized implementation slice; no seventh path is implied or authorized here. + +## 2026-08-15 — S4-R amends the stop with a finding→correction mapping; one finding unresolved + +- **What:** A separate, plan-only Claude session (native route, quota-exhausted Codex route excluded + per `CLAUDE.md`'s documentation-authoring exception) mapped all 13 new leaf-owned `private-type-ref` + findings (contracts 3, SDK 10) to individual type-safe corrections, verified against isolated + scratch probes of `deno doc --lint`'s actual resolution rules and against the real installed + `@orpc/*` `.d.ts` files. Full mapping is in `worklog.md` § S4-R. +- **Result:** 12 of 13 findings resolve cleanly (all 10 SDK findings; 2 of 3 contracts findings — + `BaseContractErrors` and `Schema`). One contracts finding — `baseContract → ContractBuilder` — does + **not** resolve within the three-file/no-new-export ceiling: it requires either re-exporting + `ContractBuilder`/`Schema` from `src/public/mod.ts` (the forbidden fourth path), duplicating oRPC's + entire builder class locally (rejected as an AP-1/AP-9 violation and a drift/maintenance hazard), or + reverting to an inference-erasing annotation (rejected — it reintroduces the exact "six codes erase + to open `ErrorMap`" regression #1350 exists to fix). +- **Severity:** significant — narrows the S4 blocker from 2 files/13 findings to 1 file/1 finding, but + does not close it; a coordinator ruling is still required before the repair slice can proceed. +- **Action:** Coordinator must choose: (a) authorize a narrow `src/public/mod.ts` re-export of + `ContractBuilder`/`Schema` type names as a scope amendment, or (b) accept `baseContract → + ContractBuilder` as permanent, irreducible, leaf-owned known-red debt alongside the existing pinned + baseline (contracts would then land at 9 base + 1 new = 10 total findings, down from today's 11, + with the 1 remaining new finding explicitly justified rather than silently carried). +- **Scope:** No product/test/docs/lock file was touched to produce this mapping. `#1348`/`#1466` + untouched. The repair itself remains a fresh, separately authorized implementation slice; this + session did not implement it. diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan-eval.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan-eval.md new file mode 100644 index 0000000000..d4c9d2acf7 --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan-eval.md @@ -0,0 +1,196 @@ +# PLAN-EVAL — fix-sdk-typed-error-channel--0.0.7-wave1 + +- Plan evaluator session: Claude bg session `50898ac7-6e79-4f31-ae18-694bb36b7c79` · 2026-08-15 +- Run: `fix-sdk-typed-error-channel--0.0.7-wave1` · PR #1671 (draft) · issue #1350 +- Surface / archetype: `packages/contracts` + `packages/sdk` published surface · Archetype 1 slice +- Scope overlays: `docs` + +## Identity (Step 0) + +| Field | Observed | +| --- | --- | +| Session PID / bridge | pid `636411` (`~/.claude/sessions/636411.json`), `bridgeSessionId` `session_015RuDy1h3UiCkLzo1PLk5Sc`, Remote Control `https://claude.ai/code/session_015RuDy1h3UiCkLzo1PLk5Sc` | +| Job | `50898ac7`; `respawnFlags` = `--permission-mode bypassPermissions --remote-control --name "NetScript 0.0.7 #1671 PLAN-EVAL" --effort medium --model claude-fable-5` | +| Requested route | `formal_plan_evaluation` → native Claude · `claude-fable-5` · medium · Remote Control (`lane-policy.md:45`, `routing-policy.ts:326-332`) | +| Observed route | Claude · `claude-fable-5` · medium · Remote Control — **matched** | +| Generator separation | Plan author is Codex `01a006f3-ae2d-7941-bd17-2ac71dd3d0f0` (`supervisor.md`); this is a separate opposite-family session — invariant holds | +| cwd | `/home/codex/repos/netscript-007-leaf-typed-error` | +| `git rev-parse HEAD` | `2fa2f71dc5b498c16221461439e53b9f5dc1d5d5` == immutable head; `gh pr view 1671 --json headRefOid` == same | + +## Step 1 — phase invariant + +```text +$ git diff --name-only 0ef48c2ec..HEAD +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/codex-thread-ids.md +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/context-pack.md +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/drift.md +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan.md +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/research.md +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/supervisor.md +.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/worklog.md +``` + +`.llm/runs/` only; working tree clean. **PASS.** + +## Step 2 — ownership boundary (re-derived from live records) + +- #1348 body header (fetched via `gh issue view 1348`): "Stage 0 is accepted; #1466 owns procedure + metadata; #1349–#1353 are amended below." The older `NetScriptProcedureMeta.policy` prose is + declared historical/non-normative in that same header. +- #1350 comment `5227724542` (rickylabs, 2026-08-08): #1350 "remains focused on the + `safe()`/literal-preserving error repair and must land the shared four-generic `ContractBuilder` + spelling … It does **not** silently own `NetScriptProcedureMeta` initialization." +- #1466 (OPEN, milestone 0.0.7): "define NetScriptProcedureMeta without erasing contract errors" — + owns public shape, propagation to clients/generated clients/query factories, docs. +- `rfcs/0001-sdk-client-contributions.md:1269-1272` stage table: 1a (#1350) "Preserve the concrete + base error map and client error channel exactly as filed"; 1b metadata "owner selected in Stage 0" + → #1466 per #1348 header. `:347-370` requires the explicit four-generic annotation and forbids + `ReturnType`. + +Plan judgement: `plan.md` "Locked error contract" pins the builder to +`ContractBuilder, typeof commonErrorMap>, Record>` +and states "assigns no fields or semantics to [the fourth slot]"; "What becomes public" says no +metadata type/field/initialization/export. The only metadata proof is the compile-only +`Equal>` guard, which I executed at base — it is **already green at +base** (see Step 5), so it is a regression guard, not an acceptance claim depending on #1466. No +plan acceptance item requires #1466. **PASS.** + +Advisory (non-blocking, close-gate): #1350's own acceptance boxes 2–4 still literally mention "the +RFC 0001 procedure-metadata type … through … client inference, and handler inference". The PR body +carries `Closes #1350`; at IMPL/close the PR must state explicitly which boxes are satisfied under +comment `5227724542` (slot preserved, no vocabulary) and that metadata *semantics* are #1466 — do not +tick metadata boxes. (I did not touch #1348/#1466/#1350.) + +## Step 3 — scope ceiling + +- `plan.md` "Exact six-path ceiling" lists exactly the six authorized paths; "A seventh … is a + rescope requiring a fresh coordinator ruling"; `public/mod.ts`, `contracts/README.md`, bench files + named as not authorized. `research.md:199-200`: "`packages/contracts/src/public/mod.ts` is + explicitly denied for this leaf; there is no metadata definition/export branch." `drift.md` + entry 4 "Denied: The contracts public barrel". No conditional branch survives. **Confirmed.** +- Is `ports/service-client.ts` sufficient end-to-end? Executed probe (transient file under + `packages/sdk/tests/`, deleted; tree clean): + - `createORPCClient(link) as { list: (input) => ClientPromiseResult> }` + compiles → the runtime cast in `packages/sdk/src/client/service-client.ts:65` stays comparable + once `ServiceClientMethod` carries `TError`; that file does **not** need to change. **Confirmed + sufficient** in type space (`errors.ts` extraction + port `TError` derivation). + - `service-query-utils.ts:161` `call: ServiceClientMethod` and + `tests/type-fixtures/sdk-client-contributions-rfc_type.ts:194-198` remain assignable iff the + added error generic is defaulted — the plan's compatibility-default risk row covers it. + - `rg "errors\.[A-Z_]+\("` across `packages plugins` finds only `errors.INTERNAL(` in a plugin-core + comment; no baseContract handler uses an undeclared code, so tightening the error map does not + ripple into a seventh path. +- Hidden-seventh-path check: `packages/sdk/deno.json` maps **no `zod`** (executed: a probe + `import { z } from 'zod'` under `packages/sdk/tests` → `TS2307`). The plan's fixture "builds a + real route from `baseContract`" and needs schemas; it must use `@netscript/contracts`-exported + schemas (`CursorPaginationInputSchema`, `SuccessSchema`, `NotFoundErrorSchema` — resolvable, probe + compiled) rather than adding a `zod` mapping. See advisory A2. + +**PASS.** + +## Step 4 — breaking-change honesty + +Upstream pinned `@orpc/client@1.14.6` (`dist/index.d.mts:139-161`): `SafeResult` = success arm | +`[Exclude, undefined, false, false]` | `[Extract, undefined, +true, false]`; `safe(promise: ClientPromiseResult<…>)`. Current +NetScript (`packages/sdk/src/client/errors.ts:39-49,86`): one failure arm, `data: null`, +`isDefined: boolean`, `SafeResult`, `safe(PromiseLike)`. + +- The plan's declared break (`null`→`undefined` failure `data`, one arm→two literal arms, tighter + `baseContract` key space, `TError` default `unknown`→`ThrowableError`) is **correct and not + over-declared**. Executed repo search (`SafeFailure|data === null` over packages/plugins/docs) + finds only `errors.ts`, `client/mod.ts` and the generated reference table (names only) — matches + research §3. +- Blast radius, repo-wide consumer search (research §3, executed `rg` commands), migration note, + 0.0.7-minor placement are all present. **Confirmed.** +- One understatement (advisory A3): the locked shape writes `type SafeFailure` with **no + default**, whereas today `SafeFailure` is legal to reference bare. That is an + extra, avoidable break not listed; keep a default (`= ThrowableError`) unless dropping it is + intended and then declare it. + +**PASS.** + +## Step 5 — proof and docs + +Executed at base with the plan's exact snippet against real exports (`baseContract`, +`ServiceClient`, `safe`, `isDefinedError`): + +```text +TS18046 [ERROR]: 'result.error' is of type 'unknown'. ← plan snippet: result.isDefined && result.error.code +TS2339 [ERROR]: Property 'code' does not exist on type 'never'. ← isDefinedError(error) path (#1350's probe) +keyof typeof baseContract['~orpc']['errorMap'] = 'NOT_DECLARED' ← accepted at base (no error) +Equal> / IsAny false ← both GREEN at base +Equal> / unknown ← RED at base (lines 8,9) +``` + +- The RED is genuine and for #1350's reason (`TError` erased → `unknown` → `never` after + `Extract`). However the plan's validation row 1 says the *plan snippet* fails with "TS2339 `code` + on `never`"; the snippet as written fails with **TS18046 on `unknown`** — the `never` diagnostic + only appears via `isDefinedError`. Advisory A1: record both diagnostics as the expected RED. +- Six literal codes + code-specific `data` + `NOT_DECLARED` negative + non-oRPC `Error` in the + `isDefined:false` arm do prove the acceptance; the meta-slot `Equal` is a guard already green at + base — correctly framed as "retained", not proven-new. +- Docs: grep of both pages for `isDefinedError|safe\(|error\.(code|data)|typed|inferred|drift` + shows every error-story location is inside the disposition tables (`sdk.md` 12-20, 31-38, 58-70, + 113-114, 196-198; `discover-services.md` 9-14, 96-101, 114-115, 135-154, 205-229). Remaining + "typed client" mentions (`sdk.md:237`, `discover-services.md:20,26-27,37-38,243,249,279`) are + input/output claims that stay true; no overcorrection planned. +- Out-of-scope prose: `packages/bench/.../reference/README.md:45-47` and `router.ts:7-8` say + `baseContract` "is type-erased (`{ '~orpc': any }`)" — already stale today, becomes more so; + `rubric.md:16` stays true. `packages/contracts/README.md:13-76` does **not** describe erasure + (it says the error map "is already applied" — true before and after); research §3 overstates + that file. Deferring is honest, but "tracked follow-up debt" currently has no home: + `.llm/harness/debt/arch-debt.md` has no entry (grep `bench|contracts/README|1350` → none). See A4. + +**PASS.** + +## Checklist results + +| Plan-Gate item | Result | Evidence / location | +| --------------------------------------- | ------ | ------------------- | +| Research present and current | PASS | `research.md` "Re-baseline and authority" against `0ef48c2ec`; spot-checked §1 loss points at `contract-primitives.ts:81,98`, `errors.ts:39-49,75,86`, `ports/service-client.ts:168-171`, `client/service-client.ts:65` — all accurate | +| Decisions locked | PASS | `plan.md` "Locked error contract", "Source authority by decision" | +| Open-decision sweep | PASS | `plan.md` "Plan-Gate state"; evaluator sweep below found none that force rework | +| Commit slices (< 30, gate + files each) | PASS | 4 slices, each with proof/files/gate | +| Risk register | PASS | 7 rows with mitigations | +| Gate set selected | PASS | Archetype-1 F-set matches `archetype-gate-matrix.md:22-40`; docs overlay; 12-row validation table | +| Deferred scope explicit | PASS | `plan.md` "Debt and deferred scope" | +| jsr-audit surface scan (pkg/plugin) | PASS | `research.md` "JSR/publish surface scan" (raw dry-run authority, F-DOCT-5 known red) | + +## Tier-A claims tested + +| Claim | Verdict | +| --- | --- | +| Head carries no product code | Confirmed | +| #1466 owns metadata; #1350 keeps only the empty fourth slot | Confirmed against #1348 header, #1350 comment 5227724542, #1466 body, RFC stage table | +| Six paths exact; `public/mod.ts` denied, not conditional | Confirmed | +| `ports/service-client.ts` is the real loss point and is sufficient | Confirmed by cast-comparability probe | +| Breaking, not patch-level | Confirmed (plus one unlisted default-drop, A3) | +| RED fails at base for #1350's reason | Confirmed; diagnostic text differs from plan (A1) | +| Meta-slot assertion proves slot retained without vocabulary | Confirmed (already green at base — guard) | +| Stale README/bench prose honestly deferred | Confirmed for bench; research overstates contracts README (A4) | + +## Open-decision sweep (evaluator-run) + +None that would force rework. Two implementation choices are left open but are safe to defer: (a) +whether the port derives `TError` via upstream `ErrorFromErrorMap` or a NetScript-owned structural +`DefinedError` union (both work with the structural `DefinedErrorLike` extraction); +(b) whether `safe()`'s parameter names upstream `ClientPromiseResult` or a NetScript-owned +`Promise & { __error?: { type: TError } }` alias (avoids an upstream type in the published d.ts; +AP-14 concerns re-export, so either is permitted). + +## Verdict + +**`PASS`** — implementation may begin. + +### Advisories (non-blocking) + +- **A1 RED diagnostic.** Record the expected RED as *both* `TS18046 'result.error' is of type + 'unknown'` (plan snippet) and `TS2339 … 'never'` (`isDefinedError` path). Same defect, two texts. +- **A2 No `zod` in `packages/sdk`.** Build the fixture route from `@netscript/contracts`-exported + schemas; adding a `zod` mapping to `packages/sdk/deno.json` would be a seventh path. +- **A3 `SafeFailure` default.** Keep `SafeFailure` (or declare the dropped + default as part of the break). +- **A4 Debt home.** In slice 4 add an `arch-debt.md` (or issue) entry for the bench prose so + "tracked" is literal; correct research §3's claim about `packages/contracts/README.md`. +- **A5 Close gate.** Do not tick #1350 metadata boxes; state the split under comment 5227724542. diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan.md new file mode 100644 index 0000000000..842cdc6ac9 --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/plan.md @@ -0,0 +1,285 @@ +# Plan: preserve contract errors through `safe()` and `isDefinedError` + +## Run Metadata + +| Field | Value | +| -------------- | ----------------------------------------------------------------------------------------------------------- | +| Run ID | `fix-sdk-typed-error-channel--0.0.7-wave1` | +| Branch | `fix/sdk-typed-error-channel` | +| Phase | `implementation` — PLAN-EVAL PASS; S1 authorized and isolated | +| Target | `packages/contracts`, `packages/sdk`, and two published docs pages | +| Archetype | `1 — Small Contract` for this bounded contract slice (the SDK package remains doctrine Archetype 2 overall) | +| Scope overlays | `docs` | + +## Archetype and doctrine verdict + +This slice changes a small published type/result contract with no new runtime subsystem, so the +owner-selected Archetype 1 profile is the effective slice profile. Doctrine currently classifies +`@netscript/contracts` as Archetype 1 / Keep and `@netscript/sdk` as Archetype 2 / Keep +(`docs/architecture/doctrine/10-codebase-verdict-and-handoff.md:25-35,53`). The plan preserves the +SDK's existing discovery/client boundary and changes only the type identity/result normalization +needed for error flow. + +## Goal + +A contract-defined oRPC error keeps its literal code and schema-derived data type from +`baseContract`, through the contract procedure and `ServiceClient` method's promise, into `safe()`. +`SafeResult` discriminates defined vs non-defined failures, and `isDefinedError` narrows the same +error union without widening to `string`/`unknown` or collapsing to `never`. + +## Locked error contract + +The following is the planned public TypeScript shape. Helper aliases used to describe the shape stay +internal to `errors.ts`; existing published names remain the only SDK error exports. + +```ts +interface DefinedError extends Error { + readonly defined: true; + readonly code: TCode; + readonly status: number; + readonly data: TData; +} + +type DefinedErrorLike = Error & { + readonly defined: boolean; + readonly code: string; + readonly status: number; + readonly data: unknown; +}; + +type NarrowDefined = Extract & DefinedError; + +type SafeFailure = + | ([Exclude, undefined, false, false] & { + error: Exclude; + data: undefined; + isDefined: false; + isSuccess: false; + }) + | ([NarrowDefined, undefined, true, false] & { + error: NarrowDefined; + data: undefined; + isDefined: true; + isSuccess: false; + }); + +type SafeResult = + | SafeSuccess + | SafeFailure; + +function isDefinedError(error: T): error is NarrowDefined; + +function safe( + promise: ClientPromiseResult, +): Promise>; +``` + +Rationale: + +- It matches upstream stable-v1 `safe` behavior: two failure arms distinguished by + literal `isDefined`, with failure `data`/tuple slot 2 as `undefined`, while retaining NetScript's + package-owned `DefinedError` name and runtime `defined: true` guarantee. +- `NarrowDefined` retains each upstream member's literal `code` and schema-derived `data` + through intersection; it does not replace them with `string` and `unknown`. +- A plain `unknown` still does not become a contract error merely because a runtime predicate was + called. The type must originate in the client promise's real error channel. + +The contract builder shape is: + +```ts +type BaseContractErrors = MergedErrorMap, typeof commonErrorMap>; + +const baseContract: ContractBuilder< + Schema, + Schema, + BaseContractErrors, + Record +> = /* explicit builder chain */; +``` + +Both generic choices are locked: errors use `typeof commonErrorMap`, never open `ErrorMap`; the +fourth slot remains explicitly `Record`. This leaf proves the metadata slot is present +and not widened to `any`/`unknown`, but assigns no fields or semantics to it. #1466 will later +replace that slot with its owned vocabulary. + +## Breaking-change verdict + +**Breaking published contract; not patch-level.** `SafeFailure` currently has one arm with +`isDefined: boolean` and failure `data: null`; the accepted/upstream-compatible contract changes it +to two literal-discriminated arms and `data: undefined`. Code that asserts `result.data === null`, +constructs a `SafeFailure`, or expects one undifferentiated failure arm must change. Tightening +`baseContract` from an open error-map key space to the six declared literals also intentionally +rejects consumers that treated undeclared codes as valid. Repository search found no package source +constructing `SafeFailure`, but external JSR consumers remain affected. This belongs in the 0.0.7 +minor line with explicit migration notes; it must not be described as a patch-compatible change. + +## What becomes public + +- **No new SDK or contracts export names.** Existing `baseContract`, `BaseContract`, + `BaseContractRoute`, `BaseContractOutputRoute`, `DefinedError`, `SafeSuccess`, `SafeFailure`, + `SafeResult`, `safe`, `isDefinedError`, and client types acquire corrected signatures. +- No new SDK barrel, subpath, helper export, or error code is added. +- No metadata type, field vocabulary, initialization, root export, or semantic fixture is + introduced; those belong to #1466. + +## Source authority by decision + +| Decision | Normative text | +| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Six literal common errors, no new code | Current declaration at `packages/contracts/src/application/contract-primitives.ts:21-52`; issue boundary; RFC exact error inference gate at `rfcs/0001-sdk-client-contributions.md:1317-1333`. | +| Contract vs preparation failures stay separate | RFC error model at `rfcs/0001-sdk-client-contributions.md:1032-1088`. | +| Four-generic explicit builder; no `ReturnType` erasure | RFC metadata section at `rfcs/0001-sdk-client-contributions.md:347-370`. | +| Safe result discriminates defined/non-defined | Accepted issue target plus upstream v1.14.6 `deno doc`; the issue's older prose is used only where it agrees with merged RFC. | +| Metadata ownership | Coordinator ruling: #1466 owns definition, initialization, and export; #1350 preserves only the empty fourth generic slot. | + +## Exact six-path ceiling + +These are the complete authorized product/test/docs paths: + +1. `packages/contracts/src/application/contract-primitives.ts` +2. `packages/sdk/src/client/errors.ts` +3. `packages/sdk/src/ports/service-client.ts` +4. `packages/sdk/tests/readme-doctest_test.ts` +5. `docs/site/services-sdk/sdk.md` +6. `docs/site/services-sdk/how-to/discover-services.md` + +This ceiling is exact. A seventh product, test, or docs path is a rescope requiring a fresh +coordinator ruling. In particular, `packages/contracts/src/public/mod.ts`, +`packages/contracts/README.md`, and the benchmark reference files are not authorized. Only the +benchmark reference prose is confirmed stale and remains coordinator-owned follow-up debt; the +contracts README's statement that the error map is applied remains true. + +## Planned docs dispositions + +Every current error-story location has an explicit disposition in `research.md` §4. Implementation +must update all of these as a coherent narrative, not just the line containing `error.code`: + +- SDK page: lines 12-20, 31-38, 58-70, 113-114 retained only with accurate scope; lines 196-198 + fully replaced with a discriminated error example and explanatory wording. +- Discovery how-to: lines 9-14, 96-101, and 114-115 qualified/retained after proof; lines 135-154 + and 205-229 fully rewritten so non-defined errors cannot fall through as success and `data` is + described as schema-derived. + +## Behavioral proof and expected RED + +The implementation-phase test must first be committed/run as RED against the real public exports. It +will build a real route from `baseContract`, derive a real `ServiceClient`, and check: + +```ts +const result = await safe(usersClient.getById({ id: 'usr_1' })); +if (!result.isSuccess && result.isDefined) { + const code: + | 'NOT_FOUND' + | 'VALIDATION_ERROR' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'RATE_LIMITED' + | 'SERVICE_UNAVAILABLE' = result.error.code; +} +``` + +Expected base RED: + +```text +TS18046: 'discriminated.error' is of type 'unknown'. +TS2339: Property 'code' does not exist on type 'never'. +``` + +Negative assertions must prove: + +- `NOT_DECLARED` is rejected by `keyof baseContract['~orpc']['errorMap']`; +- a non-oRPC `Error` remains in the `isDefined: false` arm and does not narrow; +- the exact `data` type changes with `code` (for example NOT_FOUND vs VALIDATION_ERROR); +- `typeof baseContract['~orpc']['meta']` is exactly `Record` rather than `any` or + `unknown`, proving the fourth generic slot was retained without assigning metadata semantics. + +The metadata probe names no production vocabulary: it compares the existing `~orpc.meta` type to +`Record` in the compile-only fixture. It does not define fields, initialize metadata, +export a type, or claim the later #1466 metadata flow is proven. + +```ts +type IsAny = 0 extends (1 & T) ? true : false; +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true + : false; +type Assert = T; + +type BaseMeta = typeof baseContract['~orpc']['meta']; +type _MetaIsNotAny = Assert, false>>; +type _MetaSlotPreserved = Assert>>; +``` + +The test is meaningful because it is red at this base for the issue's named `never` erasure. A new +test written only after the implementation or one using ambient re-declarations is insufficient. + +## Commit slices (blocked until fresh Tier-A review and PLAN-EVAL PASS) + +| # | What the slice proves | Files | Proving gate | +| - | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------- | +| 1 | RED fixture uses real exports and fails with TS18046 plus TS2339; then the four-generic builder retains all six error keys and the empty fourth metadata slot without defining metadata vocabulary. | `contract-primitives.ts`, `readme-doctest_test.ts` | Focused structured test/check; negative type assertions | +| 2 | A real service method carries its six-literal contract error union into `safe()`; `SafeResult`/`isDefinedError` preserve code-specific data, reject plain errors, and the client port remains compatible with existing consumers. | `errors.ts`, `ports/service-client.ts`, `readme-doctest_test.ts` | Focused structured test/check; exact base RED turns green | +| 3 | Both published pages tell one consistent, compile-accurate error story, including non-defined failure handling. | `sdk.md`, `how-to/discover-services.md` | `docs-source-format`, `docs-accuracy`, doctest | +| 4 | Published surfaces remain curated and all package gates are recorded without laundering known reds. | run artifacts only | full selected validation set below | + +## Validation plan + +Structured wrappers are the verdict source. The gate set is locked; command filters may be +mechanically finalized after PLAN-EVAL but cannot expand the six-path edit ceiling: + +| Order | Gate | Planned command/evidence | Expected result | +| ----- | -------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| 1 | RED | Structured focused test/check on the real-surface fixture before implementation | FAIL once with TS18046 `unknown` and TS2339 `code` on `never` | +| 2 | Check | `.llm/tools/run-deno-check.ts` scoped to affected TS, with `--unstable-kv` | PASS after implementation | +| 3 | Test | `.llm/tools/run-deno-test.ts -- --allow-all packages/sdk/tests/readme-doctest_test.ts` plus affected package consumer tests | PASS; if `typed-queue_test.ts` hits #1667 `expected 1, got 2`, report once and do not rerun | +| 4 | Lint | `.llm/tools/run-deno-lint.ts` scoped to affected package TS | PASS | +| 5 | Format | `.llm/tools/run-deno-fmt.ts --ext ts,tsx` scoped to affected package TS | PASS | +| 6 | Quality | `deno task quality:scan` | PASS for the leaf diff | +| 7 | Doctrine | `deno task arch:check` | PASS for the leaf; known base findings separated | +| 8 | Public surface | base-vs-head `surface:diff` sets/signatures | Raw gate remains known-red at base; no unintended new export, only approved signature changes | +| 9 | Docs | repository `docs-source-format` and `docs-accuracy` tasks | PASS for both authorized pages | +| 10 | Doc lint | `deno task doc:lint --root packages/contracts --pretty` and SDK equivalent | Compare with pinned raw baselines; do not relabel baseline reds green | +| 11 | Publish | `deno task publish:dry-run` plus affected per-package raw dry-run where attribution is needed | PASS; contracts sanctioned oRPC slow-type information reported honestly | +| 12 | JSR | repository JSR audits for `packages/contracts` and `packages/sdk` | Report known SDK `F-DOCT-5` red; no new finding | + +No Aspire, Docker, runtime lease, or `e2e:cli` gate is applicable or permitted. + +## Risk register + +| Risk | Mitigation | +| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Broad default error type makes docs compile without preserving identity | RED uses a real client and exact code/data assertions; broad `string`/`unknown` fails. | +| `DefinedError.defined: true` is incompatible with upstream boolean marker | Normalize by intersection only after extracting the original upstream-shaped union; retain original literal code/data. | +| Public failure `null` → `undefined` breaks consumers | Declare breaking change explicitly, search construction/assertions repo-wide, document migration, release in 0.0.7 rather than calling it patch-compatible. | +| Tight builder annotation breaks CRUD/plugin/handler inference | Run named internal/external consumer checks from research §3 and preserve default generics. | +| Empty metadata slot is accidentally omitted or widened | Explicit four-generic annotation plus an exact `Record` compile assertion; no metadata fields or exports. | +| One corrected sentence leaves false surrounding prose | Apply every disposition in research §4 and run docs accuracy over whole pages. | +| JSR helper false-positive banner is reported as new slow type | Use raw publish output as authority and record helper vs raw distinction. | + +## Anti-patterns and fitness gates + +- Avoid AP-1/AP-9: do not grow `errors.ts` into a generic error framework or invent a second client + algebra. +- Avoid AP-14: do not re-export upstream helpers/types; only use an upstream type internally where + the hidden promise error marker requires identity. +- Avoid AP-22: no new barrel and no change to the curated contracts barrel. +- Avoid AP-25: all contract/error helpers remain side-effect-free. +- Required Archetype-1 gates: F-1, F-5, F-6, F-7, F-8, F-10, F-11, F-12, F-14, F-15, F-16, F-17, + F-18, F-19, plus package `quality:scan`, `arch:check`, docs overlay gates, publish dry-run, and + JSR audit. + +## Debt and deferred scope + +- No new architecture debt is accepted by this plan. +- Existing `packages/contracts/crud` root-layout debt remains unrelated. +- SDK JSR `F-DOCT-5`, raw doc-lint baselines, base-red `surface:diff`, and #1667 remain pre-existing + and must not be reported as green or as leaf regressions. +- No new error codes, client construction seam (#1349), server raising behavior (#1263), oRPC v2, + type-soundness sweep (#1278), plugin-local base-contract repair, or generated reference rewrite. +- Metadata definition, initialization, export, and semantic flow remain #1466 scope. Stale benchmark + reference prose is coordinator-owned follow-up debt; neither category may consume a seventh path + in this leaf without a fresh ruling. The contracts README is not stale on this point. + +## Plan-Gate state + +The coordinator ruling resolved every “must resolve now” decision. PLAN-EVAL returned terminal +`PASS` in `plan-eval.md` at evaluator commit `f76a3c45b`. S1 alone is authorized; each landed slice +still stops for fresh Tier-A review before the coordinator may authorize the next slice. diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/research.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/research.md new file mode 100644 index 0000000000..d34b6921c4 --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/research.md @@ -0,0 +1,221 @@ +# Research — sdk-typed-error-channel (#1350) + +## Re-baseline and authority + +- Re-derived on 2026-08-15 against `main@0ef48c2ec661a7e6d55ec2faf5def6ae7dd2e6eb`; the issue's + older evidence was against `fac9e339042c`. +- Normative for error behavior: the merged, Accepted RFC 0001 requires exact server-defined error + inference after metadata initialization (`rfcs/0001-sdk-client-contributions.md:1317-1333`) and + keeps contribution/preparation failures outside the contract-defined channel + (`rfcs/0001-sdk-client-contributions.md:1032-1088`). +- Normative for the builder: RFC 0001 requires an explicit publishable annotation that preserves the + concrete common error map and the fourth metadata generic, and rejects the current + `ReturnType` spelling (`rfcs/0001-sdk-client-contributions.md:347-370`). +- Normative coordinator amendment on 2026-08-15: #1350 owns only the literal-preserving error + repair. #1466 owns procedure-metadata definition, initialization, and export. This supersedes the + earlier brief wording and resolves the conflict recorded in the original research. This leaf keeps + the fourth generic explicitly as `Record` so #1466 can replace it later, but does + not define, export, or semantically prove any metadata vocabulary. +- Normative scope ceiling: the original five paths plus `packages/sdk/src/ports/service-client.ts` + are the exact six authorized product/test/docs paths. Any seventh path requires a fresh + coordinator ruling. + +## 1. Current error path and exact loss points + +The executed public-surface inspection reported: + +```text +deno doc --filter safe packages/sdk/src/client/mod.ts + safe(promise: PromiseLike): Promise> + +deno doc --filter isDefinedError packages/sdk/src/client/mod.ts + isDefinedError(error: T): error is Extract + +deno doc --filter SafeResult packages/sdk/src/client/mod.ts + SafeResult +``` + +The path is: + +1. `commonErrorMap` declares six literal keys and schema-specific payloads + (`packages/contracts/src/application/contract-primitives.ts:21-52`). +2. `baseContract` immediately widens those literals to `ReturnType` + (`packages/contracts/src/application/contract-primitives.ts:81`). `BaseContractErrors` widens a + second time to the open upstream `ErrorMap` + (`packages/contracts/src/application/contract-primitives.ts:91-99`), and both published route + aliases consume that widened type + (`packages/contracts/src/application/contract-primitives.ts:125-159`). An executed check accepted + `const undeclared: keyof typeof baseContract['~orpc']['errorMap'] = 'NOT_DECLARED'`, proving the + literal vocabulary is already gone at the contract boundary. +3. The SDK's structural `ContractProcedureMetadata` retains only input/output schemas + (`packages/sdk/src/ports/service-client.ts:48-60`). `ContractProcedureLike` therefore has no + error-map parameter (`packages/sdk/src/ports/service-client.ts:75-91`). +4. `ServiceClientMethod` returns plain `Promise` + (`packages/sdk/src/ports/service-client.ts:165-171`), and `ServiceClientShape` derives only input + and output (`packages/sdk/src/ports/service-client.ts:181-196`). The actual oRPC client is cast + to that narrowed shape (`packages/sdk/src/client/service-client.ts:41-65`). This is the decisive + loss of the promise's hidden `TError` channel before the caller passes it to `safe()`. +5. `SafeResult` defaults `TError` to `unknown`, and its single failure arm exposes only a boolean + `isDefined` (`packages/sdk/src/client/errors.ts:36-49`). `safe()` does not accept or infer a + `TError` at all (`packages/sdk/src/client/errors.ts:79-91`). +6. `isDefinedError` computes `Extract` (`packages/sdk/src/client/errors.ts:69-77`). + For the `unknown` error emitted by `safe()`, that is `never`; additionally, the local + `DefinedError.defined` property is the literal `true` (`packages/sdk/src/client/errors.ts:9-24`), + whereas the upstream oRPC error class exposes a boolean marker, so a raw upstream error union is + not structurally assignable to the current interface without normalization. + +The exact base RED was executed without creating a product fixture: + +```text +cd packages/sdk +deno eval --check --unstable-kv "import { isDefinedError, safe } from './src/client/mod.ts'; ..." +TS2339: Property 'code' does not exist on type 'never'. +``` + +The S1 real-export fixture sharpened the base RED with one structured check and recorded both +diagnostics required by PLAN-EVAL: + +```text +TS18046: 'discriminated.error' is of type 'unknown'. +TS2339: Property 'code' does not exist on type 'never'. +``` + +Together they prove the named defect: direct discrimination still leaves `safe()`'s erased error as +`unknown`, while `isDefinedError()` narrows that erased value to `never`. The later S2 proof must +turn both green and assert the exact six-code union plus schema-specific `data`; a broad +`DefinedError` would not satisfy the proof. + +## 2. Current contract guarantees and public surface + +- `commonErrorMap` defines `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, + `RATE_LIMITED`, and `SERVICE_UNAVAILABLE`, with statuses and Zod data schemas + (`packages/contracts/src/application/contract-primitives.ts:21-52`). It is internal. +- `baseContract`, `BaseContract`, `BaseContractRoute`, and `BaseContractOutputRoute` are public via + the curated root barrel (`packages/contracts/src/public/mod.ts:1-6`) and package root + (`packages/contracts/mod.ts:1-22`). `BaseContractErrors` is currently exported from its source + file but is not a package-root export. +- The current builder JSDoc claims the route marker is genuinely typed and that precise schema types + reach handlers (`packages/contracts/src/application/contract-primitives.ts:54-68`). That statement + is accurate for input/output schemas, but false for the error map and metadata. +- Current metadata is `Record` in both route aliases + (`packages/contracts/src/application/contract-primitives.ts:125-159`). An executed check of + `typeof baseContract['~orpc']['meta'].access` fails with TS2339. +- `@netscript/sdk/client` publicly exports `safe`, `isDefinedError`, `DefinedError`, `SafeSuccess`, + `SafeFailure`, and `SafeResult` (`packages/sdk/src/client/mod.ts:15-17`), and the SDK root + re-exports the entire client subpath (`packages/sdk/mod.ts:46-48`). These are already stable + published names; the repair does not need a new SDK error export. + +## 3. Executed whole-repo consumer map + +Executed searches (not inference from one package): + +```text +rg -l --hidden --glob '!.git/**' --glob '!packages/service/assets/**' \ + --glob '!.llm/runs/**' \ + '\b(isDefinedError|SafeResult|SafeSuccess|SafeFailure|DefinedError)\b|\bsafe\(' . + +rg -n '\b(BaseContract|BaseContractErrors|BaseContractRoute|BaseContractOutputRoute|baseContract)\b' \ + packages plugins --glob '!**/assets/**' --glob '!**/*.template' + +rg -n '\bServiceClient(Method|Shape|<)|Procedure(Output|Input)FromNode' \ + packages/sdk packages/fresh packages/service plugins --glob '!**/assets/**' +``` + +### Inside `packages/sdk` + +| Consumer | Evidence | Consequence | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Client and root barrels | `packages/sdk/src/client/mod.ts:15-35`; `packages/sdk/mod.ts:46-79` | Existing public error and client type names change in place; no new SDK export is required. | +| Direct client construction | `packages/sdk/src/client/service-client.ts:41-65` | Cast currently discards upstream error identity; must participate in an end-to-end repair. | +| `defineServices` | `packages/sdk/src/presets/define-services.ts:47-69` | Maps every service to `ServiceClient` and will inherit the repaired channel. | +| Query factories | `packages/sdk/src/query/query-factory.ts:197-224` | Assert clients are `ServiceClient`; default-compatible type changes must keep assignments green. | +| Query utils | `packages/sdk/src/ports/service-query-utils.ts:161-170` | Exposes `call: ServiceClientMethod`; any appended error generic needs a compatibility default. | +| Desktop client | `packages/sdk/src/desktop/domain/types.ts:147-155` | Aliases `ServiceClient` and therefore asserts on the same public contract. | +| Type/mocked consumers | `packages/sdk/tests/query/query-factory_test.ts:60-257`; `packages/sdk/tests/type-fixtures/sdk-assignability_type.ts:58`; `packages/sdk/tests/type-fixtures/define-services_type.ts:54-57`; `packages/sdk/tests/type-fixtures/desktop-consumer_type.ts:39` | Plain Promise mocks and old positional generics must remain assignable. | +| Public-surface baseline/reference | `.llm/tools/release/baselines/public-surfaces.json:18811-19194`; `docs/site/reference/sdk/index.md:38-60` | `surface:diff` must compare base-vs-head sets/signatures; generated reference is a consumer but outside this leaf's editable surface. | + +### Outside `packages/sdk` + +| Consumer | Evidence | Consequence | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@netscript/fresh` diagnostics | `packages/fresh/src/diagnostics/error/extract.ts:1-39` | Imports `isDefinedError`, then manually casts code/status. It must still compile and preserve runtime detection; it is the only non-SDK package source importing the helper. | +| `@netscript/contracts/crud` | `packages/contracts/crud/create-crud-contract.ts:29-44,109-117,350-379` | Directly consumes public `baseContract` and `BaseContractRoute`; exact error/meta generics must remain assignable. | +| Workers health soundness test | `plugins/workers/services/src/routers/health-soundness_test.ts:3-53` | Asserts `BaseContractRoute` handler inference; metadata/error tightening must not regress input/output soundness. | +| CLI scaffold/template assertions | `packages/cli/e2e/tests/application/gates/generated-router-template_test.ts:35-41`; `packages/cli/src/kernel/adapters/contracts/contract-source.ts:63-77`; `packages/cli/src/public/features/plugins/new/new-plugin-use-case.ts:390-416` | Some generated contracts import root `baseContract`; the parser recognizes its route chain. No template change is planned. | +| Benchmark/reference consumers | `packages/bench/tasks/t1-storefront-api/rubric.md:16`; `packages/bench/tasks/t1-storefront-api/reference/README.md:45-47`; `packages/bench/tasks/t1-storefront-api/reference/netscript/router.ts:7-8` | The reference README/router explicitly describe the old erasure and are coordinator-owned follow-up debt outside this leaf's exact six-path ceiling; the rubric remains true. | +| Contracts README | `packages/contracts/README.md:13-76` | Says the common error map is applied. That remains true before and after this repair; PLAN-EVAL correctly found that the original research overstated this consumer. | + +First-party plugin-core files found by the base-contract search define their own local +`baseContract: ReturnType` rather than importing `@netscript/contracts` +(`packages/plugin-ai-core/src/contracts/v1/ai.contract.ts:147-157`, +`packages/plugin-auth-core/src/contracts/v1/auth.contract.ts:182-195`, +`packages/plugin-sagas-core/src/contracts/v1/sagas.contract.ts:99-110`, +`packages/plugin-triggers-core/src/contracts/v1/triggers.contract.ts:134-145`, and +`packages/plugin-workers-core/src/contracts/v1/workers.contract-definition.ts:70-88`). They do not +consume this leaf's public error channel, but the executed search shows the same erasing pattern is +not globally repaired by #1350. + +## 4. Authorized docs: current claims and dispositions + +### `docs/site/services-sdk/sdk.md` + +| Lines | Current claim | Truth at base | Planned disposition | +| ------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 12-20 | The contract supplies everything a caller needs and caller/server cannot drift. | Too broad for errors today; method input/output are typed, error channel is not. | Retain after the repair, but qualify the nearby error example so “contract-derived” explicitly includes only declared server errors, not arbitrary thrown/transport errors. | +| 31-38 | L1 method signatures are inferred from the contract. | True for input/output, false for the promise error channel. | Retain after end-to-end method return typing is repaired; otherwise it must be narrowed. | +| 58-70 | End-to-end types and contract changes flow to the client. | Already false for error codes/data. | Retain only if the RED proves error flow; no one-sentence patch while this surrounding story remains unqualified. | +| 113-114 | A direct call is “fully inferred from the contract.” | False for errors at base. | Retain with an explicit cross-reference to the safe error story after proof. | +| 196-198 | “Safe error narrowing” example says `isDefinedError(error)` yields typed `code/status`. | False; executed check gives TS2339 on `never`; the destructured `isDefined` is unused. | Replace the complete tab text. Explain and demonstrate the discriminated `SafeResult`: `isSuccess`, then `isDefined`, exact code/data; use `isDefinedError` in a second narrow or explain when it is useful. | + +### `docs/site/services-sdk/how-to/discover-services.md` + +| Lines | Current claim | Truth at base | Planned disposition | +| --------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 9-14 | The discovered client is “fully typed.” | False for errors. | Retain only after the end-to-end error RED turns green; qualify that transport/unknown throws remain non-defined failures. | +| 96-101, 114-115 | Client is fully typed from the contract and fields drift at compile time. | True for input/output, false for error codes/data. | Retain with wording that includes the newly proven error union, avoiding an isolated snippet-only fix. | +| 135-154 | Step 4 says `safe()` plus `isDefinedError` provides typed contract code/data. | False at base. It also falls through an arbitrary non-defined error into the success `else`, making `result.items` narratively unsafe. | Rewrite the whole Step 4 explanation and block: branch on `isSuccess`; branch on literal `isDefined`; surface/throw the non-defined error; state `error.data` is selected by the error code's Zod schema. | +| 205-229 | End-to-end copyable example repeats safe narrowing and then returns `result.items`. | False for the same typing reason and unsafe narrative. | Rewrite the full function consistently with Step 4; do not retain the old fallthrough. | + +## 5. Granted doctest surface + +`packages/sdk/tests/readme-doctest_test.ts` does not execute package examples against package +exports. It: + +1. reads only `packages/sdk/README.md` (`packages/sdk/tests/readme-doctest_test.ts:3,117-120`); +2. extracts every fenced TypeScript block (`packages/sdk/tests/readme-doctest_test.ts:92-109`); +3. strips all imports and `export` keywords (`packages/sdk/tests/readme-doctest_test.ts:111-115`); +4. prepends a fictional ambient API, including locally declared `safe` and `isDefinedError` + signatures that are stronger than the real exports + (`packages/sdk/tests/readme-doctest_test.ts:5-90`, especially `:36-37`); +5. runs `deno check --no-config` for each temporary snippet + (`packages/sdk/tests/readme-doctest_test.ts:124-141`); and +6. separately parses JSON fences (`packages/sdk/tests/readme-doctest_test.ts:144-152`). + +The granted test can be changed to import the real error helpers and add a real compile-only +contract/client fixture, but it cannot make the actual client promise preserve a contract error +unless `packages/sdk/src/ports/service-client.ts` is changed. + +## JSR/publish surface scan + +- `deno doc` confirmed the currently published signatures before source inspection. +- The repository JSR audit reports `@netscript/contracts` with the sanctioned oRPC slow-type INFO; + raw `deno publish --dry-run --allow-dirty --allow-slow-types` exits 0. The explicit four-generic + builder annotation must not deepen that sanctioned warning. +- The audit reports SDK `F-DOCT-5` because `src/` has 13 immediate children, a known pre-existing + red. Its helper also reports a slow-type warning by counting the “Checking for slow types” banner; + raw `deno publish --dry-run --allow-dirty` exits 0 with no actual slow-type diagnostic. Record the + raw result as authority per `netscript-tools`. +- No new entrypoint is needed or authorized. `packages/contracts/src/public/mod.ts` is explicitly + denied for this leaf; there is no metadata definition/export branch in the approved plan. + +## Coordinator ruling and remaining deferred scope + +1. **Resolved:** `packages/sdk/src/ports/service-client.ts` is the authorized sixth path. It is + required to preserve the real promise error channel. +2. **Resolved:** #1466 owns metadata definition, initialization, and export. #1350 preserves only + the existing fourth `ContractBuilder` generic as `Record` and makes no metadata + semantic claim. +3. **Safe to defer:** benchmark reference prose remains coordinator-owned follow-up debt and is not + authorized here. `packages/contracts/README.md` is also outside scope but is not stale on this + point. Any attempt to edit either location—or any other seventh product/test/docs path—is a + rescope requiring a fresh ruling. diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/supervisor.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/supervisor.md new file mode 100644 index 0000000000..553ed838f5 --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/supervisor.md @@ -0,0 +1,57 @@ +# Supervisor Identity — fix-sdk-typed-error-channel--0.0.7-wave1 + +Written at run start per `workflow/lane-policy.md` § Supervisor identity. + +| Field | Value | +| -------- | ------------------------------------------------------------ | +| Model | Codex · OpenAI · GPT-5.6 Sol · medium | +| Session | `01a006f3-ae2d-7941-bd17-2ac71dd3d0f0` | +| Host | `YogaBook9i` · WSL2 Linux · `codex` | +| Checkout | `/home/codex/repos/netscript-007-leaf-typed-error` | +| Worktree | `/home/codex/repos/netscript-007-leaf-typed-error` | +| Branch | `fix/sdk-typed-error-channel` | +| Baseline | `main@0ef48c2ec661a7e6d55ec2faf5def6ae7dd2e6eb` · 2026-08-15 | +| Run ID | `fix-sdk-typed-error-channel--0.0.7-wave1` | + +## Routes in force + +| Task lane | Provider / model / effort | Role in this run | +| ----------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| `normal_implementation` | OpenAI / GPT-5.6 Sol / medium | Research and plan generator; implementation is prohibited until external PLAN-EVAL passes. | +| `review_codex` | Native opposite-family route selected by the topic orchestrator | PLAN-EVAL; not launched by this session. | + +## Owner constraints + +- Plan-first. This session must not implement or launch an evaluator. +- The product/test/docs surface is exactly the six paths enumerated in `plan.md`; any seventh path + is a rescope requiring a fresh coordinator ruling. +- #1466 owns all metadata vocabulary, initialization, and exports. This leaf preserves only the + existing empty fourth generic slot and must not touch the contracts public barrel. +- No Aspire, Docker, `e2e:cli`, runtime lease, lock deletion, cache deletion, or reload. + +## S4-R Amendment Supervisor Identity + +Written at S4-R amendment start. S4-R is a separate, plan-only session from the S1-S4 generator +above; it amends the S4 stop with a finding→correction mapping and does not implement. + +| Field | Value | +| --- | --- | +| Model (requested) | native Claude · `claude-sonnet-5` · effort `high` · Remote Control, `documentation_review` lane | +| Model (observed) | `claude-sonnet-5`, effort `high`, `--remote-control` present in `respawnFlags` | +| Route verdict | **MATCHED** on model/effort/remote-control (directly verifiable via `argv`); the `documentation_review` lane is a routing-policy designation, not a CLI flag, so it is not independently verifiable from `respawnFlags` alone — consistent with intent (job name `NetScript 0.0.7 #1671 S4-R doc-amendment`) but not a byte-for-byte flag match. | +| OS PID | `1035332` | +| Session file | `~/.claude/sessions/1035332.json` | +| Session bridge id | `session_01TYBPuyVoK8Bc8926DfnPah` | +| Remote Control URL | `https://claude.ai/code/session_01TYBPuyVoK8Bc8926DfnPah` | +| Job id | `944115a6` | +| respawnFlags | `--permission-mode bypassPermissions --remote-control --name "NetScript 0.0.7 #1671 S4-R doc-amendment" --effort high --model claude-sonnet-5` | +| cwd | `/home/codex/repos/netscript-007-leaf-typed-error-s4r` | +| `git rev-parse HEAD` at start | `db8aadd9542c38a305efffbd7017c56d0abf4e01` | +| Branch | `s4r/doc-amendment` (scratch; pushes by explicit refspec to `fix/sdk-typed-error-channel`) | + +Route reason: the canonical Codex implementation route is quota-exhausted account-wide +(`usageLimitExceeded`, `willRetry:false`, `balance:"0"`, reset 2026-08-20 05:31), reproduced in a +fresh thread/fresh worktree. S4-R is run-artifact-only plan maintenance, inside the +documentation-authoring exception in `CLAUDE.md`, so it is routed to a native Claude session instead +of blocking on the exhausted Codex route. The product repair itself stays parked on the Codex route +and is out of scope for this session. diff --git a/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/worklog.md b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/worklog.md new file mode 100644 index 0000000000..e3572525dc --- /dev/null +++ b/.llm/runs/fix-sdk-typed-error-channel--0.0.7-wave1/worklog.md @@ -0,0 +1,1141 @@ +# Worklog: sdk-typed-error-channel (#1350) + +## Run Metadata + +| Field | Value | +| -------------- | ------------------------------------------ | +| Run ID | `fix-sdk-typed-error-channel--0.0.7-wave1` | +| Branch | `fix/sdk-typed-error-channel` | +| Archetype | `1 — Small Contract` slice | +| Scope overlays | `docs` | + +## Design + +### Public surface + +- Existing: `baseContract`, `BaseContract`, `BaseContractRoute`, `BaseContractOutputRoute` — exact + error/meta generics corrected in place. +- Existing: `DefinedError`, `SafeSuccess`, `SafeFailure`, `SafeResult`, `safe`, `isDefinedError`, + and `ServiceClient*` — exact error channel corrected in place. +- Existing contract builder metadata slot stays explicitly `Record`; no metadata + vocabulary, initialization, or export is introduced. +- No internal helper, adapter, upstream module, or additional subpath becomes public. + +### Domain vocabulary + +- `BaseContractErrors` — exact `typeof commonErrorMap` merge, not open `ErrorMap`. +- `DefinedErrorLike` — private structural extraction boundary matching the upstream error identity + without exporting the upstream class. +- `NarrowDefined` — private conditional type retaining the original code/data members and + adding the runtime-proven `defined: true` marker. +- `SafeResult` — success, non-defined failure, and defined failure discriminated by + `isSuccess`/`isDefined`. +- No metadata domain vocabulary in this leaf; #1466 owns it. + +### Ports + +- No new port. The existing `ServiceClient` contract in `src/ports/service-client.ts` must retain + the promise error identity; inventing a parallel port in `errors.ts` would not repair end-to-end + flow. + +### Constants + +- No new constants. The six existing common error-map keys remain the complete vocabulary. + +### Commit slices + +See `plan.md` “Commit slices.” All implementation slices are blocked pending fresh Tier-A review and +PLAN-EVAL PASS. + +### Deferred scope + +- Client contributions, transport changes, new codes, server raising behavior, oRPC v2, plugin-local + base-contract erasures, and broad soundness cleanup. + +### Contributor path + +Future contract error additions update the private explicit `CommonErrorMap` shape and its +`commonErrorMap` value together; the explicit annotation is required by `isolatedDeclarations` +because the published builder references `typeof commonErrorMap`. The real-export fixture then +forces the exact key union to stay synchronized. No code union or metadata vocabulary is exported. + +## Progress log + +| Time | Slice | Step | Notes | +| ---------- | ----- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-08-15 | Plan | Bootstrap/research | Re-baselined exact branch/base; read required skills, doctrine, harness, Accepted RFC, issues, and docs. | +| 2026-08-15 | Plan | Public inspection | `deno doc` confirmed current `safe`, `isDefinedError`, `SafeResult`, and `baseContract` signatures. | +| 2026-08-15 | Plan | RED | Executed TS2339 `error.code` on `never` with `deno eval --check --unstable-kv`. | +| 2026-08-15 | Plan | Consumer scan | Executed whole-repo searches; identified `@netscript/fresh`, CRUD, query/desktop/type fixtures, CLI/template, docs, and baseline consumers. | +| 2026-08-15 | Plan | Rescope | Required out-of-scope `service-client.ts`; conditional contracts barrel; live #1466 ownership conflict. Stopped all product work. | +| 2026-08-15 | Plan | Amendment | Coordinator authorized the sixth client-port path, assigned metadata vocabulary to #1466, denied the barrel, and locked six paths. | +| 2026-08-15 | Eval | PLAN-EVAL | Terminal PASS at `f76a3c45b`; incorporated advisories A1-A5 without editing the evaluator artifact. | +| 2026-08-15 | S1 | RED | Real-export fixture recorded TS18046 (`unknown`) and TS2339 (`never`) together in one structured run; not rerun for tidier output. | +| 2026-08-15 | S1 | Builder | Exact `typeof commonErrorMap` error generic and explicit `Record` fourth slot; no metadata vocabulary or new export. | +| 2026-08-15 | S1 | Fixture | Uses contracts-root `CursorPaginationInputSchema` and `SuccessSchema`; asserts six keys, undeclared rejection, and empty meta slot. | +| 2026-08-15 | S1 | Gates | Focused structured check/test/lint/format pass; CRUD and workers soundness consumers included. Root/final-slice gates not run. | +| 2026-08-15 | S1 | Tier-A | Fresh Tier-A PASS at `dc034d680`; coordinator authorized S2 only. | +| 2026-08-15 | S2 | Public inspection | `deno doc` confirmed upstream `ClientPromiseResult`, `ErrorFromErrorMap`, `ThrowableError`, and literal `SafeResult` arms before source edit. | +| 2026-08-15 | S2 | Error channel | Defaulted three-parameter `ServiceClientMethod` carries the real error map; `SafeFailure` retains its default. | +| 2026-08-15 | S2 | Assertions | Removed both S2 suppressions and replaced them with exact code-union/defined-error assertions plus code-specific data and plain-error guards. | +| 2026-08-15 | S2 | Consumers | Checked query, desktop, RFC, assignability, service-query, and Fresh consumers; ran SDK query/desktop plus Fresh extraction tests. | +| 2026-08-15 | S2 | Reconcile | Observed S1 Tier-A PASS; no issue/label/checklist mutation, no #1348/#1466 touch, no scope adjustment, and S3 remains blocked. | +| 2026-08-15 | S3 | Narrative | Both published pages now scope contract typing to input/output/declared errors and keep transport/arbitrary throws on the non-defined branch. | +| 2026-08-15 | S3 | Examples | Literal `isSuccess` then `isDefined` branches terminate every failure path; all six codes and code-selected schema data are shown. | +| 2026-08-15 | S3 | Compile proof | SDK fence passed `docs:snippets`; discover Step 3/4 and copyable function passed page-isolated checks with a real `baseContract` fixture. | +| 2026-08-15 | S3 | Gates | Source format, accuracy, source/rendered links, caveats, build, doctest, and focused example compilation passed; final gates remain S4-only. | + +## Gate results + +| Gate | Result | Evidence | +| ------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------ | +| Current SDK raw publish dry-run | PASS (baseline inspection only) | `deno publish --dry-run --allow-dirty`, exit 0; no actual slow-type diagnostic | +| Current contracts raw publish dry-run | PASS with sanctioned slow-type warning | `deno publish --dry-run --allow-dirty --allow-slow-types`, exit 0 | +| Current JSR audit | KNOWN RED / INFO | SDK `F-DOCT-5`; contracts sanctioned slow-type INFO; not leaf verdicts | +| S1 RED | EXPECTED FAIL | Exactly TS18046 + TS2339 in one structured check | +| S1 focused check | PASS | 4 files, 1 batch, 0 diagnostics; includes CRUD + workers consumer | +| S1 focused test | PASS | 3/3 results; SDK doctest + workers health soundness | +| S1 focused lint | PASS | 2 edited TS files, 0 findings | +| S1 focused format write/check | PASS | 2 edited TS files, 0 findings; final check clean | +| S2 focused check | PASS | 15 affected files, 1 batch, 0 diagnostics | +| S2 focused/consumer tests | PASS | 12/12 results across 4 test files | +| S2 focused lint | PASS after one recorded correction | Initial 2 `verbatim-module-syntax`; final 3 files, 0 findings | +| S2 focused format write/check | PASS | 3 edited TS files, 0 findings | +| S3 docs source format | PASS | Structured receipt; exit 0, `Docs source format: OK` | +| S3 docs accuracy | PASS | Structured receipt; 199 published source pages checked | +| S3 SDK snippet compile | PASS | 36 Tier-1 candidates, 22 checked, 14 existing exemptions | +| S3 discover example compile | PASS | Step 3/4 pair and end-to-end function; real base-contract fixture | +| S3 SDK doctest | PASS | 3/3 results | +| S3 source links | PASS | 103 docs; 0 broken links, anchors, or orphans | +| S3 site verify | PASS | Build, rendered-output, rendered links, and caveats; raw exit 0 | +| S3 scope/lock hygiene | PASS | Four landed product/test files and `deno.lock` unchanged | +| Root/final-slice gates | NOT RUN | Explicitly reserved for slice 4 | + +### S1 structured JSON verdicts + +The RED fixture was checked once before product implementation: + +```json +{ + "exitCode": 1, + "command": "deno check --unstable-kv ", + "selection": { "filesSelected": 1, "batches": 1, "failedBatches": 1 }, + "summary": { + "totalOccurrences": 2, + "uniqueOccurrences": 2, + "uniqueCodes": 2, + "uniquePaths": 1 + }, + "groups": [ + { + "code": "TS18046", + "message": "'discriminated.error' is of type 'unknown'.", + "count": 1, + "location": "packages/sdk/tests/readme-doctest_test.ts:21:5" + }, + { + "code": "TS2339", + "message": "Property 'code' does not exist on type 'never'.", + "count": 1, + "location": "packages/sdk/tests/readme-doctest_test.ts:26:19" + } + ] +} +``` + +Two post-change check iterations failed and were fixed rather than hidden: + +```json +{ + "exitCode": 1, + "command": "deno check --unstable-kv ", + "selection": { "filesSelected": 4, "batches": 1, "failedBatches": 1 }, + "summary": { "totalOccurrences": 2, "uniqueCodes": 2, "uniquePaths": 1 }, + "groups": [ + { "code": "TS9010", "count": 1, "location": "contract-primitives.ts:21:7" }, + { "code": "TS9027", "count": 1, "location": "contract-primitives.ts:21:7" } + ], + "resolution": "Added an exact private CommonErrorMap annotation required by isolatedDeclarations." +} +``` + +```json +{ + "exitCode": 1, + "command": "deno check --unstable-kv ", + "selection": { "filesSelected": 4, "batches": 1, "failedBatches": 1 }, + "summary": { "totalOccurrences": 1, "uniqueCodes": 1, "uniquePaths": 1 }, + "groups": [ + { "code": "TS2322", "count": 1, "location": "contract-primitives.ts:54:7" } + ], + "resolution": "Removed a redundant satisfies expression whose contextual type widened literal values; oc.errors still enforces ErrorMap compatibility." +} +``` + +Final focused check: + +```json +{ + "exitCode": 0, + "command": "deno check --unstable-kv ", + "selection": { "filesSelected": 4, "batches": 1, "failedBatches": 0 }, + "summary": { + "totalOccurrences": 0, + "uniqueOccurrences": 0, + "uniqueCodes": 0, + "uniquePaths": 0 + }, + "groups": [] +} +``` + +Focused tests: + +```json +{ + "exitCode": 0, + "command": [ + "deno", + "test", + "--reporter=tap", + "--allow-all", + "packages/sdk/tests/readme-doctest_test.ts", + "plugins/workers/services/src/routers/health-soundness_test.ts" + ], + "summary": { + "passed": 3, + "failed": 0, + "ignored": 0, + "totalResults": 3, + "uniqueFailures": 0 + }, + "failures": [] +} +``` + +Focused lint: + +```json +{ + "exitCode": 0, + "selection": { "filesSelected": 2, "batches": 1 }, + "summary": { + "totalOccurrences": 0, + "uniqueOccurrences": 0, + "uniqueRules": 0, + "uniquePaths": 0 + }, + "groups": [] +} +``` + +Focused format write and final check: + +```json +{ + "exitCode": 0, + "command": "deno fmt", + "mode": "write", + "summary": { + "filesSelected": 2, + "batches": 1, + "failedBatches": 0, + "findings": 0, + "ignoredFindings": 0 + }, + "findings": [] +} +``` + +```json +{ + "exitCode": 0, + "command": "deno fmt --check", + "mode": "check", + "summary": { + "filesSelected": 2, + "batches": 1, + "failedBatches": 0, + "findings": 0, + "ignoredFindings": 0 + }, + "findings": [] +} +``` + +### S2 structured JSON verdicts + +Final focused check, including SDK query/desktop/RFC/assignability/service-query consumers and the +Fresh error-extraction consumer: + +```json +{ + "exitCode": 0, + "command": "deno check --unstable-kv ", + "selection": { "filesSelected": 15, "batches": 1, "failedBatches": 0 }, + "summary": { + "totalOccurrences": 0, + "uniqueOccurrences": 0, + "uniqueCodes": 0, + "uniquePaths": 0 + }, + "groups": [] +} +``` + +Final focused and consumer tests: + +```json +{ + "exitCode": 0, + "command": [ + "deno", + "test", + "--reporter=tap", + "--allow-all", + "packages/sdk/tests/readme-doctest_test.ts", + "packages/sdk/tests/query/query-factory_test.ts", + "packages/sdk/tests/desktop/desktop-rpc-client_test.ts", + "packages/fresh/src/diagnostics/error/extract_test.ts" + ], + "summary": { + "passed": 12, + "failed": 0, + "ignored": 0, + "totalResults": 12, + "uniqueFailures": 0 + }, + "failures": [] +} +``` + +The first lint run failed and was corrected rather than hidden: + +```json +{ + "exitCode": 1, + "selection": { "filesSelected": 3, "batches": 1 }, + "summary": { + "totalOccurrences": 2, + "uniqueOccurrences": 2, + "uniqueRules": 1, + "uniquePaths": 1 + }, + "groups": [ + { + "rule": "verbatim-module-syntax", + "message": "Import identifier only used in types", + "count": 2, + "path": "packages/sdk/tests/readme-doctest_test.ts", + "locations": ["4:3", "6:3"] + } + ], + "resolution": "Marked both exported schema imports type-only." +} +``` + +Final focused lint: + +```json +{ + "exitCode": 0, + "selection": { "filesSelected": 3, "batches": 1 }, + "summary": { + "totalOccurrences": 0, + "uniqueOccurrences": 0, + "uniqueRules": 0, + "uniquePaths": 0 + }, + "groups": [] +} +``` + +Final focused format write and check: + +```json +{ + "exitCode": 0, + "command": "deno fmt", + "mode": "write", + "summary": { + "filesSelected": 3, + "batches": 1, + "failedBatches": 0, + "findings": 0, + "ignoredFindings": 0 + }, + "findings": [] +} +``` + +```json +{ + "exitCode": 0, + "command": "deno fmt --check", + "mode": "check", + "summary": { + "filesSelected": 3, + "batches": 1, + "failedBatches": 0, + "findings": 0, + "ignoredFindings": 0 + }, + "findings": [] +} +``` + +The S2 doctest-only run also passed 3/3 before the broader 12-test consumer run. The S1 RED block +above was not edited or rerun. `typed-queue_test.ts` was not selected, so #1667 was not encountered. +Root/final-slice gates were not run. + +### S3 disposition receipt + +| Planned location | Disposition | Final location and wording | +| ------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `sdk.md:12-20` | Retained with required scope | `sdk.md:12-22` now limits non-drift to input, output, and declared-error types; transport/arbitrary throws are explicitly non-defined failures. | +| `sdk.md:31-38` | Retained and corrected | `sdk.md:33-41` says the L1 method's input, output, and declared-error union are inferred from the contract. | +| `sdk.md:58-70` | Retained and corrected | `sdk.md:61-71` says `baseContract` error schemas flow through the client promise and `safe()`, while runtime non-defined failures remain outside the contract. | +| `sdk.md:113-114` | Retained with cross-reference | `sdk.md:118-120` scopes direct-call inference to input/output/declared errors and points rejecting calls to the `safe()` section. | +| `sdk.md:196-198` | Replaced completely | `sdk.md:203-249` names all six codes, branches on literal `isSuccess` then `isDefined`, throws non-defined failures, demonstrates `NOT_FOUND` schema data, and scopes `isDefinedError`. | +| `discover-services.md:9-14` | Retained with required scope | `discover-services.md:11-15` names input/output/declared errors and keeps discovery/transport failures non-defined. | +| `discover-services.md:96-101,114-115` | Retained and corrected | `discover-services.md:99-118` states `baseContract` carries the six errors and the call comment includes declared-error inference; the prerequisite row also names declared errors. | +| `discover-services.md:135-154` | Replaced completely | `discover-services.md:137-176` names all six codes, uses literal discriminants, terminates every failure, demonstrates `NOT_FOUND` schema data, and scopes `isDefinedError`. | +| `discover-services.md:205-229` | Replaced completely | `discover-services.md:227-264` makes the copyable function return only from `isSuccess`, throws the non-defined branch, and demonstrates `VALIDATION_ERROR` schema data before throwing the defined failure. | + +### S3 structured JSON verdicts + +Final source-format receipt: + +```json +{ + "gateId": "docs-source-format", + "invocationId": "fix-sdk-typed-error-channel-s3-source-format-final", + "gitHead": "ca7ade409be0cc0c064e75f5bfa1bd109e06d013", + "outcome": "PASS", + "exitCode": 0, + "durationMs": 444, + "stdout": "Docs source format: OK" +} +``` + +Final docs-accuracy receipt: + +```json +{ + "gateId": "docs-accuracy", + "invocationId": "fix-sdk-typed-error-channel-s3-docs-accuracy-final", + "gitHead": "ca7ade409be0cc0c064e75f5bfa1bd109e06d013", + "outcome": "PASS", + "exitCode": 0, + "durationMs": 11532, + "summary": "199 published source pages; 91/91 root/direct public commands from 149 recursive paths" +} +``` + +This accuracy PASS proves only the invariants implemented by that script; it is not treated as proof +of the page-level error narrative. + +SDK-page snippet compilation: + +```json +{ + "gate": "docs:snippets", + "outcome": "PASS", + "exitCode": 0, + "census": { + "scanned": 581, + "tsLike": 298, + "tier1": 36, + "checked": 22, + "exempt": 14, + "outsideFloor": 262, + "malformed": 0 + } +} +``` + +The SDK error fence is in the Tier-1 floor and compiles directly against the real +`@netscript/contracts` and `@netscript/sdk/client` workspace entrypoints. Discover-services is +outside the current day-one snippet floor, so its changed examples were compiled page-isolated in +two batches. The only supplied prelude replaced the project-local `@my-app/contracts` import with a +real `baseContract` route of the same `list` input/output shape; the documented client construction, +branching, exact code annotation, and code-specific `data` accesses were unchanged: + +```json +{ + "gate": "discover-services-error-examples", + "verdict": "PASS", + "results": [ + { + "source": [105, 146], + "code": 0, + "rootLockUnchanged": true, + "temporaryLockRewritten": true, + "diagnostics": "Check discover-services.md Step 3/4" + }, + { + "source": [232], + "code": 0, + "rootLockUnchanged": true, + "temporaryLockRewritten": true, + "diagnostics": "Check discover-services.md end-to-end function" + } + ] +} +``` + +SDK doctest: + +```json +{ + "schemaVersion": 1, + "command": [ + "deno", + "test", + "--reporter=tap", + "--allow-all", + "packages/sdk/tests/readme-doctest_test.ts" + ], + "exitCode": 0, + "durationMs": 5485, + "summary": { "passed": 3, "failed": 0, "ignored": 0, "totalResults": 3 }, + "failures": [] +} +``` + +Link/build/scope verdicts: + +```json +{ + "docsSourceLinks": { + "command": "deno task docs:links", + "outcome": "PASS", + "exitCode": 0, + "docs": 103, + "brokenLinks": 0, + "brokenAnchors": 0, + "orphans": 0 + }, + "siteVerify": { + "command": "deno task --cwd docs/site verify", + "outcome": "PASS", + "exitCode": 0, + "covers": ["source-format", "build", "rendered-output", "rendered-links", "caveats"] + }, + "scope": { + "outcome": "PASS", + "productAndTestFilesByteIdentical": true, + "denoLockUnchanged": true, + "newFiles": 0 + } +} +``` + +The known raw-red `surface:diff`, JSR `F-DOCT-5`, and pinned raw doc-lint baselines were not run in +this slice and remain known red; no green claim is made. `typed-queue_test.ts` was not selected, so +#1667 was not encountered. Root/final-slice gates remain reserved for S4. + +## Handoff + +S3 is complete and stops here for fresh Tier-A review. No S4 work started, no evaluator was +launched, no acceptance box was ticked, and no metadata vocabulary or acceptance claim was +introduced. + +## S4 final-gate receipt — stopped on new raw doc-lint findings + +All executed S4 receipts certify immutable content head `c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd`. +For every receipt below, the head read before the gate and the actual head verified afterward were +identical; no waiver was used. `deno.lock` also remained byte-identical throughout. + +The matrix stopped after raw Contracts and SDK `deno doc --lint` attribution proved new leaf-owned +private-type-reference findings. Correcting those findings would require edits to the already-landed +S1/S2 product files, which S4 forbids. The remaining JSR audits and specifier/export guards were +therefore not executed and are not claimed. + +### Executed structured verdicts + +```json +{ + "gateId": "root-check", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "uncached": true, + "selection": { "filesSelected": 2925, "batches": 25, "failedBatches": 0 }, + "summary": { "totalOccurrences": 0, "uniqueOccurrences": 0, "uniqueCodes": 0, "uniquePaths": 0 } +} +``` + +```json +{ + "gateId": "root-test", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "uncached": true, + "durationMs": 335918, + "summary": { + "passed": 4207, + "failed": 0, + "ignored": 19, + "totalResults": 4226, + "uniqueFailures": 0 + }, + "typedQueue1667Encountered": false +} +``` + +```json +{ + "gateId": "scoped-lint", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "selection": { + "roots": ["packages/contracts", "packages/sdk"], + "filesSelected": 105, + "batches": 1 + }, + "summary": { "totalOccurrences": 0, "uniqueOccurrences": 0, "uniqueRules": 0, "uniquePaths": 0 } +} +``` + +```json +{ + "gateId": "scoped-fmt", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "mode": "check", + "selection": { + "roots": ["packages/contracts", "packages/sdk"], + "filesSelected": 105, + "batches": 1 + }, + "summary": { "failedBatches": 0, "findings": 0, "ignoredFindings": 0 } +} +``` + +```json +{ + "gateId": "quality-scan", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "rootCoverage": { + "workspaceMembers": 37, + "publishableMembersInsideBoundary": 35, + "uncoveredPublishedMembers": [] + }, + "scan": { + "roots": ["packages", "plugins", "docs/site"], + "findings": [], + "allowCount": 7, + "allowanceFailures": [] + }, + "existingAllowances": { "issue": 1276, "count": 7 } +} +``` + +```json +{ + "gateId": "arch-check", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "uncachedUnderlyingCommands": true, + "affectedPackages": { + "contracts": { "fail": 0, "warn": 2, "info": 1 }, + "sdk": { "fail": 0, "warn": 1, "info": 1 } + }, + "notes": [ + "The repository's existing npm-catalog warnings were emitted but are non-failing.", + "zod-alignment PASS: zod@3.25.76 and zod@4.4.3; residual v3 owners unchanged." + ] +} +``` + +```json +{ + "gateId": "docs-source-format", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "summary": "Docs source format: OK" +} +``` + +```json +{ + "gateId": "docs-accuracy", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "summary": { + "publishedSourcePages": 199, + "shippedCorpusFiles": 181, + "publicCommands": "91/91 root/direct from 149 recursive paths", + "freshRootImports": 6 + }, + "existingWarning": "@tanstack/ai-preact@0.10.4 peer expects @tanstack/ai@^0.41.0; resolved 0.39.1" +} +``` + +### Public-surface attribution + +The raw gate remains RED at both base and head. Raw finding sets are not equal because this leaf is +an explicitly breaking published-contract change. The delta is exactly the 15 planned signature +changes on existing symbols/export paths; it adds no symbol or export path. After subtracting that +authorized delta, the base and head finding sets are byte-for-byte equivalent as normalized JSON. + +```json +{ + "gateId": "surface-diff", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "RED", + "exitCode": 1, + "classification": "KNOWN BASE RED plus authorized breaking signature delta", + "base": { + "gitTree": "0ef48c2ec661a7e6d55ec2faf5def6ae7dd2e6eb", + "findingCount": 972, + "undeclaredMajorCount": 524, + "findingSetSha256": "55744a8522197fbb450349ca7185631188c97add132c356e173259875ae9406c" + }, + "head": { + "findingCount": 987, + "undeclaredMajorCount": 539, + "findingSetSha256": "7720483a169a126af09a3117875d410ac74171f94005b8386dd32f201e8690a1" + }, + "attribution": { + "authorizedDeltaCount": 15, + "unexpectedAdded": [], + "unexpectedRemoved": [], + "headAfterAuthorizedDeltaCount": 972, + "headAfterAuthorizedDeltaSha256": "55744a8522197fbb450349ca7185631188c97add132c356e173259875ae9406c", + "attributedBaselineSetsEqual": true, + "newExports": [] + }, + "authorizedSymbols": [ + "@netscript/contracts . baseContract", + "@netscript/sdk . SafeFailure", + "@netscript/sdk . SafeResult", + "@netscript/sdk . ServiceClientMethod", + "@netscript/sdk . ServiceClientShape", + "@netscript/sdk . isDefinedError", + "@netscript/sdk . safe", + "@netscript/sdk ./client SafeFailure", + "@netscript/sdk ./client SafeResult", + "@netscript/sdk ./client ServiceClientMethod", + "@netscript/sdk ./client ServiceClientShape", + "@netscript/sdk ./client isDefinedError", + "@netscript/sdk ./client safe", + "@netscript/sdk ./ports ServiceClientMethod", + "@netscript/sdk ./ports ServiceClientShape" + ] +} +``` + +### Publish dry-runs + +```json +{ + "gateId": "publish-dry-run", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "summary": "Workspace dry run complete", + "existingWarnings": [ + "unanalyzable dynamic imports in unrelated packages", + "ignored npm lifecycle scripts" + ] +} +``` + +```json +{ + "gateId": "contracts-raw-publish-dry-run", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "command": "deno publish --dry-run --allow-dirty --allow-slow-types", + "sanctionedWarning": "Publishing a library with slow types is not recommended" +} +``` + +```json +{ + "gateId": "sdk-raw-publish-dry-run", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "PASS", + "exitCode": 0, + "command": "deno publish --dry-run --allow-dirty", + "actualSlowTypeDiagnostics": 0 +} +``` + +### Blocking raw doc-lint delta + +Both raw gates remain RED, but their finding sets are not merely the pinned baseline. Contracts has +three added findings and one removed minified upstream identity (`oc`), for a net +2. SDK has ten +added findings and no removals. Every added finding names an S1/S2 public signature or a private +helper/upstream alias introduced to express it. + +```json +{ + "gateId": "contracts-raw-doc-lint", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "RED", + "exitCode": 1, + "base": { "gitTree": "0ef48c2ec661a7e6d55ec2faf5def6ae7dd2e6eb", "count": 9 }, + "head": { "count": 11 }, + "added": [ + ["baseContract", "ContractBuilder"], + ["baseContract", "Schema"], + ["baseContract", "BaseContractErrors"] + ], + "removed": [["baseContract", "oc"]], + "classification": "NEW LEAF-OWNED RED; S4 BLOCKER" +} +``` + +```json +{ + "gateId": "sdk-raw-doc-lint", + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "RED", + "exitCode": 1, + "base": { "gitTree": "0ef48c2ec661a7e6d55ec2faf5def6ae7dd2e6eb", "count": 3 }, + "head": { "count": 13 }, + "added": [ + ["ServiceClientMethod", "ThrowableError"], + ["ServiceClientMethod", "ClientPromiseResult"], + ["ServiceClientShape", "ProcedureErrorFromNode"], + ["SafeFailure", "ThrowableError"], + ["SafeFailure", "NonDefinedSafeFailure"], + ["SafeFailure", "DefinedSafeFailure"], + ["SafeResult", "ThrowableError"], + ["isDefinedError", "NarrowDefined"], + ["safe", "ThrowableError"], + ["safe", "ClientPromiseResult"] + ], + "removed": [], + "classification": "NEW LEAF-OWNED RED; S4 BLOCKER" +} +``` + +### Not executed after the mandated stop + +```json +{ + "gitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "actualGitHead": "c7cba6d9bd6aef1fbeb0e8e9778a5d979c8544bd", + "waiver": null, + "outcome": "NOT_RUN", + "reason": "S4 stopped when new doc-lint findings proved that source changes would be required", + "gates": [ + "contracts-jsr-audit", + "sdk-jsr-audit (known F-DOCT-5 red not reclassified)", + "netscript-jsr-specifiers", + "selected export guards" + ] +} +``` + +No Aspire, Docker, browser, `e2e:cli`, runtime lease, or evaluator was used. No issue, label, +checkbox, readiness, or metadata state was changed. + +## S4-R — plan-only amendment: finding→correction mapping + +Generator: native Claude session (see `supervisor.md` § S4-R Amendment Supervisor Identity). This +section amends the S4 stop with a per-finding disposition. No product/test/docs/lock file was +touched to produce this section; every mechanism below was verified against an isolated scratch +probe outside the repository tree (`deno doc --lint` on synthetic fixtures), not against the actual +S1/S2 source, and against the real `@orpc/client`/`@orpc/contract`/`@orpc/shared` `.d.ts` files +already resolved in this workspace's Deno cache. The repair itself remains a separate, freshly +authorized implementation slice. + +### How `deno doc --lint`'s `private-type-ref` actually resolves (probe-verified) + +The diagnostic is **syntactic and single-hop**, not a deep semantic graph walk: + +1. It flags a directly-named identifier in a public declaration's own text (including default type + parameter values) when that identifier resolves to a symbol not exported/reachable from the + entrypoint being linted. +2. It does **not** recurse into a flagged private type's own body — `BaseContractErrors` (private + relative to `packages/contracts/src/public/mod.ts`, though file-level `export`ed) is flagged when + named by `baseContract`, but its own internal reference to `MergedErrorMap`/`commonErrorMap` is + never separately checked, because `BaseContractErrors` itself isn't a root symbol being linted + from that entrypoint. +3. TypeScript's built-in utility types (`Omit`, `Extract`, `Exclude`, `Record`, `Pick`, `Readonly`) + are never themselves flagged; only their generic *arguments* are, if those name private types. +4. An anonymous inline object/tuple/conditional type literal with no name at all cannot be flagged — + there is nothing to resolve. +5. `typeof aBinding` is flagged exactly like a named type alias when `aBinding` is a private local or + an un-reexported import — **but** `typeof aPublicAliasForTheSameValue` is not flagged, because the + check resolves the specific identifier used, not the underlying value. + +Probe evidence (scratch fixture, not part of this repo): + +```text +public type 'usesTypeofDirectly' references private type 'secretConst' # typeof private const -> flagged +public type 'usesPrivateAliasByName' references private type 'PrivateAlias' # named private alias -> flagged +usesInlineAnon: { x: number } # inline literal -> clean +public type 'fnWithDefault' references private type 'PrivateDefault' # generic default -> flagged +fnWithErrorDefault # global default -> clean +public type 'usesOmit' references private type 'Base' # Omit flags Base, not Omit +usesPrivateBinding -> flagged; usesPublicBinding (typeof PublicName, same value) -> clean +``` + +Also confirmed directly against `packages/contracts/mod.ts`/`packages/sdk` entrypoints at head +`c7cba6d9b`: the 3 contracts and 10 SDK additions are exactly and only the ones the S4 receipt +already named; the other 4 pre-existing findings (7 in `BaseContractRoute`/`BaseContractOutputRoute`, +1 in `crud/create-crud-contract.ts`, plus SDK's 3 unrelated pre-existing findings in +`query-client.ts`/`query-client-factory.ts`/`plugin-streams-core`) are baseline noise, unchanged by +this leaf, and out of scope for this amendment. + +### Root-cause origin of each private type + +| Private type | Origin | Nature | +| --- | --- | --- | +| `ThrowableError` | `@orpc/shared`, re-exported by `@orpc/client` | `type ThrowableError = Registry extends {throwableError: infer T} ? T : Error` — with the empty ambient `Registry` interface (confirmed: no `declare module '@orpc/shared'` augmentation exists anywhere in this repo), this resolves to exactly `Error`. | +| `ClientPromiseResult` | `@orpc/client`/`@orpc/shared` | `= PromiseWithError = Promise & { __error?: { type: E } }` — a real `Promise` plus an optional phantom marker property that never exists at runtime, used only so `safe()` can recover `E` at the type level. | +| `NonDefinedSafeFailure`, `DefinedSafeFailure`, `NarrowDefined`, `DefinedErrorLike` | local to `errors.ts`, declared without `export` | Package-owned helper aliases already private by design; the finding is that the *public* `SafeFailure`/`isDefinedError` declarations name them directly. | +| `ProcedureErrorFromNode` | local to `service-client.ts`, declared without `export` | Reaches into oRPC's own `ErrorMap`/`ErrorFromErrorMap` (also private to us) to derive `TError` from a raw `~orpc.errorMap`. | +| `ContractBuilder`, `Schema` | `@orpc/contract` | `Schema = StandardSchemaV1` (a real standard, but not re-exported by us); `ContractBuilder` is oRPC's own chainable builder **class** (`.route()/.input()/.output()/.errors()/.meta()`, ~15+ members across 4 interfaces). | +| `BaseContractErrors` | local to `contract-primitives.ts`, `export`ed from the file but not re-exported by `src/public/mod.ts` | Private relative to the publish entrypoint, not relative to the file. | + +### Finding → correction mapping (13 findings, grouped by symbol per the requested table) + +| # | Finding (public type → private type) | Disposition | Correction | Why type-safe, not a suppression | +| - | --- | --- | --- | --- | +| 1 | `SafeFailure` → `ThrowableError` | **Inlined (default swap)** | `SafeFailure` (was `= ThrowableError`) | `ThrowableError` resolves to exactly `Error` in this dependency graph today (empty `Registry`, no augmentation anywhere in-repo). Same resolved default, spelled without importing the private name. Documented trade-off below. | +| 2 | `SafeFailure` → `NonDefinedSafeFailure` | **Restructured away (inlined)** | The public `SafeFailure` union's non-defined arm is written inline: `[Exclude, undefined, false, false] & { error: Exclude; data: undefined; isDefined: false; isSuccess: false }`, where `DEL` is `DefinedErrorLike`'s body inlined (see #3). | Byte-identical resolved type; only the source-level reference to the private alias is removed. `NonDefinedSafeFailure` itself stays declared, unchanged, for internal use by `createSafeFailure` (a non-exported function, never linted). | +| 3 | `SafeFailure` → `DefinedSafeFailure` | **Restructured away (inlined)** | Defined arm inlined the same way: `[Extract & DefinedError, undefined, true, false] & {...}`, with `DEL = Error & { readonly defined: boolean; readonly code: string; readonly status: number; readonly data: unknown }` (i.e. `DefinedErrorLike`'s current body) written out at each of the (now 4, across the two arms) points it's needed. `DefinedError` is already a public export — safe to name directly. | Same reasoning as #2. `DefinedErrorLike` itself is not itself flag-eligible until named from a public declaration; inlining its body avoids ever naming it there. Verified inline-literal exemption via probe. | +| 4 | `SafeResult` → `ThrowableError` | **Inlined (default swap)** | `SafeResult` | Same as #1; `SafeResult` only adds `SafeSuccess \| SafeFailure`, no other private ref. | +| 5 | `isDefinedError` → `NarrowDefined` | **Restructured away (inlined)** | `export function isDefinedError(error: T): error is Extract & DefinedError` | `NarrowDefined = Extract & DefinedError`; both `NarrowDefined` and `DefinedErrorLike` are inlined at the one call site that needs them publicly. Predicate result type is unchanged bit-for-bit. | +| 6 | `safe` → `ThrowableError` | **Inlined (default swap)** | `safe(...)` | Same as #1. | +| 7 | `safe` → `ClientPromiseResult` | **Restructured away (inlined)** | Parameter type becomes `promise: Promise & { __error?: { type: TError } }` (drop the `type ClientPromiseResult`/`type ThrowableError` imports from `@orpc/client`; keep the runtime `isDefinedError as orpcIsDefinedError` value import, which is not a type and was never flagged). | Structurally identical to `PromiseWithError` (`@orpc/shared`'s real definition, confirmed from the installed `.d.ts`). Any real oRPC client promise is structurally assignable to this literal; the phantom `__error` marker is preserved so `TError` inference still works — it is not dropped, only un-named. | +| 8 | `ServiceClientMethod` → `ThrowableError` | **Inlined (default swap)** | `ServiceClientMethod` | Same reasoning as #1, in `service-client.ts`. | +| 9 | `ServiceClientMethod` → `ClientPromiseResult` | **Restructured away (inlined)** | Return type becomes `(input: TInput, options?: ServiceRequestOptions) => Promise & { __error?: { type: TError } }` | Same reasoning as #7; duplicated locally in `service-client.ts` rather than shared across files, since sharing would require exporting a new internal type (forbidden). | +| 10 | `ServiceClientShape` → `ProcedureErrorFromNode` | **Restructured away (inlined, redesigned)** | See dedicated subsection below — this is the one finding that required redesigning the private helper's *body*, not just its call site. | See below. | +| 11 | `baseContract` → `BaseContractErrors` | **Restructured away (inlined)** | See dedicated subsection below. | See below. | +| 12 | `baseContract` → `Schema` | **Made public (existing NetScript alias)** | See dedicated subsection below. | See below. | +| 13 | `baseContract` → `ContractBuilder` | **UNRESOLVED — reports, does not plan around** | None proposed. | See below. | + +### #10 — `ServiceClientShape` → `ProcedureErrorFromNode` (service-client.ts) + +Naively inlining `ProcedureErrorFromNode`'s current body into `ServiceClientShape` does **not** +work: its body directly names `ErrorMap` and `ErrorFromErrorMap` (both private, from +`@orpc/contract`), so inlining would just relocate the private reference one level up (new findings +on those two names instead of on `ProcedureErrorFromNode`) — that is the "suppression wearing a +different hat" failure mode the brief warns about, so it was rejected. + +Correction: redesign `ProcedureErrorFromNode`'s body so it never names an oRPC-private type, mirroring +the same "NetScript-owned structural mirror" technique `ContractSchemaInput`/`ContractSchemaOutput`/ +`ContractProcedureMetadata` already use for input/output instead of depending on oRPC's own procedure +type. Inlined directly into `ServiceClientShape`: + +```ts +export type ServiceClientShape = TContract extends + ContractProcedureLike ? ServiceClientMethod< + ProcedureInputFromNode, + ProcedureOutputFromNode, + TContract extends { + readonly '~orpc': { + readonly errorMap: infer TErrorMap extends Record; + }; + } ? { + [K in keyof TErrorMap]: K extends string + ? TErrorMap[K] extends { readonly data?: infer TDataSchema } + ? Error & { + readonly defined: true; + readonly code: K; + readonly status: number; + readonly data: ContractSchemaOutput; + } + : never + : never; + }[keyof TErrorMap] | Error + : Error + > + : { [K in keyof TContract]: TContract[K] extends ContractLike ? ServiceClient : never }; +``` + +Why this is type-safe rather than a shim: it does not import `DefinedError` from `errors.ts` (that +would fail differently — see note below), it does not name `ErrorMap`/`ErrorFromErrorMap`/`ORPCError`, +and it reuses `ContractSchemaOutput`, already public and already used elsewhere in this file for the +identical purpose (schema → inferred value type). The intersection literal `Error & {readonly +defined: true; code: K; status: number; data: ContractSchemaOutput}` is structurally +identical to what `DefinedError>` would produce (confirmed +against `errors.ts`'s own `DefinedError` interface shape) and to what oRPC's real `ORPCError` +class actually looks like at runtime (`extends Error { readonly defined: boolean; code; status; data +}`, confirmed from `@orpc/client`'s installed `.d.ts`) — real thrown contract errors structurally +satisfy it. This is the same "duck-type against oRPC's real runtime shape" technique `errors.ts` +already uses for `DefinedErrorLike`, applied at a different call site. The constraint on `infer +TErrorMap` is narrowed from oRPC's own `ErrorMap` to an equivalent-width `Record` structural bound — width-compatible with any real `~orpc.errorMap` value, so +no real contract narrows out. + +**Note on why `DefinedError` (from `errors.ts`) was not imported instead:** `ports/mod.ts` (an +existing SDK entrypoint that re-exports `ServiceClientShape`) does not currently re-export +`DefinedError` — it is exported only via `client/mod.ts`. Since the private-type-ref check is +per-entrypoint, importing `DefinedError` into `service-client.ts` would make `ServiceClientShape` +pass when linted through `client/mod.ts` but newly fail when linted through `ports/mod.ts` (a type +public via one subpath is still "private" relative to another subpath that doesn't re-export it). +Fixing that would require editing `ports/mod.ts` — a fourth file, out of scope. The inline +intersection literal above avoids the cross-entrypoint dependency entirely. + +### #11 — `baseContract` → `BaseContractErrors` (contract-primitives.ts) + +Inlining `BaseContractErrors`'s current body (`MergedErrorMap, typeof +commonErrorMap>`) naively relocates the problem to `MergedErrorMap` (private) and then to +`commonErrorMap` (a private local const — probe-confirmed that `typeof aPrivateConst` is itself +flagged). Two facts resolve this cleanly: + +1. **`MergedErrorMap, T>` is provably equivalent to plain `T`.** Verified against + the real `@orpc/contract@1.14.6` types in a scratch fixture: `type ErrorsA = MergedErrorMap, CommonErrorMapType>` and `type ErrorsB = CommonErrorMapType` type-checked as mutually + assignable (`ErrorsA extends ErrorsB` and `ErrorsB extends ErrorsA` both `true`) — merging onto an + empty base is a no-op. So the third generic slot can be `typeof commonErrorMap`'s structure + directly, no `MergedErrorMap`/`Omit` needed. +2. **The six schemas already have public aliases.** `domain/schemas.ts` exports both the private + lowercase values used to build `commonErrorMap` (`notFoundErrorSchema`, etc.) *and* public + PascalCase re-exports of the identical value (`export const NotFoundErrorSchema = + notFoundErrorSchema;`, etc.), and the six PascalCase names are already re-exported through + `packages/contracts/src/public/mod.ts`. Probe-confirmed: `typeof PublicAlias` (for the same value) + is not flagged even though `typeof privateBinding` is. + +Correction — switch `contract-primitives.ts`'s import of the six schemas from the private lowercase +names to the public PascalCase names (same file, same `../domain/schemas.ts` module, no edit to +`domain/schemas.ts` itself), use them for both `commonErrorMap`'s value *and* the inlined annotation: + +```ts +Readonly<{ + NOT_FOUND: Readonly<{ status: 404; message: 'Resource not found'; data: typeof NotFoundErrorSchema }>; + VALIDATION_ERROR: Readonly<{ status: 422; message: 'Validation failed'; data: typeof ValidationErrorSchema }>; + UNAUTHORIZED: Readonly<{ status: 401; message: 'Authentication required'; data: typeof UnauthorizedErrorSchema }>; + FORBIDDEN: Readonly<{ status: 403; message: 'Access denied'; data: typeof ForbiddenErrorSchema }>; + RATE_LIMITED: Readonly<{ status: 429; message: 'Too many requests'; data: typeof RateLimitErrorSchema }>; + SERVICE_UNAVAILABLE: Readonly<{ status: 503; message: 'Service temporarily unavailable'; data: typeof ServiceUnavailableErrorSchema }>; +}> +``` + +This preserves the exact six literal codes and code-specific `data` types (the public PascalCase +export is declared `ContractSchema`-typed on the same runtime value, itself a public, +already-used NetScript structural type — see #12). **Verification owed at implementation time:** +`deno check` must confirm `oc.errors(commonErrorMap)`'s actual call-site constraint +(`U extends ErrorMap`) still accepts the value once `commonErrorMap` is built from the +`ContractSchema`-typed public aliases rather than the raw Zod-typed private ones; this was verified +structurally (standard-schema shape) but not against the exact live `oc.errors` overload. + +### #12 — `baseContract` → `Schema` (contract-primitives.ts) + +`Schema` is oRPC's private alias for `StandardSchemaV1`. NetScript already has a +public structural mirror of exactly this shape: `ContractSchema` +(`domain/schema-types.ts`, re-exported via `src/public/mod.ts`), documented in-file as "Standard +Schema metadata consumed by oRPC and other validator-neutral callers" — i.e. it was built to be +duck-type-compatible with standard-schema consumers such as oRPC's own builder. + +Correction: replace both `Schema` generic arguments on `baseContract`'s annotation +with `ContractSchema`. This makes the reference public (option "make the referenced +type public" from the diagnostic's own hint) using a name NetScript already exports — no new export +is introduced. **Verification owed at implementation time:** `deno check` must confirm +`ContractBuilder`'s generic constraint (`TInputSchema/TOutputSchema extends AnySchema`) accepts +`ContractSchema` in the "no `.input()`/`.output()` called yet" position, and that +the class's covariance/method-checking doesn't reject the substitution. The structural shapes match +(`~standard: {version, vendor, validate}`), but class-generic substitutability was reasoned, not +`deno check`-proven, in this plan-only turn. + +### #13 — `baseContract` → `ContractBuilder` (contract-primitives.ts) — UNRESOLVED + +This is the one finding this amendment does **not** resolve, and reports rather than plans around, +per the brief's explicit instruction. + +`ContractBuilder` is oRPC's own chainable builder **class** (`ContractProcedureBuilder` / +`...WithInput` / `...WithOutput` / `...WithInputOutput`, ~15+ chained methods, extending +`ContractProcedure`). An explicit type annotation on `baseContract` is mandatory — `deno doc --lint` +requires one (an unannotated `const` initialized from a generic call is a different, worse +diagnostic class: `missing-explicit-type`), and the annotation must name the actual return type of +`oc.errors(commonErrorMap)`, which is a `ContractBuilder<...>` instance. + +Three paths were considered and all three are blocked by this leaf's own constraints: + +1. **Re-export `ContractBuilder` (and `Schema`, transitively) from `src/public/mod.ts`.** This is the + textbook "make the referenced type public" fix the diagnostic itself suggests — but `src/public/mod.ts` + is the explicitly forbidden fourth file, and doing so is also a new export name, both prohibited + by this amendment's scope. +2. **Locally reconstruct `ContractBuilder`'s full structural surface as an inline/private type.** + Technically possible (TypeScript permits anonymous structural interfaces with methods) but this + means hand-duplicating oRPC's entire builder algebra (`.route()/.input()/.output()/.errors()/.meta()` + across four builder interfaces plus the base `ContractProcedure` members) inside NetScript's own + package, permanently coupled to oRPC's exact internal shape and guaranteed to drift on the next + `@orpc/contract` version bump. This is exactly what doctrine AP-1/AP-9 forbid ("do not grow + `errors.ts`/contract primitives into a generic error framework or invent a second client algebra") + and what the plan's own risk register already flags ("tight builder annotation breaks + CRUD/plugin/handler inference"). Rejected as unsafe, not merely inconvenient. +3. **Revert to an inference-erasing annotation** (e.g. `ReturnType`, the pre-leaf + base state, which only names `oc` and reproduces the *already-pinned* baseline finding instead of + a new one). This was the actual base-commit approach. Rejected: `ReturnType` on an *uninstantiated* + generic function collapses `oc.errors`'s type parameter to its `ErrorMap` upper bound, which erases + the exact six literal codes back to oRPC's full ~13-code vocabulary — the precise regression #1350 + exists to fix. Directly violates the "accepted exact six error codes" invariant this amendment is + bound to preserve. Rejected. + +No fourth option was found that both (a) keeps the explicit, literal-preserving annotation and (b) +never names `ContractBuilder`. This is reported as a genuine architectural limit of the three-file, +no-new-export ceiling, not an oversight — a coordinator ruling is needed on whether to authorize a +narrow, single-purpose re-export of `ContractBuilder`/`Schema` type names only (a scope amendment +touching `src/public/mod.ts`), or to accept `baseContract → ContractBuilder` as permanent, +irreducible, leaf-owned known-red debt alongside the existing pinned baseline. + +### Surface-delta effect + +Every proposed correction above (#1–#12) is **purely notational**: each rewrites how a type is +*spelled* in source (inlined vs. named, or renamed to an existing public alias for the identical +value), without changing what the type *resolves to*. `SafeFailure` resolves to the +same shape as `SafeFailure` did (given the empty `Registry`); the inlined +`ProcedureErrorFromNode` replacement produces the same per-code `DefinedError`-shaped union the +locked plan already specified. None of this changes any of the 15 authorized breaking signature +changes recorded in `worklog.md`'s `surface-diff` receipt — the delta stays exactly 15, neither +exceeded nor silently shrunk. Finding #13 (`ContractBuilder`) is unresolved but also does not change +the surface delta; it leaves `baseContract`'s resolved type exactly as S1 already locked it (only its +open doc-lint finding count is unchanged: contracts moves from 11 to net **9 pinned-baseline-parity + +1 new (`ContractBuilder`, replacing the pinned `oc`)** if this specific mapping is implemented as +proposed — i.e. from +2 new down to +1 new, not to zero, pending the coordinator ruling on #13). SDK +resolves cleanly to **0 new** (all 10 findings dispositioned without residue). + +### What this amendment does not do + +- It does not implement any of the above in `contract-primitives.ts`, `errors.ts`, or + `service-client.ts`. Those remain at the S1–S3 landed content; nothing in the three authorized + product files changed as part of S4-R. +- It does not add, suppress, or ignore any doc-lint diagnostic. +- It does not touch `packages/contracts/src/public/mod.ts`, `#1348`, or `#1466`. +- It does not tick any PR checkbox or change `status:` labels. diff --git a/docs/site/services-sdk/how-to/discover-services.md b/docs/site/services-sdk/how-to/discover-services.md index 830c611ab2..86547a1ab5 100644 --- a/docs/site/services-sdk/how-to/discover-services.md +++ b/docs/site/services-sdk/how-to/discover-services.md @@ -10,8 +10,9 @@ oldUrl: /how-to/discover-services/ **Goal:** call another plugin's (or another workspace member's) oRPC service from your app, end to end — declare the dependency so Aspire injects the callee's URL, then obtain a -fully typed client from `@netscript/sdk` that resolves that URL at request time. No registry, -no hardcoded `localhost:`, no codegen. +fully typed client from `@netscript/sdk` that resolves that URL at request time. The shared +contract supplies the method inputs, outputs, and declared errors; discovery or transport failures +remain non-defined failures. No registry, no hardcoded `localhost:`, no codegen. {{ comp.badge({ status: "alpha" }) }} @@ -35,7 +36,7 @@ import the contract, construct the client — that is the whole recipe. { name: "A NetScript workspace", type: "netscript init", desc: "An existing project with at least the caller (an app, plugin, or service) and the callee service already present. Run commands from the workspace root." }, { name: "The callee service", type: "services//", desc: "A target service that answers on its own port over /api/rpc/* — e.g. the example users service on its assigned port. See the Add a service recipe to create one." }, { name: "@netscript/sdk", type: "import alias", desc: "The SDK provides the typed client (createServiceClient / defineServices) and the discovery readers (@netscript/sdk/discovery). It is a workspace dependency of apps and consuming services." }, - { name: "The shared contract", type: "@/contracts", desc: "Both the service and the caller import the SAME oRPC contract object through the project alias, so the client's input/output types are inferred — never duplicated." }, + { name: "The shared contract", type: "@/contracts", desc: "Both the service and the caller import the SAME oRPC contract object through the project alias, so the client's input, output, and declared-error types are inferred — never duplicated." }, { name: "Aspire (for resolved URLs)", type: "aspire start", desc: "Aspire injects the services____http__ env vars that discovery reads. Without it you must set those vars yourself (see pitfalls)." } ] }) }} @@ -98,7 +99,8 @@ caller's environment. In the caller, import the **same contract object** the service implements and hand it to `createServiceClient`. The `serviceName` you pass is the discovery key — it must match the name you referenced in Step 1. The client is fully typed from the contract; there is no -generated client file to import. +generated client file to import. When its routes are composed from `baseContract`, every method +also carries the six standard declared errors through the returned promise. ```ts // web/src/clients/users.ts — a typed client for the discovered users service @@ -111,7 +113,7 @@ export const usersClient = createServiceClient({ serviceName: 'users', }); -// Fully inferred input/output — a renamed contract field is a compile error here. +// Input, output, and declared errors are inferred; a renamed field is a compile error. const { items } = await usersClient.list({ limit: 20 }); ``` @@ -134,25 +136,45 @@ default it targets `/api/rpc/v1/` over HTTP. ## Step 4 — Call it (and handle typed errors) -The client methods mirror the contract's procedure tree. Wrap calls in the SDK's `safe()` -helper to get a tuple instead of a throw, and narrow defined errors with `isDefinedError`: +The client methods mirror the contract's procedure tree. For a `baseContract` route, the defined +error codes are exactly `NOT_FOUND`, `VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, +`RATE_LIMITED`, and `SERVICE_UNAVAILABLE`. Wrap a call in the SDK's `safe()` helper, test the +literal `isSuccess` discriminant first, and then test `isDefined` on the failure. A plain error, +transport failure, or other non-defined value must be thrown or surfaced from that second branch; +it must not fall through to the successful output path. ```ts // web/src/routes/users.ts — typed call with safe-style error handling -import { isDefinedError, safe } from '@netscript/sdk/client'; +import { safe } from '@netscript/sdk/client'; import { usersClient } from '../clients/users.ts'; -const [error, result] = await safe(usersClient.list({ limit: 20 })); - -if (error && isDefinedError(error)) { - // error.code and error.data are typed from the contract - console.error('users.list failed:', error.code, error.data); -} else { - // result.items is the contract's output type - return result.items; +type UsersErrorCode = + | 'NOT_FOUND' + | 'VALIDATION_ERROR' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'RATE_LIMITED' + | 'SERVICE_UNAVAILABLE'; + +export async function listUsers() { + const result = await safe(usersClient.list({ limit: 20 })); + if (result.isSuccess) return result.data.items; + if (!result.isDefined) throw result.error; + + const code: UsersErrorCode = result.error.code; + if (result.error.code === 'NOT_FOUND') { + // The code selects NOT_FOUND's schema-derived data shape. + console.error(result.error.data.resourceType, result.error.data.resourceId); + } + throw new Error(`users.list failed: ${code}`, { cause: result.error }); } ``` +On the defined branch, `error.data` is selected by `error.code` from the schema declared in the +contract; it is not an open bag and it is not `any`. `isDefinedError(failure.error)` is the +predicate form for an already typed failure union. It preserves the defined members present in +that union rather than promoting arbitrary `unknown` values into contract errors. + ## Many services at once — `defineServices` When the caller talks to several services, `defineServices` builds the clients (plus @@ -209,7 +231,7 @@ run the command sequence from the workspace root: ```ts // web/src/clients/users.ts — the resulting file (contract in, typed client out) -import { createServiceClient, isDefinedError, safe } from '@netscript/sdk/client'; +import { createServiceClient, safe } from '@netscript/sdk/client'; import { UsersContractV1 } from '@my-app/contracts'; // serviceName is the discovery key — resolves services__users__http__0 at call time. @@ -218,13 +240,26 @@ export const usersClient = createServiceClient({ serviceName: 'users', }); +type UsersErrorCode = + | 'NOT_FOUND' + | 'VALIDATION_ERROR' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'RATE_LIMITED' + | 'SERVICE_UNAVAILABLE'; + // A typed call with safe-style error handling — no hardcoded URL anywhere. export async function firstUsers() { - const [error, result] = await safe(usersClient.list({ limit: 20 })); - if (error && isDefinedError(error)) { - throw new Error(`users.list failed: ${error.code}`); + const result = await safe(usersClient.list({ limit: 20 })); + if (result.isSuccess) return result.data.items; + if (!result.isDefined) throw result.error; + + const code: UsersErrorCode = result.error.code; + if (result.error.code === 'VALIDATION_ERROR') { + // VALIDATION_ERROR selects { formErrors, fieldErrors } from its contract schema. + console.error(result.error.data.formErrors, result.error.data.fieldErrors); } - return result.items; + throw new Error(`users.list failed: ${code}`, { cause: result.error }); } ``` @@ -246,7 +281,9 @@ deno task --cwd web dev With `aspire start` up, `usersClient.list({ limit: 20 })` resolves `services__users__http__0`, issues the oRPC call to `/api/rpc/v1/users`, and returns -the contract's typed `items`. No registry, no `localhost:`, no generated client file. +the contract's typed `items` on success. Its declared error union comes from the same contract; +non-defined runtime failures stay separate. No registry, no `localhost:`, no generated client +file. ## Production pitfalls diff --git a/docs/site/services-sdk/sdk.md b/docs/site/services-sdk/sdk.md index abd0ed2006..dd928fa853 100644 --- a/docs/site/services-sdk/sdk.md +++ b/docs/site/services-sdk/sdk.md @@ -17,7 +17,9 @@ The `@netscript/sdk` is the **typed client and data layer** for NetScript: it tu wraps each contract action in a **cache-first query factory** (KV-backed SWR), and bridges those into [TanStack Query](https://tanstack.com/query) for islands. The contract object is the single source of truth — the *same* object the [service](/services-sdk/services/) -implements is the one the client imports, so caller and server **cannot drift**. +implements is the one the client imports, so its input, output, and declared-error types cannot +drift between caller and server. Transport failures and arbitrary thrown values are not declared +contract errors; `safe()` keeps those failures on its non-defined branch. {{ comp.badge({ status: "pre-1.0" }) }} {{ comp.diagram({ @@ -30,7 +32,8 @@ implements is the one the client imports, so caller and server **cannot drift**. The SDK is layered. **L1** is the typed client: `createServiceClient()` resolves a service URL from [Aspire service discovery](/explanation/aspire/) and returns a callable -object whose method signatures are inferred from the contract. **L2** wraps each contract +object whose method input, output, and declared-error union are inferred from the contract. +**L2** wraps each contract action in a query factory (`createQueryFactories`) that runs through a shared KV-backed cache provider and exposes `queryOptions`/`mutationOptions`/`clientKey`/`key`/`getCachedEntry` per action. **L3** is the `defineServices()` preset that builds clients, server query factories, @@ -62,8 +65,10 @@ lives: not inside server code, but in a standalone, versioned contract package t the client, **and** the OpenAPI/Scalar surface all import. And the derivation goes further than method types — from one contract action the factory derives the server KV cache key (`key()`), the client TanStack key (`clientKey()`), the `queryOptions`/`mutationOptions` helpers, and the -SWR cache reads. One edit to the contract updates the client, the docs surface, and the query -layer together — there is no separate turn to keep them in sync. +SWR cache reads. For routes built from `baseContract`, the same derivation carries the declared +error schemas through the client promise and `safe()`. One edit to those contract-owned types +updates the client, the docs surface, and the query layer together; transport and arbitrary thrown +failures remain runtime, non-defined failures rather than being promoted into that contract. This is how a production chat application built on NetScript wires its dashboard: the typed client is built straight off the contract type, so a route added to the contract shows @@ -110,7 +115,8 @@ export const ordersQueries = createQueryFactories({ orders: { contract: ordersContract, client: ordersClient }, }).orders; -// Direct typed call: `.list()` is fully inferred from the contract. +// Direct call: input, output, and declared errors are inferred from the contract. +// Calls still reject on failure; use safe() as shown below when you want a result value. const recent = await ordersClient.list({ limit: 10 }); ``` @@ -191,14 +197,57 @@ The L3 alternative builds all three layers from one map: label: "Island (TanStack hydration)", lang: "tsx", code: "// orders/(_islands)/OrdersQueryIsland.tsx\nimport { useQuery, useQueryClient } from '@netscript/fresh/query';\nimport { ordersQueries } from '@app/lib/orders.ts';\n\n// Same contract action → same query key as the server loader, so the island\n// hydrates from server state instead of refetching on mount.\nconst OrdersList = () => {\n const qc = useQueryClient();\n const orders = useQuery(ordersQueries.list.queryOptions({ limit: 20 }));\n // invalidate by prefix after a write:\n const refresh = () => qc.invalidateQueries({ queryKey: ordersQueries.list.clientKey() });\n return null; // render orders.data\n};" - }, - { - label: "Safe error narrowing", - lang: "ts", - code: "// services/orders/src/routers/v1.ts — service-to-service call\nimport { safe, isDefinedError } from '@netscript/sdk/client';\nimport { usersClient } from '@app/lib/users.ts';\n\nconst [error, user, isDefined] = await safe(usersClient.getById({ id }));\nif (error) {\n // narrow to a typed, contract-declared error\n if (isDefinedError(error)) return { code: error.code, status: error.status };\n throw error;\n}" } ] }) }} +## Safe error narrowing + +For a route built from `baseContract`, the defined channel is exactly `NOT_FOUND`, +`VALIDATION_ERROR`, `UNAUTHORIZED`, `FORBIDDEN`, `RATE_LIMITED`, or `SERVICE_UNAVAILABLE`. +`safe()` returns a `SafeResult`: test `isSuccess` first, then test `isDefined` on the failure. +That second test prevents a transport failure or arbitrary thrown value from falling through as a +successful result. On the defined branch, `data` is inferred from the schema registered for the +selected `code`; it is neither an open property bag nor `any`. + +```ts +import { baseContract } from '@netscript/contracts'; +import { createServiceClient, safe } from '@netscript/sdk/client'; +import { z } from 'zod'; + +const usersContract = { + getById: baseContract + .route({ method: 'GET', path: '/users/{id}' }) + .input(z.object({ id: z.string() })) + .output(z.object({ id: z.string(), name: z.string() })), +}; +const usersClient = createServiceClient({ contract: usersContract, serviceName: 'users' }); + +type StandardErrorCode = + | 'NOT_FOUND' + | 'VALIDATION_ERROR' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'RATE_LIMITED' + | 'SERVICE_UNAVAILABLE'; + +export async function loadUser(id: string) { + const result = await safe(usersClient.getById({ id })); + if (result.isSuccess) return result.data; + if (!result.isDefined) throw result.error; + + const code: StandardErrorCode = result.error.code; + if (result.error.code === 'NOT_FOUND') { + // NOT_FOUND selects its schema: { resourceType, resourceId }. + console.error(result.error.data.resourceType, result.error.data.resourceId); + } + throw new Error(`users.getById failed: ${code}`, { cause: result.error }); +} +``` + +`isDefinedError(failure.error)` is the predicate form when you already have a typed failure union. +It preserves the defined members present in that input type; it does not turn an arbitrary +`unknown` value into one of the contract's declared errors. + ## Production notes {{ comp callout { type: "important", title: "/cache and /discovery are server-only" } }} diff --git a/packages/contracts/src/application/contract-primitives.ts b/packages/contracts/src/application/contract-primitives.ts index 46c107174a..8be140a4f2 100644 --- a/packages/contracts/src/application/contract-primitives.ts +++ b/packages/contracts/src/application/contract-primitives.ts @@ -1,9 +1,9 @@ import { oc } from '@orpc/contract'; import type { AnySchema, + ContractBuilder, ContractProcedureBuilderWithInputOutput, ContractProcedureBuilderWithOutput, - ErrorMap, MergedErrorMap, Schema, } from '@orpc/contract'; @@ -16,9 +16,40 @@ import { validationErrorSchema, } from '../domain/schemas.ts'; -type OrpcErrorMap = Parameters[0]; +type CommonErrorMap = Readonly<{ + NOT_FOUND: Readonly<{ + status: 404; + message: 'Resource not found'; + data: typeof notFoundErrorSchema; + }>; + VALIDATION_ERROR: Readonly<{ + status: 422; + message: 'Validation failed'; + data: typeof validationErrorSchema; + }>; + UNAUTHORIZED: Readonly<{ + status: 401; + message: 'Authentication required'; + data: typeof unauthorizedErrorSchema; + }>; + FORBIDDEN: Readonly<{ + status: 403; + message: 'Access denied'; + data: typeof forbiddenErrorSchema; + }>; + RATE_LIMITED: Readonly<{ + status: 429; + message: 'Too many requests'; + data: typeof rateLimitErrorSchema; + }>; + SERVICE_UNAVAILABLE: Readonly<{ + status: 503; + message: 'Service temporarily unavailable'; + data: typeof serviceUnavailableErrorSchema; + }>; +}>; -const commonErrorMap = { +const commonErrorMap: CommonErrorMap = { NOT_FOUND: { status: 404, message: 'Resource not found', @@ -49,7 +80,7 @@ const commonErrorMap = { message: 'Service temporarily unavailable', data: serviceUnavailableErrorSchema, }, -} as const satisfies OrpcErrorMap; +}; /** * Common oRPC contract primitive with NetScript's standard error map applied. @@ -78,7 +109,12 @@ const commonErrorMap = { * .output(z.object({ items: z.array(z.unknown()) })); * ``` */ -export const baseContract: ReturnType = oc.errors(commonErrorMap); +export const baseContract: ContractBuilder< + Schema, + Schema, + BaseContractErrors, + Record +> = oc.errors(commonErrorMap); /** * Concrete type of {@link baseContract} — the real oRPC contract builder with @@ -95,7 +131,7 @@ export type BaseContract = typeof baseContract; * vocabulary merged onto an empty map. Mirrors the `BaseErrors` alias used by * the first-party `@netscript/plugin-*-core` contract definitions. */ -export type BaseContractErrors = MergedErrorMap, ErrorMap>; +export type BaseContractErrors = MergedErrorMap, typeof commonErrorMap>; /** * Sound type of a route built via diff --git a/packages/sdk/src/client/errors.ts b/packages/sdk/src/client/errors.ts index bf47f0ca88..b6d4efc9ec 100644 --- a/packages/sdk/src/client/errors.ts +++ b/packages/sdk/src/client/errors.ts @@ -4,7 +4,11 @@ * @module */ -import { isDefinedError as orpcIsDefinedError } from '@orpc/client'; +import { + type ClientPromiseResult, + isDefinedError as orpcIsDefinedError, + type ThrowableError, +} from '@orpc/client'; /** * Public shape of an oRPC defined error. @@ -33,20 +37,47 @@ export type SafeSuccess = [null, TOutput, false, true] & { isSuccess: true; }; +type DefinedErrorLike = Error & { + readonly defined: boolean; + readonly code: string; + readonly status: number; + readonly data: unknown; +}; + +type NarrowDefined = Extract & DefinedError; + +type NonDefinedSafeFailure = [ + Exclude, + undefined, + false, + false, +] & { + error: Exclude; + data: undefined; + isDefined: false; + isSuccess: false; +}; + +type DefinedSafeFailure = [NarrowDefined, undefined, true, false] & { + error: NarrowDefined; + data: undefined; + isDefined: true; + isSuccess: false; +}; + /** * Failure branch returned by {@link safe}. */ -export type SafeFailure = [TError, null, boolean, false] & { - error: TError; - data: null; - isDefined: boolean; - isSuccess: false; -}; +export type SafeFailure = + | NonDefinedSafeFailure + | DefinedSafeFailure; /** * Tuple/object result returned by {@link safe}. */ -export type SafeResult = SafeSuccess | SafeFailure; +export type SafeResult = + | SafeSuccess + | SafeFailure; function createSafeSuccess(data: TOutput): SafeSuccess { const tuple = [null, data, false, true] as SafeSuccess; @@ -57,11 +88,20 @@ function createSafeSuccess(data: TOutput): SafeSuccess { return tuple; } -function createSafeFailure(error: TError, isDefined: boolean): SafeFailure { - const tuple = [error, null, isDefined, false] as SafeFailure; - tuple.error = error; - tuple.data = null; - tuple.isDefined = isDefined; +function createSafeFailure(error: TError): SafeFailure { + if (isDefinedError(error)) { + const tuple = [error, undefined, true, false] as DefinedSafeFailure; + tuple.error = error; + tuple.data = undefined; + tuple.isDefined = true; + tuple.isSuccess = false; + return tuple; + } + + const tuple = [error, undefined, false, false] as NonDefinedSafeFailure; + tuple.error = error as Exclude; + tuple.data = undefined; + tuple.isDefined = false; tuple.isSuccess = false; return tuple; } @@ -72,7 +112,7 @@ function createSafeFailure(error: TError, isDefined: boolean): SafeFailu * @param error - Unknown thrown value. * @returns `true` when the error is an oRPC defined error. */ -export function isDefinedError(error: T): error is Extract { +export function isDefinedError(error: T): error is NarrowDefined { return orpcIsDefinedError(error); } @@ -83,10 +123,12 @@ export function isDefinedError(error: T): error is Extract { * @param promise - Promise to resolve safely. * @returns Safe tuple/object result. */ -export async function safe(promise: PromiseLike): Promise> { +export async function safe( + promise: ClientPromiseResult, +): Promise> { try { return createSafeSuccess(await promise); } catch (error) { - return createSafeFailure(error, isDefinedError(error)); + return createSafeFailure(error as TError); } } diff --git a/packages/sdk/src/ports/service-client.ts b/packages/sdk/src/ports/service-client.ts index 73e3bd36be..ff7c669a70 100644 --- a/packages/sdk/src/ports/service-client.ts +++ b/packages/sdk/src/ports/service-client.ts @@ -4,6 +4,9 @@ * @module */ +import type { ClientPromiseResult, ThrowableError } from '@orpc/client'; +import type { ErrorFromErrorMap, ErrorMap } from '@orpc/contract'; + /** * Minimal structural representation of a standard-schema-compatible type. */ @@ -162,13 +165,18 @@ export interface ServiceRequestOptions { context?: ServiceClientContext; } +type ProcedureErrorFromNode = TNode extends { + readonly '~orpc': { readonly errorMap: infer TErrorMap extends ErrorMap }; +} ? ErrorFromErrorMap + : ThrowableError; + /** * Typed service-client method derived from a contract procedure. */ -export type ServiceClientMethod = ( +export type ServiceClientMethod = ( input: TInput, options?: ServiceRequestOptions, -) => Promise; +) => ClientPromiseResult; /** * Compile-time marker that preserves the source contract for inference. @@ -182,8 +190,11 @@ export interface ServiceClientContract { * Recursive callable/router shape for a typed service client. */ export type ServiceClientShape = TContract extends - ContractProcedureLike - ? ServiceClientMethod, ProcedureOutputFromNode> + ContractProcedureLike ? ServiceClientMethod< + ProcedureInputFromNode, + ProcedureOutputFromNode, + ProcedureErrorFromNode + > : { [K in keyof TContract]: TContract[K] extends ContractLike ? ServiceClient : never; }; diff --git a/packages/sdk/tests/readme-doctest_test.ts b/packages/sdk/tests/readme-doctest_test.ts index 71ca21acc0..857db24037 100644 --- a/packages/sdk/tests/readme-doctest_test.ts +++ b/packages/sdk/tests/readme-doctest_test.ts @@ -1,4 +1,137 @@ -import { assertEquals } from './test-helpers.ts'; +import { + baseContract, + CursorPaginationInputSchema, + type NotFoundErrorSchema, + SuccessSchema, + type ValidationErrorSchema, +} from '@netscript/contracts'; +import { type ClientPromiseResult, ORPCError } from '@orpc/client'; +import { assert, assertEquals } from './test-helpers.ts'; +import { + type ContractSchemaOutput, + type DefinedError, + isDefinedError, + safe, + type SafeFailure, + type ServiceClient, +} from '../mod.ts'; + +type IsAny = 0 extends (1 & T) ? true : false; +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true + : false; +type Assert = T; + +type BaseErrorCode = keyof typeof baseContract['~orpc']['errorMap']; +type ExpectedBaseErrorCode = + | 'NOT_FOUND' + | 'VALIDATION_ERROR' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'RATE_LIMITED' + | 'SERVICE_UNAVAILABLE'; +type _BaseErrorCodesPreserved = Assert>; +type _UndeclaredBaseErrorRejected = Assert< + Equal, never> +>; + +type BaseMeta = typeof baseContract['~orpc']['meta']; +type _BaseMetaIsNotAny = Assert, false>>; +type _EmptyBaseMetaSlotPreserved = Assert>>; + +const typedErrorContract = { + list: baseContract + .route({ method: 'GET', path: '/typed-error-probe' }) + .input(CursorPaginationInputSchema) + .output(SuccessSchema), +}; + +declare const typedErrorClient: ServiceClient; + +async function assertRealExportErrorChannel(): Promise { + const discriminated = await safe(typedErrorClient.list({ limit: 10 })); + if (!discriminated.isSuccess && discriminated.isDefined) { + type _SafePreservesExactErrorCode = Assert< + Equal + >; + const preservedCode: ExpectedBaseErrorCode = discriminated.error.code; + void preservedCode; + + if (discriminated.error.code === 'NOT_FOUND') { + type _SafePreservesNotFoundData = Assert< + Equal< + typeof discriminated.error.data, + ContractSchemaOutput + > + >; + } + + if (discriminated.error.code === 'VALIDATION_ERROR') { + type _SafePreservesValidationData = Assert< + Equal< + typeof discriminated.error.data, + ContractSchemaOutput + > + >; + } + } + + const guarded = await safe(typedErrorClient.list({ limit: 10 })); + if (!guarded.isSuccess && isDefinedError(guarded.error)) { + type _GuardNarrowsToExactDefinedError = Assert< + Equal + >; + const definedError: DefinedError = guarded.error; + void definedError; + + if (guarded.error.code === 'NOT_FOUND') { + type _GuardPreservesNotFoundData = Assert< + Equal> + >; + } + } +} + +type _PlainErrorRemainsNonDefined = Assert< + Equal, { isDefined: false }>['error'], Error> +>; +type _PlainErrorRejectedFromDefinedArm = Assert< + Equal, { isDefined: true }>['error'], never> +>; + +void assertRealExportErrorChannel; + +Deno.test('safe preserves defined and non-defined failure identity', async () => { + const contractError = new ORPCError('NOT_FOUND', { + defined: true, + status: 404, + data: { resource: 'Order', id: 'ord_1' }, + }); + const contractFailure: ClientPromiseResult = Promise.reject( + contractError, + ); + const definedResult = await safe(contractFailure); + + assert(!definedResult.isSuccess, 'expected contract failure'); + assert(definedResult.isDefined, 'expected a defined contract failure'); + assertEquals(definedResult.error, contractError); + assertEquals(definedResult.data, undefined); + assertEquals(definedResult[0], contractError); + assertEquals(definedResult[1], undefined); + assertEquals(definedResult[2], true); + assertEquals(definedResult[3], false); + + const plainError = new Error('network unavailable'); + const plainResult = await safe(Promise.reject(plainError)); + + assert(!plainResult.isSuccess, 'expected plain failure'); + assert(!plainResult.isDefined, 'plain errors must not become defined errors'); + assertEquals(plainResult.error, plainError); + assertEquals(plainResult.data, undefined); + assertEquals(plainResult[0], plainError); + assertEquals(plainResult[1], undefined); + assertEquals(plainResult[2], false); + assertEquals(plainResult[3], false); +}); const README_PATH = new URL('../README.md', import.meta.url);