From b8d4cfe90790fdd93be545d38e6f25a03a61bd66 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 18 May 2026 12:46:25 +0200 Subject: [PATCH] chore(work): decompose product-analytics-channel epic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated 1 epic + 9 stories under docs/work/epics/ from the approved PRD docs/work/prds/product-analytics-channel.prd.md. Stories follow the sequencing hints from the PRD (brand+wrapper → manifest+wireUseCase → assertFeatureConformance → BindContext+ESLint → React provider → docs). Ready for `pnpm work dispatch --execute` to begin implementation. --- docs/work/_system/_state.json | 177 +++++++++++++++++- .../_story.md | 52 +++++ .../02-recording-analytics/_story.md | 42 +++++ .../_story.md | 46 +++++ .../_story.md | 50 +++++ .../_story.md | 45 +++++ .../_story.md | 42 +++++ .../_story.md | 43 +++++ .../08-react-provider/_story.md | 43 +++++ .../09-documentation/_story.md | 51 +++++ .../epics/product-analytics-channel/_epic.md | 30 +++ 11 files changed, 618 insertions(+), 3 deletions(-) create mode 100644 docs/work/epics/product-analytics-channel/01-scaffold-core-analytics-package/_story.md create mode 100644 docs/work/epics/product-analytics-channel/02-recording-analytics/_story.md create mode 100644 docs/work/epics/product-analytics-channel/03-analyzed-brand-and-with-analytics-wrapper/_story.md create mode 100644 docs/work/epics/product-analytics-channel/04-manifest-schema-and-wire-use-case/_story.md create mode 100644 docs/work/epics/product-analytics-channel/05-assert-feature-conformance-analyzed/_story.md create mode 100644 docs/work/epics/product-analytics-channel/06-analytics-protocol-bind-context/_story.md create mode 100644 docs/work/epics/product-analytics-channel/07-eslint-rule-no-undeclared-analytics-event/_story.md create mode 100644 docs/work/epics/product-analytics-channel/08-react-provider/_story.md create mode 100644 docs/work/epics/product-analytics-channel/09-documentation/_story.md create mode 100644 docs/work/epics/product-analytics-channel/_epic.md diff --git a/docs/work/_system/_state.json b/docs/work/_system/_state.json index 26441f5..f098e08 100644 --- a/docs/work/_system/_state.json +++ b/docs/work/_system/_state.json @@ -1,5 +1,5 @@ { - "updated_at": "2026-05-18T10:35:02.002Z", + "updated_at": "2026-05-18T10:46:27.408Z", "epics": { "binder-wrap-helper": { "status": "done", @@ -240,9 +240,180 @@ "blocks": [] } } + }, + "product-analytics-channel": { + "status": "in-progress", + "title": "Product analytics as a fourth capture channel (ADR-024 implementation)", + "prd": "docs/work/prds/product-analytics-channel.prd.md", + "stories": { + "01-scaffold-core-analytics-package": { + "status": "in-progress", + "title": "Scaffold @repo/core-analytics package with IAnalytics and NoopAnalytics", + "ac_total": 2, + "ac_completed": 0, + "depends_on": [], + "blocks": [] + }, + "02-recording-analytics": { + "status": "todo", + "title": "Add RecordingAnalytics to @repo/core-testing", + "ac_total": 1, + "ac_completed": 0, + "depends_on": [ + "01-scaffold-core-analytics-package" + ], + "blocks": [ + "08-react-provider" + ] + }, + "03-analyzed-brand-and-with-analytics-wrapper": { + "status": "todo", + "title": "Add Analyzed brand and withAnalytics wrapper", + "ac_total": 2, + "ac_completed": 0, + "depends_on": [ + "01-scaffold-core-analytics-package" + ], + "blocks": [ + "04-manifest-schema-and-wire-use-case" + ] + }, + "04-manifest-schema-and-wire-use-case": { + "status": "todo", + "title": "Extend manifest schema and wireUseCase with analyticsEvents", + "ac_total": 2, + "ac_completed": 0, + "depends_on": [ + "03-analyzed-brand-and-with-analytics-wrapper" + ], + "blocks": [] + }, + "05-assert-feature-conformance-analyzed": { + "status": "todo", + "title": "Extend assertFeatureConformance to check Analyzed brand", + "ac_total": 1, + "ac_completed": 0, + "depends_on": [ + "04-manifest-schema-and-wire-use-case" + ], + "blocks": [] + }, + "06-analytics-protocol-bind-context": { + "status": "todo", + "title": "Add AnalyticsProtocol and BindContext.analytics", + "ac_total": 1, + "ac_completed": 0, + "depends_on": [ + "05-assert-feature-conformance-analyzed" + ], + "blocks": [ + "09-documentation" + ] + }, + "07-eslint-rule-no-undeclared-analytics-event": { + "status": "todo", + "title": "Add no-undeclared-analytics-event ESLint rule to @repo/core-eslint", + "ac_total": 1, + "ac_completed": 0, + "depends_on": [ + "05-assert-feature-conformance-analyzed" + ], + "blocks": [ + "09-documentation" + ] + }, + "08-react-provider": { + "status": "todo", + "title": "Add React provider to @repo/core-analytics", + "ac_total": 1, + "ac_completed": 0, + "depends_on": [ + "02-recording-analytics" + ], + "blocks": [ + "09-documentation" + ] + }, + "09-documentation": { + "status": "todo", + "title": "Documentation — analytics.md, conformance-quickref, CLAUDE.md, template-tiers", + "ac_total": 2, + "ac_completed": 0, + "depends_on": [], + "blocks": [] + } + } } }, - "ready": [], - "blocked": [], + "ready": [ + { + "epic": "product-analytics-channel", + "story": "01-scaffold-core-analytics-package", + "title": "Scaffold @repo/core-analytics package with IAnalytics and NoopAnalytics" + }, + { + "epic": "product-analytics-channel", + "story": "09-documentation", + "title": "Documentation — analytics.md, conformance-quickref, CLAUDE.md, template-tiers" + } + ], + "blocked": [ + { + "epic": "product-analytics-channel", + "story": "02-recording-analytics", + "title": "Add RecordingAnalytics to @repo/core-testing", + "waiting_on": [ + "product-analytics-channel/01-scaffold-core-analytics-package" + ] + }, + { + "epic": "product-analytics-channel", + "story": "03-analyzed-brand-and-with-analytics-wrapper", + "title": "Add Analyzed brand and withAnalytics wrapper", + "waiting_on": [ + "product-analytics-channel/01-scaffold-core-analytics-package" + ] + }, + { + "epic": "product-analytics-channel", + "story": "04-manifest-schema-and-wire-use-case", + "title": "Extend manifest schema and wireUseCase with analyticsEvents", + "waiting_on": [ + "product-analytics-channel/03-analyzed-brand-and-with-analytics-wrapper" + ] + }, + { + "epic": "product-analytics-channel", + "story": "05-assert-feature-conformance-analyzed", + "title": "Extend assertFeatureConformance to check Analyzed brand", + "waiting_on": [ + "product-analytics-channel/04-manifest-schema-and-wire-use-case" + ] + }, + { + "epic": "product-analytics-channel", + "story": "06-analytics-protocol-bind-context", + "title": "Add AnalyticsProtocol and BindContext.analytics", + "waiting_on": [ + "product-analytics-channel/05-assert-feature-conformance-analyzed" + ] + }, + { + "epic": "product-analytics-channel", + "story": "07-eslint-rule-no-undeclared-analytics-event", + "title": "Add no-undeclared-analytics-event ESLint rule to @repo/core-eslint", + "waiting_on": [ + "product-analytics-channel/05-assert-feature-conformance-analyzed" + ] + }, + { + "epic": "product-analytics-channel", + "story": "08-react-provider", + "title": "Add React provider to @repo/core-analytics", + "waiting_on": [ + "product-analytics-channel/02-recording-analytics" + ] + } + ], "needs_prd_ship": [] } diff --git a/docs/work/epics/product-analytics-channel/01-scaffold-core-analytics-package/_story.md b/docs/work/epics/product-analytics-channel/01-scaffold-core-analytics-package/_story.md new file mode 100644 index 0000000..99b671b --- /dev/null +++ b/docs/work/epics/product-analytics-channel/01-scaffold-core-analytics-package/_story.md @@ -0,0 +1,52 @@ +--- +id: 01-scaffold-core-analytics-package +epic: product-analytics-channel +title: Scaffold @repo/core-analytics package with IAnalytics and NoopAnalytics +type: technical-story +status: in-progress +feature: core-analytics +depends-on: [] +blocks: + [ + 02-recording-analytics, + 03-analyzed-brand-and-with-analytics-wrapper, + 06-analytics-protocol-bind-context, + 08-react-provider, + ] +created: 2026-05-18T12:00:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Produce a green `@repo/core-analytics` package — generator-scaffolded skeleton, then fleshed out with `IAnalytics` interface (`track`, `identify`, `pageView`, `flush`), `NoopAnalytics` default implementation, supporting types (`AnalyticsAttributeValue`, `AnalyticsUser`), and full method-level test coverage. The package is vendor-neutral: no third-party analytics SDK is bundled. + +## Why + +All downstream stories depend on `IAnalytics` existing as the contract surface. Landing this first means every subsequent story compiles and imports from a real package rather than a placeholder. + +## Done when + +- `packages/core-analytics/` exists as a workspace package `@repo/core-analytics`. +- `IAnalytics`, `AnalyticsAttributeValue`, `AnalyticsUser` are exported from the package root. +- `NoopAnalytics` implements `IAnalytics`; `flush()` resolves with a microtask (`Promise.resolve()`). +- Sibling tests cover all four interface methods on `NoopAnalytics`. +- `pnpm typecheck && pnpm lint && pnpm test && pnpm conformance && pnpm fallow:audit && pnpm coverage:diff` all pass. + +## In scope + +- `packages/core-analytics/` — package scaffold, `IAnalytics` interface, `AnalyticsAttributeValue` + `AnalyticsUser` types, `NoopAnalytics` impl, sibling tests, root barrel export. +- No subpath exports in this story (`./react` lands in Story 08). +- No `withAnalytics` wrapper in this story (Story 03). + +## Out of scope + +- `RecordingAnalytics` (Story 02). +- `Analyzed` brand / `withAnalytics` wrapper (Story 03). +- React provider (Story 08). +- Backend vendor integration — template ships Noop only. + +## Tasks + +- [ ] Run `pnpm turbo gen core-package analytics` to scaffold `@repo/core-analytics` — verify the generator output compiles and all gates pass on this commit alone. +- [ ] Replace generator placeholder content with `IAnalytics` interface (`track`, `identify`, `pageView`, `flush`), `AnalyticsAttributeValue` + `AnalyticsUser` types, and `NoopAnalytics` implementation (`flush()` returns `Promise.resolve()`); add sibling tests covering all four methods; export everything from the package root barrel — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/02-recording-analytics/_story.md b/docs/work/epics/product-analytics-channel/02-recording-analytics/_story.md new file mode 100644 index 0000000..217d9fc --- /dev/null +++ b/docs/work/epics/product-analytics-channel/02-recording-analytics/_story.md @@ -0,0 +1,42 @@ +--- +id: 02-recording-analytics +epic: product-analytics-channel +title: Add RecordingAnalytics to @repo/core-testing +type: technical-story +status: todo +feature: core-testing +depends-on: [01-scaffold-core-analytics-package] +blocks: [08-react-provider] +created: 2026-05-18T12:01:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Add `RecordingAnalytics` to `@repo/core-testing` as an in-memory test double that implements `IAnalytics`, parallel to `RecordingAuditLog`. Consumers use it in unit tests and the React provider test. + +## Why + +Every story that needs to assert analytics behaviour (including the React provider test in Story 08) requires a deterministic implementation. `RecordingAnalytics` provides the same recording double pattern already established by `RecordingAuditLog`. + +## Done when + +- `RecordingAnalytics` implements `IAnalytics` and records calls to `tracked`, `identified`, `pageViewed` arrays. +- `flush()` resolves with a microtask and clears or drains the in-memory buffer. +- Exported from `@repo/core-testing`'s barrel. +- Sibling test covers all four methods and the flush drain behaviour. +- All gates pass. + +## In scope + +- `packages/core-testing/src/recording-analytics.ts` + sibling test. +- Export line in `packages/core-testing/src/index.ts`. + +## Out of scope + +- React provider wiring (Story 08). +- Any vendor-specific recording behaviour. + +## Tasks + +- [ ] Add `RecordingAnalytics` implementing `IAnalytics` to `@repo/core-testing` — recorded arrays for `track`, `identify`, `pageView`; `flush()` returns `Promise.resolve()`; sibling test covers all methods and flush; export added to barrel — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/03-analyzed-brand-and-with-analytics-wrapper/_story.md b/docs/work/epics/product-analytics-channel/03-analyzed-brand-and-with-analytics-wrapper/_story.md new file mode 100644 index 0000000..55154b1 --- /dev/null +++ b/docs/work/epics/product-analytics-channel/03-analyzed-brand-and-with-analytics-wrapper/_story.md @@ -0,0 +1,46 @@ +--- +id: 03-analyzed-brand-and-with-analytics-wrapper +epic: product-analytics-channel +title: Add Analyzed brand and withAnalytics wrapper +type: technical-story +status: todo +feature: core-shared +depends-on: [01-scaffold-core-analytics-package] +blocks: [04-manifest-schema-and-wire-use-case] +created: 2026-05-18T12:02:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Add the `Analyzed` brand to `core-shared/conformance/` and the `withAnalytics(analytics, factory)` higher-order wrapper to `@repo/core-analytics`. Together they enable brand-based conformance detection at DI bind time, mirroring the `Audited` / `withAudit` pattern. + +## Why + +`wireUseCase` (Story 04) and `assertFeatureConformance` (Story 05) both key off the `Analyzed` brand at runtime. The brand must exist in `core-shared` (so the assertion layer can import it without a circular dep) while the wrapper lives in `core-analytics` (analytics is optional; can't pollute core-shared with the implementation). + +## Done when + +- `Analyzed = F & { readonly __analyzed: true }` exists in `packages/core-shared/src/conformance/brands.ts` and `isAnalyzed` guard exists in `packages/core-shared/src/conformance/brand-runtime.ts`. +- `withAnalytics(analytics, factory)` lives in `packages/core-analytics/src/with-analytics.ts`, attaches `Analyzed` via `attachBrand` from `core-shared/conformance/brand-runtime`, and is exported from `@repo/core-analytics`. +- `with-analytics.test.ts` asserts brand is present after wrapping and absent before. +- All gates pass on each commit independently. + +## In scope + +- `packages/core-shared/src/conformance/brands.ts` — `Analyzed` type addition. +- `packages/core-shared/src/conformance/brand-runtime.ts` — `isAnalyzed` guard. +- `packages/core-core-shared/src/conformance/index.ts` — export additions. +- `packages/core-analytics/src/with-analytics.ts` + sibling test. +- `packages/core-analytics/src/index.ts` — export addition. + +## Out of scope + +- `wireUseCase` composition (Story 04). +- `assertFeatureConformance` extension (Story 05). +- `analyticsEvents` manifest field (Story 04). + +## Tasks + +- [ ] Add `Analyzed` brand type to `packages/core-shared/src/conformance/brands.ts` and `isAnalyzed(f): f is Analyzed` guard to `brand-runtime.ts`; export both from the conformance index — all gates pass on this commit. +- [ ] Add `withAnalytics(analytics, factory)` wrapper to `packages/core-analytics/src/with-analytics.ts` using `attachBrand` from `core-shared/conformance/brand-runtime` + `with-analytics.test.ts` asserting brand attached after wrapping and absent before; export from `@repo/core-analytics` root barrel — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/04-manifest-schema-and-wire-use-case/_story.md b/docs/work/epics/product-analytics-channel/04-manifest-schema-and-wire-use-case/_story.md new file mode 100644 index 0000000..1db099a --- /dev/null +++ b/docs/work/epics/product-analytics-channel/04-manifest-schema-and-wire-use-case/_story.md @@ -0,0 +1,50 @@ +--- +id: 04-manifest-schema-and-wire-use-case +epic: product-analytics-channel +title: Extend manifest schema and wireUseCase with analyticsEvents +type: technical-story +status: todo +feature: core-shared +depends-on: [03-analyzed-brand-and-with-analytics-wrapper] +blocks: + [ + 05-assert-feature-conformance-analyzed, + 07-eslint-rule-no-undeclared-analytics-event, + ] +created: 2026-05-18T12:03:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Add `analyticsEvents: string[]` (default `[]`) to the per-use-case manifest schema in `define-feature.ts`, update the feature generator template to emit the field, and extend `wireUseCase` to compose `withAnalytics` when an `analytics` arg is provided and the use case declares at least one analytics event. + +## Why + +The manifest field is the declaration surface the ESLint rule (Story 07) and boot assertion (Story 05) both read from. `wireUseCase` is the composition site that attaches the `Analyzed` brand; it must compose analytics in the right position (`factory → withAnalytics → withAudit → withCapture → withSpan`). + +## Done when + +- `useCases..analyticsEvents: string[]` is valid in every feature manifest; existing manifests stay green with an absent or empty field. +- The feature generator template emits `analyticsEvents: []` for each scaffolded use case. +- `wireUseCase({ ..., analytics? })` composes `withAnalytics` when `analytics` is provided; existing callers without `analytics` are unaffected. +- `wire-use-case.test.ts` covers the analytics path (brand present) and the no-analytics path (brand absent). +- All gates pass on each commit independently. + +## In scope + +- `packages/core-shared/src/conformance/define-feature.ts` — schema extension. +- Feature generator template (`turbo/generators/`) — `analyticsEvents: []` added to use-case scaffold output. +- `packages/core-shared/src/conformance/wire-use-case.ts` — `analytics?` arg + `withAnalytics` composition. +- `packages/core-shared/src/conformance/wire-use-case.test.ts` — analytics path coverage. + +## Out of scope + +- `assertFeatureConformance` extension (Story 05). +- ESLint rule (Story 07). +- Migrating existing feature manifests (out of scope per PRD). + +## Tasks + +- [ ] Add `analyticsEvents: string[]` (default `[]`) to the use-case schema in `define-feature.ts` and update the feature generator template to emit the field — existing manifests remain valid; all gates pass on this commit. +- [ ] Extend `wireUseCase` to accept optional `analytics` arg and compose `withAnalytics` (innermost, before `withAudit`) when provided + update `wire-use-case.test.ts` with analytics path (Analyzed brand present) and no-analytics path (Analyzed brand absent) — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/05-assert-feature-conformance-analyzed/_story.md b/docs/work/epics/product-analytics-channel/05-assert-feature-conformance-analyzed/_story.md new file mode 100644 index 0000000..5d3de37 --- /dev/null +++ b/docs/work/epics/product-analytics-channel/05-assert-feature-conformance-analyzed/_story.md @@ -0,0 +1,45 @@ +--- +id: 05-assert-feature-conformance-analyzed +epic: product-analytics-channel +title: Extend assertFeatureConformance to check Analyzed brand +type: technical-story +status: todo +feature: core-shared +depends-on: [04-manifest-schema-and-wire-use-case] +blocks: + [ + 06-analytics-protocol-bind-context, + 07-eslint-rule-no-undeclared-analytics-event, + ] +created: 2026-05-18T12:04:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Extend `assertFeatureConformance` to reject a binding at boot time when the manifest's use case declares `analyticsEvents.length > 0` but the bound function does not carry the `Analyzed` brand. Error message names the use case and the missing brand. + +## Why + +This is the boot-time gate that prevents analytics events from being declared in the manifest but silently skipped at runtime. Mirrors the existing `Audited` check for the same guarantee at the same latency. + +## Done when + +- `assertFeatureConformance` throws `ConformanceError` naming the missing `Analyzed` brand when `analyticsEvents.length > 0` and the bound function is not `isAnalyzed`. +- A synthetic conformance test (parallel to `assert-bindings.test.ts`) covers: passes when Analyzed present + events declared, throws with message naming `Analyzed` when events declared + brand missing, passes when events empty + brand absent. +- All gates pass. + +## In scope + +- `packages/core-shared/src/conformance/assert-bindings.ts` — Analyzed check addition. +- Conformance test file for the new assertion path. + +## Out of scope + +- `BindContext.analytics` field (Story 06). +- ESLint rule (Story 07). +- Any template feature wiring analytics (out of scope per PRD). + +## Tasks + +- [ ] Add `Analyzed` brand check to `assertFeatureConformance` when `manifest.useCases[name].analyticsEvents.length > 0` + synthetic conformance test asserting `ConformanceError` with message naming `Analyzed` for unwrapped bindings — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/06-analytics-protocol-bind-context/_story.md b/docs/work/epics/product-analytics-channel/06-analytics-protocol-bind-context/_story.md new file mode 100644 index 0000000..a6c5ff6 --- /dev/null +++ b/docs/work/epics/product-analytics-channel/06-analytics-protocol-bind-context/_story.md @@ -0,0 +1,42 @@ +--- +id: 06-analytics-protocol-bind-context +epic: product-analytics-channel +title: Add AnalyticsProtocol and BindContext.analytics +type: technical-story +status: todo +feature: core-shared +depends-on: [05-assert-feature-conformance-analyzed] +blocks: [09-documentation] +created: 2026-05-18T12:05:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Add `AnalyticsProtocol` to `core-shared/di/bind-protocols.ts` and an optional `analytics?: AnalyticsProtocol` field to `BindContext`. This is the DI seam that lets feature binders receive an analytics implementation at bind time without importing the concrete class. + +## Why + +Feature binders that need analytics accept `ctx.analytics` from the aggregator's `BindContext`. Without the protocol type and context field, there's no typed path for passing analytics through the DI layer. Mirrors how `IEventBus`, `IAuditLog`, and `IJobQueue` are threaded through `BindContext`. + +## Done when + +- `AnalyticsProtocol` structural type exists in `packages/core-shared/src/di/bind-protocols.ts` and is exported. +- `BindContext` (and `BindProductionContext`) gain `analytics?: AnalyticsProtocol`. +- Existing binder call-sites compile without changes (field is optional). +- All gates pass. + +## In scope + +- `packages/core-shared/src/di/bind-protocols.ts` — `AnalyticsProtocol` type. +- `packages/core-shared/src/di/bind-context.ts` — `analytics?:` field on both context shapes. +- Exports from `@repo/core-shared/di` barrel. + +## Out of scope + +- Wiring analytics in any actual feature binder (no template feature adopts analytics per PRD). +- `IAnalytics` class methods — `AnalyticsProtocol` is the structural type used for DI; it mirrors the `IAnalytics` shape but lives in `core-shared` to avoid a circular dependency. + +## Tasks + +- [ ] Add `AnalyticsProtocol` structural type to `packages/core-shared/src/di/bind-protocols.ts` and `analytics?: AnalyticsProtocol` to `BindContext` + `BindProductionContext`; export from the `@repo/core-shared/di` barrel — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/07-eslint-rule-no-undeclared-analytics-event/_story.md b/docs/work/epics/product-analytics-channel/07-eslint-rule-no-undeclared-analytics-event/_story.md new file mode 100644 index 0000000..6591af2 --- /dev/null +++ b/docs/work/epics/product-analytics-channel/07-eslint-rule-no-undeclared-analytics-event/_story.md @@ -0,0 +1,43 @@ +--- +id: 07-eslint-rule-no-undeclared-analytics-event +epic: product-analytics-channel +title: Add no-undeclared-analytics-event ESLint rule to @repo/core-eslint +type: technical-story +status: todo +feature: core-eslint +depends-on: [05-assert-feature-conformance-analyzed] +blocks: [09-documentation] +created: 2026-05-18T12:06:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Add the `conformance/no-undeclared-analytics-event` rule to `@repo/core-eslint` at warn severity. The rule finds `analytics.track("X", ...)` string-literal calls in `*.use-case.ts` files and warns when `"X"` is not declared in the file's feature manifest under `analyticsEvents`. Mirrors `no-undeclared-audit` and `no-undeclared-event-publish`. + +## Why + +Boot-time conformance (Story 05) catches missing brands at process start, but it can't catch event-slug typos or new undeclared slugs introduced during development before the app is ever booted. The ESLint rule provides sub-second feedback in the editor and in CI, closing the latency gap. + +## Done when + +- `packages/core-eslint/rules/no-undeclared-analytics-event.js` exists and passes RuleTester fixtures. +- Fixtures cover: declared slug → no warning, undeclared slug → warn, non-use-case file → no-op, manifest with no use cases → no-op. +- `_manifest-ast.js` parses `analyticsEvents` arrays (extends existing AST helper). +- Rule registered in `plugin.js` and `base.js` at `"warn"` severity. +- `pnpm lint` runs the rule; all gates pass. + +## In scope + +- `packages/core-eslint/rules/no-undeclared-analytics-event.js` + RuleTester test file. +- `packages/core-eslint/rules/_manifest-ast.js` — `analyticsEvents` parsing addition. +- `packages/core-eslint/plugin.js` + `packages/core-eslint/base.js` — rule registration at warn. + +## Out of scope + +- Migrating existing use cases (no template feature declares analytics events per PRD). +- Auto-fix — warn only, no `--fix` path in this slice. + +## Tasks + +- [ ] Add `no-undeclared-analytics-event.js` rule to `@repo/core-eslint` — rule implementation cross-checking `analytics.track("X", ...)` literal slug against manifest `analyticsEvents`, extend `_manifest-ast.js` to parse the field, RuleTester fixtures (declared passes, undeclared warns, non-use-case no-op, no-manifest no-op), register in `plugin.js` + `base.js` at `"warn"` — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/08-react-provider/_story.md b/docs/work/epics/product-analytics-channel/08-react-provider/_story.md new file mode 100644 index 0000000..daa04bf --- /dev/null +++ b/docs/work/epics/product-analytics-channel/08-react-provider/_story.md @@ -0,0 +1,43 @@ +--- +id: 08-react-provider +epic: product-analytics-channel +title: Add React provider to @repo/core-analytics +type: technical-story +status: todo +feature: core-analytics +depends-on: [02-recording-analytics] +blocks: [09-documentation] +created: 2026-05-18T12:07:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Add a `./react` subpath export to `@repo/core-analytics` containing `` and `useAnalytics(): IAnalytics`. `useAnalytics()` throws a clear error when called outside a provider. No auto-wired router events. + +## Why + +Server-side use cases receive analytics through `BindContext.analytics`. Client-side components need an equivalent contract surface so `analytics.track(...)` reads the same whether called from a use case or a React component. The provider bridges `IAnalytics` from DI land to React's context tree without coupling consumers to a specific vendor. + +## Done when + +- `@repo/core-analytics/react` subpath exports `AnalyticsProvider` and `useAnalytics`. +- `useAnalytics()` throws a named error (`AnalyticsContextError` or similar) when called outside ``. +- React Testing Library test: render a child inside ``, child calls `useAnalytics().track("test.event")`, assert `recordingAnalytics.tracked` contains the event. +- All gates pass. + +## In scope + +- `packages/core-analytics/src/react/analytics-provider.tsx` + `useAnalytics.ts` (or colocated). +- `packages/core-analytics/src/react/index.ts` — subpath barrel. +- `package.json` `exports` map: `"./react": "./dist/react/index.js"` (or equivalent for the build config). +- React Testing Library test. + +## Out of scope + +- Auto-wiring Next App Router or TanStack Router route-change hooks (deferred per PRD). +- SSR hydration concerns — provider is a pure context wrapper; consumers handle hydration. + +## Tasks + +- [ ] Add `AnalyticsProvider` + `useAnalytics()` to `packages/core-analytics/src/react/`, wire `./react` subpath export in `package.json`, write React Testing Library test using `RecordingAnalytics` asserting `track` flows through context — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/09-documentation/_story.md b/docs/work/epics/product-analytics-channel/09-documentation/_story.md new file mode 100644 index 0000000..4a86841 --- /dev/null +++ b/docs/work/epics/product-analytics-channel/09-documentation/_story.md @@ -0,0 +1,51 @@ +--- +id: 09-documentation +epic: product-analytics-channel +title: Documentation — analytics.md, conformance-quickref, CLAUDE.md, template-tiers +type: technical-story +status: todo +feature: docs +depends-on: + [ + 06-analytics-protocol-bind-context, + 07-eslint-rule-no-undeclared-analytics-event, + 08-react-provider, + ] +blocks: [] +created: 2026-05-18T12:08:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Write `docs/guides/analytics.md` and update `docs/guides/conformance-quickref.md`, `CLAUDE.md`, and `docs/architecture/template-tiers.md` to reflect the completed channel. All docs reference the final shipped shape. + +## Why + +Docs land last (per PRD sequencing) so they can reference the real API rather than draft shapes. The conformance-quickref and CLAUDE.md rule-count bump are load-bearing for agents that use those files as context pointers. + +## Done when + +- `docs/guides/analytics.md` exists and documents: server-side wiring path (`BindContext.analytics` → feature binder), client-side wiring path (`` + `useAnalytics()`), consumer vendor evaluation step (ADR-022 `/evaluate-library` gate), and PII boundary deferral (ADR-024 §"PII boundary"). +- `docs/guides/conformance-quickref.md` rule table shows 7 ESLint rules (adds `no-undeclared-analytics-event`) and drift patterns include analytics-event sprawl. +- `CLAUDE.md` rule count updated from 6 to 7 in the conformance ESLint rules list. +- `docs/architecture/template-tiers.md` lists `core-analytics` in the optional-cores table. +- All gates pass on each commit independently. + +## In scope + +- `docs/guides/analytics.md` — new guide. +- `docs/guides/conformance-quickref.md` — rule table + drift patterns update. +- `CLAUDE.md` — conformance rule count line update. +- `docs/architecture/template-tiers.md` — optional-cores list update. + +## Out of scope + +- Glossary update (already done in the ADR-024 commit per PRD). +- ADR-024 itself (already exists). +- Storybook component or demo app (deferred per PRD). + +## Tasks + +- [ ] Write `docs/guides/analytics.md` covering server-side wiring (`BindContext.analytics`), client-side wiring (`` + `useAnalytics()`), vendor evaluation step (ADR-022), and PII boundary note — all gates pass on this commit. +- [ ] Update `docs/guides/conformance-quickref.md` (seventh rule + analytics-event drift pattern), `CLAUDE.md` (6 → 7 conformance ESLint rules), and `docs/architecture/template-tiers.md` (add `core-analytics` to optional-cores list) — all gates pass on this commit. diff --git a/docs/work/epics/product-analytics-channel/_epic.md b/docs/work/epics/product-analytics-channel/_epic.md new file mode 100644 index 0000000..4701a7d --- /dev/null +++ b/docs/work/epics/product-analytics-channel/_epic.md @@ -0,0 +1,30 @@ +--- +id: product-analytics-channel +prd: docs/work/prds/product-analytics-channel.prd.md +title: Product analytics as a fourth capture channel (ADR-024 implementation) +type: epic +status: in-progress +features: [core-analytics, core-shared, core-testing, core-eslint] +created: 2026-05-18T12:00:00Z +updated: 2026-05-18T10:46:27.080Z +--- + +## Goal + +Ship `@repo/core-analytics` as an optional core package that codifies product analytics as the fourth capture channel alongside `ITracer`, `ILogger`, and `IAuditLog`. Adds `IAnalytics` interface, `NoopAnalytics` + `RecordingAnalytics` implementations, `Analyzed` brand, `withAnalytics` wrapper, `analyticsEvents` manifest field, `assertFeatureConformance` extension, `AnalyticsProtocol` in `BindContext`, `no-undeclared-analytics-event` ESLint rule, and a React provider scaffold — all mirroring the audit channel shape with ADR-024's three deliberate divergences. + +## Why + +Consumers routinely bolt analytics SDKs on at the React component layer, bypassing manifests, brands, and the five-gate conformance system. Codifying analytics as a channel with the same structural shape as `core-audit` means the conformance gates extend to a fourth signal at zero new gate count. + +## Stories + +- [ ] [01 — Scaffold @repo/core-analytics package](01-scaffold-core-analytics-package/_story.md) +- [ ] [02 — Add RecordingAnalytics to @repo/core-testing](02-recording-analytics/_story.md) +- [ ] [03 — Add Analyzed brand and withAnalytics wrapper](03-analyzed-brand-and-with-analytics-wrapper/_story.md) +- [ ] [04 — Extend manifest schema and wireUseCase with analyticsEvents](04-manifest-schema-and-wire-use-case/_story.md) +- [ ] [05 — Extend assertFeatureConformance to check Analyzed brand](05-assert-feature-conformance-analyzed/_story.md) +- [ ] [06 — Add AnalyticsProtocol and BindContext.analytics](06-analytics-protocol-bind-context/_story.md) +- [ ] [07 — Add no-undeclared-analytics-event ESLint rule](07-eslint-rule-no-undeclared-analytics-event/_story.md) +- [ ] [08 — Add React provider to @repo/core-analytics](08-react-provider/_story.md) +- [ ] [09 — Documentation](09-documentation/_story.md)