II.
Page overview
Reference · livepage:docs-specs-platform-gaps-phase0-effect-baseline
WP-T3-32 stage 1 record: Effect toolchain baseline (pin, patch port, register) overview
Inspect the raw attributes, linked wiki pages, and inbound or outbound graph edges for page:docs-specs-platform-gaps-phase0-effect-baseline.
Attributes
nodeKind
Page
sourcePath
docs/specs/platform-gaps/phase0/effect-baseline.md
sourceKind
repo-docs
title
WP-T3-32 stage 1 record: Effect toolchain baseline (pin, patch port, register)
displayName
WP-T3-32 stage 1 record: Effect toolchain baseline (pin, patch port, register)
slug
docs/specs/platform-gaps/phase0/effect-baseline
articlePath
wiki/docs/specs/platform-gaps/phase0/effect-baseline.md
article
# WP-T3-32 stage 1 record: Effect toolchain baseline (pin, patch port, register)
Executed 2026-09-28 on branch `phase0/hard-split-registry-guards` of `a5c-ai/fabric`. Spec: `docs/specs/platform-gaps/10-t3code-migration.md` "WP-T3-32" (stage 1: AC1, AC2, AC6; stage 2 out of scope), §2 B5 and the toolchain exclusion list; research `research/t3code-architecture.md` §15 and §18.12 (items 3, 12); intent `00-intent.md` D5, D15, D17–D21; roadmap `50-roadmap-workpackages.md` (WP-T3-32 row "0→2 (staged)", M0 exit criterion 5: "`npm ls effect` resolves a single `effect@4.0.0-rc.115` workspace-wide and the patch-regression test passes"). Builds on WP-RN-03/04 (registry + guards; the `test` job and the npm/Windows spawning conventions) and WP-RN-08. The public repository `a5c-ai/babysitter` was never written to (`gh api repos/a5c-ai/babysitter --jq .pushed_at` → `2026-09-16T19:59:50Z` at start and end; only GET calls were issued; `git remote -v` → `upstream no_push (push)`), nothing was published anywhere (no `npm publish`, no `npm pack`), no infrastructure changed.
Owner document: `docs/EFFECT-BASELINE.md`. Inventory: `t3code-patches-inventory.md` beside this file.
## What landed
| Item | Where |
|---|---|
| Workspace-wide pin `effect@4.0.0-rc.115`, `@effect/vitest@4.0.0-rc.115` | root `package.json` `overrides` + root `devDependencies` (both exact); `package-lock.json` resolves both |
| Patch mechanism | `patch-package 8.0.1` (root devDependency), `postinstall: patch-package --error-on-fail` |
| Ported patch | `patches/effect+4.0.0-rc.115.patch` (from t3code `patches/effect@4.0.0-rc.115.patch` at `de251fc2971a884cb5b1305ba4daf309dc8cccb0`); `patches/README.md` states the rules |
| Attribution (D15) | `third_party/t3code-patches/LICENSE` (verbatim upstream MIT, T3 Tools Inc.) + `PATCHES.md` (provenance), following the `third_party/webpackbar/` convention (`LICENSE` + `PATCHES.md`); the repository has no root THIRD-PARTY/NOTICE file |
| Tests | `scripts/ci/effect-baseline.test.mjs`, 13 cases, `npm run test:effect-baseline` |
| CI | `.github/workflows/ci.yml` `test` job, step "Effect baseline (single pin, patch regression, inventory)" right after `npm ci` (which runs the postinstall) — the dependency-free `publish-isolation` job cannot host it |
| Docs | `docs/EFFECT-BASELINE.md` (pin owner, provenance, `@effect/vitest` decision, unstable-namespace register with rc→stable criteria, Node floors as stage-2 input, upgrade playbook, stage-2 deferrals); `t3code-patches-inventory.md` |
Not landed (stage 2, depends on WP-T3-01): apps re-platform on stock Vite/vitest/tsc, `@voidzero-dev/*` grep gate, workspace `engines` ≥22.16 with the sub-floor CI run, the tracked-fork build matrix (AC3, AC4, AC5).
## Pin resolution
`npm install --legacy-peer-deps` (CI's flag; `@effect/vitest@4.0.0-rc.115` declares `peerDependencies.vitest ">=5.0.0 <6.0.0"` against fabric's `vitest 4.1.6`, so the flag is required, exactly as it is in every CI job) → `added 11 packages, removed 65 packages, changed 13 packages, and audited 3330 packages in 30s`, with the postinstall log:
```text
> babysitter@6.0.3 postinstall
> patch-package --error-on-fail
patch-package 8.0.1
Applying patches...
effect@4.0.0-rc.115 ✔
```
`npm ls effect --all`:
```text
babysitter@6.0.3 C:\work\fabric
├─┬ @effect/vitest@4.0.0-rc.115 overridden
│ └── effect@4.0.0-rc.115 deduped
└── effect@4.0.0-rc.115 overridden
```
`npm ls @effect/vitest --all` → `└── @effect/vitest@4.0.0-rc.115 overridden`. Before this WP, `npm ls effect --all` printed `(empty)` and `package-lock.json` had no `node_modules/effect` or `node_modules/@effect/*` entry, so the pin is the first Effect dependency in the tree (roadmap risk 1: "before any Effect code is imported").
**Lockfile note.** The `package-lock.json` diff removes 45 stale entries unrelated to this WP (`jsdom`, `postject`, `electron-winstaller`, `@anthropic-ai/sdk`, `temp`, …). Proven pre-existing: with this WP's `package.json` and lockfile stashed, `npm install --package-lock-only --legacy-peer-deps --ignore-scripts` on the pristine tree removes the same 45 entries (1,680 deletions). The committed lockfile was simply behind the manifests; `npm run guard:packages` (lockfile sync) passes after the change.
## Patch port
Conversion (scratch script, no hand edits): `diff --git a/<f> b/<f>` → `a/node_modules/effect/<f> b/node_modules/effect/<f>`, `--- a/<f>` → `--- a/node_modules/effect/<f>`, `+++ b/<f>` → `+++ b/node_modules/effect/<f>`; every other line untouched. Verified: `diff` of the two files with those three header kinds removed → identical (352 lines each). patch-package's parser (`patch-package/dist/patch/parse.js`) reads 5 file parts, 23 hunks: `dist/unstable/ai/McpServer.js` 2, `dist/unstable/http/HttpClientResponse.js` 1, `dist/unstable/rpc/RpcClient.d.ts` 2, `dist/unstable/rpc/RpcClient.js` 17, `src/unstable/http/HttpClientResponse.ts` 1. Applied on the first `npm install` with no rejected hunk (log above). Post-apply spot checks in `node_modules/effect`: `RequestHooks` ×3 in `RpcClient.js`, `getSetCookie?.() ?? []` in `HttpClientResponse.js:183` and `HttpClientResponse.ts:324`, `router.add("DELETE"` in `McpServer.js`, `missedPongs` in `RpcClient.js:744-753`. The `src/` hunk applies because the published `effect` tarball ships `src/` (checked, not assumed).
**`@effect/vitest` patch not ported — DROPPED-toolchain.** Its 7 hunks are all `"vitest"` → `"vite-plus/test"` in `dist/` and `src/`; under fabric's stock vitest that is the wrong direction and `vite-plus` is not installed. The unpatched package imports `vitest` (`node_modules/@effect/vitest/dist/index.js:1,6` → `from "vitest"`), which test 10 pins. The `vitest >=5 <6` peer range is recorded in `docs/EFFECT-BASELINE.md` §3 as a stage-2 input: the package is locked but not yet runnable as a harness here.
## Inventory counts
All 23 files of the snapshot's `patches/` directory read hunk by hunk; consumers derived from `grep -l "<pkg>" apps/*/package.json packages/*/package.json infra/*/package.json scripts/package.json package.json` in the snapshot. Result: **PORTED 1** (`effect`), **DROPPED-toolchain 1** (`@effect/vitest`), **DROPPED-mobile-only 17**, **DEFERRED-stage-2 4** (`@ff-labs/fff-node` → server / WP-T3-19; `dbus-next` → desktop / WP-T3-26; `@legendapp/list` and `@pierre/diffs` → declared by `t3:apps/web` as well as mobile / WP-T3-25). The brief's "19 mobile-only" is therefore **17**: `@legendapp/list` and `@pierre/diffs` are web consumers too (`t3:apps/web/package.json`), and their hunks touch web entrypoints (`react.js`, `react-native.web.*`; `dist/react/utils/useFileInstance.js`). Row 14 notes that fabric's own root carries `expo-notifications ^55` for the retired mobile shells; the upstream patch targets 57.0.15 and does not apply to that line.
## Tests (red, then green)
`npm run test:effect-baseline` → `# tests 13 / # pass 13 / # fail 0 / # duration_ms 5756`. Cases:
| # | Case | Kind |
|---|---|---|
| 1 | root manifest: one exact version in `overrides`, mirrored in `devDependencies`, `patch-package --error-on-fail` on postinstall, installed `effect` version equals the pin | manifest |
| 2 | no workspace manifest declares its own `effect`/`@effect/*` version (B5) | manifest (walks the root `workspaces` list) |
| 3 | `npm ls effect --all --json` → exactly one version == pin | singleton (AC1) |
| 4 | `npm ls @effect/vitest --all --json` → exactly one version == pin | singleton |
| 5 | `import('effect/unstable/rpc/RpcClient')` exports `RequestHooks` with key `effect/rpc/RpcClient/RequestHooks`; `.d.ts` declares it and its four hooks; control: `ConnectionHooks.key` | patch regression, observable (AC2) |
| 6 | `HttpClientResponse.fromWeb(req, { status, headers: {…} }).cookies` is empty instead of throwing; control: a real `Response` still parses `a=b`; `src/…/HttpClientResponse.ts` carries the fallback | patch regression, observable (AC2) |
| 7 | pinger: `missedPongs >= 3`, `onPingTimeout`, `makePinger(writePing, hooks)`; `.d.ts` `onPing/onPong/onPingTimeout` | patch regression, source-text fallback (internal to a live socket) |
| 8 | `McpServer.js`: `router.add("DELETE", options.path, …)`, session delete, and no DELETE in the method-not-allowed routes | patch regression, source-text fallback (needs an HttpRouter + RpcServer stack) |
| 9 | patch file parses to the 5 files / 23 hunks, only patch-package-named files in `patches/`, `patch-package --error-on-fail` exits 0 naming `effect@4.0.0-rc.115` | patch file |
| 10 | `@effect/vitest` installed at the pin, imports `vitest`, never `vite-plus`, no `@effect+vitest*.patch` | decision pin |
| 11 | inventory names all 23 upstream patch files with a decision each, the snapshot SHA, `Total 23`, `PORTED 1` | inventory |
| 12 | `docs/EFFECT-BASELINE.md` names `effect@4.0.0-rc.115`, "Pin owner", "fabric maintainers (Tal)", the five namespaces, "rc→stable", the upgrade playbook, the snapshot SHA | AC6 |
| 13 | `ci.yml` runs `npm run test:effect-baseline` after `npm ci --prefer-offline --legacy-peer-deps` | CI wiring |
Red proof (an upgrade that silently drops the patch): `node node_modules/patch-package/index.js --reverse` (→ `RequestHooks` count in `RpcClient.js` = 0), then the suite → `not ok 5`, `not ok 6`, `not ok 7`, `not ok 8`; `# pass 9 / # fail 4`. Case 9 stays green on that tree because it runs `patch-package --error-on-fail` itself and thereby re-applies the patch (count back to 3); cases 5–8, which run first, are the drop detectors, and case 9 turns red when the patch *file* is missing, misnamed for the resolved version, or no longer applies. Re-running the suite after the re-apply → `13 / 13 / 0`.
Also proven: `patch-package --error-on-fail` on an already-patched tree prints `effect@4.0.0-rc.115 ✔` and exits 0 (so the CI `npm install --no-save …linux optional deps` re-run of postinstall is harmless).
## Docs and guards
- `npx markdownlint-cli2 docs/EFFECT-BASELINE.md docs/specs/platform-gaps/phase0/t3code-patches-inventory.md patches/README.md third_party/t3code-patches/PATCHES.md` → `Summary: 0 error(s)`; `node scripts/docs-style-check.cjs` on the two docs → `issueCount: 0` each; `js-yaml` loads `ci.yml` (jobs `public-push-guard,publish-isolation,docs-quality,test,gateway-node-engine,workspace-coverage,observer-dashboard`; `test` steps `… / Install dependencies / Prepare artifact directories / Effect baseline (single pin, patch regression, inventory) / Lint`).
- `node scripts/ci/no-double-rename.cjs --self-test` → `repository scan — 59 manifest(s), 0 findings, 0 notice(s), 0 new unit(s)`, `10/10 assertions passed` (no new workspace was added: the pin lives in root `devDependencies`, so no `@fabricstack/*` name question arises).
- `node scripts/ci/publish-isolation-guard.cjs --self-test` → `self-test passed; repository scan clean (122 files, 0 findings, 8 legacy-train notice(s))`.
- `node scripts/ci/public-push-guard.cjs` → `scanned 215 files, 0 finding(s), 0 suppressed`.
- `npm run guard:packages` → `package-integrity guard passed (lockfile sync, native bindings, metadata, arch boundaries)`.
- `npm run test:publish-guards` → `17 / 17 / 0`; `node --test scripts/__tests__/publishable-packages.test.mjs` → `7 / 7 / 0`; `node scripts/ci/check-spec10-names.cjs --self-test` → passed.
## Judgement calls
1. Root `devDependencies` over a dedicated workspace: matches how the repository holds shared dev tooling, avoids inventing a non-spec unit name (WP-RN-08 gate), and still resolves the pin in the lockfile.
2. `--error-on-fail` on postinstall: a silently skipped patch is exactly the failure mode AC2 exists to prevent, so the install fails loudly; `npm install --ignore-scripts` bypasses it, which is why the regression test (not only the install) asserts the behaviour.
3. `@effect/vitest` pinned but unpatched (above). The alternative — carrying the vite-plus patch — would have been a faithful port of something that cannot load here.
4. The 17-vs-19 mobile-only count is reported as read, not as briefed.
5. Node floors are quoted and recorded, not enforced: enforcing ≥22.16 is stage 2 (AC5) and would otherwise change `engines` of packages the public train still publishes.
documents
[]
Outgoing edges
None.
Incoming edges
contains_page1
- page:docs·PageBabysitter Docs