chore(template): clean-slate template snapshot from bb4a0c7

Curated, product-agnostic snapshot of the post-story-04 tree: demo
content deleted, auth-only reference feature, web-next shell, all gates
green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor
library traces, and product naming are curated out; generic template
repairs (coverage provider devDeps, root test:coverage script, live
lint fixes, root-only release-please) are kept. See TEMPLATE.md for
provenance, curation list, and usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
2026-07-12 20:40:54 +02:00
commit f77e6ea881
1062 changed files with 105156 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
export type AnalyticsAttributeValue = string | number | boolean;
export type AnalyticsUser = {
id: string;
};
export interface IAnalytics {
track(
event: string,
attributes?: Record<string, AnalyticsAttributeValue>,
): void;
identify(
user: AnalyticsUser,
attributes?: Record<string, AnalyticsAttributeValue>,
): void;
pageView(
path: string,
attributes?: Record<string, AnalyticsAttributeValue>,
): void;
flush(): Promise<void>;
}

View File

@@ -0,0 +1,8 @@
export type {
AnalyticsAttributeValue,
AnalyticsUser,
IAnalytics,
} from "./analytics.interface";
export { NoopAnalytics } from "./noop-analytics";
export type { Analyzed } from "./with-analytics";
export { withAnalytics } from "./with-analytics";

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { NoopAnalytics } from "@/noop-analytics";
describe("NoopAnalytics", () => {
it("track() does not throw with event name only", () => {
const analytics = new NoopAnalytics();
expect(() => analytics.track("page_viewed")).not.toThrow();
});
it("track() does not throw with event name and attributes", () => {
const analytics = new NoopAnalytics();
expect(() =>
analytics.track("button_clicked", {
label: "signup",
count: 1,
active: true,
}),
).not.toThrow();
});
it("identify() does not throw with user only", () => {
const analytics = new NoopAnalytics();
expect(() => analytics.identify({ id: "user-123" })).not.toThrow();
});
it("identify() does not throw with user and attributes", () => {
const analytics = new NoopAnalytics();
expect(() =>
analytics.identify({ id: "user-123" }, { plan: "pro", trial: false }),
).not.toThrow();
});
it("pageView() does not throw with path only", () => {
const analytics = new NoopAnalytics();
expect(() => analytics.pageView("/dashboard")).not.toThrow();
});
it("pageView() does not throw with path and attributes", () => {
const analytics = new NoopAnalytics();
expect(() =>
analytics.pageView("/dashboard", { referrer: "/home", duration: 120 }),
).not.toThrow();
});
it("flush() resolves without throwing", async () => {
const analytics = new NoopAnalytics();
await expect(analytics.flush()).resolves.toBeUndefined();
});
it("flush() returns a Promise", () => {
const analytics = new NoopAnalytics();
expect(analytics.flush()).toBeInstanceOf(Promise);
});
});

View File

@@ -0,0 +1,23 @@
import type {
AnalyticsAttributeValue,
AnalyticsUser,
IAnalytics,
} from "./analytics.interface";
export class NoopAnalytics implements IAnalytics {
track(
_event: string,
_attributes?: Record<string, AnalyticsAttributeValue>,
): void {}
identify(
_user: AnalyticsUser,
_attributes?: Record<string, AnalyticsAttributeValue>,
): void {}
pageView(
_path: string,
_attributes?: Record<string, AnalyticsAttributeValue>,
): void {}
flush(): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from "vitest";
import { render, renderHook } from "@testing-library/react";
import { RecordingAnalytics } from "@repo/core-testing";
import {
AnalyticsContextError,
AnalyticsProvider,
useAnalytics,
} from "@/react/index";
function Tracker() {
const analytics = useAnalytics();
analytics.track("test.event");
return null;
}
describe("AnalyticsProvider", () => {
it("makes analytics available through context and track flows through", () => {
const recording = new RecordingAnalytics();
render(
<AnalyticsProvider value={recording}>
<Tracker />
</AnalyticsProvider>,
);
expect(recording.tracked).toContainEqual({ event: "test.event" });
});
});
describe("useAnalytics", () => {
it("throws AnalyticsContextError when called outside a provider", () => {
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
expect(() => renderHook(() => useAnalytics())).toThrow(
AnalyticsContextError,
);
} finally {
spy.mockRestore();
}
});
});

View File

@@ -0,0 +1,33 @@
import { createContext, useContext, type ReactNode } from "react";
import type { IAnalytics } from "../analytics.interface";
const AnalyticsContext = createContext<IAnalytics | null>(null);
export class AnalyticsContextError extends Error {
constructor() {
super("useAnalytics() must be called within an <AnalyticsProvider>.");
this.name = "AnalyticsContextError";
}
}
export function AnalyticsProvider({
value,
children,
}: {
value: IAnalytics;
children: ReactNode;
}) {
return (
<AnalyticsContext.Provider value={value}>
{children}
</AnalyticsContext.Provider>
);
}
export function useAnalytics(): IAnalytics {
const analytics = useContext(AnalyticsContext);
if (analytics === null) {
throw new AnalyticsContextError();
}
return analytics;
}

View File

@@ -0,0 +1,5 @@
export {
AnalyticsProvider,
useAnalytics,
AnalyticsContextError,
} from "./analytics-provider";

View File

@@ -0,0 +1,55 @@
import { describe, it, expect, expectTypeOf } from "vitest";
import { withAnalytics, type Analyzed } from "@/with-analytics";
import type { IAnalytics } from "@/analytics.interface";
import { isAnalyzed } from "@repo/core-shared/conformance";
function makeAnalytics(): IAnalytics {
return {
track: () => undefined,
identify: () => undefined,
pageView: () => undefined,
flush: () => Promise.resolve(),
};
}
describe("withAnalytics", () => {
it("returns an Analyzed<F>", () => {
const analytics = makeAnalytics();
const fn = async (_input: { id: string }) => ({ ok: true });
const wrapped = withAnalytics(analytics, fn);
expectTypeOf(wrapped).toMatchTypeOf<Analyzed<typeof fn>>();
});
it("attaches __analyzed as a non-enumerable property on the wrapped function", () => {
const analytics = makeAnalytics();
const fn = async () => ({ ok: true });
const wrapped = withAnalytics(analytics, fn);
expect(isAnalyzed(wrapped)).toBe(true);
expect(Object.keys(wrapped)).not.toContain("__analyzed");
});
it("does NOT pollute the original input function with the brand", () => {
const analytics = makeAnalytics();
const fn = async () => ({ ok: true });
const wrapped = withAnalytics(analytics, fn);
expect(isAnalyzed(fn)).toBe(false);
expect(wrapped).not.toBe(fn);
});
it("passes input and output through unchanged", async () => {
const analytics = makeAnalytics();
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
const wrapped = withAnalytics(analytics, fn);
const result = await wrapped({ id: "abc" });
expect(result).toEqual({ ok: true, id: "abc" });
});
it("propagates errors", async () => {
const analytics = makeAnalytics();
const err = new Error("boom");
const wrapped = withAnalytics(analytics, async () => {
throw err;
});
await expect(wrapped()).rejects.toBe(err);
});
});

View File

@@ -0,0 +1,35 @@
import type { IAnalytics } from "./analytics.interface";
import { attachBrand } from "@repo/core-shared/conformance";
/**
* Phantom-type brand attached at wrap time by `withAnalytics`. The conformance
* system uses this as the type-level seam for use cases that declare
* `analyticsEvents: [...]` in their manifest — without `__analyzed`, the
* binding is not assignable to `ProductionUseCase<I, O, M>` when M demands it.
* At runtime the brand is a non-enumerable property attached by `attachBrand`
* from `@repo/core-shared/conformance`, so the boot-time assertion can verify
* the binding went through the analytics-aware path.
*/
export type Analyzed<F> = F & { readonly __analyzed: true };
/**
* Use-case wrapper applied at DI bind time. The wrapper is a thin closure
* that forwards to `fn` unchanged and carries the `__analyzed` brand. The
* forward closure (instead of returning `fn` directly) keeps the brand on
* a fresh function so the caller's original `fn` is not mutated — important
* when the same factory output is used elsewhere unwrapped (dev-seed paths,
* tests).
*/
export function withAnalytics<Args extends unknown[], R>(
// The wrapper attaches the brand and ensures the analytics dependency is
// available at bind time. Actual `analytics.track()` calls live in the
// use case body — only the use case knows which properties to extract
// from its input/output for the analytics event.
analytics: IAnalytics,
fn: (...args: Args) => Promise<R>,
): Analyzed<(...args: Args) => Promise<R>> {
void analytics;
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
attachBrand(wrapped, "__analyzed");
return wrapped as Analyzed<(...args: Args) => Promise<R>>;
}