Commit Graph

50 Commits

Author SHA1 Message Date
f3182537c1 fix(boundaries): allow feature-to-feature imports of public exports
turbo.json's boundary config already allows `feature -> feature`, and
the cross-feature event system depends on it — a consumer must import
the publisher's event contract from `@repo/<publisher>`. But the ESLint
boundaries config, ADR-010, and AGENTS.md still declared
`feature -> [core, tooling]`, contradicting turbo.json and the shipped
code (marketing-pages imports @repo/auth).

Align all three to turbo.json: a feature may import another feature's
published public exports. Internals stay sealed by the `exports` map,
and cross-feature behaviour still flows through IEventBus.
2026-05-21 14:30:37 +02:00
d3944f40db feat(core-eslint): add entity-must-have-test and no-relative-parent-import rules
Two CLAUDE.md conventions had no mechanical gate, so both drifted:
entity models shipped without sibling tests, and feature test files
imported src modules via `../` instead of the `@/` alias.

- `entity-must-have-test` — every entities/models/<x>.ts needs a sibling
  <x>.test.ts (errors and barrels excluded).
- `no-relative-parent-import-in-tests` — feature test files must import
  src via `@/`, not `../`. Scoped to feature packages; core packages are
  governed by their own generator templates.

Both register at warn level, bringing the conformance rule count to 15.
2026-05-21 11:49:45 +02:00
c099d7182b fix(core-eslint): resolve event descriptors in no-undeclared-event-publish
The rule only matched bus.publish("string-literal", ...), but the
canonical pattern that `gen event` prescribes is
bus.publish(eventDescriptor, payload) — an imported identifier, never a
literal. The rule therefore never fired on real code, which is how the
auth signUp publish drifted from its manifest undetected.

Add `_event-ast.js`: resolves a `bus.publish(<identifier>)` argument by
following the import to the event-contract file and extracting the name
from either `defineEvent("...", schema)` or an inline `{ name }` object.
Unresolvable arguments are skipped, so the rule never false-positives.
2026-05-21 11:49:36 +02:00
b61bb0c11e feat(auth): add signIn rate-limit backfill with dual ip/account budgets
Wires the rate-limit primitive end-to-end through auth.signIn as the
canonical credential-stuffing defence example:

- manifest: rateLimit [ip 5/1m, account 10/1h] on signIn use case
- use case: rateLimit: IRateLimit dep; dual consume + TooManyRequestsError
- binders: ctx.rateLimit ?? new NoopRateLimit() in bind-production + bind-dev-seed
- tRPC: TooManyRequestsError → TOO_MANY_REQUESTS error code in authProcedure
- tests: RecordingRateLimit dual-consume assertion; InMemoryRateLimit
  budget-1 ip + account rejection; coverage 100% on use-cases layer
- ESLint: _manifest-ast.js extractRateLimitNames handles RateLimitBudget
  objects ({name,window,budget}) in addition to plain string literals,
  no-undeclared-rate-limit passes on both "ip" and "account" call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:22:41 +00:00
18cef2f889 feat(core-eslint): add no-undeclared-rate-limit conformance rule
Adds the `no-undeclared-rate-limit` ESLint rule (warn severity) that
enforces rate-limit drift at lint time:

- Warns when rateLimit.consume("X", _) is called inside a use-case but
  "X" is absent from manifest.useCases[name].rateLimit
- Warns when a declared rateLimit budget has no matching consume call
  in the use-case body (unusedDeclaration)
- Is a no-op outside use-case files

Extends _manifest-ast.js to extract the rateLimit[] field from both
parseManifestUseCases and parseManifestFully. Updates _manifest-ast
tests to include the new field in expected shapes. Registers the rule
at warn severity in plugin.js and base.js. Adds RuleTester fixtures
for all four cases (declared+matching, undeclared, unused, non-use-case).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 08:43:30 +00:00
1cab88916a feat(core-eslint): add no-undeclared-consent-check rule (conformance gate 12)
Extends the conformance ESLint layer with the consent-check rule:

- `no-undeclared-consent-check` (warn): `consent.isGranted("X")` in a
  use-case file must match a category declared in `manifest.requiresConsent`;
  also warns when requiresConsent is declared but no isGranted call is found.
- `_manifest-ast.js`: adds `parseManifestFully` which extracts top-level
  `name`, `requiredCores`, `requiresConsent`, and per-use-case maps from the
  manifest AST; `requiresConsent` extraction tested in `_manifest-ast.test.js`.
- `_rule-context.js` / `_rule-schema.js`: shared helpers extracted from the
  existing per-rule files so the new rule can resolve use-case name + feature
  root without duplication.
- Existing rules (`no-undeclared-audit`, `no-undeclared-event-publish`,
  `no-undeclared-analytics-event`) updated to use the shared helpers.
- `plugin.js` + `base.js` register the rule at warn severity.
- CLAUDE.md + conformance-quickref.md: rule count advanced from 11 → 12.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:51:30 +00:00
1eb32ab23b feat(core-eslint): add pii-declaration-must-be-complete rule
Adds the `conformance/pii-declaration-must-be-complete` ESLint rule at
warn severity. The rule detects `custom: { pii: { ... } }` blocks in
Payload config files and warns when any of the four required sub-fields
(`category`, `purpose`, `exportable`, `restrictable`) is missing.

Incomplete PII declarations can produce incorrect audit reports —
sub-second editor feedback catches the gap before it reaches
compliance/data-map.yml.

- Rule + 7 RuleTester fixtures (complete passes, each missing field
  warns, non-pii custom block is no-op, malformed custom.pii is no-op)
- Registered in plugin.js + base.js at "warn"
- Conformance rule count bumped 7 → 8 in CLAUDE.md +
  conformance-quickref.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:33:48 +00:00
395143466c feat(core-eslint): add no-undeclared-analytics-event ESLint rule
Adds the conformance/no-undeclared-analytics-event rule at warn severity,
mirroring no-undeclared-audit and no-undeclared-event-publish. The rule
cross-checks analytics.track("X", ...) literal slugs in *.use-case.ts
files against manifest.useCases[name].analyticsEvents, providing
sub-second editor feedback before boot-time conformance fires.

- Extends _manifest-ast.js to parse analyticsEvents arrays in both
  extractUseCaseEntry helpers
- Registers rule in plugin.js and base.js at ["warn", { repoRoot }]
- RuleTester fixtures: declared pass, undeclared warn, non-literal no-op,
  non-use-case file no-op, missing manifest entry no-op

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 15:43:37 +00:00
8cb531e0cd feat(core-eslint): add usecase-must-be-wired conformance rule
Catches manifest use cases that aren't wired through wireUseCase(...) in
bind-production.ts / bind-dev-seed.ts. wireUseCase is the canonical helper
that attaches __instrumented / __captured / __audited brands — skipping
it produces an unbranded binding that assertFeatureConformance would
reject at boot. This rule shifts that detection from ~3s (boot) to <1s
(lint), keeping the layered conformance pattern: TS brands (compile),
ESLint (lint), boot assertion (dev), smoke tests (CI).

CLAUDE.md + conformance-quickref.md updated for the new rule (5 → 6).
2026-05-18 11:03:17 +02:00
2edc76002a refactor(docs): strip residual Phase/Plan setup-history references
Final sweep for setup-process bookkeeping not caught by template-reset-v1.
ADRs drop Plan-N qualifiers; spec collapses the historical 11-phase
migration table; scaffolding guide drops "Phase added" column; comment
prefixes referencing R-numbers in test describes / eslint inline comments
are normalized. Architecture-level rule IDs (R40, R52, E0, J0, etc.) are
preserved where they serve as stable cross-references in ADRs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 10:28:31 +02:00
69d84a598d feat(core-eslint): wire frontend conformance rules into plugin + base
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:41:24 +02:00
159db9e542 feat(core-eslint): atomic-tier-import-direction rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:41:01 +02:00
f933ca74ff feat(core-eslint): component-must-have-test rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:40:40 +02:00
9ef545b714 feat(core-eslint): component-must-have-story rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 07:40:19 +02:00
171ed20527 refactor(core-eslint): readManifestSource delegates to AST parser 2026-05-13 07:34:00 +02:00
d83d97755e feat(core-eslint): parseManifestFully — AST-based full manifest extraction 2026-05-13 07:33:41 +02:00
79f96a94a1 feat(core-eslint): flip feature-must-have-manifest from warn to error 2026-05-13 00:10:48 +02:00
4d7b25fd15 feat(core-eslint): wire AST-aware conformance rules into plugin + base
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:52:31 +02:00
15e90820d2 feat(core-eslint): no-undeclared-audit rule
AST-aware ESLint rule that catches auditLog.record({ type: "X" }) calls
in use-case files where X is not declared in the matching
manifest.useCases[name].audits array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:51:42 +02:00
3e2b096c3c feat(core-eslint): no-undeclared-event-publish rule
AST-aware ESLint rule that catches bus.publish("X") calls in use-case
files where X is not declared in the matching manifest.useCases[name].publishes array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:51:06 +02:00
21a6822d55 feat(core-eslint): use-case name helper (file slug → manifest key) 2026-05-12 23:49:32 +02:00
7cfb78b99e feat(core-eslint): manifest AST parser for per-use-case attributes 2026-05-12 23:49:12 +02:00
b9ac64b36b feat(core-eslint): register conformance plugin + structural rules in base config 2026-05-12 23:24:01 +02:00
8a2c8db955 feat(core-eslint): conformance plugin module + ./plugin export 2026-05-12 23:23:20 +02:00
3117df1c14 feat(core-eslint): required-cores-installed rule 2026-05-12 23:22:43 +02:00
d585b59590 feat(core-eslint): usecase-must-have-test-file rule 2026-05-12 23:21:38 +02:00
b7bb37023f feat(core-eslint): feature-must-have-manifest rule
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:20:37 +02:00
6f8e75accb feat(core-eslint): workspace helper for conformance rules 2026-05-12 23:19:11 +02:00
41ffe9f787 feat(core-eslint): manifest source helper for conformance rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 23:18:02 +02:00
bd95315a44 refactor(core-eslint): finalize OTel + Sentry import allowlist (R40 + R52) 2026-05-11 12:12:57 +02:00
53bfe727b4 refactor(core-shared): rename bindSentryInstrumentation → bindOtelInstrumentation
git mv bind-sentry-instrumentation → bind-otel-instrumentation; function
renamed; new impl calls initOtelServerNode and binds OtelTracer (SentryLogger
kept for Phase 3). Barrel re-exports both new name and deprecated alias
bindSentryInstrumentation for one release. ESLint allowlist updated to the
new filename. init-server-node opts gains release? field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 11:42:07 +02:00
f94f09a9fa feat(core-shared): OTel barrel + ESLint allowlist for SDK packages 2026-05-11 11:25:29 +02:00
a9f27f0d7e refactor: remove core-events from main (scaffoldable via gen core-package events)
- Delete packages/core-events/ (15 files)
- Strip @repo/core-events from all 5 feature package.json + apps/web-next/package.json
- Strip @repo/core-events from apps/web-next/next.config.mjs transpilePackages
- Strip E1 + J no-restricted-syntax blocks from core-eslint/base.js (anchor remains)
- Update bind-production.ts: drop bus construction + IEventBus import; rename
  resolveEventsAndJobsProduction → resolveJobsProduction (queue only),
  resolveEventsAndJobsDevSeed → resolveJobsDevSeed (queue only);
  ctx no longer has bus field; BindProductionContext generic arg narrowed
- Update bind-production.test.ts: assert ctx.bus is undefined, drop
  PayloadJobsEventBus/InMemoryEventBus instanceof checks
- Update sign-up-welcome-email.test.ts: assert mailer stays empty without bus
- Inline userSignedUpEvent in auth (drop defineEvent import from core-events)
- Drop InMemoryEventBus fallback from auth/di/module.ts

Feature binders' bus?.subscribe/publish calls remain as no-ops. Scaffold
@repo/core-events back via: pnpm turbo gen core-package events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:00:35 +02:00
2266429efd feat(core-eslint): anchor for events rules splice
Add // <gen:events-rules> anchor to base.js near the E1 + J
no-restricted-syntax blocks, and assert it in anchors.test.js.
After core-events removal the anchor remains as the splice point
for pnpm turbo gen core-package events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 13:52:23 +02:00
57b2ff5191 refactor: remove core-realtime from main (scaffoldable via gen core-package realtime) 2026-05-09 13:45:52 +02:00
c4035e912e feat(core-eslint): anchors for realtime rules splice 2026-05-09 13:28:35 +02:00
6a0ac63bb9 fix(realtime): CI gate fixes surfaced by Phase 11 smoke tests
- no-direct-socket-io: extend allowlist to cover apps/*/src/**/*.test.ts so
  the realtime-ping e2e integration test can import socket.io/socket.io-client
  directly; add two valid test cases to keep the rule's own test suite green
- tsconfig.json (root): add root-level tsconfig with experimentalDecorators +
  emitDecoratorMetadata and no "include" so tsx 4.21.0's createFilesMatcher
  resolves decorator config for all workspace packages, not just web-next's
  own source tree
- web-next dev script: pass TSX_TSCONFIG_PATH=../../tsconfig.json so the
  custom Node server uses the root tsconfig for all modules it loads
- next.config.mjs: add @repo/core-events and @repo/core-realtime to
  transpilePackages so Next.js webpack can resolve their workspace source files
- server.ts: replace static authContainer import with a dynamic import inside
  IRealtimeAuthenticator.authenticate so Inversify decorators are applied only
  after bindAll() has already populated the container

All CI gates pass: lint (0 errors), typecheck, 20 tests (incl. realtime-ping
e2e), boundaries (0 issues).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 01:32:43 +02:00
c882614153 test(core-eslint): extend anchor-presence guard for realtime anchors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:07:21 +02:00
122a81b2ca feat(core-eslint): rule no-realtime-handler-reexport
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:01:03 +02:00
77467de702 feat(core-eslint): rule no-direct-socket-io
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:59:52 +02:00
1a0e3c20ff chore(core-realtime): align scaffold style with core-events (review polish) 2026-05-08 21:09:50 +02:00
fd76aa445d chore(core-eslint,turbo): tag @repo/core-realtime as core
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:59:01 +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
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
955a763c66 feat(eslint+ci): R40 boundary rule for @sentry/* + R31 sendDefaultPii grep gate
Adds two flat-config blocks to core-eslint/base.js: (1) repo-wide
no-restricted-imports for @sentry/* with the R40 message, (2) an
allowlist override for the only paths permitted to import the Sentry
SDK directly — core-shared/instrumentation/sentry/**, the bind-sentry
DI files, the no-sentry test guards, and apps' instrumentation* /
next.config / vite.config / sentry.*.config files. Patterns use
**/-prefix so they match whether ESLint runs from the repo root or
from inside a sub-package.

Also adds the standard `argsIgnorePattern: "^_"` config (used
throughout the repo) and a Node-globals override for *.mjs/*.cjs/*.js
and *.config.{ts,tsx} so withSentryConfig in next.config.mjs lints
clean. Required adding `globals` as a core-eslint dep.

Adds .github/workflows/sentry-pii-guard.yml — a lightweight CI step
that fails any PR introducing `sendDefaultPii: true` (R31). Excludes
node_modules / dist / .next / .turbo from the grep so vendored SDK
JSDoc examples don't false-positive.

Pre-existing lint nits cleared as part of getting `pnpm lint` green:
- core-testing define-contract-suite.test.ts: void the unused
  receivedTracer (mirrors the next test's pattern)
- marketing-pages bind-dev-seed.ts: drop unused MockSiteSettingsRepository
  import
- marketing-pages get-site-settings.use-case.ts: drop the now-redundant
  eslint-disable for `_input`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:31:15 +02:00
4ca083690f feat(core-testing): scaffold shared testing utilities package
Adds @repo/core-testing (tag: tooling) with:
- factory/defineFactory: monotonic-sequence object factories with overrides
- contract/defineContractSuite: shared test suites runnable against multiple impls
- react/renderWithProviders + createMockTrpcClient: RTL helpers
- payload/stubPayloadConfig + mockPayloadModule: Payload mocking helpers
- setup/{jsdom,node}: vitest setup files

Spec: docs/superpowers/specs/2026-05-05-tdd-foundation-design.md §5

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:37:35 +02:00
2c2375920f docs: reflect tooling-package rename + Turbo boundaries enforcement
- Rename eslint-config → core-eslint, typescript-config → core-typescript
  in all docs (package map, AGENTS.md, overview.md, dependency-flow.md, etc.)
- Document the five-tag model (app, feature, core, core-composition,
  tooling) — refinement of ADR-006's three-tag mention
- Document core-trpc's core-composition tag (transitively reaches features
  through core-api's AppRouter type)
- Note Turborepo boundaries as a second enforcement layer alongside ESLint
- Add ADR-010 explaining the two-layer enforcement decision and
  five-tag refinement

Files updated:
- docs/architecture/overview.md: package map, enforcement layers, five-tag section
- docs/architecture/dependency-flow.md: boundary rules, enforcement strategy
- docs/architecture/vertical-feature-spec.md: package names, five-tag model
- AGENTS.md: package map, boundary rules, commands
- CLAUDE.md: tooling package names, quick-start command
- packages/core-eslint/AGENTS.md: tag clarification
- packages/core-typescript/AGENTS.md: tag clarification
- packages/core-api/AGENTS.md: core-composition tag
- packages/core-cms/AGENTS.md: core-composition tag
- packages/core-trpc/AGENTS.md: core-composition tag + rationale
- docs/decisions/adr-010-turbo-boundaries.md: new ADR

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:24:04 +02:00
eb55a698f3 fix(turbo-boundaries): resolve 32 violations
- Re-tag core-trpc as core-composition (transitively reaches features
  through core-api's AppRouter type) and allow core-composition packages
  to depend on other core-composition packages in turbo.json
- Add @payloadcms/richtext-lexical as direct dep of apps/cms (was pulled
  transitively via core-cms; Payload's importMap.js imports it directly)
- Add @storybook/react as devDep of core-ui and apps/storybook (stories
  belong with components per Atomic Design)
- Update apps/storybook .storybook/{main,preview}.ts to point at
  packages/core-ui (was still referencing deleted packages/ui) and export
  CSS via package exports instead of direct file import
- Add JSDoc type annotation to Payload-generated importMap.js to suppress
  TypeScript's TS2742 error from complex inferred types
- Add storybook-static to eslint ignores in core-eslint base config

pnpm turbo boundaries now reports zero violations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:12:27 +02:00
9899e213ca build(turbo): add boundaries enforcement matching ESLint dependency graph
Per-package tags + root boundaries block now enforce the same three-tag
model as eslint-plugin-boundaries:
- app          → may depend on: app, core, core-composition, feature, tooling
- feature      → may depend on: core, tooling
- core         → may depend on: core, core-composition, tooling
- core-composition (core-api, core-cms) → may depend on: core, feature, tooling
- tooling (core-eslint, core-typescript) → may depend on: tooling

Tags applied: 4 apps (app), 3 core foundation packages (core),
2 core composition packages (core-composition), 5 features (feature),
2 tooling packages (tooling). All test suites pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:59:44 +02:00
0972645ebb refactor: rename eslint-config + typescript-config to core-eslint + core-typescript
Aligns tooling packages with the core-* naming convention used by all
other foundation packages (core-shared, core-cms, core-api, core-trpc,
core-ui). Updates ~50 files: package.json names, devDependencies,
tsconfig extends, eslint.config imports, vitest.config imports, AGENTS.md
references, and the boundaries plugin patterns to match the new paths.

The tooling-specific patterns in boundaries/elements are now ordered BEFORE
the broader core-* pattern to ensure correct first-match-wins behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:37:07 +02:00