Skip to content

Backmerge Zealot → template: candidates from Zealot main (week of 7/6), PR #1510, and open PRs #75

Description

@agreenspan

Tracking issue for the next backmerge wave from Zealot-Monorepo, same mechanic as PR #70 (lane/outbox hardening). Sourced from a 3-way scan on 2026-07-09: Zealot main commits since 7/2, Zealot PR #1510, and all open Zealot PRs, each diffed against this repo. (Replaces Linear ZLT-3378 — tracking lives here.)

Re-scanned 2026-07-21 (main 7/9→7/21, 50 commits; #1510 branch commits through 7/21, 50 commits; open-PR sweep + merge-gate statuses). Deltas tagged (7/21). Merge gates lifted: Zealot #1610, #1611, #1661, #1663, #1682 all merged — the affected items below now port straight from Zealot main.

Wave 1 — standalone, no dependencies (all S)

✅ All shipped in PR #81 (2026-07-21).

  • clientIp trusted-hop util (Zealot #1611 / ZLT-3195 — merged 2026-07-10, 5d50d941): our middleware/rateLimit.ts trusts x-forwarded-for[0] — spoofable; any client mints a fresh identity per request and bypasses IP-keyed limits. Port apps/api/src/lib/clientIp.ts + test (rightmost-trusted-hop XFF, IPv6 /64 bucketing) and swap into apiRateLimit/rateLimit. Security fix — port first regardless of the rest.
  • incrementFixedWindows Lua limiter (Zealot #1611 — merged, same sha): atomic INCR+PEXPIRE returning [count, pttl]. Our limiter has the crash-strands-TTL-less-key bug (incr then separate expire). Upgrade standalone even without MCP.
  • isUniqueConstraintError (from the ZLT-3008 squash, not covered by PR feat(jobs): harden supersede lanes — TTL refresh + fenced releaseLane #70): duck-typed P2002 narrowing; we re-declare the inline check in errorHandlerMiddleware.ts and hooks/contactRules/hook.ts.
  • (7/21) isWriteConflictError (Zealot #1510, 656cb…): duck-typed P2034 (deadlock/serialization victim) narrowing — first-create races treat "deadlock victim, winner committed" like P2002 (re-read the winner). Util only; its consumer is product code. Pairs naturally with the item above; applies to Postgres (P2034 covers deadlock + serialization failure).
  • (7/21) unregisterDbHook(name) (Zealot #1510, 480fb4a): targeted hook deregistration (registrations map keyed by name; unregister filters the handler out of each slot). Our packages/db/src/extensions/hookRegistry.ts only has clearHookRegistry() (nukes production hooks — unusable mid-suite), so a leaked test hook is unremovable across bun's shared process. Registry shape matches — near-verbatim drop-in.
  • withRetry (packages/shared/src/utils/retry.ts from Zealot PR #1510): generic exponential backoff + isRetryable matcher; we have no retry util. #1510 still open (merges ~early Aug); the file is stable since extraction (ceba2a64, unchanged through 7/21) — safe to cherry-pick the two files now.
  • ws/pubsub.ts publish-always-through-Redis (Zealot #1682 — merged 2026-07-11, 96163d3e): our publish() falls back to local delivery when pubsub is uninitialized, and jobs/worker.ts never calls initWebSocketPubSub() — a job handler emitting a websocket handoff would silently drop it. Latent today; port Zealot's worker-safe publish path. Re-verify against 0f32ac81 (ws pubsub init-after-subscribe fix) first.

Wave 2 — CI rules (scripts/ci/rules/** is protected, so explicit user-driven change)

✅ All shipped in PR #81 (2026-07-21) — user-authorized ("do all of this"). no-direct-prisma-in-tests landed diff-mode (~70 legacy sites converge as files are touched; packages/db/src/test/** exempt).

  • no-select-on-mutations (Zealot #1610 — merged 2026-07-11, e49f5b7c): CI rule + mutationLifeCycle seam throw + pass/fail fixtures. Our mutationLifeCycle.ts has no select/omit guard. Biggest CI-rule candidate (M with the seam throw).
  • (7/21) env-override test seam + no-test-env-mutation rule (Zealot #1510, 0ae3928f — M, strongest new item): setEnvOverride/resetEnvOverrides/withEnv/getEnv layered on the zod-parsed env (reads resolve override ?? real), global afterEach(resetEnvOverrides) backstop, plus a CI rule + fixtures banning env.X = …/process.env.X = … in tests — the process-global leak class in bun's single-process suite. We commit this sin today: apps/api/src/jobs/outbox/drain/tests/drain.test.ts (8 sites), apps/api/src/jobs/handlers/tests/sendWebhook.test.ts:28, packages/db/src/lib/encryption/helpers.test.ts:14; no-raw-global-assign-in-tests.sh doesn't catch env mutation. Our env.ts has the identical zod shape, so the seam drops in; port cost includes converting the ~10 violating call sites.
  • no-raw-transaction: bans raw $transaction in favor of db.txn() (raw bypasses the AsyncLocalStorage store mutationLifeCycle reads). We have the same primitives, no rule.
  • no-direct-bulk-writes: bans createMany/updateMany (lifecycle hooks never fire) in favor of *AndReturn. Verify our mutationLifeCycle handles the AndReturn variants first.
  • no-direct-prisma-in-tests: factories-only in tests; we have the factories. Adjust the @zealot/db-test path.
  • Better no-prisma-maps: Zealot's awk block-parser allows enum-value @map (legacy stored strings) while banning model-level @map/@@map; ours is a blunt grep that false-positives on enum maps. Take Zealot's version.

Wave 3 — needs design input before porting

  • MCP delivery slice (Zealot #1611 / ZLT-3195 — merged 2026-07-10; follow-on #1626/ZLT-3254 826d0367 merged 2026-07-21, M–L): apps/api/src/modules/mcp/ (OpenAPI→tools agent surface, in-process dispatch via app.fetch) + serializeBracketQuery move packages/uipackages/shared (prereq: apps/api can't import packages/ui). Design blocker: we have no AuthTier / x-auth-tier extensions — route templates use buildTags({ admin, internal }) — so the agent-surface filter needs a template-native tier story, and Token has no scopes field (scope-less v1 or schema add). Also take the small operationId-override fix (our read.ts spreads ...args before operationId:, clobbering overrides). (7/21) the slice grew: acting-principal context getters (getActingBrandUserUuid — session brand user, else token's configured actor; getActingPrincipalLabel⚠️ exists to feed legacy string audit columns, which collides with our no-attribution-columns doctrine; our shape = API-token actor feeds the audit actor context, which the audit workstream partially covers); the tool-exposure guard test pattern (backfillToolExposure.test.ts — mount real route aggregators, run the production derivation per scope, pin the expected tool-name set); ApiToken factory note (unique on (brandUuid, name) → derive factory name from key prefix). Consider splitting to its own issue when picked up.
  • Audit-log deltas (Zealot PR #1663 — merged 2026-07-13): parameterized ignoreFields/redactFields registries and shared hooks/shared/hookRows.ts. Taken over by the observe-channel workstream (template hookRows already pushed there) — coordinate before touching; do NOT port Zealot's sync fire-and-forget AppEvent persist (we route observe through BullMQ; CLAUDE.md §6 — divergence is a decision).
  • AI-review pipeline skeleton (Zealot #1664/#1676 — both merged 2026-07-09, L): engine + eval harness + parseUnifiedDiff + 3 workflows + KB-drift gate are nearly Zealot-free; the knowledge base / .opencode agents / corpus are not. Port = engine + workflows + template-authored taxonomy and fresh docs; start with an empty corpus. Decide if/when worth it.
  • (7/21) check-lockfile.sh (Zealot #1780, open): self-contained ~77-line lockfile-drift gate (bun install --dry-run against the pinned bun version) — the concrete answer to the ecosystem-wide stale-lockfile epidemic. We have no .github/workflows/ by doctrine (deploy-via-init; CI is a parked want), so this sits in the parked-CI bucket: take the script whenever CI unparks.
  • silent-success-failures skill (Zealot #1457, S): the 8 failure modes are fully generic; every example is Zealot. Port the skeleton, rewrite examples against template models.
  • runJobInlineForTest chain serialization (Zealot #1679, S, optional): AsyncLocalStorage 1-concurrency chaining so concurrent test enqueues run sequentially without nested-enqueue self-deadlock. Only needed if tests hit fan-out races.

Email/COMM mirror track (carry-at-port items, not this issue's waves)

Rides the COMM tickets, listed here so they're not lost: #1657 composition contract + #1658 MJML slot grammar (both merged); (7/21) #1698 regions/parseBlocks/authoring (open, large — extension of the same track); (7/21) #1655 system.* reserved tokens (~40 lines engine: system.now/system.year resolver allowlist, non-overridable, BCP-47 locale in InterpolateOptions); #1689 {{#each}} loop fixes per the 07-12 review.

Reverse direction (template → Zealot, tracked here so it isn't lost)

  • (7/21) Zealot's merged #1661 interpolate lacks our prototype-pollution guard: bare lodash get with a segment regex that admits __proto__/constructor, and no function-value skip (functions render as source text). Our packages/email/src/render/interpolate.ts has UNSAFE_PATH_SEGMENTS + the function skip. Open Zealot #1655 touches the same file but guards only its new system.* prefix. Fix shape: extend the guard to all prefixes — best delivered as a review comment on #1655.

Scanned and ruled out (don't re-scan)

  • Zealot #1684 lens deletedAt: null injection — we landed first (3125e22f) with the fuller at-every-visit shape; Zealot cites it.
  • Zealot #1679 inline enqueue — was Zealot catching up to us ("template parity"); already here.
  • Zealot #1621 visit-wheres lens traversal — template→Zealot lineage; we're ahead.
  • Zealot #1661 email dotted paths — our interpolate.ts already does lodash-get traversal AND carries the UNSAFE_PATH_SEGMENTS guard. Reverse-check done 7/21: Zealot's merged version LACKS the guard — see reverse-direction section above.
  • Zealot #1510 product mass (Opportunity, BrandReferenceRequest v2, manualEngagements retirement) — product-only; generic residue extracted: withRetry, isWriteConflictError, unregisterDbHook, env-override seam (all tracked above).
  • ZLT-3008 squash — lanes/outbox/supersede already backported via PR feat(jobs): harden supersede lanes — TTL refresh + fenced releaseLane #70; MJML email rides the COMM track.
  • Zealot #1640 sendmessage throttle — all under apps/lambdas, no counterpart here. #1668 — product schema. #1680 chat, #1655 admin UI (engine hunk tracked in email section), Devin/Macroscope/dependabot PRs — product/deps.
  • Zealot #1681 collation + migration-collation/no-foreign-keys rules — MySQL-specific; we're Postgres. CI fixture pattern already identical.
  • Worktree scripts — parallel forks; our slot system is ahead. (7/21) confirmed again: Zealot a584db6e docker-PATH fix is the parallel workstream landing on Zealot main; our create.sh/destroy.sh already carry the Postgres-adapted version.
  • perBrandFlag/posthogClient — feature-flag provider adapter is a design decision, flagged not ported. (7/21) same ruling covers #1510's useFeatureFlag defaultValue enhancement.
  • (7/21) Zealot c051230e typecheck plumbing — we don't share the broken pattern: apps/api/tsconfig.json already maps bare @template/db, every workspace has typecheck, root aggregates via bun run '--filter=*' typecheck.
  • (7/21) Zealot 4de700cd PDF-iframe sanitizer — no UGC-HTML surface exists here (zero DOMPurify/dangerouslySetInnerHTML hits); remember the pattern (host allowlist + forced sandbox via ADD_TAGS hook) if we ever grow rich text. Related landmine if dompurify is ever added: Zealot pinned 3.3.1 because later versions sanitize silently wrong under happy-dom.
  • (7/21) Zealot e161b457 webhook-breaker integration exemption — keys on integrationSourceUuid (ownership concept we don't have) + their metrics counter; our sendWebhook.ts breaker is otherwise the same. Product-conditioned.
  • (7/21) Zealot 52f4d311/5eb29e12/479e9ab/566e328c (db.parallel, afterCommit drain, false-polymorphic AuditLog) — all template→Zealot ports; our versions are the origin and richer. 5208a4a5 delegate-caching fix — lives in their emulated createMany/updateMany paths; we ban those and resolve delegates fresh per call.
  • (7/21) Zealot fetchAllPages — we don't have the base util; adopting it is a from-scratch decision, not a backmerge. If ever adopted, carry #1510's maxRows/onCapped delta (d1502fd7).
  • (7/21) main window 7/9→7/21 otherwise — 40+ product commits (advocacy/hub/rewards/missions/DM/SSO/survey/SFDC), prod one-off scripts, Slack env gating, Qdrant/Gong optional init: no template surface.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions