page:docs-specs-platform-gaps-phase0-effect-baseline
WP-T3-32 stage 1 record: Effect toolchain baseline (pin, patch port, register) reference
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 .pushedat → 2026-09-16T19:59:50Z at start and end; only GET calls were issued; git remote -v → upstream nopush (push)), nothing was published anywhere (no npm publish, no npm pack), no infrastructure changed.
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:
> 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:
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 overriddennpm 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.cjson the two docs →issueCount: 0each;js-yamlloadsci.yml(jobspublic-push-guard,publish-isolation,docs-quality,test,gateway-node-engine,workspace-coverage,observer-dashboard;teststeps… / 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 rootdevDependencies, 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.