Skip to content

Backmerge wave: Zealot → template (#75 Waves 1–2) - #81

Merged
agreenspan merged 15 commits into
mainfrom
backport/zealot-wave-2026-07-21
Sep 17, 2026
Merged

agreenspan merged 15 commits into
mainfrom
backport/zealot-wave-2026-07-21

Conversation

@agreenspan

@agreenspan agreenspan commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Executes the Wave 1 + Wave 2 items from #75 (the Zealot→template backmerge tracker), same mechanic as #70. One commit per item, then a merge of today's main and a scope cut per review on 2026-09-17.

Wave 1 — standalone

  • isUniqueConstraintError + isWriteConflictError (7d3c6b86): duck-typed P2002/P2034 narrowing in packages/db/src/utils/; error middleware uses both (P2002 → 409 exists, P2034 → 409 write conflict, P2025 → 404).
  • withRetry (2b55363c, cherry-picked from Zealot #1510): exponential backoff with isRetryable matcher, packages/shared/src/utils/retry.ts + tests.
  • unregisterDbHook (a4811ef5, Zealot #1510): registrations recorded by name so a single hook can be removed — previously the only removal API was clearHookRegistry(), which nukes production hooks, so a test-registered hook leaked across bun's single-process suite unremovably. Coexists with main's registerDbInvariant/unregisterDbInvariant and bridged stores. +3 registry tests.
  • ws pubsub publish-always-through-Redis (fd22c5cc, Zealot #1682): publish() gated on pubsubEnabled, which only the API server entry ever set — a job handler emitting a websocket handoff delivered into the worker's empty local registry and silently vanished. Publish now always goes through Redis; subscribing remains the socket-holding process's job. Includes Zealot's round-trip proof test.
    • Test-seam fix this surfaced: in tests, getRedisSub() returned the same shared RedisMock instance as the general client, so the first subscribe put the whole suite's Redis into subscriber mode. The test subscriber is now a duplicate() of the shared mock.

Wave 2 — CI rules (scripts/ci/rules/**)

  • no-select-on-mutations (Zealot #1610): brace-aware bun detector + rule (diff-mode vs base branch) + fixtures. A narrowed write result breaks the after-write hooks. The sweep fixed two real write sites: createToken omitted keyHash at the write, and sendEmail's communicationLog.createManyAndReturn selected {id, idempotencyKey}. Both now take the full row and shape afterwards. Runtime guard dropped per review — this is a code-shape rule and the CI script is the enforcement.
  • env-override test seam + no-test-env-mutation (2c69d03b, Zealot #1510): reshaped for this repo — here process.env is the config (env.ts replaces it with the parsed schema), so the registry lives in @template/shared/utils (setEnvOverride / resetEnvOverrides / withEnv) and the proxy wraps process.env itself: installed by env.ts in test mode for apps/api, and by a root bunfig preload for package suites. Global afterEach backstops clear overrides between tests. All mutating test files converted, including main's newer redis/client.test.ts and the mid-drain shutdown test. The CI rule bans env.X = … / process.env.X = … in tests across apps + packages.
  • no-raw-transaction, no-direct-bulk-writes, no-direct-prisma-in-tests, awk no-prisma-maps (30ddd73e):
    • raw $transaction banned outside packages/db/src/client.ts. Repo scan: clean.
    • createMany/updateMany banned in favor of the hook-firing *AndReturn variants. Repo scan: clean.
    • factories-only in tests, diff-mode: legacy direct-create sites converge as files get touched. packages/db/src/test/** exempt. New jobOutboxFactory + drain.test.ts converted.
    • no-prisma-maps upgraded to Zealot's model-block awk parser: enum-value @map allowed, field/table maps still banned.

Moved out (2026-09-17 review)

  • Rate limitingclientIp trusted-hop util and the incrementFixedWindows Lua window are pulled from this PR and land with the full port of Zealot's rule-composition limiter (identity + context buckets, Retry-After, fail-open) in a dedicated PR. middleware/rateLimit.ts is back to main's state here.
  • The email test files that predated main's data.* rule vocabulary were dropped in the merge; main's versions stand.

Validation (after merging main at 4da32f00)

  • typecheck green on every workspace (sdk needs generate:sdk in a fresh worktree — staleness, unrelated)
  • bun run test: permissions 81, db 305, email 474, api 1102 — 0 fail
  • all scripts/ci/rules/*.sh pass on the merged tree
  • biome: no findings on touched files

Closes the Wave 1 + Wave 2 checklists on #75.

🤖 Generated with Claude Code

agreenspan and others added 11 commits July 21, 2026 15:16
…able x-forwarded-for[0]

Port of Zealot clientIp (ZLT-3195 / Zealot #1611): rightmost-trusted-hop
XFF parsing with IPv6 /64 bucketing, replacing the client-supplied
x-forwarded-for[0] that let any caller mint a fresh identity per request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port of Zealot incrementFixedWindows (Zealot #1611): a crash between the
separate incr and expire calls could strand a TTL-less key that wedges an
identity over-limit forever. Restructures middleware/rateLimit.ts into a
directory (no consumers yet, zero import churn) and returns [count, pttl]
per window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backport from Zealot (ZLT-3008 squash + #1510): shared duck-typed P2002 and
P2034 narrowing so insert-race call sites stop re-declaring the inline code
check. Error middleware now uses isUniqueConstraintError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cherry-pick of packages/shared/src/utils/retry.ts from Zealot #1510
(stable since extraction): attempts/baseDelayMs/isRetryable matcher.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backport from Zealot #1510 (480fb4adc): a test-registered hook leaked across
bun's single-process file boundary and the only removal API was
clearHookRegistry(), which nukes production hooks too. Registrations are now
recorded by name so unregisterDbHook(name) can filter just that handler out
of each slot; clearHookRegistry also clears the registration map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…API sockets

Backport from Zealot #1682: publish() gated on pubsubEnabled, which is only
set by initWebSocketPubSub() — called solely by the API server entry. A job
handler emitting a websocket handoff delivered into the worker process's
empty local registry and silently dropped. Publish now always goes through
Redis (local delivery only as the Redis-failure fallback); subscribing stays
the socket-holding process's job.

Test seam: getRedisSub() in tests now duplicate()s the shared mock — a
subscribed connection can only run subscriber commands, so sharing one mock
instance between sub and general commands poisoned the whole suite.

Includes Zealot's round-trip proof test: publish → Redis → subscriber →
registered socket, plus wrong-channel isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backport of Zealot #1610 (ZLT-3167): a write that narrows its result with
select/omit hands the after-write lifecycle hooks a partial row — a hidden
changed column reads as a no-op, or the key-deriving field is stripped.

- assertNoResultNarrowing at the six result-returning write seams
- scripts/ci/rules/no-select-on-mutations.sh + brace-aware bun detector
  (lib/noSelectOnMutations.ts) + pass/fail fixtures; diff-mode vs base branch
- repo scanned with the detector: no pre-existing violations

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…row caught

createToken omitted keyHash at the write and sendEmail's communicationLog
createManyAndReturn selected {id, idempotencyKey} — both now take the full
row and shape afterwards. Found at runtime by assertNoResultNarrowing; the
source detector missed them in the earlier sweep because the file list came
from a proxied find. Re-swept with git ls-files: clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backport of Zealot #1510 (0ae3928f), reshaped for this repo: Zealot proxies
its exported env object; here process.env IS the config (env.ts replaces it
with the parsed schema), so the registry lives in @template/shared/utils and
the proxy wraps process.env itself — installed by env.ts in test mode for
apps/api and by a root/bunfig preload for package suites.

- setEnvOverride / resetEnvOverrides / withEnv / wrapEnvWithOverrides
- afterEach backstop in apps/api globalSetup + scripts/testEnvOverrides.ts
  (packages/email gets its own bunfig — bun test --cwd skips the root one)
- no-test-env-mutation.sh + fixtures: bans env.X = / process.env.X = in
  tests across apps + packages
- converted every mutating test: drain (8 sites), sendWebhook, encryption
  helpers + validation, email evaluateConditions + interpolate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ct prisma in tests, awk no-prisma-maps

Backport from Zealot main:
- no-raw-transaction: bans .$transaction( outside packages/db/src/client.ts
  (raw txn bypasses db.txn's AsyncLocalStorage store, so mutationLifeCycle
  reissues each write into its own separate transaction)
- no-direct-bulk-writes: bans createMany/updateMany in favor of the
  hook-firing *AndReturn variants; diff-mode vs base branch
- no-direct-prisma-in-tests: factories-only in tests, diff-mode so the ~70
  pre-existing sites converge as files are touched; packages/db/src/test/**
  exempt (db-layer seam tests). New jobOutboxFactory + drain.test.ts
  converted (this PR touches it, so diff-mode applies to it)
- no-prisma-maps: Zealot's model-block awk parser replaces the blunt grep —
  enum-value @Map (legacy stored strings) now allowed, field/table maps
  still banned; new pass/enum-map fixture

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…thout assign-in-expression)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
agreenspan and others added 3 commits September 17, 2026 16:09
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…redis url test on the env override seam

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ate limiting to its own PR

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@agreenspan
agreenspan merged commit 0428687 into main Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant