Commit Graph

370 Commits

Author SHA1 Message Date
82002a0ee8 docs(plan): realtime layer implementation plan (45 tasks, 11 phases) 2026-05-08 20:31:26 +02:00
500d835f0e docs(spec): realtime layer (Socket.IO) design
Long-form design for the bidirectional realtime layer the user
brainstormed on 2026-05-08. Builds on ADR-015 (events and jobs);
hybrid pattern where IRealtimeBroadcaster is layered alongside
IEventBus, with a bridge for forwarding allowlisted bus events to
realtime channels.

v1 ships: @repo/core-realtime package, custom Node server in
apps/web-next, cookie-session auth, four scope kinds, two
generators (gen realtime channel|handler), three new anchors per
feature, RecordingRealtimeBroadcaster test helper, realtime-ping
proof-of-life. Dashboard, DB-backed roles/permissions, multi-instance
fanout, and a generic core-package generator are deferred — captured
in §13 / §14 / §15.

Companion HTML explainer at realtime-auth-explainer.html (project root,
uncommitted) walks the four-checkpoint auth model visually. ADR-016
will be created during implementation.
2026-05-08 19:51:06 +02:00
69c445de26 docs: refresh explainers + adding-a-feature with full binder signature
di-explainer and data-flow-explainer narratives still showed the
pre-ADR-014 1-arg bindProductionBlog(config) form. Updated both
explainers to (config, tracer, logger, bus, queue), and adjusted the
narrative arc so the production-swap step mentions the
resolveEventsAndJobs* preamble.

adding-a-feature's Step 16 sketch was the same 1-arg shape; replaced
with the canonical 5-arg signature and a short note that the bus/queue
params are accept-and-forward until gen event consume / gen job
generators inject usage at the anchors. Pointer to a real feature's
bind-production.ts for the complete reference.

Final sweep is clean — no stale (container, config) or 1-arg
bindProduction signatures remain in docs/.
2026-05-08 17:54:11 +02:00
5a2234f7ad docs: refresh per-package AGENTS.md + di-explainer to current binders
Six per-package AGENTS.md tables and the di-explainer wiring trace all
showed pre-ADR-014 binder shapes. Refreshed to the post-ADR-015
reality:

- Per-package "Public exports" tables now show the
  (config, tracer, logger, bus, queue) production signature and the
  (tracer, logger, bus, queue) dev-seed companion. auth and
  marketing-pages also list their newly-public ./di/container,
  ./di/symbols, and (marketing-pages only) ./services/mailer +
  ./services/recording-mailer subpaths.
- di-explainer's wiring trace adds the resolveEventsAndJobs* step in
  bindAll(), the (config, tracer, logger, bus, queue) binder
  signature, and the // <gen:event-handlers> / // <gen:jobs>
  injection sites.

No code change; pre-existing AGENTS.md staleness predates ADR-014 +
ADR-015 — this commit catches both up.
2026-05-08 17:51:35 +02:00
9e8ff6411c docs: align spec + dependency-flow with ADR-015
Three follow-up corrections from the docs audit:

- vertical-feature-spec § 13 dropped the events/jobs bullet from the
  "Out of scope (deferred)" list — they shipped, the bullet didn't
  belong there. The spec already mentions ADR-015 elsewhere (§ 4
  optional folders).

- vertical-feature-spec § 14 success criterion replaced "Any
  cross-feature import fails pnpm lint" with the post-ADR-015 reality:
  cross-feature imports are restricted to event contracts (the feature
  boundary tag now accepts feature-tagged deps), but rule E1
  (no-handler-reexport) keeps consumer handlers / use cases /
  repositories private.

- dependency-flow now shows the resolveEventsAndJobs* step in bindAll
  and the (config, tracer, logger, bus, queue) binder signature, plus
  the // <gen:event-handlers> / // <gen:jobs> anchor lines where the
  generators inject subscribe / register calls.
2026-05-08 17:45:24 +02:00
31fc667c97 Merge branch 'feat/events-and-jobs': cross-feature events and background jobs
Implements ADR-015 in 52 commits. Highlights:

- New @repo/core-events package: IEventBus, defineEvent + EventDescriptor,
  InMemoryEventBus, PayloadJobsEventBus.
- New @repo/core-shared/jobs subpath: IJobQueue, InMemoryJobQueue (with
  register), PayloadJobQueue.
- Recording test helpers (RecordingEventBus, RecordingJobQueue) in
  @repo/core-testing/instrumentation, using local-type-alias pattern to
  avoid build-graph cycles.
- ESLint rules E1 (no-handler-reexport) and J (no-direct-payload-jobs).
- Feature binders accept (bus, queue); apps/web-next bindAll resolves
  Payload-backed in production and in-memory in dev-seed.
- Three Plop generators: gen event publish, gen event consume, gen job.
  Each one threads through fixed // <gen:*> anchor comments enforced by
  a CI guard.
- Proof-of-life flow: signing up in @repo/auth publishes
  userSignedUpEvent on the shared bus; @repo/marketing-pages subscribes
  and enqueues a send-welcome-email job; the in-memory queue dispatches
  it to the wrapped job which records on RecordingMailerService. e2e
  test at apps/web-next/src/__tests__/sign-up-welcome-email.test.ts.
- Documentation: ADR-015, docs/guides/events-and-jobs.md, AGENTS.md +
  CLAUDE.md + scaffolding-guide updates, vertical-feature-spec deferred
  lines retired.

Workspace green at the merge boundary: typecheck (15/15), lint (16/16),
test (17/17), turbo boundaries (clean).
2026-05-08 17:26:21 +02:00
a98e41d080 chore: workspace green check (Task 56)
Three issues uncovered by the full pnpm typecheck/test/boundaries pass
and resolved here:

- core-testing was importing IEventBus / IJobQueue from core-events /
  core-shared, creating two boundary violations (tooling → core) and a
  build-graph cycle. Inlined the type aliases (mirroring how
  RecordingTracer / RecordingLogger handle ITracer / ILogger).
  recording-event-bus.test.ts replaces defineEvent() with an inline
  descriptor literal so no runtime import is needed either. core-events
  and core-shared are removed from core-testing dependencies.

- turbo.json: typecheck and test no longer dependsOn ^typecheck / ^build.
  Each package's tsc / vitest resolves cross-package types via
  node_modules independently, and dropping the topological dep avoids the
  spurious cycle warning that appeared once core-testing started
  importing core-events / core-shared.

- turbo.json: feature.dependencies.allow gains "feature". Cross-feature
  event flow (ADR-015) requires a consumer feature to import the
  publisher's event contract directly. The dangerous form (importing
  the publisher's handler/use-case/repo) is still blocked by E1's
  no-handler-reexport ESLint rule and the missing public exports.

- TaskConfig<"slug-string"> → TaskConfig<{ input; output }> in the gen
  job task template (and the shipped send-welcome-email.task.ts) since
  runtime-generated slugs aren't keys of TypedJobs['tasks'].
2026-05-08 17:21:47 +02:00
b3a2afec72 docs(spec): events/jobs no longer deferred (ADR-015) 2026-05-08 17:08:38 +02:00
1a9cd25746 docs(scaffolding): event/job generators reference 2026-05-08 17:08:08 +02:00
3a3b66f4ab docs(claude): generators + events/jobs conventions 2026-05-08 17:07:24 +02:00
ec11bfc77a docs(agents): events-and-jobs section + guide reference 2026-05-08 17:06:48 +02:00
f775725cb2 docs(guide): events-and-jobs walkthrough 2026-05-08 17:06:08 +02:00
c531b81544 docs(adr-015): cross-feature events and background jobs
Also folds in the spec correction noted in the plan's known follow-up
\#1: subscribe takes consumerFeature: string as a second arg between
descriptor and handler. § 3.3 and § 5.4 updated.
2026-05-08 17:05:05 +02:00
04899de98c test(web-next): e2e cross-feature sign-up→welcome-email flow
End-to-end proof-of-life. bindAllDevSeed wires InMemoryEventBus +
InMemoryJobQueue across all features. signUpController publishes
auth.user.signed-up; marketing-pages' handler enqueues
marketing-pages.send-welcome-email; the in-memory queue dispatches
the wrapped job which records on RecordingMailerService.

Adds ./di/container, ./di/symbols, ./services/mailer, and
./services/recording-mailer to auth + marketing-pages exports so
the e2e test can resolve from the per-feature containers without
deep-importing.
2026-05-08 16:37:37 +02:00
7e844c646d feat(marketing-pages): sendWelcomeEmail job + handler enqueues it
sendWelcomeEmailJob takes IMailerService, validates the typed input
(userId + email), and delegates to mailer.sendWelcome. The
onAuthUserSignedUpHandler now takes IJobQueue and enqueues
"marketing-pages.send-welcome-email" with the event payload.

Both binders bind a RecordingMailerService at the IMailerService
symbol (production placeholder until a real adapter ships) and pass
mailer / queue into the wrapped factories. Dev-seed additionally
queue.register()s the slug so the in-memory queue dispatches via
the wrapped job; production relies on the generated Payload event-task
to resolve the wrapped handler from the container.
2026-05-08 16:35:45 +02:00
6b57a34c0c feat(marketing-pages): subscribe to auth.user.signed-up
Generated handler + Payload event-task via gen event consume,
threaded through symbols / both binders / cms re-export at the
configured anchors. bus.subscribe wires the in-memory delivery in
dev-seed; the __events.auth.user.signed-up.marketing-pages Payload
task closes the production-bus loop.

Also fixes two generator-level issues found during Phase 8:
- Drop publisher prompt's `when` clause so --args can supply the
  4th positional argument (Plop limitation: --args cannot bypass
  conditional prompts). Validate runs only in consume mode.
- Switch event-task.ts.hbs from TaskConfig<"slug-string"> to
  TaskConfig<{ input: ...; output: object }> since runtime-generated
  event slugs are not keys of TypedJobs['tasks'].
2026-05-08 16:33:14 +02:00
c1b8a7e434 feat(auth): signUp publishes userSignedUpEvent
signUpUseCase now takes an IEventBus and publishes userSignedUpEvent
after creating the user (synthesizing email from username since auth
is username-based). Use case mocks-default in module.ts get a fresh
InMemoryEventBus per resolution; bind-production / bind-dev-seed wire
the shared bus passed by bindAll. Tests updated to inject
RecordingEventBus, including a new test that asserts publish on
success and silence on failure.
2026-05-08 16:28:21 +02:00
7f63adf740 feat(auth): userSignedUpEvent contract 2026-05-08 16:24:12 +02:00
1c13b757ed feat(marketing-pages): RecordingMailerService 2026-05-08 16:23:32 +02:00
73f6db54b3 feat(marketing-pages): IMailerService interface (proof-of-life) 2026-05-08 16:23:00 +02:00
d5f230e07f feat(turbo-gen): job generator
Adds `pnpm turbo gen job <feature> <slug> <void|typed>` which scaffolds
the factory + test + Payload TaskConfig, then modifies the feature's
job-symbol, job-bind, and cms job-task anchors to wire the wrapped
job into the per-feature container. Also registers a custom Handlebars
`eq` helper used by the void/typed branch in the job templates.
2026-05-08 16:20:33 +02:00
8e081c2e97 feat(turbo-gen): templates for gen job 2026-05-08 16:18:50 +02:00
31f825a5d7 feat(turbo-gen): wire gen event consume actions
Adds consumeActions for the event generator: scaffolds the handler file
+ test, the Payload event-task that completes the production-bus loop,
modifies the consumer feature's symbols (// <gen:event-handler-symbols>),
both binders (// <gen:event-handlers>), and the cms re-export
(// <gen:job-tasks>). The bind block wraps the handler in span+capture,
binds it into the per-feature container, and subscribes it on the bus.
2026-05-08 16:18:12 +02:00
d53869f712 feat(turbo-gen): templates for gen event consume (handler + Payload task) 2026-05-08 16:16:35 +02:00
5cc6099015 feat(turbo-gen): event generator (publish mode)
Wires `pnpm turbo gen event` with publish/consume mode prompt; publish
branch generates the contract file + test and threads it through the
feature's // <gen:events> anchor. Consume branch is a stub that throws
until Task 40 lands the handler-side templates and modify-blocks.
2026-05-08 16:16:04 +02:00
0801469e89 feat(turbo-gen): templates for gen event publish 2026-05-08 16:13:07 +02:00
cf71547223 feat(turbo-gen): assertAnchors helper 2026-05-08 16:12:52 +02:00
d800d98574 feat(web-next): bindAll resolves IEventBus + IJobQueue per env
resolveEventsAndJobsProduction wires PayloadJobsEventBus + PayloadJobQueue
against the bootstrapped Payload instance; resolveEventsAndJobsDevSeed
wires InMemoryEventBus + InMemoryJobQueue. Each per-feature binder now
receives (bus, queue) so Phase 7 generators can subscribe handlers and
register job tasks at the <gen:event-handlers>/<gen:jobs> anchors.
2026-05-08 16:09:57 +02:00
2b4e576b0d feat(navigation): bind binders accept (bus, queue) params 2026-05-08 16:07:05 +02:00
4e77134ff6 feat(marketing-pages): bind binders accept (bus, queue) params 2026-05-08 16:05:49 +02:00
a9100de052 feat(media): bind binders accept (bus, queue) params 2026-05-08 16:04:37 +02:00
d6fe37450a feat(blog): bind binders accept (bus, queue) params 2026-05-08 15:17:40 +02:00
b0bd4c0bb3 feat(auth): bind binders accept (bus, queue) params 2026-05-08 15:16:16 +02:00
721d373856 chore(generators): add // <gen:*> anchor comments for event/job generators 2026-05-08 14:09:05 +02:00
8b3b118bb0 test(core-eslint): anchor-presence CI guard for // <gen:*> comments
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:49:35 +02:00
bd1610a888 chore(navigation): add // <gen:*> anchor comments for event/job generators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:48:24 +02:00
3c0eb34f5e chore(marketing-pages): add // <gen:*> anchor comments for event/job generators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:47:40 +02:00
b672506c30 chore(media): add // <gen:*> anchor comments for event/job generators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:47:04 +02:00
2506c2676b chore(blog): add // <gen:*> anchor comments for event/job generators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:46:31 +02:00
73bfb22fe2 chore(auth): add // <gen:*> anchor comments for event/job generators
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:23:52 +02:00
5b766242be feat(core-eslint): rules E1 (no handler re-exports) and J (no direct payload.jobs)
Implements spec § 7 via no-restricted-syntax built-in selectors — mirrors
the R40 no-restricted-imports precedent; no custom plugin required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:18:14 +02:00
1357e45f55 feat(core-testing): RecordingEventBus
Adds RecordingEventBus implementing IEventBus for use in unit tests.
Validates payloads via the descriptor schema, records all publish calls,
and delivers events to subscribed handlers synchronously in subscription order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:05:34 +02:00
590b92f190 feat(core-testing): RecordingJobQueue
Adds RecordingJobQueue implementing IJobQueue for use in unit tests.
Records all enqueue calls with taskSlug/input/options and returns
synthetic incrementing jobIds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 12:04:22 +02:00
05f0b02856 docs(core-events): clarify subscribe consumerFeature + failFast drop
Code-quality review nits: IEventBus.subscribe docstring said "debug
tag" but InMemoryEventBus actually ignores the param entirely. Also
add a comment to InMemoryEventBus.publish noting that under failFast
only the first rejection rethrows; other failures are intentionally
dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:02:45 +02:00
430c831743 feat(core-events): public barrel
Export EventDescriptor, defineEvent, IEventBus, EventHandler,
CORE_EVENTS_SYMBOLS, InMemoryEventBus, InMemoryEventBusOptions, and
PayloadJobsEventBus from src/index.ts. All typecheck, test (11 tests),
and lint checks pass. turbo boundaries: 454 files, no issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:58:07 +02:00
ef451de437 feat(core-events): PayloadJobsEventBus (fan-out via IJobQueue)
TDD: red (test-only), then green. PayloadJobsEventBus validates before
enqueueing, names tasks __events.<event>.<consumer> deterministically, and
enqueues one task per subscriber via Promise.all. 3 new tests (11 total).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:57:48 +02:00
cb785b600a feat(core-events): InMemoryEventBus with failFast option
TDD: red (test-only), then green. InMemoryEventBus validates payloads via
descriptor.schema before fanout, delivers to all handlers via Promise.allSettled,
swallows handler errors by default, and rethrows first error when failFast:true.
5 new tests (8 total passing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:57:19 +02:00
3da93efb89 feat(core-events): IEventBus interface + symbol registry
IEventBus defines publish<T> and subscribe<T> with consumerFeature parameter.
CORE_EVENTS_SYMBOLS.IEventBus uses Symbol.for for cross-module identity.
Typecheck passes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:56:52 +02:00
ba3b6c9af6 feat(core-events): EventDescriptor + defineEvent
TDD: red (test-only), then green. defineEvent(name, schema) returns a typed
EventDescriptor<TName, TSchema>. Also added @repo/core-testing to devDeps
(required for nodeVitestConfig's setupFiles). 3 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:56:37 +02:00
f99a307f72 chore(core-eslint,turbo): tag @repo/core-events as core
The existing turbo.json boundaries use tag names (not package names), and
the core-eslint boundaries config uses the packages/core-* glob. Both already
cover @repo/core-events through its turbo.json "tags": ["core"] declaration.
pnpm turbo boundaries confirms: 445 files in 18 packages, no issues found.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 11:55:44 +02:00