Implement whatever the decision card settled, so that dispatchEventToEngine resolves the real subscriber set when called from an ordinary Next.js request path — and add the guard that fails if it ever stops doing so.
Do not start from the fix. The claim this card is built on is that engineSubscribers(name) returns [] on a request path; reproduce that before changing anything, so the guard you write is known to fail against origin/main rather than assumed to.
The cheapest reproduction is the guard itself (below) written first: it fails on the unfixed tree and passes after. If it passes before the fix, the premise is wrong — stop and report rather than proceeding, because everything downstream in this story assumes the defect is real.
Per the ADR amendment. The recommendation there is a data-only manifest: the emit path reads { id, trigger, cron, maxAttempts } and never the handler, so the module it imports pulls in no services and closes no import cycle. dispatchEventToEngine reads only sub.id and sub.maxAttempts today, and hasInngestSubscribers only id and trigger — verified on origin/main@b944dab5 — so no emit-path caller loses anything.
Whatever the mechanism, two properties must hold and both are worth stating as invariants rather than as steps:
defineJob is the single choke point every job passes through, and registerEngineJob / registerSchedule both already register from inside it for exactly that reason. A manifest assembled by hand is the second list lib/jobs/engine/registry.ts's own header refuses; do not introduce one.scripts/worker.ts executes handlers and must go on importing @/lib/jobs/registry for its side effect. Its comment explains why that import looks unused — keep it, and update it if this card changes what it guarantees.ENDS at: the emit path resolving subscribers correctly, with the guard, in this repo, on this PR.
Does NOT change: any file under lib/jobs/definitions/ (no job's behaviour moves), the routing set's semantics in lib/jobs/engine/cutover.ts, or lib/jobs/schedules.ts — the schedule registry has the identical defect and belongs to MOTIR-3416; the ADR amendment states the answer for it so that story inherits it rather than re-deriving it. If the chosen mechanism happens to fix both at no extra cost, take it and say so in the PR body.
Does NOT move any job onto the engine. MOTIR_POSTGRES_JOB_IDS is untouched by this PR; this card makes the switch WORK, it does not throw it.
tests/jobs/engine-subscriber-reachability.test.ts — asserts the emit path's subscriber set equals the full registry's, by building the two sets from DIFFERENT module graphs: inside vi.resetModules(), import ONLY @/lib/jobs/sendEvent and read engineSubscribers from the same fresh graph; then reset again, import @/lib/jobs/registry, and read it again. The two sets must be equal. Deriving both from one import is the failure mode this test exists to catch, so it cannot be written that way.import '@/lib/jobs/registry', and a comment says why — the poisoning import at tests/jobs/engine-dispatcher.test.ts:24 is precisely what has kept this defect invisible.work-item/transitioned resolves four subscribers from the emit-path graph.lib/jobs/definitions/ is modified, and no job's observable behaviour changes.pnpm lint and the typecheck pass, with no new ESLint disable and no widening of the JOB_ENGINE_RESTRICTION import boundary.lib/jobs/engine/registry.ts — registerEngineJob, engineSubscribers, and the header on import-completenesslib/jobs/engine/dispatcher.ts — the two emit-path readers and the fields they actually uselib/jobs/sendEvent.ts — the emit pathlib/jobs/defineJob.ts — the choke point, and registerSchedule beside itscripts/worker.ts — the load-bearing side-effect importtests/jobs/engine-dispatcher.test.ts — the import that makes the existing suite greeneslint.config.mjs — JOB_ENGINE_RESTRICTION, the boundary a new module must stay inside