Directory Web Template
The Directory Web Template is a modern, full-stack directory website solution built with Next.js 16 and organized as a Turborepo monorepo. It's designed to help you create professional directory websites for tools, services, products, or any other type of listing platform.
Key Features
- Modern Tech Stack: Next.js 16, React 19, TypeScript, Tailwind CSS, HeroUI React
- Turborepo Monorepo: pnpm workspaces with shared configs, web app, e2e tests, and docs
- Flexible Authentication: NextAuth.js v5, Supabase Auth, OAuth providers (Google, GitHub, Facebook, Twitter, Microsoft)
- Payment Integration: Stripe, LemonSqueezy, Polar, subscription management
- Internationalization: Multiple languages supported with full RTL support via next-intl
- Git-based CMS: Content synchronization from Git repositories with YAML-based structure
- Theming System: Built-in themes with dynamic color generation
- Maps & Location: Mapbox / Google Maps abstraction with marker clustering, auto-geocoding from YAML addresses, and an opt-in Map view for listings (sidebar + map, Zillow / Airbnb style)
- Analytics & Monitoring: PostHog, Sentry, performance monitoring
- Admin Dashboard: Content management, user management, and analytics
- SEO Optimized: Sitemap generation, structured data (JSON-LD), meta tags
Quick Start
# Clone the monorepo
git clone https://github.com/ever-works/directory-web-template.git
cd directory-web-template
# Install dependencies (pnpm required)
pnpm install
# Set up environment for the web app
cp apps/web/.env.example apps/web/.env.local
# Edit apps/web/.env.local with your configuration
# Start development server
pnpm run dev
Visit http://localhost:3000 to see your site!
Next Steps
- Installation Guide -- Complete setup instructions
- Quick Start Guide -- Get up and running in under 10 minutes
- Architecture Overview -- Understand the system design
- Deployment Guide -- Deploy to production
For Contributors & AI Agents
The template uses spec-driven development following the GitHub Spec Kit convention. Every non-trivial change goes through a spec → plan → tasks trio.
AGENTS.md-- Cross-cutting rules for any AI agent operating in this monorepo..specify/README.md-- Spec Kit workflow..specify/memory/constitution.md-- Ten durable principles every plan must respect.- Spec Index -- Per-feature spec/plan/tasks documents under
docs/spec/. - Change Log -- Running log of doc and spec changes.
- Open Questions -- Open questions with chosen defaults.
- Plugin System (Architecture) -- The pluggable, modular core (see Spec 002).
- Authoring a Plugin -- Walk-through for authoring your first plugin.
- Plugin Lifecycle -- Boot, validation, enable/disable, and teardown semantics every plugin must cooperate with.
- Testing a Plugin -- Unit-test the manifest, register through
createTestRegistry, render slots in isolation, and add a Playwright smoke spec. - Plugin Capabilities Reference -- Complete
auth/payment/analytics/search/content-source/maps/newsletter/notifications/ai/ui-slotcapability surface, with lookup-style and provider-resolution rules. - Plugin Slots Reference -- Per-slot contract for every canonical slot id (
header.left,home.before-listing,admin.dashboard.widgets, …) with composition rules and the checklist for adding a new slot. - Plugin Loader Reference -- Per-API reference for
loadPlugins/mergeConfigSourcespaired withpackages/plugin-runtime/src/loader.ts: env / DB / override precedence, the failure matrix, and how to test the loader directly. - Plugin Registry Reference -- Per-API reference for the
PluginRegistryclass paired withpackages/plugin-runtime/src/registry.ts:register/enable/disable/get/list/slotsFor/list_allsemantics, the read-vs-write surface summary, and the duplicate-name / unregistered / throwing-teardown failure matrix. - Plugin SlotHost Reference -- Per-component reference for
<SlotHost slotId registry fallback? />paired withpackages/plugin-runtime/src/SlotHost.tsx: theslotId/registry/fallbackprops, the empty-vs-non-empty rules, server-friendliness, the composition rules that follow fromslotsFor, and the failure matrix that anchors stable React keys on the registry's duplicate-name guarantee. - Plugin Testing Reference -- Per-helper reference for
createTestRegistry({ plugins })paired withpackages/plugin-runtime/src/testing.ts: the four-step internal flow overnew PluginRegistry()+loadPlugins, the read / write surface summary, the failure matrix that distinguishes silent Zod drops from propagated duplicate-name throws, the dual import surface (@ever-works/plugin-runtimebarrel and@ever-works/plugin-runtime/testingsub-path), the three worked Vitest examples (happy path, config-required, disable round-trip), the five anti-patterns, and the explicit non-goals that point atloadPluginsandnew PluginRegistry()for non-default config, persistence-callback assertions, and rejection inspection. - Plugin Manifest Reference -- Per-field reference for
PluginManifest<C>paired withpackages/plugin-sdk/src/manifest.ts: every manifest field (name,version,description,templateRange,capabilities,config,defaultEnabled,adminToggleable,homepage) with its type, default, semantics, and authoring guidance; thePluginConfig<C>type alias; the failure matrix that mapstemplateRangeandconfigfailures ontoLoadPluginsResult.rejected[name].reason, distinguishes the duplicate-name throw as the only manifest-level propagated failure, and clarifies thatadminToggleableis a UI hint (not an authorization check); plus the checklist for adding a new manifest field that pairs the SDK source change with thedocs/log.mdentry. - Plugin Definition Reference -- Per-export reference for
defineDirectoryPluginpaired withpackages/plugin-sdk/src/plugin.ts: the factory's role in inferringC extends z.ZodTypeAnyfrommanifest.config; theDirectoryPlugin<C>shape (manifest, optionalsetup/teardownhooks,slots,providers); thePluginContext<TConfig>runtime context handed tosetupand every slot component (config,name,enabled, optionallogger); theSlotComponentProps<TConfig>slot-component contract that limits the props surface to a singlectxfield; thePluginProvidersmap keyed onCapability(with'ui-slot'typed asnever) and thePluginSlots<TConfig>map keyed onSlotId; the read / write surface summary that maps every caller (plugin author,loadPlugins,PluginRegistry.register,PluginRegistry.disable,<SlotHost />,createTestRegistry, slot components) to the fields they touch; and the failure matrix that surfaces every observable failure in the loader / registry /<SlotHost />layers the plugin returns into (hand-rolled plugin losesCinference, duplicatenamethrows viaregister,manifest.config/templateRangerejections route throughLoadPluginsResult.rejected, throwingsetupis plugin-local, throwingteardownis swallowed bydisable, slot components throw through React, and TypeScript catches'ui-slot'provider attempts and unknown slot ids at compile time). - Plugin Providers Reference -- Per-export reference for the nine concrete capability-provider interfaces paired with
packages/plugin-sdk/src/providers.ts: one section perAuthProvider,PaymentProvider,AnalyticsProvider,SearchProvider,ContentSource,MapsProvider,NewsletterProvider,NotificationsProvider,AIProvider, with every member's type, nullability, and per-member type-system notes (the(string & {})literal-with-fallback trick onPaymentProvider.idthat keeps the union open without giving up autocomplete, thePromise<unknown[]>widening contract onSearchProvider.searchthat defersItem-shape assertion to the host, thePromise<unknown | undefined>absent-vs-error distinction onContentSource.getItem, thevoid | Promise<void>sync-or-async pattern on optional hooks, the{ ok; reason? }result envelope onNewsletterProviderthat surfaces provider-specific failures as data); theCapabilityProviderMapmapped type that binds eachCapabilitymember to its interface and typesPluginRegistry.get<C>/list<C>andPluginProvidersgenerically; the'ui-slot' = neverlockout that turnsproviders: { 'ui-slot': anything }into a TypeScript compile error; the read / write surface that maps every caller (plugin author,defineDirectoryPlugin,PluginRegistry.register,get<C>,list<C>,<SlotHost />, host code underapps/web/lib/<capability>/**) to the fields they touch; and the failure matrix that maps every observable failure (compile-time mis-typing, throwingsetup→LoadPluginsResult.rejected[name].reason: 'setup', fan-out swallow vs. single-lookup propagation, runtime malformed shape, two enabled plugins on the same single-lookup capability) onto the layer that surfaces it. - Plugin Packages -- SDK, runtime, and demo packages overview.
- Reference Plugin (
@ever-works/plugin-demo) -- Per-source-file reference for the bundled reference / demo plugin paired withpackages/plugin-demo/src/index.tsx,config.ts, andHeader.tsx: the at-a-glance manifest summary (name'demo',templateRange '>=0.1 <1.0','ui-slot'capability,'header.right'slot,defaultEnabled: true,adminToggleable: true); the file map (config → manifest schema, Header → slot component, index →defineDirectoryPlugincomposition); the per-line walk-through ofConfigSchemaandDemoConfig(Zod defaults that make the inferred type non-optional, theenabled/greetingtwo-key surface); theDemoHeaderBadgeprops / render contract / disabled-config short-circuit and the stabledata-plugin="demo"/data-testid="demo-plugin-badge"test hooks; thedefineDirectoryPlugininvocation broken down by manifest field and slot binding with the type-inference path that tiesConfigSchematoSlotComponentProps<DemoConfig>; the three call sites (loader Zod parse + register, registry key, slot host render); the failure matrix that maps demo-plugin manifestations onto the loader / registry / slot-host failure surfaces (Zod-rejected config,templateRangemismatch, admin override flippingenabled, duplicate-name throw); the replace-the-demo-plugin recipe that exercises the slot ordering guarantee + admin toggle +defaultEnabled: falselever without removing the reference package from tree; and the evolution checklist that pairs every source-file change with the matching SDK reference page anddocs/log.mdentry. - Plugin SDK Public Surface Reference -- Per-source-file reference for the SDK barrel paired with
packages/plugin-sdk/src/index.ts: every public name the barrel re-exports (CAPABILITIES,isCapability,Capability,SLOT_IDS,isSlotId,SlotId,PluginManifest<C>,PluginConfig<C>, the nine capability-provider interfaces andCapabilityProviderMap,defineDirectoryPlugin, and the five plugin-shape types) split by kind (value vs. type-only) with one row per export and a link to the per-source-file reference that owns its shape; thepackage.json#exportssub-path map (.,./capabilities,./slots) and the rationale for keepingmanifest,providers,plugin,loader,registry, andSlotHostreachable only through the barrel; the per-line walkthrough ofindex.tsthat pins each line to a documentation impact (the JSDoc preamble's framework-agnostic / React-as-peer / architecture-cross-link invariants, the capability re-exports, the slot re-exports, the manifest type re-exports, the provider type re-exports, thedefineDirectoryPluginvalue re-export, and the plugin-shape type re-exports); the value-vs-type contract that calls out moving a name across the boundary as a breaking change and points at@typescript-eslint/consistent-type-exportsas the lint rule; the failure matrix that maps barrel-level mistakes (non-public sub-path import, value-vs-type mis-import, lostCinference when authors skipdefineDirectoryPlugin, capability not added toCAPABILITIES, droppedsideEffectsflag) onto the layer that surfaces them; and the public-surface change checklist that ties any addition / removal back to Spec Kit, thedocs/log.mdentry, and thepnpm tsc --noEmit/ Playwright verification step. - Plugin Runtime Public Surface Reference -- Per-source-file reference for the runtime barrel paired with
packages/plugin-runtime/src/index.ts: every public name the barrel re-exports (PluginRegistry,loadPlugins,mergeConfigSources,PluginConfigSources,LoadPluginsResult,SlotHost,SlotHostProps,createTestRegistry) split by kind (value vs. type-only) and grouped by the three concerns the runtime owns -- the host-app boot pipeline (PluginRegistry+loadPlugins+mergeConfigSources+PluginConfigSources+LoadPluginsResult), the React render surface (SlotHost+SlotHostProps), and the unit-test helper (createTestRegistry); thepackage.json#exportssub-path map (.,./registry,./SlotHost,./loader,./testing) and the rationale for keeping the four narrowed sub-paths so a server action can importPluginRegistryfrom@ever-works/plugin-runtime/registrywithout dragging React into the server bundle, a test file can importcreateTestRegistryfrom@ever-works/plugin-runtime/testingwithout spinning up a JSDOM environment, and a host layout can import<SlotHost />from@ever-works/plugin-runtime/SlotHostto keep the React boundary explicit in bundle reports; the per-line walkthrough ofindex.tsthat pins each line to a documentation impact (the JSDoc preamble's React-aware-only-in-SlotHost/ owns-registry-loader-host / cross-link invariants, the registry value re-export, the loader value and type re-exports, the slot-host value and props re-exports, and the testing-helper value re-export with the explicit no-export typecompanion line because the helper's options object is an inline anonymous type); the value-vs-type contract that locks moving a name across the boundary as a breaking change and points at@typescript-eslint/consistent-type-exportsas the lint rule; the failure matrix that maps barrel-level mistakes (non-public sub-path import, value-vs-type mis-import onLoadPluginsResult, treeshake-strippedPluginRegistryconstructor, registry-instance mismatch betweenloadPluginsand<SlotHost />, droppedsideEffectsflag pulling the entire runtime into amergeConfigSources-only consumer, React leaking into a server bundle when a host action imports from the barrel instead of the narrowed./registrysub-path) onto the layer that surfaces them; and the public-surface change checklist that ties any addition / removal back to Spec Kit, thedocs/log.mdentry, and thepnpm tsc --noEmit/ Playwright verification step. - Plugin SDK Package Manifest Reference -- Per-source-file reference for the SDK package manifest paired with
packages/plugin-sdk/package.json: every load-bearing field (name,version,license,private,type: "module",sideEffects: false,main,types,exports."."/./capabilities/./slots,files,scripts.typecheck/scripts.lint,dependencies.zod,peerDependencies.reactwithpeerDependenciesMeta.react.optional, and thedevDependenciesset) with its purpose, why-it-matters note, and the change-event-class it implies for plugin authors; the sub-path map that locks the barrel-vs-narrowed contract (the two narrowed sub-paths are a strict subset of the barrel and resolve to the same module instance via Node's path-keyed module cache, andmanifest.ts/providers.ts/plugin.ts/index.tsdeliberately have no narrowed sub-path because their exports are types-only or single-author-facing-factory); the failure matrix that maps each manifest-level mistake (non-public sub-path import, CJS-without-import(), droppedsideEffectsflag, non-workspace:*specifier, React-18-typings, Zod-3-schema, public-name-without-exports-entry, file-without-barrel-re-export, breakingversionbump) onto the layer that surfaces them; and the public-surface change checklist that ties any field change to a SDK Public Surface cross-check, apackages.mdcross-check, anapps/web/package.jsonpeer-range / Zod-major propagation check, adocs/log.mdentry, an open-questions register entry, thepnpm tsc --noEmitand Playwright smoke-spec verification step, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability). - Plugin Runtime Package Manifest Reference -- Per-source-file reference for the runtime package manifest paired with
packages/plugin-runtime/package.json: every load-bearing field (name,version,description,license,private,type: "module",sideEffects: false,main,types,exports."."/./registry/./SlotHost/./loader/./testing,files,scripts.typecheck/scripts.lint,dependencies.@ever-works/plugin-sdkwithworkspace:*,dependencies.zod,peerDependencies.reactwithoutpeerDependenciesMeta.react.optional(unlike the SDK because<SlotHost />is a React function component), and thedevDependenciesset) with its purpose, why-it-matters note, and the change-event-class it implies for host-app authors; the sub-path map that locks the barrel-vs-narrowed contract (the four narrowed sub-paths are a strict subset of the barrel and resolve to the same module instance via Node's path-keyed module cache, and each one isolates a different concern --./registrykeeps React out of server-only callers,./loaderis the boot pipeline,./SlotHostmakes the React boundary explicit,./testingkeeps JSDOM out of server-side unit tests); the failure matrix that maps each manifest-level mistake (non-public sub-path import, server action importingPluginRegistryfrom the barrel instead of./registry, lowercasedslothost, CJS-without-import(), droppedsideEffectsflag, non-workspace:*specifier, runtime-version-diverges-from-SDK-version, host-installs-no-React, Zod-3-schema, public-name-without-exports-entry, file-without-barrel-re-export) onto the layer that surfaces them; and the public-surface change checklist that ties any field change to a Runtime Public Surface cross-check, a SDK Package Manifest cross-check, apackages.mdcross-check, anapps/web/package.jsonpeer-range / Zod-major propagation check, adocs/log.mdentry, an open-questions register entry, thepnpm tsc --noEmitand Playwright smoke-spec verification step, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability). - Plugin Demo Package Manifest Reference -- Per-source-file reference for the demo plugin package manifest paired with
packages/plugin-demo/package.json: every load-bearing field (name,version,description,license,private,type: "module",sideEffects: false,main,types,exports."."(single entry pointing at./src/index.tsxbecause the entry composes JSX),files,scripts.typecheck/scripts.lint,dependencies.@ever-works/plugin-sdkwithworkspace:*,dependencies.zod,peerDependencies.reactwithoutpeerDependenciesMeta.react.optional(becauseHeader.tsxships a slot component that always needs React), and thedevDependenciesset) with its purpose, why-it-matters note, and the change-event-class it implies for downstream plugin authors who copy this manifest as a starting point; the deliberately-empty sub-path map (no narrowed sub-paths because the demo is a leaf consumer with a singledefaultexport — narrowing would imply public structure insideHeader.tsx/config.tswhich the demo intentionally hides); themanifest.versionvs.package.json#versiondrift contract (the manifest version gatestemplateRange; the package version is workspace-graph metadata only); the.tsx-vs-.ts-extension-on-the-entry rationale (the entry composes JSX throughHeader.tsx, so.tsxopens the JSX scope underjsx: "preserve"); the failure matrix that maps demo-level manifestations onto the resolution / type-check / install layer (non-public sub-path import like@ever-works/plugin-demo/Header, CJS-without-import(), droppedsideEffectsflag, non-workspace:*SDK specifier,.tsx-flipped-to-.ts, React-18-typings, Zod-3-schemas,manifest.version/package.json#versiondrift,templateRangewidened beyond SDKversion, downstream-author-keeps-@ever-works-scope, downstream-author-keeps-private: true-while-publishing, downstream-author-keeps-required-React-peer-on-non-React-plugin); and the public-surface change checklist that ties any field change to a Reference Plugin cross-check, an SDK Package Manifest and Runtime Package Manifest cross-check (the three manifests move in lock-step onversion, Zod range, React peer range, andsideEffectsflag), apackages.mdcross-check, anapps/web/package.jsonlockfile cross-check, adocs/log.mdentry, an open-questions register entry, thepnpm tsc --noEmitand Playwright smoke-spec verification step, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability). - Plugin Package TypeScript Configurations -- Per-source-file reference for the three byte-identical
tsconfig.jsonfiles atpackages/plugin-sdk/tsconfig.json,packages/plugin-runtime/tsconfig.json, andpackages/plugin-demo/tsconfig.json: the shared five-line shape (extends: "@ever-works/tsconfig/base.json",compilerOptions.jsx: "react-jsx",compilerOptions.types: ["react"],include: ["src/**/*"],exclude: ["node_modules", "dist"]); the field-by-field walkthrough that pins each line to a documentation impact (the inherited base config'starget: "ES2017"lowering floor,lib: ["dom", "dom.iterable", "esnext"]DOM types,allowJs: trueescape hatch,skipLibCheck: truetransitive-typings opt-out,strict: trueload-bearing flag,noEmit: trueno-build-step posture,esModuleInterop: trueZod CJS-shim compatibility,module: "esnext"+moduleResolution: "bundler"ESM-with-bundler resolution,resolveJsonModule: trueJSON import support,isolatedModules: trueswc/esbuild compatibility,incremental: true.tsbuildinfocache); thereact-jsxautomatic-runtime rationale (noimport React from 'react'needed in.tsxfiles; the SDK'splugin.tsreferencesReact.ComponentTypetypes so the JSX scope must be open even where no JSX is authored); thetypes: ["react"]whitelist semantics (transitive@types/node/@types/jest/ DOM-polyfill packages cannot leak ambient types into the plugin's compilation); theinclude-and-excluderationale that locks the package boundary atsrc/and forward-guards against a futuredist/build step; the "How the three packages diverge from this baseline" matrix that lists every hypothetical override (types: ["react", "node"],declaration: true,outDir,composite: true,lib: ["esnext"], widenedincludefor scripts, narrowedexcludefor tests) with the reason it is and is not warranted today; the failure matrix that maps eachtsconfig.jsonmistake (JSX element implicitly has type 'any'from a dropped React-types entry,Cannot use JSX unless the '--jsx' flag is providedfrom a removed JSX flag,'process' is not definedfrom a missing Node-types entry, slowpnpm tsc --noEmitfrom anincremental: falseregression, stray@types/jestsymbols from a removedtypeswhitelist,dist/index.js has not been built from sourcefrom an accidentalnoEmit: false,'isolatedModules' may not be used with 'composite'from acomposite: trueoverride, the demo'sCannot find module 'react/jsx-runtime'symptom of a React-18 lockfile downgrade, and a downstream plugin's silent-strict-mode regression from a missedextendsdirective) onto the layer that surfaces them; and the public-surface change checklist that ties any option change to a matching package-manifest cross-check, an Authoring a Plugin cross-check, apackages.mdcross-check, the dualpnpm tsc --noEmitruns (workspace-root + per-package), adocs/log.mdentry, an open-questions register entry, and the Constitution-Check note in the PR description for Article II (TypeScript-Only) and Article III (Public-Surface Stability). - Shared ESLint Config (
@ever-works/eslint-config) -- Per-source-file reference for the workspace's shared ESLint flat config paired withpackages/eslint-config/nextjs.mjsandpackages/eslint-config/package.json: the at-a-glance summary (single sub-path./nextjs, default factorynextjsConfig(tsconfigPath = './tsconfig.json'), ESLint v9 flat config format,eslint@^9peer-dep, four direct deps@typescript-eslint/eslint-plugin,@typescript-eslint/parser,eslint-plugin-react,eslint-plugin-react-hooks); the file map (nextjs.mjsships the factory,package.jsondeclares the sub-path and deps); the per-block walk-through of the three flat-config blocks the factory returns (block 1ignoresfor**/node_modules/**,**/.next/**,**/out/**,**/build/**,**/dist/**,**/*.config.{js,ts,mjs}with the rationale for each; block 2 JS/TS shared rules for*.{js,jsx,ts,tsx}withreact-hooks/rules-of-hooks: 'error',react-hooks/exhaustive-deps: 'warn', the deliberate'no-unused-vars': 'off'to defer to the TS-aware variant,'no-console': 'off'to allow the structured-logging convention; block 3 TS-only typed rules for*.{ts,tsx}with the typed parser threadingparserOptions.project: tsconfigPath,@typescript-eslint/no-unused-vars: 'warn'with the^_prefix convention for_request,catch (_) { ... }, and head-discarded destructuring); thepackage.jsonfield-by-field walkthrough (name,version: '0.0.0',private: true,license: AGPL-3.0,exports."./nextjs", the four direct deps with their workspace-floor ranges, theeslint@^9peer-dep that pins the flat-config format); the consumer table that maps the four current consumers (apps/web,apps/docs,apps/web-e2e, plugin packages) onto how each callsnextjsConfig(...)and the Phase-D plan to wire the per-package lint gate; the failure matrix that maps each configuration mistake (Cannot find module '@ever-works/eslint-config/nextjs'from a lost sub-path entry,Parsing error: Cannot find module '@typescript-eslint/parser'from a stale ESLint-8 lockfile,Configuration for rule "react-hooks/rules-of-hooks" is invalidfrom a stale plugin pin,'_request' is defined but never usedfrom a re-enabled JSno-unused-vars,'console' is not definedfrom a flippedno-console, raised-to-errorreact/jsx-keyfrom a consumer override, invalidtsconfigPath,.next/-build-output linting from a removed ignore, tooling-config linting from a removed*.config.*ignore, eslintrc-syntax-mixed-with-flat-config from a regression,eslint@9.x not foundpeer-dep refusal) onto the layer that surfaces them; and the public-surface change checklist that ties any rule or field change to a Plugin Package TypeScript Configurations cross-check, an Authoring a Plugin cross-check, anapps/web/eslint.config.mjspropagation check, the workspace-rootpnpm lintrun, thepnpm installlockfile run, adocs/log.mdentry, an open-questions register entry, and the Constitution-Check note in the PR description for Article II (TypeScript-Only) and Article IX (Test Coverage Bar). - Shared TypeScript Presets (
@ever-works/tsconfig) -- Per-source-file reference for the workspace's shared TypeScript preset package paired withpackages/tsconfig/base.json,packages/tsconfig/nextjs.json,packages/tsconfig/playwright.json, andpackages/tsconfig/package.json: the at-a-glance summary (package name@ever-works/tsconfig,private: true,version: '0.0.0'pinned because consumed viaworkspace:*only, three preset files declared inpackage.json#files, six current consumers acrossapps/web,apps/web-e2e, and the three plugin packages, nodependencies/devDependencies/peerDependencies/scripts); the file map (base.jsonis the workspace's TypeScript posture,nextjs.jsonis the Next.js leaf,playwright.jsonis the Playwright leaf,package.jsondeclares the package andfilesarray); the per-field walk-through ofbase.json(target: 'ES2017'lowering floor,lib: ['dom', 'dom.iterable', 'esnext']ambient types,allowJs: truefor.mjstooling configs,skipLibCheck: truetransitive-typings escape hatch,strict: trueand the seven sub-flags it enables,noEmit: trueno-build-step posture,esModuleInterop: trueZod CJS-shim compatibility,module: 'esnext'+moduleResolution: 'bundler'ESM-with-bundler resolution,resolveJsonModule: trueJSON import support,isolatedModules: trueswc/esbuild compatibility,incremental: true.tsbuildinfocache that cuts CI lint times from ~45s to ~12s,exclude: ['node_modules']); the per-field walk-through ofnextjs.json(relativeextends: './base.json'for inheritance lock,jsx: 'react-jsx'automatic React-17+ runtime,plugins: [{ name: 'next' }]editor-only LSP plugin); the per-field walk-through ofplaywright.json(relativeextends: './base.json',types: ['node']whitelist, redundantnoEmit: truere-pin); the per-field walk-through ofpackage.json(name,version: '0.0.0',private: true,license: AGPL-3.0,fileswhitelist) plus the matrix of deliberately-absent fields (type,main,types,exports,dependencies,devDependencies,peerDependencies,scripts) and what each absence implies; the inheritance ASCII diagram showing the two leaves fanning out frombase.jsonand the three plugin packages bypassing the leaves to extendbase.jsondirectly; the consumer table that maps each of the six current consumers to itsextendstarget with the rationale for the choice (Next.js wantsreact-jsxand the LSP plugin, Playwright wants@types/nodeand no DOM globals, plugins wantreact-jsxwithout the LSP plugin); the deliberateapps/docsout-of-scope note (extends@docusaurus/tsconfigdirectly because Docusaurus ships its own preset for.mdxambient typings, tracked indocs/questions.md); the cross-cutting concerns walkthrough (target: 'ES2017'and what it covers / doesn't cover,module+moduleResolutionpair semantics,strictsub-flags,incrementalcache mechanics); the "How the leaves diverge from the base" matrix that lists every option each leaf adds or overrides plus the per-consumer concerns (paths,baseUrl,outDir,declaration,composite,targetoverride,liboverride) deliberately excluded from the leaves; the failure matrix that maps each preset-level mistake (droppedfilesentry, flippedprivate, addeddependencies, bumpedversion, removedtarget/strict/moduleResolutionfrom the base, switched a leaf'sextendsfrom relative to package-rooted self-reference, droppedjsx/pluginsfrom the Next.js leaf, droppedtypes: ['node']from the Playwright leaf or stuffed it with['jest'], addedcomposite: truewithout project references, flippedincremental: false) onto the layer that surfaces them (pnpm install mirror, per-consumertsc --noEmit, reviewer's eye, CI runtime regression); and the public-surface change checklist that ties any preset change to a Plugin Package TypeScript Configurations cross-check, a Shared ESLint Config cross-check, apackages.mdcross-check, the dualpnpm tsc --noEmitruns (workspace-root + per-package), adocs/log.mdentry, an open-questions register entry, the Playwright smoke run, and the Constitution-Check note in the PR description for Article II (TypeScript-Only) and Article III (Public-Surface Stability). - pnpm Workspace Manifest (
pnpm-workspace.yaml) -- Per-source-file reference for the monorepo's pnpm workspace declaration paired withpnpm-workspace.yamlat the repo root, the same waytsconfig-presets.mdpairs with the four files insidepackages/tsconfig/,eslint-config.mdpairs with the two files insidepackages/eslint-config/, and the per-package manifest references each pair with onepackages/*/package.json. Documents the at-a-glance summary (path at the repo root, YAML 1.2 format, singlepackagestop-level key, two globs"apps/*"and"packages/*", eight resolved members acrossapps/web,apps/docs,apps/web-e2e, and the fivepackages/*, micromatch glob engine, pinned topnpm@10.31.0viapackage.json#packageManager, Prettier*.ymloverride that pins YAML to spaces withtabWidth: 2); the file-contents walk-through (the three-line file with one row per field —packagesarray and the two globs); the "Why a glob, not an explicit list" rationale (new packages auto-register, removed packages auto-unregister, the convention scales to two roots without over-matchingapps/web/.content/**); the resolved-members table that maps each of the eight current members to its path, glob, and matching per-package reference page; the glob-semantics matrix that pins what"apps/*"and"packages/*"match versus do not match (one level deep, no**recursion); theworkspace:*resolution walk-through that traces the four-step chain pnpm performs at install time; the "Deliberately absent fields" matrix coveringcatalog/catalogs,linkWorkspacePackages,preferWorkspacePackages,sharedWorkspaceLockfile,saveWorkspaceProtocol,injectWorkspacePackages,overrides,peerDependencyRules,packageExtensions, andonlyBuiltDependencieswith the default behaviour we accept and why each one is not set today; the "Why this file lives at the repo root" rationale (pnpm walks up the directory tree and uses the firstpnpm-workspace.yamlit finds as the workspace anchor; same property asturbo.json); the consumer table that maps each reader (pnpm install,pnpm -r,pnpm --filter,turbo run, the script aliases likepnpm dev:web, and tooling that imports@pnpm/find-workspace-packages) to how it consumes this file; the failure matrix that maps each workspace-level mistake (file deleted, file renamed.yml, file moved out of root, globs narrowed, globs broadened to**, two members declared with the samename, YAML indentation mistake,packageskey renamed to Yarn'sworkspaces:, package added without apackage.json, package'snamechanged without updating consumers, glob uses Windows-style backslashes,apps/web/.content/accidental inclusion) onto the layer that surfaces them; and the public-surface change checklist that ties any glob change to apnpm installround-trip, aturbo run --dry-rundiscovery check, a Packages Overview cross-check, anapps/web/package.jsonlockfile cross-check, adocs/log.mdentry, an open-questions register entry, and the Constitution-Check note in the PR description for Article I (Plugin-First) and Article III (Public-Surface Stability). - Turborepo Pipeline Configuration (
turbo.json) -- Per-source-file reference for the monorepo's Turborepo task pipeline paired withturbo.jsonat the repo root, the second of the two root-level config references (the first ispnpm-workspace.md). Wherepnpm-workspace.mddocuments which folders become workspace members, this page documents what tasks those members can run, in what order, with what inputs. Documents the at-a-glance summary (path at the repo root, JSON-with-comments format,$schema: "https://turbo.build/schema.json", two top-level keys$schemaandtasks, six task entriesbuild,build:en,lint,dev,test:e2e,clean, four cached tasks plus two uncached, one persistent taskdev, local.turbo/cache backend with no remote-cache wired today); the file-contents walk-through (the full JSON file); the per-task field-by-field walkthrough —buildwithdependsOn: ["^build"]upstream-first ordering, theoutputs: [".next/**", "!.next/cache/**", "build/**", "dist/**"]artefact whitelist with the Next.js cache exclusion rationale, and the 19-entryenvallow-list (ANALYZE,AUTH_*,COOKIE_SECRET,CRON_SECRET,DATA_REPOSITORY,DATABASE_*,EMAIL_*,GH_*,GITHUB_*,NEXT_PUBLIC_*,PLATFORM_API_*,POLAR_*,POSTHOG_*,RESEND_*,SENTRY_*,SMTP_*,STRIPE_*,TRIGGER_DEV_*,VERCEL_*) with the per-family rationale for why each must be in the cache key;build:enwith the narrowedoutputs: ["build/**"]Docusaurus-only artefact set;lintwithdependsOn: ["^build"]so generated typings compile first;devwithcache: false+persistent: truefor the long-running watch process;test:e2ewithdependsOn: ["build"](no^prefix so the local-package build runs first) andcache: falsebecause Playwright runs are not deterministic functions of source content;cleanwithcache: falsebecause a delete operation is meaningless to cache; the workspace-and-task-graph composition walk-through showing howpnpm-workspace.yamlexpands → workspace members →package.json#dependenciesDAG → Turborepo's^buildrule → the four-stage build chainplugin-sdk → plugin-runtime + plugin-demo (parallel) → web; the "Why some tasks declareoutputsand others don't" matrix; theenvallow-list family-by-family table (Bundle analysis / Auth / Cookies / Cron / Content repo / Database / Email transport / GitHub integration / Public client vars / Platform API / Polar / PostHog / Resend / Sentry / SMTP / Stripe / Trigger.dev / Vercel) with the consumer file path and why each family must contribute to the cache key; the "Deliberately absent fields" matrix covering top-levelglobalDependencies,globalEnv,globalDotEnv,remoteCache,ui,daemon,concurrency, and per-taskinputs,passThroughEnv,dotEnv,cache: false on build,interactive,extendswith the default behaviour we accept and why each one is not set today; the "Why this file lives at the repo root" rationale (Turborepo walks up the directory tree and uses the firstturbo.jsonit finds as the workspace anchor; same property aspnpm-workspace.yaml); the consumer table that maps each reader (pnpm run build,pnpm run lint,pnpm run dev,pnpm run test:e2e, thedev:web/dev:docsscript aliases using--filter, CI'sturbo run build lint --filter, editor tooling that reads$schema) to how each consumes this file; the failure matrix that maps each pipeline-level mistake (file deleted, file renamed, file moved out of root, dropping the^fromdependsOnonbuild, removingdependsOnfrombuild, wideningoutputsto["**"], narrowingoutputsto drop.next/**, removing anenvfamily entry likeNEXT_PUBLIC_*, adding a literalMY_VAR=valuenon-pattern entry, removingcache: falsefromdevortest:e2e, removingpersistent: truefromdev, adding a new task withoutcache/outputs/envdecisions, changing the$schemaURL, JSON syntax errors, task-name collisions with a future per-packageturbo.json) onto the layer that surfaces each one; and the public-surface change checklist that ties any pipeline change to aturbo run --dry-runround-trip, a--summarizecache-key cross-check, a pnpm Workspace Manifest cross-check, anapps/web/.env.examplepropagation check, adocs/log.mdentry, an open-questions register entry, and the Constitution-Check note in the PR description for Article I (Plugin-First), Article III (Public-Surface Stability), and Article IX (Test Coverage Bar). - Workspace Hoisting Posture (
.npmrc) -- Per-source-file reference for the monorepo's pnpm install-time hoisting posture paired with.npmrcat the repo root, the fourth root-level config reference afterpnpm-workspace.md,turbo-config.md, andworkspace-root-manifest.md. Wherepnpm-workspace.mddocuments which folders become workspace members,turbo-config.mddocuments what tasks those members can run, in what order, with what inputs, andworkspace-root-manifest.mddocuments the version-pinning, native-build allow-list, and Prettier baseline for the entire repo, this page documents the install-time hoisting posture itself — the two load-bearing settings (shamefully-hoist=truethat flattens every transitive dependency into the workspace root'snode_modules/, andpublic-hoist-pattern[]=*@heroui/*that forces every@heroui/*package into the workspace-root public-hoist directory so HeroUI's internal-peer model resolves identically from every workspace member) plus the rationale for each (Next.js / ESLint / Tailwind / PostCSS plugin discovery from the project root for the first;@heroui/system/@heroui/shared-utils/ per-component peer resolution for the second), the.npmrcprecedence chain (system → user → project → env-vars → CLI flags) that explains why a personal~/.npmrccannot weaken the workspace's posture but a one-offpnpm install --shamefully-hoist=falseflag can produce the expected resolution-failure smoke signal, the failure matrix that pins each setting flip back to a concrete user-visible failure (Next.js font / MDX / built-in-provider resolverCannot find moduleerrors, ESLint plugin resolution failures, Tailwind plugin loader breakage, HeroUI internal-peer resolution failures showing up asuseTheme is not a functionor two-versions-of-React errors, equivalent-but-slowernode-linker=hoistedaliasing,auto-install-peers=falsepeer-dependency install drift, and CRLF / encoding parse errors on POSIX CI runners), the per-line walkthrough for the file's two lines (theshamefully-hoist=truerationale plus the two compile-and-lint-time safety nets in@ever-works/tsconfigand@ever-works/eslint-configthat catch the legitimate "imported a hoisted-but-not-declared package" cost; thepublic-hoist-pattern[]=*@heroui/*rationale and the[]array-syntax accumulation rule plus the leading-*glob safety choice), and the.npmrc-change checklist that ties any flip back to the four cross-cutting rules — keep the doc and the file in lock-step in the same PR, add a one-line entry indocs/log.mdunder the current date heading, cross-reference the Spec 002 — Plugin Architecture public-surface contract, and route the change through reviewer eyes since.npmrcflips affect every contributor'spnpm installoutcome and every CI runner's build environment. - Web App TypeScript Configuration (
apps/web/tsconfig.json) -- Per-source-file reference for the host web application's TypeScript configuration paired withapps/web/tsconfig.json, sitting one directory below the shared@ever-works/tsconfigpresets the same wayplugin-tsconfigs.mdsits one directory below those presets for the three plugin packages. Documents the at-a-glance summary table of every load-bearing field (theextends: "@ever-works/tsconfig/nextjs.json"chain that locks the workspace-wide TypeScript posture in one place; the singlecompilerOptions.pathsentry{ "@/*": ["./*"] }that is the Next.js convention for an internal-import alias rooted at the application's top-level directory and powers every internal import in the App Router; the six-entryincludearray —next-env.d.tsfor Next-generatedprocess.env.*andnext/imagetypings,**/*.tsand**/*.tsxfor every TypeScript and JSX source file because**/*.tsdoes not match.tsx,.next/types/**/*.tsfor thenext buildroute-typed-link declarations,scripts/generate-openapi.tsfor the OpenAPI-generation script that lives outside the App Router tree, and.next/dev/types/**/*.tsfor the Next 16 dev-server variant of typed routes; the singleexclude: ["node_modules"]entry); the full file annotated line-by-line with theextendschain explained (nextjs.json→base.json, lockingtarget: ES2017,module: esnext,moduleResolution: bundler,strict: true,noEmit: true,esModuleInterop: true,resolveJsonModule: true,isolatedModules: true,incremental: true,jsx: react-jsx, thenextLSP plugin,allowJs: true,skipLibCheck: true, and thedom/dom.iterable/esnextlib set); the "Why the@/*path alias" walkthrough with a four-row consumer table that traces each alias use back to the resolved file; the "Why the six-entryincludearray" walkthrough that pins each entry to a concrete failure if dropped (typed routes regress when.next/types/**/*.tsis dropped, dev-server typed routes regress when.next/dev/types/**/*.tsis dropped, the OpenAPI generator's type errors escape detection whenscripts/generate-openapi.tsis dropped, every.tsxsource file falls out of scope when**/*.tsxis dropped); the "Why theexcludeentry" rationale (resilience against a future preset change that might drop thenode_modulesexclude); the failure matrix that maps eachtsconfig.jsonmistake (extendsdropped → mass type errors,extendsswitched to a non-Next preset → JSX transform breaks,@/*alias dropped → every internal import that uses the alias fails to resolve,**/*.tsxdropped → JSX source files fall out of scope,.next/types/**/*.tsdropped → typed routes regress,node_modulesexclude dropped → orders of magnitude slower type-check) onto the layer that surfaces each one; the per-line walkthrough table; and thetsconfig.json-change checklist that ties any field change to antsconfig-presets.mdcross-check, adocs/log.mdentry, a Spec 002 — Plugin Architecture cross-link, the dualpnpm tsc --noEmitruns (workspace-root + per-app), and a reviewer pass. - E2E TypeScript Configuration (
apps/web-e2e/tsconfig.json) -- Per-source-file reference for the Playwright e2e suite's TypeScript configuration paired withapps/web-e2e/tsconfig.json, sitting one directory below the shared@ever-works/tsconfigpresets the same wayweb-app-tsconfig.mdsits one directory below those presets for the host web app andplugin-tsconfigs.mdsits one directory below them for the three plugin packages. Documents the at-a-glance summary table of every load-bearing field (theextends: "@ever-works/tsconfig/playwright.json"chain that inherits the workspace's TypeScript posture —target: ES2017,module: esnext,moduleResolution: bundler,strict: true,noEmit: true,esModuleInterop,resolveJsonModule,isolatedModules,incremental, thedom/dom.iterable/esnextlib set — plus the Playwright leaf'stypes: ["node"]whitelist that opens upprocess.env.*,URL,Buffer, and the rest of the Node ambient surface; the single-entryincludearray["./**/*.ts"]that scopes the type-checker to the suite's own source tree picking up the entiretests/tree undertests/api/,tests/admin/,tests/auth/,tests/client/,tests/i18n/,tests/public/,tests/smoke/, plusfixtures/,helpers/,page-objects/, and the four top-level globalsglobal-setup.ts,global-teardown.ts, andplaywright.config.ts; the singleexclude: ["node_modules"]entry); the full file annotated line-by-line with theextendschain explained (playwright.json→base.json, locking the workspace-wide compiler-options posture and adding the Node-ambient whitelist plus the redundant-but-deliberatenoEmit: truere-pin); the "Why theextendschain matters" walkthrough that lists every inherited compiler option and which preset layer contributes it; the "Why the singleincludeglob" walkthrough that maps each path underapps/web-e2e/(tests/api/**/*.ts,tests/admin/**/*.ts,tests/auth/**/*.ts,tests/client/**/*.ts,tests/i18n/**/*.ts,tests/public/**/*.ts,tests/smoke/**/*.ts,fixtures/**/*.ts,helpers/**/*.ts,page-objects/**/*.ts,global-setup.ts,global-teardown.ts,playwright.config.ts) onto whether the glob picks it up plus the deliberately-absent matches (no**/*.tsxfor the JSX-free suite, nonode_modules/**, noplaywright-report/**, notsconfig.tsbuildinfo, no adjacent workspace members); the "Why theexcludeentry" rationale (defensive re-statement against a future preset change that might drop the line); the "How the e2e suite diverges from the host app" matrix that pins every divergence betweenapps/web/tsconfig.jsonandapps/web-e2e/tsconfig.json(different leaf presetnextjs.jsonvsplaywright.json, no@/*alias for the e2e suite, the inheritedtypes: ["node"]whitelist swap for the host app's Next ambient layer, no**/*.tsxglob in the e2e suite, no.next/types/**/*.tsor.next/dev/types/**/*.tsentries, noscripts/generate-openapi.tsentry, the leading-./anchor on the e2e include glob); the failure matrix that maps eachtsconfig.jsonmistake (extendsdropped → mass type errors,extendsswitched tonextjs.json→ Node-ambient regression forprocess.env.*,extendsswitched tobase.jsondirectly → loses both the Node whitelist and thenoEmitre-pin,./**/*.tsglob narrowed totests/**/*.ts→global-setup.ts/global-teardown.ts/playwright.config.ts/fixtures//helpers//page-objects/fall out of scope,**/*.tsxadded → drift away from the suite's TS-only posture,node_modulesexclude dropped → orders of magnitude slower type-check,composite: trueadded →'isolatedModules' may not be used with 'composite'panic,noEmit: falseflipped →.jscontamination next to every.tsfile) onto the layer that surfaces each one; the per-line walkthrough table; and thetsconfig.json-change checklist that ties any flip back to atsconfig-presets.mdcross-check, adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link, the dualpnpm tsc --noEmitruns (e2e + workspace-root), the Playwright smoke run, and a reviewer pass. - E2E Package Manifest (
apps/web-e2e/package.json) -- Per-source-file reference for the Playwright e2e suite's package manifest paired withapps/web-e2e/package.json, the test-only manifest companion to the four runtime manifest references (workspace-root-manifest.md,runtime-package-manifest.md,sdk-package-manifest.md,plugin-demo-package-manifest.md). Where those four documents document the manifest of a host-app or library workspace member, this one documents the manifest of a test-only workspace member — a member that ships no runtime exports, nomain/types/exportsmap, declares everything indevDependenciesbecause the package is consumed only by the workspace itself, and deliberately omits adependenciesblock. Documents the at-a-glance summary table of every load-bearing field (thename: '@ever-works/web-e2e'workspace identifier thatpnpm --filter @ever-works/web-e2eand Turborepo'stest:e2etask resolve through; theversion: '0.0.0'symbolic-only pin justified byprivate: true; theprivate: truehard-block onpnpm publish; thelicense: 'AGPL-3.0'workspace-wide license inheritance; the five Playwrightscripts.*entriestest:e2e/test:e2e:ui/test:e2e:chromium/test:e2e:headed/test:e2e:debugand the no-opscripts.lintecho that lets the workspace-widepnpm -r lintwalk this member without a per-package opt-out; the fourdevDependencies@ever-works/tsconfigwithworkspace:*for the in-tree TypeScript preset chain extended bye2e-tsconfig.md,@playwright/test^1.58.2for the runner this manifest gates,@faker-js/faker^10.1.0for synthetic data whene2e-test-data.md'sTEST_DATA.generate*()is too coarse,dotenv^16.4.7for the cross-app.env.localloadplaywright-config.mdperforms at boot,typescript^5matching the workspace's pin); the file-contents walk-through (the full JSON file); the per-field walkthrough that pins each field to a concrete responsibility — the three load-bearing properties ofname(the@ever-works/scope match for thepnpm --filter '@ever-works/*'glob, theweb-e2esuffix mirroring the directory name sopnpm-workspace.yaml'sapps/*glob auto-registers it, and the presence of anameat all being what makes the directory a workspace member), the three rejectedscripts.lintalternatives (drop the script →pnpm -r lintexits non-zero withmissing script "lint", wire up a real lint → duplicates thepnpm tsc --noEmitsafety net and requires anextjsConfiginvocation the suite doesn't use, switch totrue→ works but says nothing), the three rationales for thedevDependencies.@ever-works/tsconfigworkspace:*specifier (always tracks the workspace's TypeScript posture, letspnpm installresolve from in-tree source, matches every other@ever-works/*workspace member's convention), the three required cross-checks for a@playwright/testmajor bump (aplaywright-config.mdcross-check, anauth-fixture.mdcross-check, apnpm installround-trip), and the rationale for@faker-js/fakeranddotenvbeing indevDependenciesrather than runtime helpers (they are consumed only at test-runner time and at config-boot time respectively); the deliberately-absent fields matrix (nodescription/homepage/repository/bugs/author/keywordsbecause those surface only on published packages, noengines/packageManager/pnpm.*/prettierbecause those are inherited fromworkspace-root-manifest.md, notypebecause Playwright's CLI handles both ESM and CJS spec files and the suite'stsconfig.jsonsetsmodule: 'esnext'directly, nomain/types/exports/bin/peerDependencies/peerDependenciesMeta/filesbecause the package is a leaf consumer with no public surface, nodependenciesbecause every reach for a third-party library is at test-runner time, noscripts.dev/scripts.build/scripts.startbecause the suite has no dev or build step); the consumer table that maps each reader (pnpm installresolving theapps/*workspace glob,pnpm --filter @ever-works/web-e2e <script>, Turborepo'stest:e2etask, CI workflows, the Playwright runner's CLI walk-up, TypeScript'stsc --noEmitgate, Renovate / Dependabot for upgrade-PR generation, editors for workspace-treeing) to the fields it consumes; the failure matrix that maps each manifest-level mistake (drop thenamefield → workspace member becomes unaddressable, rename to a non-@ever-works/*scope → CI'spnpm --filter '@ever-works/*'glob silently skips the package, dropprivate: true→ nextpnpm publish -rpushes@ever-works/web-e2e@0.0.0to the npm registry, drop thelicensefield → AGPL-3.0 inheritance breaks on root drift, dropscripts.test:e2e→ Turborepo'stest:e2etask fails withCouldn't find script, dropscripts.lint→ workspace-widepnpm -r lintexits non-zero, switch the no-opscripts.lintto a real lint without wiringeslint.config.mjs→Cannot find module 'eslint', dropdevDependencies.@playwright/test→pnpm test:e2eexits with module-not-found, drop or changedevDependencies.@ever-works/tsconfigfromworkspace:*→Cannot find base configerrors at TS gate, dropdevDependencies.dotenv→playwright.config.ts'simport dotenvfails at runner-boot and host-app env-driven branches flap, dropdevDependencies.@faker-js/faker→ faker-using specs fail at runner-boot while simpler specs mask the breakage, dropdevDependencies.typescript→ workspace-wide TS gate fails, tighten the Playwright range to1.58.2→ Renovate stops opening minor-bump PRs, loosen to*→ next 2.x major silently lands and breaks the suite en masse, move the file → workspace member silently no-ops, add adependenciesblock → implies a runtime contract the suite does not have, add"type": "module"→ risks regression on future Playwright CJS-only utility, bumpversionaway from0.0.0→ drift between field andprivate: trueconstraint) onto the layer that surfaces each one; the per-line walkthrough table that pins each line of the 21-line file to its purpose; and thepackage.json-change checklist that ties any field change to the appropriate cross-check (pnpm-workspace.mdonnamechange,playwright-config.mdon Playwright or dotenv change,e2e-tsconfig.mdon tsconfig or typescript change,auth-fixture.mdon Playwright major bump,e2e-test-data.mdon Faker major bump,turbo-config.mdon new workspace-spanning script,workspace-root-manifest.mdonengines/packageManager/prettier/pnpm.*posture divergence), apnpm installround-trip, a dualpnpm tsc --noEmitrun (workspace-root + e2e), a smoke-subset Playwright run (pnpm --filter @ever-works/web-e2e test:e2e:chromium), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Fixtures Barrel (
apps/web-e2e/fixtures/index.ts) -- Per-source-file reference for the Playwright e2e suite's fixtures-directory barrel module paired withapps/web-e2e/fixtures/index.ts, the directory-level public-surface companion toauth-fixture.md(which the barrel re-exports from). Whereauth-fixture.mddocuments the authenticated-fixture boundary (how the persisted storage states minted at pre-flight become per-test isolated browser contexts), this page documents the directory-level fixture-export boundary — what thefixtures/directory exposes as a single import target, what the import-from-the-file versus import-from-the-directory shapes guarantee, and how future fixture modules compose through this barrel. Documents the at-a-glance summary table of every load-bearing element (the singleexport { test, expect } from './auth.fixture're-export statement that forwards bothtestandexpectso the canonicalimport { test, expect } from '../../fixtures'shape resolves through the barrel; the.tsfile extension matching the suite's TS-only posture documented ine2e-tsconfig.mdand theinclude: ["./**/*.ts"]glob; the deliberately single-line file body that keeps the barrel tightly scoped to re-exports only — adding logic here defeats the "directory-level public surface" intent and invites drift fromauth-fixture.md; the trailing newline matching the workspace's PrettierendOfLine: lfposture inherited from theworkspace-root-manifest.mdprettierblock); the full file annotated line-by-line with the three load-bearing properties of the re-export (forwarding bothtestANDexpectto prevent the "importedtestfrom one place butexpectfrom another" anti-pattern that breaks Playwright's test soft-failure aggregation, the relative./auth.fixturesource path that resolves throughmoduleResolution: "bundler"without going through anypathsmapping, the baretestandexpectnames without renaming because Playwright's runner contract requires the test function be namedtest); the "Why a barrel module instead of importing the file directly" walkthrough that pins the three failure modes of file-direct imports (a new fixture module's exports cannot be composed into the sametestrunner without touching every consumer, a future move ofauth.fixture.tswould force every consumer to update its import, the "import from the directory" shape is the JavaScript ecosystem's lingua franca that the barrel teaches the suite to speak) against the lowest-cost composable barrel shape; the "Why a single re-export and notexport *" walkthrough that pins the three failure modes ofexport *(type-only exports are dropped without an additionalexport type *companion, the barrel's intent becomes opaque, implicit re-exports surface accidental additions of internal-only helpers likegetAuthState()/requireAuthState()/AUTH_FIXTURES) against the lowest-coupling named-re-export shape; the failure matrix that maps each barrel-level mistake (drop the re-export ofexpect→ consumer imports fail to resolve, drop the re-export oftest→ suite cannot author authenticated specs through the barrel, switch toexport *→ type-only exports drop silently and future internal helpers leak, renametesttoe2eTeston the way out → spec authors' editors flag everytest(...)call and the suite drifts away from Playwright convention, switch the source path to a package-rooted self-reference →Cannot find modulebecause the suite has nopathsmapping, add helper code inside the file → the barrel's "directory-level public surface" intent breaks, move the file toapps/web-e2e/fixtures.ts→ directory shape goes away and future fixture modules cannot live alongside the barrel, ship the file with a CRLF line ending → Prettier diff on POSIX CI runners, drop the trailing newline → Prettier diff and CI lint failure, switch the file extension to.tsx→include: ["./**/*.ts"]glob does not match and the file falls out of the type-checker's scope, author a parallel barrel inhelpers/index.ts→ two directory-level public surfaces compete and specs drift between import paths) onto the layer that surfaces each one; the per-line walkthrough table; and theindex.ts-change checklist that ties any re-export change to anauth-fixture.mdcross-check (every symbol re-exported here originates there today; a new auth fixture export must land inauth.fixture.tsfirst, then flow through this barrel), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob already picks upfixtures/index.ts; if the glob narrows or if the file moves, the type-checker stops walking the file), ane2e-package-manifest.mdcross-check (the package'sdevDependencies.@playwright/testunderwrites thetestandexpecttypes this barrel forwards; a Playwright major bump may change the type signatures), every consumer (today's authenticated specs underapps/web-e2e/tests/admin/andapps/web-e2e/tests/client/import from'../../fixtures/auth.fixture'; new specs should import from'../../fixtures'to use the barrel; consumer audit on every PR keeps the convention consistent), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run that confirms the runner discovers the same specs as before, the authenticated specs see the same fixtures, and trace recordings remain isolated per spec, adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new fixture pair that affects test authoring conventions, and a reviewer pass. - Playwright Auth Fixture (
apps/web-e2e/fixtures/auth.fixture.ts) -- Per-source-file reference for the Playwright e2e suite's authenticated-context fixture paired withapps/web-e2e/fixtures/auth.fixture.ts, the authenticated-fixture companion toglobal-setup.md(which mints the persisted authentication storage states),global-teardown.md(today a no-op placeholder), ande2e-test-data.md(which exportsADMIN_STATE_FILEandCLIENT_STATE_FILE). Whereglobal-setup.mddocuments the suite's pre-flight boundary,global-teardown.mddocuments the suite's post-flight boundary, ande2e-test-data.mddocuments the suite's shared-data boundary, this page documents the suite's authenticated-fixture boundary — how the persisted storage states minted at pre-flight become per-test isolated browser contexts. Documents the at-a-glance summary table of every load-bearing element (/* eslint-disable react-hooks/rules-of-hooks */file-scoped directive that suppresses the false-positive flag on Playwright'suseparameter name;import { test as base, type Page, type BrowserContext } from '@playwright/test'with the mandatoryas baserename to free uptestfor the local export and the type-only imports that stay out of the runtime bundle;import fs from 'fs'+import path from 'path'for therequireAuthState()existsSynccheck and thepath.resolve(__dirname, '..', ...)absolute-path computation;import { ADMIN_STATE_FILE, CLIENT_STATE_FILE } from '../helpers/test-data'so the fixture never types the literal'auth-states/admin.json';ADMIN_STATE_PATH/CLIENT_STATE_PATHresolved-once-at-module-load absolute paths with the__dirname-anchored shape that surviveswebServer.cwd: '../..';requireAuthState(filePath)fail-fast guard that throws with a contributor-actionable message naming the file path AND the most likely causeSEED_ADMIN_EMAIL/SEED_ADMIN_PASSWORD; theAuthFixturestype with the four fixture names (adminContext: BrowserContext,adminPage: Page,clientContext: BrowserContext,clientPage: Page); the fourbase.extend<AuthFixtures>(...)factories with theadminContextdepending onbrowserandadminPagedepending onadminContext— the only shapes that load the storage state at context creation rather than after-the-fact; the per-testawait context.close()/await page.close()teardown that prevents memory leaks under high-parallelism workers; and theexport { expect } from '@playwright/test're-export that saves every spec one import line and prevents the "importedtestfrom one place butexpectfrom another" anti-pattern that breaks Playwright's test-soft-failure aggregation); the full file annotated chunk-by-chunk; the "How a spec uses the fixture" walkthrough showing the canonicalimport { test, expect } from '../../fixtures/auth.fixture'shape and theasync ({ adminPage }) => { ... }parameter destructure pattern; the "Why a fixture instead of atest.beforeEach()hook" walkthrough that pins the three failure modes of the hook approach (testInfo.contextis not a public stash, teardown drift acrossbeforeEach/afterEach, every test pays the cost regardless of whether it destructures the fixture) against the lazy-composition fixture model; the "WhyBrowserContextper fixture, not a shared one" walkthrough that pins the three failure modes of the shared-context optimisation (cross-test cookie /localStoragepollution, parallel-page races on sharedlocalStorage/IndexedDB/ service-worker scope, per-test trace recordings contain operations from every test) against the fresh-context-per-test isolation; the failure matrix that maps eachauth.fixture.tsmistake (drop therequireAuthStateguard → cryptic 30-s timeout instead of fail-fast, switch tofs.statSync→ low-levelENOENTinstead of contributor-actionable message, drop thepath.resolve(__dirname, '..', ...)shape → relative paths resolve against the wrongwebServer.cwd, hard-code the literal'auth-states/admin.json'→ drift on a future directory rename, drop theas baserename → TypeScript redeclaration error, drop theAuthFixturestype parameter → loss of IntelliSense and typo-driven runtime errors, reuse a sharedBrowserContext→ cross-test pollution and races, drop theawait close()teardown → OOM under high parallelism, drop there-export { expect }→ spec drift back to dual imports breaking soft-failure aggregation, switch theadminContextfactory's dependency frombrowsertocontext→ silent breakage of the "pre-loaded with admin auth" guarantee, switch theadminPagefactory's dependency fromadminContexttopage→ silent breakage of the "authenticated page" guarantee, move the file fromapps/web-e2e/fixtures/→Cannot find moduleon every consumer import, add a third fixture pair without updating theAuthFixturestype → new fixture not destructurable from spec, remove theeslint-disabledirective → CI lint failure on the false-positive flag) onto the layer that surfaces each one; the per-line walkthrough table; and theauth.fixture.ts-change checklist that ties any fixture change to aglobal-setup.mdcross-check (the storage-state files this fixture reads are the filesglobal-setup.tswrites), aglobal-teardown.mdcross-check (the future cleanup bucket will use the sameAUTH_STATE_DIRconstant), ane2e-test-data.mdcross-check (ADMIN_STATE_FILE/CLIENT_STATE_FILEand any future role constants live there), aplaywright-config.mdcross-check (thewebServer.cwdresolves the relative paths), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob picks up this file), every authenticated spec underapps/web-e2e/tests/admin/andapps/web-e2e/tests/client/(they all import{ test, expect }from this file via the relative path), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run of the admin / client spec set that confirms the fixture loads both storage-state files (norequireAuthStatethrow), the admin specs see admin-only routes, the client specs see client-only routes, and each spec's trace contains operations from only that spec, adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if a new auth role or fixture pair is added, and a reviewer pass. - Workspace
.gitignore(.gitignore) -- Per-source-file reference for the monorepo's workspace-root git-ignore manifest paired with.gitignore, the single git-ignore boundary that gates every file the workspace-rootgit status, everypnpm install, everypnpm tsc --noEmit, everypnpm build, everypnpm test:e2erun, and every CIactions/checkoutstep decide whether to track. Sits at the workspace root the same waypnpm-workspace.mdsits at the root for workspace membership andturbo-config.mdsits at the root for task orchestration. Wherepnpm-workspace.mddocuments the workspace-membership boundary andturbo-config.mddocuments the task-graph boundary, this page documents the tracked-file boundary — which artefacts the workspace deliberately keeps out of source control, why each pattern is in the file, what the consumers expect, and what the failure modes look like when an entry drifts. Documents the at-a-glance summary table of every section (# dependenciescoveringnode_modules/.pnp/.pnp.*/.yarn/*/!.yarn/patches/!.yarn/plugins/!.yarn/releases/!.yarn/versions/.pnp.jsfor npm + Yarn PnP + Yarn Berry;# turbocovering Turborepo's per-task.turbocache directory documented inturbo-config.md;# testingcoveringcoverage/**/auth-states//**/test-results//**/playwright-report//**/.playwright/for the Playwright suite's runtime artefacts and the auth-state cache documented inauth-fixture.mdandglobal-setup.md;# next.jscovering.next/build output and the legacyout/static-export target;# docusauruscovering**/build/and**/.docusaurus/for the docs workspace member atapps/docs/;# productioncovering genericdist;# misccovering.DS_Storeand*.pem;# debugcovering all four package-manager debug logs;# env filescovering the security-critical.env*glob plus!.env.examplere-include — the single most important block for the workspace's secret posture;# vercelcovering.vercel;# typescriptcovering*.tsbuildinfoandnext-env.d.ts;# contentcovering.content(the Git-CMS content directory cloned at runtime byapps/web/scripts/clone.cjsfromDATA_REPOSITORY) andanalyze/;# vscode AI rulescovering the single-file.github/instructions/codacy.instructions.mdexclusion;# cachecovering.cacheand the duplicate.pnpm-debug.logpattern;# OpenAPI backupscovering the threepublic/openapi.backup.json/**/*.backup.openapi.json/**/openapi.backup.jsonpatterns that exhaustively cover theapps/web/scripts/generate-openapi.tsscript's backup output;# claudecovering the.claudeper-checkout state directory); the full file annotated section-by-section; the "Why a single workspace-root.gitignoreand not per-package files" walkthrough that pins the three rejected alternatives (per-package.gitignorefor each workspace member multiplying maintenance burden,.gitignoreonly at directories that need extra exclusions creating redundancy with the**/-anchored root patterns, workspace-root.gitignoreplus per-developer.gitignore_globalforcing every contributor to configurecore.excludesFile) against the single-file posture; the "Why**/auth-states/and not justauth-states/" walkthrough that pins the three reasons for the**/anchor (resolves at any depth so the actualapps/web-e2e/auth-states/location is matched, future-proofs against a refactor that moves the e2e suite, symmetric with the other test patterns) plus the security posture that the persisted-storage-state files contain NextAuth session cookies and a leaked file is a leaked admin / client account; the "Why.env*plus!.env.exampleand not a positive include list" walkthrough that pins the three reasons for the glob-then-re-include shape (defence in depth against future Next env-file conventions like a hypothetical.env.preview, defence against typos like.env.locall,.env.examplebeing the documentation surface contributors should commit while every other variant stays out); the "Why.contentis gitignored" walkthrough that pins the three reasons (lives in a separate repo atDATA_REPOSITORY, regenerated on everydev/buildby the idempotentapps/web/scripts/clone.cjs, per-deployment customisation requires the gitignore as the single chokepoint that prevents customer content leaking back into the template); the "Why.claudeis gitignored" walkthrough that pins the three reasons (per-developer state, cache freshness, the workspace ships its rules throughCLAUDE.md/AGENTS.md/.specify/rather than per-checkout.claude/); the "Why the OpenAPI backup patterns repeat across**/" rationale that pins the three patterns to their distinct coverage shapes (canonical-pathpublic/openapi.backup.json, suffix-pattern**/*.backup.openapi.json, bare-name**/openapi.backup.json); the failure matrix that maps each gitignore-level mistake (dropnode_modules→pnpm installoutputs hundreds of thousands of files, drop.next/→ ~10k generated files pernext dev, drop**/auth-states/→ security regression with persisted NextAuth cookies committed, narrow**/auth-states/toauth-states/→ security regression with the actual nested location becoming trackable, drop**/test-results/→ trace / video / screenshot pollution, drop**/playwright-report/→ HTML report pollution, drop.env*→ security regression with production secrets leaking on the first commit, drop!.env.example→ the canonical env-var list becomes un-trackable on certain creation orders, switch.env*to a positive list → future Next env conventions silently leak, drop.content→ Git-CMS content gets committed and diverges fromDATA_REPOSITORY, drop.vercel→ Vercel CLI per-project link gets committed, drop*.tsbuildinfo→ cross-developer cache contamination, dropnext-env.d.ts→ contributors with different Next versions diverge on the file, drop.turbo→ cache invalidation drifts across developers, drop**/build/→ Docusaurus output gets committed, drop**/.docusaurus/→ Docusaurus internal cache gets committed, drop*.pem→ security regression with TLS / SSH keys committed, drop.DS_Store→ cross-platform contributors see noise diffs, drop the*-debug.log*patterns → per-package-manager debug logs get committed, drop.cache→ generic-library caches get committed, dropanalyze/→ bundle-analyzer HTML output gets committed, drop.claude→ per-developer Claude Code session state gets committed, drop the OpenAPI backup patterns → backup files get committed every time the OpenAPI script runs, drop the Codacy line → editor-specific Codacy ruleset gets shipped to every contributor, add.idea//.vscode//.cursor/to the file → per-developer editor configs leak across the team, switch a**/-anchored pattern to a bare pattern → the actual nested location becomes trackable, move the file from.gitignore→ git stops reading any pattern in this file, switch the file extension to.gitignore.txt→ same as above, ship the file with a CRLF line ending → older git versions misparse the patterns and the file appears empty) onto the layer that surfaces each one; the per-section walkthrough table that maps every section of the file to its purpose; and the.gitignore-change checklist that ties any change to agit statuscleanliness verification after each of the four primary workflows (pnpm install/pnpm dev/pnpm build/pnpm test:e2e), anauth-fixture.mdcross-check (the**/auth-states/security boundary), aglobal-setup.mdcross-check (themkdirSync('auth-states/')call writes into the gitignored directory), ane2e-test-data.mdcross-check (theAUTH_STATE_DIRliteral must match the gitignore pattern), aplaywright-config.mdcross-check (the Playwright config'soutputDir/reporter/webServer.cwd/ trace / video / screenshot settings determine what appears in the worktree), aworkspace-root-manifest.mdcross-check (a newscripts.*entry that writes new files to the worktree may require a new entry), aturbo-config.mdcross-check (the.turbocache directory boundary), the.env.examplecross-check (any change to the# env filesblock must verify the canonical env-var list remains trackable), the CIactions/checkoutcross-check (CI steps that rungit statusto verify worktree cleanliness would catch a regression), a reviewer pass against the failure matrix, adocs/log.mdentry, and a Spec 010 — E2E Test Coverage cross-link if the change introduces a new test-related exclusion. - E2E Language-Switcher Page Object (
apps/web-e2e/page-objects/public/language-switcher.page.ts) -- Per-source-file reference for the Playwright e2e suite's header locale-switcher driver paired withapps/web-e2e/page-objects/public/language-switcher.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andstar-rating-page-object.mddocuments the suite's per-item rating-picker driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's locale-switching driver boundary — the smallest possible page object that lets a spec drive the global header language-switcher dropdown end-to-end (open the dropdown by clicking thearia-label="Select language"trigger button, select any locale by its full localized native display name like"Français"/"Español"/"Deutsch"/"العربية"/"中文"via thearia-label="Switch to ${fullName}"per-locale option button, read the trigger button's current text content upper-cased to expose the active locale code, and read thearia-expandedattribute to assert that the dropdown is open). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class LanguageSwitchersingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePageAND is also consumed insideselectLanguage(fullName)to construct the per-locale option Locator at call-time against page-level scope because the dropdown may be portal-rendered; thereadonly button: Locatorpage.locator('button[aria-label="Select language"]').first()exact-match selector pinned to the deliberate English literal"Select language"(not localized) for strict-mode-correctness AND so a user landing on a page in a language they cannot read can still find the switcher; theconstructor(page: Page)that stores thepageand pre-binds the trigger Locator in a single pass without asuper(page)call; theopen()minimal "open the dropdown" primitive every other action method composes against; theselectLanguage(fullName: string)composite "open the dropdown then click the per-locale option button" primitive with the load-bearing full localized native display name parameter ("Français"not"French"and not"fr") reflecting the canonical "language picker shows each language in its own language" UX convention so a non-English speaker can find their language, and the page-level Locator construction (not container-scoped) because portal-rendered dropdowns are not DOM descendants of the trigger; thegetCurrentLocaleCode(): Promise<string>accessor that reads the trigger button's text content viatextContent()?.trim().toUpperCase() ?? ''with the load-bearing.toUpperCase()casing-fold tolerating future production-source casing drift, the?.trim()whitespace-tolerance, and the?? ''nullish-coalesce that pins the public return type toPromise<string>; theisOpen(): Promise<boolean>accessor that reads thearia-expandedattribute and returns the strict-equality comparisonexpanded === 'true'collapsing both the missing-attribute case and thearia-expanded="false"case into a definitivefalsereturn that pins the boolean result type toPromise<boolean>without a bespoke null-narrowing branch); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/language-switcher.spec.ts(trigger visibility on/, dropdown opens viaaria-expanded === 'true', French selection navigates to/fr, Spanish selection navigates to/es); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the global header surface, reusability across all role trees, constructor parity with non-page widgets); the "Why the trigger pins the Englisharia-label="Select language"" walkthrough that pins the three reasons (the host app deliberately does not translate this label so a user landing on a page in a language they cannot read can still find the switcher, strict-equality survives a futurearia-label="Choose region"/aria-label="Switch currency"related-control regression, no production-source change required); the "Why per-locale options pinaria-label="Switch to ${fullName}"" walkthrough that pins the three reasons for the localized-display-name match (the language-picker UX convention, the consuming-spec mental model thatselectLanguage("French")will fail-loudly to teach the convention, no production-source change required); the "Why the option Locator does not carry.first()" walkthrough that pins the three reasons for the asymmetry (per-localearia-labelvalues are unique by design, strict-mode signal preservation against future duplicate-button regressions, symmetric posture with sibling driver per-mode buttons); the "Why.first()on the trigger button" walkthrough that pins the three failure modes of dropping it (strict-mode collision against future second switcher, against an already-rendered duplicate, against an admin-shell mirror); the "Why the constructor usesthis.page.locator(…)and not the inheritedheaderscope" walkthrough; the "WhygetCurrentLocaleCode()upper-cases the result" walkthrough that pins the three reasons (casing-drift tolerance, ISO-639-1 UI convention, defensive symmetry with sibling drivers); the "WhyisOpen()checksaria-expanded === 'true'" walkthrough that pins the three reasons (the ARIA attribute is the screen-reader contract, resilience to portal-rendered dropdowns, string-literal equality narrows the boolean return); the failure matrix covering every language-switcher-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop onpageorbutton,aria-label*="language"substring swap that breaks against"Choose region"siblings, i18n-wiring of the trigger label that breaks under non-English baselines,data-testidswap,.first()drop onbutton, visible-text match onselectLanguagethat breaks under flag-emoji-prefixed labels, locale-code parameter swap onselectLanguagethat breaks the convention, container-scoped per-locale Locator that breaks portal-rendered dropdowns,.first()add on per-locale Locator that masks duplicate-button regressions,?.trim()drop ongetCurrentLocaleCode,.toUpperCase()drop,?? ''drop, sibling-selector swap onisOpenthat breaks portal-rendered dropdowns, file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec, future smoke / a11y specs that drive remaining locale flows likeselectLanguage('Deutsch')/selectLanguage('العربية')/selectLanguage('中文'), the header production-source component for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto trigger-aria-label-localization, per-locale-aria-label-rename, locale-name-rename, casing-drift (invariant-by-design), portal-dropdown-render-change (invariant-by-design),aria-expanded-removal, locale-disable, middleware-prefix, andbaseURL-change failures; and thelanguage-switcher.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/language-switcher.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (the trigger'saria-label, the per-locale optionaria-label, thearia-expandedattribute, the trigger's text-content shape), anext-intlconfiguration cross-check (the locale set the production source surfaces in the dropdown is the set every consuming spec can pass toselectLanguage()), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future authenticated variant would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the language-switcher spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Language Switcher"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Map Page Object (
apps/web-e2e/page-objects/public/map.page.ts) -- Per-source-file reference for the Playwright e2e suite's Map View page driver paired withapps/web-e2e/page-objects/public/map.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root anditem-detail-page-object.mddocuments the suite's per-item detail-page driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's Map View driver boundary — the smallest possible page object that lets a spec drive the Map View feature (Spec 017) end-to-end (navigate to the dedicated/maproute viaBasePage.goto(), query thedata-testid="map-view"markers container or thedata-testid="map-empty-state"empty placeholder to detect whether the feature rendered successfully on environments without provider keys / coordinates, query thedata-testid="map-sidebar"rail and thedata-testid="map-sidebar-card"per-item cards inside it for sidebar interaction flows, locate the headerMapnavigation link via the inheritedheaderLocator scoped torole="link"and the case-insensitive exact-match/^Map$/regex anchored on the translationHEADER_MAPrendering as"Map"inen, locate the listing-page view-toggle Map button via the substring-matchedbutton[aria-label*="map" i]selector with.first()strict-mode-correctness append, and surface the mobile-responsiveShow map/Show listaccessible-name-matched buttons via the case-insensitive/show map/i//show list/iregexes that survive the host theme's casing drift). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; theexport class MapPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the eightreadonly Locatorfields coveringmapView/mapEmptyState/mapSidebar/sidebarCards/mapHeaderLink/viewToggleMapButton/showMapButton/showListButton; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via fourgetByTestIdselectors / one inherited-header-scopedgetByRole('link', …)exact-match / onearia-label*="map" isubstring-with-case-insensitive-flag plus.first()/ two case-insensitive accessible-name-matched buttons; thenavigate()dedicated/maproute navigation primitive via inheritedgoto(); theisPageRendered(): Promise<boolean>graceful-degradation accessor with the OR-of-two-paths overmapViewandmapEmptyStateand the.catch(() => false)error shields on bothisVisible()calls); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 017 — Map View for Listings and the consuming spec atapps/web-e2e/tests/public/map.spec.ts(the dedicated/maproute returns 200 when location features are enabled or 404 when they are gated off, the listing-page view-toggle Map button is visible when the feature is available, the headerMapnavigation link tracks theheader.map_enabledconfig gate, and the sidebar card highlights itself witharia-current="true"on click when markers are present); the "Why the class extendsBasePage" walkthrough that pins the three load-bearing reasons (page-route navigation via inheritedgoto(), globalheader/footer/navLinkschrome surfaced through inherited composite getters and consumed bymapHeaderLink'sthis.header.getByRole(...)scope reduction,waitForPageReady()post-navigation stabiliser); the "Why the view-toggle usesaria-label*="map" i" walkthrough that pins the three reasons (host-theme i18n drift acrossaria-label="Map view"/"Show as map"/"Map layout", casing drift across"Map"/"map"/"MAP"handled by theiflag,.first()strict-mode-correctness against future "Map settings" / "Open in map" siblings); the "WhyisPageRendered()accepts the empty-state path" walkthrough that pins the three reasons (dev environments without provider keys must succeed, the empty-state is a real production-source render path on tenants with location feature enabled but no items with coordinates,.catch(() => false)shields against transient Locator-resolution failures during DOM reflow); the failure matrix covering every map-page-level mistake (type-only import drop,extends BasePageremoval,readonlydrop on any Locator, CSS-class / element-tag re-bind on any of the fourdata-testidLocators,mapEmptyStatefield drop,.first()drop onviewToggleMapButton,iflag drop on the substring selector, exact-match re-bind toaria-label="Map view", non-anchored/Map/regex onmapHeaderLink, case-insensitive/^Map$/ire-bind onmapHeaderLink, top-levelpage.getByRole(...)re-bind that drops thethis.headerscope,super(page)drop,view AND emptyAND-conversion of the OR-paths inisPageRendered,view-only conversion that breaks dev / CI runners without provider keys,.catch(() => false)drop on eitherisVisible()call,.first()drop onmapView/mapEmptyStateinsideisPageRendered, exact-match re-bind onshowMapButton/showListButton,data-test/data-cy/data-qare-bind on anygetByTestIdselector, file move,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/map.spec.ts, the indirectapps/web-e2e/tests/public/seo-manifests.spec.tsreference to/mapvia the inheritedgoto(), the production-source DOM contract underapps/web/components/map/*,base-page-object.mdfor the inherited surface,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,fixtures-index.mdfor a future authenticated variant) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift ontodata-testid-rename,HEADER_MAP-translation-rename, view-toggle-aria-label-substring-drop,Show map/Show list-accessible-name-rename,header.map_enabled-config-flip,settings.location.enabled-config-flip,NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN/NEXT_PUBLIC_GOOGLE_MAPS_API_KEY-absence (gracefully handled), middleware-prefix-change, andbaseURL-change failures; and themap.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/map.spec.ts), abase-page-object.mdcross-check, a production-source cross-check (the fourdata-testids, the headerMaplink viaHEADER_MAP, the view-togglearia-label*="map"substring, the mobileShow map/Show listbutton accessible names), ane2e-tsconfig.mdcross-check, aplaywright-config.mdcross-check, afixtures-index.mdcross-check, dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the map spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Map View"), adocs/log.mdentry, a Spec 017 — Map View for Listings cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Newsletter Page Object (
apps/web-e2e/page-objects/public/newsletter.page.ts) -- Per-source-file reference for the Playwright e2e suite's footer newsletter signup form driver paired withapps/web-e2e/page-objects/public/newsletter.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andmap-page-object.mddocuments the suite's Map View page-route driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's footer newsletter-signup widget driver boundary — the smallest possible page object that lets a spec drive the footer newsletter signup form end-to-end (locate the firstinput[type="email"][name="email"]form field on the page, locate thebutton[type="submit"]that lives one DOM level up from the email input via the..parent-traversal step, locate the inline red-tinted validation message Tailwind utility classes emit (p.text-red-600/p.text-red-400) for the dark- and light-theme casing of the same paragraph, fill the email and click submit in a singlesubscribe(email)action, and detect whether a[data-sonner-toast]success toast surfaced after submission viahasSuccessToast()with the.first()strict-mode-correctness append and the.catch(() => false)graceful-degradation collapse). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import with noBasePagevalue import — the standalone-class widget posture; theexport class Newslettersingle named export with noextendsclause — the load-bearing standalone-class widget convention; the fourreadonlyfields coveringpage/emailInput/submitButton/errorMessage; the synchronous constructor that pre-binds every per-page Locator in a single pass via the compoundinput[type="email"][name="email"]+.first()selector for the email input, the..parent-traversal step +button[type="submit"]selector for the submit button, and the comma-separatedp.text-red-600, p.text-red-400selector +.first()for the inline error paragraph; thesubscribe(email)two-step composite that fills the email then clicks the submit button via two sequentialawaits; thehasSuccessToast(): Promise<boolean>graceful-degradation accessor with the[data-sonner-toast]Sonner-canonical data-attribute selector +.first()+.catch(() => false)error collapse); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and Spec 012 — Newsletter Providers; the "Why the class does not extendBasePage" three-reason analysis (footer-rendered widget vs. page-route distinction, local-to-the-footer surface scope, symmetry with sibling widget drivers); the "Why the email input uses.first()" three-reason analysis (single-form-on-the-page invariant, strict-mode-collision-protection posture, future-proofing against host-theme refactors); the "Why the submit button uses..traversal" three-reason analysis (sibling-scoped-not-page-scoped invariant,..step as the canonical CSS-equivalent of XPath'sparent::*axis, resilience to nested-wrapper drift); the "Why the error message usestext-red-600, text-red-400" three-reason analysis (light-vs-dark-theme utility-class variance, Tailwind-utility-classes-as-canonical-inline-error-styling-primitive posture,<p>element-tag prefix load-bearing constraint); the "WhyhasSuccessToast()collapses errors tofalse" three-reason analysis (Sonner-toast-lifecycle mid-fade transient errors,expect.poll-pollability primitive, stacked-toast strict-mode-collision protection); the failure matrix of 21 mistakes (drop theimport typemodifier, add anextends BasePageclause, dropreadonly, re-bindemailInputtoinput[type="email"]only, drop the.first()chain onemailInput, re-bindemailInputtogetByRole('textbox', { name: 'email' }), re-bindsubmitButtontopage.locator('button[type="submit"]'), replace..with... ..multi-step traversal, re-bindsubmitButtontogetByRole('button', { name: 'Subscribe' }), re-binderrorMessagetop.text-red-500, drop the<p>element-tag prefix, drop the.first()chain onerrorMessage, replace the comma-separated selector with a singletext-red-600, convertsubscribe()to aPromise.all([fill, click])race, drop theawaiton either step insidesubscribe(), drop the.catch(() => false)onhasSuccessToast()'sisVisible(), drop the.first()onhasSuccessToast()'s[data-sonner-toast]Locator, re-bind[data-sonner-toast]to a CSS-class selector like.sonner-toast, move the file outsideapps/web-e2e/page-objects/public/, rename the file tonewsletter.page.tsx, rename the class toNewsletterPage, commit the file with CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every consuming spec (a futureapps/web-e2e/tests/public/newsletter.spec.ts), the production-source DOM contract underapps/web/components/newsletter/*and the global footer underapps/web/components/layouts/footer/*, the Sonner integration underapps/web/lib/notifications/*,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL, andfixtures-index.mdfor a future authenticated variant; the read / write surface failure modes table covering production-source / library / config drift cases (form-fieldname="email"rename, form-fieldtype="email"change totype="text", submit buttontype="submit"change totype="button", submit button moved outside the email input's parent, inline-error-paragraph utility-class rename, Sonner library upgrade that changes thedata-sonner-toastattribute, Sonner library replacement, newsletter feature config flip, locale change with translated submit-button label,baseURLchange inplaywright-config.md); and the 12-stepnewsletter.page.ts-change checklist (audit consuming specs, cross-checkbase-page-object.md, cross-check the production source underapps/web/components/newsletter/*and the global footer, cross-check the Sonner integration, cross-checke2e-tsconfig.md, cross-checkplaywright-config.md, cross-checkfixtures-index.md, run dualpnpm tsc --noEmit(e2e package + workspace root), run a smoke-subset Playwright run targeting--grep "Newsletter", adocs/log.mdentry, a Spec 012 — Newsletter Providers cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Profile-Dropdown Page Object (
apps/web-e2e/page-objects/public/profile-dropdown.page.ts) -- Per-source-file reference for the Playwright e2e suite's header profile-dropdown menu driver paired withapps/web-e2e/page-objects/public/profile-dropdown.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andnewsletter-page-object.mddocuments the suite's footer newsletter-signup widget driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's header profile-dropdown menu driver boundary — the smallest possible page object that lets a spec drive the global header avatar / profile button dropdown end-to-end (locate the trigger button by its canonical production-sourceid="user-menu-button"identifier, locate the dropdown-menu container by its canonical production-sourceid="profile-menu"identifier, locate every[role="menuitem"]child of the menu as a collection scoped tothis.menuforclickMenuItemfiltering, locate the last menu item as the canonical "Logout" / "Sign out" button by positional convention, open the menu via a click on the trigger button, read the menu's open state via the trigger button'saria-expandedattribute strict-compared to the literal'true', click any menu item by a case-insensitiveRegExphasTextfilter on its visible label with.first()strict-mode-correctness append, and click the last menu item directly via the dedicatedlogout()shortcut). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import with noBasePagevalue import; theexport class ProfileDropdownsingle named export with noextendsclause — the standalone-class widget convention; the fivereadonlyfields coveringpage/triggerButton/menu/menuItems/logoutButton; the synchronous constructor that pre-binds every per-page Locator in a single pass via the HTML-id-based#user-menu-button/#profile-menuselectors and thethis.menu-scoped[role="menuitem"]collection plus.last(); theopen()single-step click primitive; theisOpen(): Promise<boolean>strict-equalityaria-expanded === 'true'accessor; theclickMenuItem(name: RegExp)arbitrary-menu-item composite withhasTextfilter +.first(); thelogout()shortcut bound to the last menu item); the spec context cross-links to Spec 010 — E2E Test Coverage and Spec 003 — Auth Providers; the "Why the class does not extendBasePage" three-reason analysis (header-rendered widget vs. page-route, local-to-the-header surface scope, sibling widget-driver symmetry); the "Why the trigger button uses#user-menu-button" three-reason analysis (HTMLidas canonical accessibility-wiring primitive cross-referenced byaria-controls/aria-labelledby, locale-stable selector, theme-stable selector); the "Why the logout button uses.last()" three-reason analysis (production-source positional convention, accessibility convention alignment, locale-fragility avoidance); the "WhyisOpen()checks the exact'true'string" three-reason analysis (getAttribute()returns string-or-null,Boolean('false')istruefootgun,null-coerces-to-falsefor unrendered states); the "WhyclickMenuItemtakes aRegExpnot astring" three-reason analysis (locale-sensitive label flexibility, opt-in case-insensitivity, mirroring Playwright'shasTextupstream API); the failure matrix of 22 mistakes; the per-line walkthrough table; the read / write surface tables covering futureapps/web-e2e/tests/auth/*consuming specs and the production-source DOM contract under the header profile-dropdown component (id="user-menu-button",id="profile-menu",[role="menuitem"],aria-expanded, the bottom-most-logout positional convention); the read / write surface failure modes table covering trigger-id-rename, menu-container-id-rename,aria-expanded-removal, role-menuitemcheckbox-substitution, logout-positional-drift, authentication-state-regression, locale change with translated logout label, andbaseURLchange failures; and the 12-stepprofile-dropdown.page.ts-change checklist (audit consuming specs underapps/web-e2e/tests/auth/andapps/web-e2e/tests/public/, cross-checkbase-page-object.md, cross-check the production-source header-component DOM contract, cross-check the auth-provider integration, cross-checke2e-tsconfig.md, cross-checkplaywright-config.md, cross-checkfixtures-index.mdfor a futureauthenticatedPagefixture, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting--grep "Profile Dropdown", adocs/log.mdentry, a Spec 003 — Auth Providers cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Public-Pages Page Object (
apps/web-e2e/page-objects/public/public-pages.page.ts) -- Per-source-file reference for the Playwright e2e suite's generic public content-page + error-page drivers paired withapps/web-e2e/page-objects/public/public-pages.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andprofile-dropdown-page-object.mddocuments the suite's header profile-dropdown menu driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's generic public content-page + error-page driver boundary — the smallest possible page objects that let a spec drive any of the six static / nearly-static public content routes (/collections,/categories,/tags,/cookies,/pricing,/sponsor) and the two error surfaces (404, 403) end-to-end (navigate to a chosen route via the dedicatednavigateToCollections()/navigateToCategories()/navigateToTags()/navigateToCookies()/navigateToPricing()/navigateToSponsor()shortcut methods that close over the inheritedgoto(), locate the first<h1>/role="heading"element on the page as the page title anchor, locate the<main>element as the per-route content container for visibility assertions, locate anynav[aria-label*="breadcrumb" i]or fallback<nav><ol>element as the breadcrumb trail, locate the page heading on an error page, locate the404|403literal text anywhere in the document for status-code assertions, locate the firstrole="link"matching the case-insensitive/home/iregex as the canonical "go home" recovery link, and locate the firstrole="button"matching the case-insensitive/go back/iregex as the canonical browser-history-pop button). Documents the at-a-glance summary table of every load-bearing element across both classes (the type-only Playwright import; the runtimeBasePagevalue import; theexport class PublicPagesPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the threereadonly Locatorfields coveringheading/mainContent/breadcrumb; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via thegetByRole('heading').first()accessibility-tree-canonical selector for the heading, thepage.locator('main').first()element-tag selector for the main content, and the OR-of-two-paths comma-separatednav[aria-label*="breadcrumb" i], nav olselector with.first()strict-mode-correctness append for the breadcrumb; the six route-shortcut methods (navigateToCollections/navigateToCategories/navigateToTags/navigateToCookies/navigateToPricing/navigateToSponsor) that close over the inheritedgoto; theexport class ErrorPage extends BasePagesecond named export with theextends BasePageclause — the error-page driver posture; the fourreadonly Locatorfields coveringheading/errorCode/goHomeButton/goBackButton; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via the samegetByRole('heading').first()selector for the heading, thepage.getByText(/404|403/)regex-alternation text-match selector for the error code, thepage.getByRole('link', { name: /home/i }).first()substring-regex accessibility selector for the go-home recovery link, and thepage.getByRole('button', { name: /go back/i }).first()two-word-substring-regex accessibility selector for the browser-history-pop button); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming specs atapps/web-e2e/tests/public/collections.spec.ts(three flows over/collections— loads successfully, has a heading, has a breadcrumb), the indirect consumer atapps/web-e2e/tests/public/sponsor.spec.ts(theonErrorPagebranch of the OR-of-three-statuses assertiononSignIn || onErrorPage || stayedOnSponsor), and the indirect consumer atapps/web-e2e/tests/public/error-pages.spec.ts; the "WhyPublicPagesPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgotomethod, globalheader/footer/navLinkschrome surfaced for free,waitForPageReadypost-navigation stabiliser); the "WhyPublicPagesPageandErrorPageco-habit a single file" three-reason analysis (an error page is a content page that happens to be a 404 / 403 — the structural primitives are the samerole="heading"first element /<main>content container / recoveryrole="link"to home, the two classes share the sameBasePageimport and the samePage, Locatortype-only import, the two classes are consumed together by every spec that drives a feature-flag-gated route like/sponsor); the "WhyheadingusesgetByRole('heading').first()" three-reason analysis (accessibility-tree-canonical posture matches both<h1>–<h6>element tags AND any[role="heading"]ARIA attribute override indistinguishably, strict-mode-correctness against the<h2>/<h3>siblings inside<main>, locale-stable selector independent of heading text); the "Whybreadcrumbuses an OR-of-two-paths" three-reason analysis (canonical accessibility primitive isaria-label="breadcrumb"with case-insensitiveiflag for"Breadcrumb"capitalisation, structural fallback<nav><ol>matches host themes without thearia-label,.first()strict-mode-correctness against multiple breadcrumb trails); the "WhyerrorCodeusesgetByText(/404|403/)" three-reason analysis (error code as primary user-facing discriminator, regex-form preferred over string-form to prevent future"It's a 4040 error"typo matches, no.first()required because the error code is emitted exactly once on the canonical error template); the "WhygoHomeButtonusesrole="link"" three-reason analysis (recovery link is canonically an<a href="/">withrole="link"not a<button>withrole="button",.first()strict-mode-correctness against header / footer "Home" links, case-insensitive substring regex/home/ifor locale / casing drift); the "WhygoBackButtonusesrole="button"" three-reason analysis (browser-history-pop button is canonically a<button onClick={() => history.back()}>withrole="button"not an<a href>withrole="link", two-word/go back/iregex for safety against bare"Back"button collisions in unrelated nav chrome,.first()strict-mode-correctness against unrelated"Go back to top"/"Go back to listing"siblings); the failure matrix covering every public-pages-page-level mistake (type-only import drop,extends BasePageclause drop on either class,readonlydrop on any of the seven Locator fields,super(page)drop in either constructor,.first()drop onheading/mainContent/breadcrumb, OR-of-two-paths alternation drop inbreadcrumb,iflag drop onbreadcrumb,getByRole('navigation')re-bind that hides the structural-fallback path,page.locator('h1')re-bind onheadingthat breaks<div role="heading">overrides,getByRole('main')re-bind onmainContentthat breaks the host theme today, route-literal change in any of the six route shortcuts, inlinedpage.goto(path)that dropswaitForPageReady, regex alternation drop onerrorCode,role="button"re-bind ongoHomeButton,iflag drop ongoHomeButton, exact-stringname: 'Home're-bind ongoHomeButton,.first()drop ongoHomeButton,role="link"re-bind ongoBackButton,iflag drop ongoBackButton, single-wordname: /back/ire-bind ongoBackButtonthat collides with bare"Back"siblings,.first()drop ongoBackButton, file split into separatepublic-pages.page.tsanderror.page.ts, file move,.tsxrename, class rename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming specs atcollections.spec.ts/sponsor.spec.ts/error-pages.spec.ts, future consumers atcategories.spec.ts/tags.spec.ts/cookies.spec.ts/pricing.spec.ts, the production-source DOM contract underapps/web/app/[lang]/collections//categories//tags//cookies//pricing//sponsor/, the production-source error-page DOM contract underapps/web/app/not-found.tsx/apps/web/app/error.tsx,base-page-object.mdfor the inherited surface,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,fixtures-index.mdfor a future authenticated-route variant) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto route-literal-rename,<h1>-to-<div role="heading">-survival,<main>-to-<div role="main">-breakage, breadcrumb structural-refactor failures,payment.enabled-flag-flip 404 escalation throughErrorPage, sponsor-feature-flag-flip 404/redirect tolerance,404-text-rename,"Home"/"Go back"translation drift, middleware-prefix-change, andbaseURL-change handling; and the 13-steppublic-pages.page.ts-change checklist (audit consuming specs underapps/web-e2e/tests/public/, cross-checkbase-page-object.md, cross-check the production-source DOM contract on each of the six routes, cross-check the production-source error-page contract, cross-check the route literals against the host-theme middleware, cross-checke2e-tsconfig.md, cross-checkplaywright-config.md, cross-checkfixtures-index.mdfor a futureauthenticatedPagefixture, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting--grep "Collections|Categories|Tags|Cookies|Pricing|Sponsor|Error", adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Bulk-Actions Page Object (
apps/web-e2e/page-objects/admin/bulk-actions.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin items bulk-operations driver paired withapps/web-e2e/page-objects/admin/bulk-actions.page.ts, the first per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the template the remaining sixteen admin-tree page-object docs (one per source file) will mirror — sitting inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (clients.page.ts,collections.page.ts,comments.page.ts,companies.page.ts,dashboard.page.ts,data-export.page.ts,featured-items.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andsignin-page-object.mddocuments the suite's auth-form driver boundary underapps/web-e2e/page-objects/auth/, this page documents the suite's admin items bulk-operations driver boundary — the smallest possible page object that lets a spec drive the full admin-shell items-listing bulk surface end-to-end (navigate to/admin/itemsvia the inheritedgoto(), locate the page heading, locate the first select-all checkbox via the bilingualaria-label*="Select all" i, aria-label*="SELECT_ALL" isubstring OR-of-two-paths, locate the bulk action toolbar via the canonical[role="toolbar"]selector once a row selection has triggered it, locate each of the four action buttons — approve, reject, delete, clear-selection — by their case-insensitivenameregex, locate the modal confirmation dialog via the canonical[role="dialog"][aria-modal="true"]selector once a destructive action has been clicked, and expose every individual non-select-all checkbox via the filtereditemCheckboxesgetter for per-row selection flows). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtimeBasePagevalue import; theexport class AdminBulkActionsPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the eightreadonly Locatorfields coveringheading/selectAllCheckbox/bulkActionBar/approveButton/rejectButton/deleteButton/clearSelectionButton/confirmDialog; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via thegetByRole('heading').first()accessibility-tree-canonical selector for the heading, the bilingual[aria-label*="Select all" i], [aria-label*="SELECT_ALL" i]OR-of-two-paths substring with.first()strict-mode-correctness for the select-all checkbox, the[role="toolbar"]ARIA role with.first()for the bulk-action bar, thegetByRole('button', { name: /approve|reject|delete/i }).first()accessibility-tree-canonical posture for the three workflow-state action buttons, thegetByRole('button', { name: /deselect|clear/i }).first()alternation regex tolerating "Clear selection" / "Deselect all" / "Clear" phrasings for the clear-selection button, and the[role="dialog"][aria-modal="true"]two-attribute selector with.first()for the modal confirmation dialog; the singlenavigate()shortcut method that closes over the inheritedgoto; theget itemCheckboxes(): Locatorlate-bound getter with the[aria-label*="Select" i]substring-OR'd selector and thehasNotText: /all/ifilter to exclude the global select-all checkbox from the per-row Locator collection); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/admin/bulk-actions.spec.ts(five flows over the admin-shell items-listing bulk surface — select-all checkbox is visible, clicking select-all shows the bulk action bar, bulk action bar has approve / reject / delete buttons, clicking bulk delete opens the confirmation dialog, clear selection removes the bulk action bar); the "WhyAdminBulkActionsPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgotomethod, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "Why the bilingualaria-labelOR-of-two-paths onselectAllCheckbox" three-reason analysis (production source bilingualism between the canonical"Select all"English-locale phrase and the i18n-key fallback"SELECT_ALL"for catalogue-incomplete tenants, substring tolerance via theiflag for casing variants,.first()pin against per-row select-all duplicates); the "Why[role="toolbar"]forbulkActionBar" three-reason analysis (accessibility-tree-canonical posture, production-source-first selector posture, strict-mode safety from the per-route single-instance shape with.first()against future shared admin-shell command-paletterole="toolbar"peers); the "WhygetByRole('button', { name: /…/i })for the four action buttons" three-reason analysis (accessibility-tree-canonical posture against the computed accessible name, locale-tolerant via the case-insensitive regex, strict-mode safety from the.first()pin against multi-button surfaces); the "Why[role="dialog"][aria-modal="true"]forconfirmDialog" three-reason analysis (modal vs non-modal disambiguation, strict-mode safety against tooltip / toast libraries that mount withrole="dialog",.first()pin against potential parallel modals); the "WhyitemCheckboxesis a getter and not areadonlyfield" three-reason analysis (late-binding against pagination state, symmetric with the per-callfilter()invocation, documentation-by-convention with other admin-tree page objects' getter posture for "collection of matching elements" surfaces); the "Whyaria-label*="Select" iand notgetByRole('checkbox', { name: /select/i })foritemCheckboxes" three-reason analysis (defends against role-attribute drift between native<input type="checkbox">/role="checkbox"-shaped<button>/<button>-with-icon "select chip" variants, direct symmetry withselectAllCheckbox's CSS-attribute substring shape, no collision risk with non-checkboxroleelements); the failure matrix covering every bulk-actions-page-level mistake (type-only import drop,extends BasePageclause drop,super(page)drop in the constructor,readonlydrop on any field,selectAllCheckboxswitched to a single exact-match selector that silentlytest.skip()s on catalogue-incomplete tenants,iflag drop on any substring locator,.first()drop onselectAllCheckbox/bulkActionBar/ any action button,bulkActionBarswitched to[data-testid="bulk-actions"]violating production-source-first,confirmDialogswitched to drop the[aria-modal="true"]filter,itemCheckboxesswitched togetByRole('checkbox', { name: /select/i })losing per-row<button>-with-icon coverage,hasNotText: /all/ifilter drop that bleeds the select-all checkbox into the per-row collection,navigate()method drop that forces every consuming spec to restategoto, file move, class rename,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/admin/bulk-actions.spec.ts, future per-row selection / bulk-approve / bulk-reject specs, the admin items production-source components for the DOM contract,base-page-object.mdfor the inherited surface,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL+adminPagefixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto bilingual-aria-label-rename,role="toolbar"-to-role="region"-switch,role="dialog"-to-custom-element-modal-switch, text-content-button-to-icon-only-button-switch, single-toolbar-to-per-row-action-menu-switch, empty-items-listing seeding regression that auto-test.skip()s every spec,adminPagefixture authentication-regression failures,/admin/itemsmiddleware-disabling failures,playwright.config.tsbaseURL-change failures, andnext-intl-message-catalogue drops of the canonical Englisharia-label; and the 11-stepbulk-actions.page.ts-change checklist (audit consuming specs underapps/web-e2e/tests/admin/bulk-actions.spec.ts, cross-checkbase-page-object.mdfor theBasePageposture and the standalone-class precedent set byscroll-to-top-page-object.md, cross-check the production source for the canonicalaria-label="Select all"/SELECT_ALLt-key on the select-all checkbox, therole="toolbar"on the bulk-action bar, therole="dialog"+aria-modal="true"on the confirmation modal, and the four action button accessible names, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound bulk-actions driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the bulk-actions spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Bulk Operations"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Clients Page Object (
apps/web-e2e/page-objects/admin/clients.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin clients-management driver paired withapps/web-e2e/page-objects/admin/clients.page.ts, the second per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the continuation of the rollout theadmin-bulk-actions-page-object.mdtemplate established — sitting inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts,collections.page.ts,comments.page.ts,companies.page.ts,dashboard.page.ts,data-export.page.ts,featured-items.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andadmin-bulk-actions-page-object.mddocuments the suite's admin items bulk-operations driver boundary at/admin/items, this page documents the suite's admin clients-management driver boundary at/admin/clients— the smallest possible page object that lets a spec drive the admin clients listing end-to-end (navigate to/admin/clientsvia the inheritedgoto(), locate the page heading, locate the first "Add client" trigger button by its case-insensitive/add client/iaccessible-name regex, locate the multi-step add-client form modal via the.fixed.inset-0.z-50Tailwind-overlay positional selector once the trigger has been clicked, locate the per-row delete confirmation modal via the same.fixed.inset-0.z-50positional selector filtered down to thehasText: /delete client/isubstring once a delete button has been clicked, and expose the confirm-delete / cancel-delete button pair asrole="button"Locators nested inside the delete confirmation modal). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtimeBasePagevalue import; theexport class AdminClientsPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the tworeadonly Locatorfields coveringheading/addClientButton; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via thegetByRole('heading').first()accessibility-tree-canonical selector for the heading and thegetByRole('button', { name: /add client/i }).first()accessibility-tree-canonical posture for the add-client trigger; the singlenavigate()shortcut method that closes over the inheritedgoto; the four per-page modal getters (get clientFormModal(): Locatorwith the.fixed.inset-0.z-50Tailwind-utility positional selector and.first()strict-mode-correctness append for the multi-step add-client form modal overlay;get deleteConfirmModal(): Locatorwith the same.fixed.inset-0.z-50positional selector filtered down to thehasText: /delete client/isubstring for the delete confirmation modal overlay;get confirmDeleteButton(): Locatorwith the anchored^delete$button regex scoped underdeleteConfirmModal;get cancelDeleteButton(): Locatorwith the substring/cancel/ibutton regex scoped underdeleteConfirmModal); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/admin/clients.spec.ts(four flows over the admin clients-management surface — admin can access clients management page, clients page displays client list, admin can open create client modal, admin can open delete client confirmation); the "WhyAdminClientsPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgotomethod, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "WhygetByRole('button', { name: /add client/i })foraddClientButton" three-reason analysis (accessibility-tree-canonical posture against the computed accessible name, locale-tolerant via the case-insensitive regex, strict-mode safety from the.first()pin against multi-button surfaces); the "WhyclientFormModalanddeleteConfirmModalare getters and notreadonlyfields" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric withitemCheckboxeson the bulk-actions driver, per-callfilter()invocation ondeleteConfirmModal); the "Why.fixed.inset-0.z-50for both modal surfaces" three-reason analysis (production-source posture withoutrole="dialog", substring filter disambiguates between the two modals, reuses the host app's CSS-utility convention); the "Why^delete$anchored regex forconfirmDeleteButton" three-reason analysis (defends against per-row "Delete client" heading collision, defends against per-row "Cannot delete" warning button collision, symmetric with bulk-actions driver but smaller-blast-radius for the more crowded modal scope); the "Why nesteddeleteConfirmModal.getByRole(...)and notpage.getByRole(...)" three-reason analysis (defends against per-row "Delete" / "Cancel" button collision on the underlying clients table, re-uses the late-binding lifecycle of thedeleteConfirmModalgetter, symmetric with public-tree modal drivers); the failure matrix covering every clients-page-level mistake (type-only import drop,extends BasePageclause drop,super(page)drop in the constructor,readonlydrop on any field,iflag drop on theaddClientButtonregex,.first()drop onaddClientButton,addClientButtonswitched to[data-testid="add-client"]violating production-source-first,.first()drop onclientFormModal,hasText: /delete client/ifilter drop ondeleteConfirmModal,^…$anchors drop onconfirmDeleteButton,confirmDeleteButton/cancelDeleteButtonswitched topage.getByRole(...)peers, modal getters switched to[role="dialog"][aria-modal="true"],navigate()method drop that forces every consuming spec to restategoto, file move, class rename,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/admin/clients.spec.ts, future per-row clients-table specs, future add-client form-submit specs, the admin clients production-source components for the DOM contract,base-page-object.mdfor the inherited surface,admin-bulk-actions-page-object.mdfor the per-source-file template,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL+adminPagefixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto Add-Client-button-rename,.fixed.inset-0.z-50-overlay-primitive-switch, "Delete client"-heading-rename, confirm-button-rename, cancel-button-rename, text-content-button-to-icon-only-button-switch, empty-clients-listing seeding regression that auto-test.skip()s every spec,adminPagefixture authentication-regression failures,/admin/clientsmiddleware-disabling failures, andplaywright.config.tsbaseURL-change failures; and the 11-stepclients.page.ts-change checklist (audit consuming specs underapps/web-e2e/tests/admin/clients.spec.ts, cross-checkbase-page-object.mdfor theBasePageposture, cross-checkadmin-bulk-actions-page-object.mdfor the admin-tree page-object template, cross-check the production source for the canonical "Add Client" button accessible name, the.fixed.inset-0.z-50Tailwind utility chain on both modal overlays, the "Delete client" heading text inside the delete confirmation modal, and the accessible names of the confirm-delete / cancel-delete buttons, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound clients driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the clients spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Clients Management"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Collections Page Object (
apps/web-e2e/page-objects/admin/collections.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin collections-management driver paired withapps/web-e2e/page-objects/admin/collections.page.ts, the third per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents a named-row helper API (getCollectionByName(name),editCollection(name),deleteCollection(name)) on top of the per-page Locator fields, plus a per-form fill helper (fillCollectionForm({ id?, name, description? })) that encodes the multi-input form-fill convention every future admin-form driver in the suite mirrors. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts— seeadmin-bulk-actions-page-object.md,clients.page.ts— seeadmin-clients-page-object.md,comments.page.ts,companies.page.ts,dashboard.page.ts,data-export.page.ts,featured-items.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andadmin-clients-page-object.mddocuments the suite's admin clients-management driver boundary at/admin/clients, this page documents the suite's admin collections-management driver boundary at/admin/collections— the smallest possible page object that lets a spec drive the admin collections listing end-to-end (navigate to/admin/collectionsvia the inheritedgoto(), locate the page heading, locate the first "Add collection" trigger button by its case-insensitive/add collection/iaccessible-name regex, locate the collection-form modal via the.fixed.inset-0.z-50Tailwind-overlay positional selector once the trigger has been clicked, locate every form-input field by its production-source placeholder (/frontend-frameworks/ifor the ID input,/collection name/ifor the name input,🤖exact-match for the icon input,/short description/ifor the description textarea), locate the active toggle by its[role="switch"]ARIA role, locate the cancel / create / save buttons by their case-insensitive accessible names, and expose a per-row collection finder + per-row edit / delete trigger pair via thegetCollectionByName(name)/editCollection(name)/deleteCollection(name)helper APIs). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtimeBasePagevalue import; theexport class AdminCollectionsPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the tworeadonly Locatorfields coveringheading/addCollectionButton; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via thegetByRole('heading').first()accessibility-tree-canonical selector for the heading and thegetByRole('button', { name: /add collection/i }).first()accessibility-tree-canonical posture for the add-collection trigger; the singlenavigate()shortcut method that closes over the inheritedgoto; the three named-row helpers (getCollectionByName(name): Locatorwith thediv-tag Locator + case-insensitive substring filter +.first()pin posture,editCollection(name)anddeleteCollection(name)async wrappers that resolve the row viagetCollectionByNameand click the scoped per-row edit / delete button); the nine per-form-element getters coveringcollectionFormModal(with the.fixed.inset-0.z-50Tailwind-utility positional selector and.first()strict-mode-correctness append for the form modal overlay),collectionIdInput/collectionNameInput/collectionIconInput/collectionDescriptionInput(with modal-scopedgetByPlaceholder(...)matches against the production-source-emitted placeholders),activeToggle(with the modal-scoped[role="switch"]ARIA role +.first()pin), andcancelButton/createButton/saveButton(with modal-scopedgetByRole('button', { name: /…/i })matches); the per-form fill helperfillCollectionForm(data: { id?: string; name: string; description?: string })with the named-arg shape and theif (data.id)/if (data.description)guards for the optional fields); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/admin/collections.spec.ts(five flows over the admin collections-management surface — admin can access collections management page, admin can create a new collection, admin can edit an existing collection, admin can delete a collection using native confirm dialog, collections page displays stats cards); the "WhyAdminCollectionsPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgotomethod, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "WhygetByPlaceholder(...)for every form-input field" three-reason analysis (HeroUI's<Input>component does not pair with a visible<label>element,getByRole('textbox', { name: … })would resolve via the same accessible-name computation but with an extra hop, thedata-testidposture would force a production-source change purely for the e2e suite); the "WhycollectionFormModalis a getter and not areadonlyfield" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric withclientFormModal/deleteConfirmModalon the clients driver, used as the scope-anchor for nine downstream per-form-element getters); the "Why three named-row helpers" three-reason analysis (the collections page is the first admin-tree surface with per-row edit/delete buttons in the rollout, the helpers compose with the underlying Locator API rather than replacing it, the helpers are documentation-by-default for new contributors); the "WhyfillCollectionFormaccepts an object and not positional args" three-reason analysis (optional fields with TypeScript-requiredname, self-documenting at the call site, forward-compatible with new fields); the "Why placeholder-only inputs (no per-inputaria-labelordata-testid)" three-reason analysis (production-source posture, substring-regex tolerance, locale-stability via the production-source's English-only placeholders); the failure matrix covering every collections-page-level mistake (type-only import drop,extends BasePageclause drop,super(page)drop in the constructor,readonlydrop on any field,iflag drop on any name-regex,.first()drop onaddCollectionButton/heading/collectionFormModal,addCollectionButtonswitched to[data-testid="add-collection"]violating production-source-first,collectionFormModalswitched to[role="dialog"][aria-modal="true"], any input getter switched fromgetByPlaceholder(...)togetByLabel(...), modal-scope drop on any input getter or button getter,createButtonregex switched from/create collection/ito/create/i,saveButtonregex switched from/save changes/ito/save/i,activeToggleswitched from[role="switch"]toinput[type="checkbox"],getCollectionByNamehelper drop,getCollectionByNameswitched fromdiv-tag to[role="row"],editCollection/deleteCollectionswitched topage.getByRole(...)peers,if (data.id)guard drop infillCollectionForm,navigate()method drop, file move, class rename,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/admin/collections.spec.ts, future per-row collections-table specs, future create-flow validation specs, the admin collections production-source components for the DOM contract,base-page-object.mdfor the inherited surface,admin-bulk-actions-page-object.mdfor the per-source-file template,admin-clients-page-object.mdfor the modal-overlay precedent,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL+adminPagefixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto Add-Collection-button-rename,.fixed.inset-0.z-50-overlay-primitive-switch, form-input-placeholder-rename, HeroUI-<Input>-to-<input>-with-<label>switch, HeroUI-<Switch>-to-native-<input type="checkbox">switch, create-mode-submit-button-rename, edit-mode-submit-button-rename, per-row-delete-confirm()-to-custom-React-modal switch, per-row-edit-/-delete-button-to-kebab-menu-opener switch, empty-collections-listing seeding regression,adminPagefixture authentication-regression failures,/admin/collectionsmiddleware-disabling failures, andplaywright.config.tsbaseURL-change failures; and the 11-stepcollections.page.ts-change checklist (audit consuming specs, cross-checkbase-page-object.mdfor theBasePageposture, cross-checkadmin-bulk-actions-page-object.mdandadmin-clients-page-object.mdfor the admin-tree page-object template, cross-check the production source for the canonical "Add Collection" button accessible name and the form-input placeholders, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound collections driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the collections spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Collections Management"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Comments Page Object (
apps/web-e2e/page-objects/admin/comments.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin comments-management driver paired withapps/web-e2e/page-objects/admin/comments.page.ts, the fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents a HeroUI-Modal-based delete confirmation surface (a[role="dialog"]overlay rather than the browser-nativeconfirm()dialog the collections driver documents, and rather than the custom-ReactdeleteConfirmModaloverlay the clients driver documents). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts— seeadmin-bulk-actions-page-object.md,clients.page.ts— seeadmin-clients-page-object.md,collections.page.ts— seeadmin-collections-page-object.md,companies.page.ts,dashboard.page.ts,data-export.page.ts,featured-items.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andadmin-collections-page-object.mddocuments the suite's admin collections-management driver boundary at/admin/collections, this page documents the suite's admin comments-management driver boundary at/admin/comments— the smallest possible page object that lets a spec drive the admin comments listing end-to-end (navigate to/admin/commentsvia the inheritedgoto(), locate the page heading via the inherited accessibility-tree-canonical heading Locator, locate the search input via the[role="searchbox"]ARIA role, drive the search input viasearchComments(term)/clearSearch()helpers, and locate both the deletion-confirmation HeroUI Modal and every per-row delete trigger via thedeleteCommentDialog/deleteButtonsgetters). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtimeBasePagevalue import; theexport class AdminCommentsPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the tworeadonly Locatorfields coveringheading/searchInput; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via thegetByRole('heading').first()accessibility-tree-canonical selector for the heading and thegetByRole('searchbox').first()accessibility-tree-canonical posture for the search input; the singlenavigate()shortcut method that closes over the inheritedgoto; the two per-action methodssearchComments(term)(with theawait this.searchInput.fill(term)posture) andclearSearch()(with theawait this.searchInput.clear()posture); the two per-element gettersdeleteCommentDialog(with the[role="dialog"]ARIA role + case-insensitive/delete/itext filter for the HeroUI Modal pin) anddeleteButtons(with the dual-selectorbutton[color="danger"], button.text-red-600for the per-row delete-trigger collection covering both the HeroUIcolor="danger"prop variant and the Tailwind utility-class fallback)); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/admin/comments.spec.ts(four flows over the admin comments-management surface — admin can access comments management page, comments page displays comment list, admin can search comments, admin can open delete comment dialog); the "WhyAdminCommentsPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgotomethod, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "WhygetByRole('searchbox')for the search input" three-reason analysis (HeroUI's<Input type="search">lights up the canonical role automatically, independent of placeholder text, thedata-testidposture would force a production-source change purely for the e2e suite); the "WhysearchComments/clearSearchand not direct Locator drives" three-reason analysis (documentation-by-default, forward-compatible with debouncing / IME composition / multi-step interactions, symmetric with future per-input drivers); the "WhydeleteCommentDialoganddeleteButtonsare getters and notreadonlyfields" three-reason analysis (late-binding against modal mount/unmount lifecycle, late-binding against per-row pagination, symmetric with the modal-getter posture across the admin-tree page-object directory); the "Why a HeroUI Modal (and notconfirm()or a custom-React overlay) for the delete confirmation" three-reason analysis (production-source consistency with HeroUI Modal use elsewhere in the admin shell, per-page contract divergence from collections / clients postures,[role="dialog"]lights up the screen-reader path); the "WhydeleteButtonsuses a dual-selector (button[color="danger"], button.text-red-600)" three-reason analysis (HeroUIcolor="danger"prop and Tailwind utility-class fallback are both valid production-source shapes, future-proof against HeroUI prop reshuffling, the consuming spec uses an inline svg-children selector for the delete trigger today); the failure matrix covering every comments-page-level mistake (type-only import drop,extends BasePageclause drop,super(page)drop in the constructor,readonlydrop on any field,.first()drop onheading/searchInput,searchInputswitched fromgetByRole('searchbox')togetByPlaceholder(...),searchInputswitched to[data-testid="search"]violating production-source-first,searchComments(term)method drop,clearSearch()method drop,searchCommentsswitched fromfilltotype,deleteCommentDialogswitched from[role="dialog"]to.fixed.inset-0.z-50,/delete/itext-filter drop,deleteButtonsswitched from dual-selector to a single selector,navigate()method drop, file move, class rename,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/admin/comments.spec.ts, future per-row comment-table specs, future search-flow validation specs, the admin comments production-source components for the DOM contract,base-page-object.mdfor the inherited surface,admin-bulk-actions-page-object.mdfor the per-source-file template,admin-clients-page-object.mdfor the custom-React modal-overlay precedent,admin-collections-page-object.mdfor the named-row helper API + per-form fill helper conventions,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL+adminPagefixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto HeroUI-<Input type="search">-to-non-search-<input>-switch, HeroUI-<Modal>-to-custom-React-overlay-switch, HeroUI-<Button color="danger">-to-non-color-prop-button-switch, Tailwind-text-red-600-to-semantic-token-switch, modal-text-rename, empty-comments-listing seeding regression,adminPagefixture authentication-regression failures,/admin/commentsmiddleware-disabling failures, andplaywright.config.tsbaseURL-change failures; and the 11-stepcomments.page.ts-change checklist (audit consuming specs, cross-checkbase-page-object.mdfor theBasePageposture, cross-checkadmin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.mdfor the admin-tree page-object template, cross-check the production source for the canonical[role="searchbox"]ARIA role and the[role="dialog"]ARIA role on the modal, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound comments driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the comments spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Comments Management"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Companies Page Object (
apps/web-e2e/page-objects/admin/companies.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin companies-management driver paired withapps/web-e2e/page-objects/admin/companies.page.ts, the fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents both a bare.fixed.inset-0.z-50Tailwind-overlay form modal (matching the clients driver's posture for the create / edit form) and a separate text-filtered Tailwind-overlay delete-confirmation modal (.fixed.inset-0.z-50overlay primitive scoped by ahasText: /delete company/ifilter — distinct from the clients driver's named-classdeleteConfirmModalselector and from the comments driver's[role="dialog"]selector). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts— seeadmin-bulk-actions-page-object.md,clients.page.ts— seeadmin-clients-page-object.md,collections.page.ts— seeadmin-collections-page-object.md,comments.page.ts— seeadmin-comments-page-object.md,dashboard.page.ts,data-export.page.ts,featured-items.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andadmin-comments-page-object.mddocuments the suite's admin comments-management driver boundary at/admin/comments, this page documents the suite's admin companies-management driver boundary at/admin/companies— the smallest possible page object that lets a spec drive the admin companies listing end-to-end (navigate to/admin/companiesvia the inheritedgoto(), locate the page heading, locate the first "Add Company" trigger button by its case-insensitive/add company/iaccessible-name regex, locate the company-form modal via the.fixed.inset-0.z-50Tailwind-overlay positional selector once the trigger has been clicked, locate the company-name input as the first<input>inside the modal, locate the cancel / create / update buttons via their case-insensitive accessible names, and locate the deletion-confirmation modal via the.fixed.inset-0.z-50overlay scoped by ahasText: /delete company/itext filter plus the^delete$exact-match confirm-button regex). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; the runtimeBasePagevalue import; theexport class AdminCompaniesPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the tworeadonly Locatorfields coveringheading/addCompanyButton; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass via thegetByRole('heading').first()accessibility-tree-canonical selector for the heading and thegetByRole('button', { name: /add company/i }).first()accessibility-tree-canonical posture for the add-company trigger; the singlenavigate()shortcut method that closes over the inheritedgoto; the seven per-element getters coveringcompanyFormModal(with the.fixed.inset-0.z-50Tailwind-utility positional selector and.first()strict-mode-correctness append for the form modal overlay),companyNameInput(with the modal-scopedlocator('input').first()positional first-input selector),cancelButton/createCompanyButton/updateCompanyButton(with modal-scopedgetByRole('button', { name: /…/i })matches against the per-mode submit button accessible names),deleteConfirmModal(with the.fixed.inset-0.z-50overlay primitive scoped by ahasText: /delete company/itext filter), andconfirmDeleteButton(with the modal-scopedgetByRole('button', { name: /^delete$/i })exact-match accessible-name regex)); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/admin/companies.spec.ts(four flows over the admin companies-management surface — admin can access companies management page, admin can open create company modal, admin can create a new company, admin can open delete company confirmation); the "WhyAdminCompaniesPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgotomethod, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "Why.fixed.inset-0.z-50for the form modal" three-reason analysis (production-source consistency with the clients / collections form-modal posture, no[role="dialog"]on the production source today, thedata-testidposture would force a production-source change purely for the e2e suite); the "Why.first()oncompanyFormModal(and not ondeleteConfirmModal)" three-reason analysis (.fixed.inset-0.z-50is a multi-instance selector,deleteConfirmModaluses a text filter to disambiguate, modal-mount lifecycle differences); the "WhycompanyNameInputuseslocator('input').first()(and notgetByPlaceholder/getByRole('textbox'))" three-reason analysis (no production-source-stable placeholder, no accessible-name binding via a<label>element, single-input form contract); the "WhyconfirmDeleteButtonuses an exact-match/^delete$/iregex" three-reason analysis (the HeroUI Modal emits the title as the modal's accessible name, the case-insensitive/iflag tolerates capitalisation drift, the modal-scope is the second-line defence); the "Why two distinct submit-button getters (createCompanyButton/updateCompanyButton)" three-reason analysis (the form modal mounts in two distinct modes with two distinct accessible names, per-mode test assertions, future-proof against per-mode-specific submit behaviours); the "WhycompanyFormModalis a getter and not areadonlyfield" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric with the modal-getter posture across the admin-tree page-object directory, used as the scope-anchor for downstream getters); the failure matrix covering every companies-page-level mistake (type-only import drop,extends BasePageclause drop,super(page)drop in the constructor,readonlydrop on any field,.first()drop onheading/addCompanyButton/companyFormModal,/iflag drop onaddCompanyButtonregex,companyFormModalswitched to[role="dialog"][aria-modal="true"],companyNameInputswitched fromlocator('input').first()togetByPlaceholder(...), modal-scope drop on any input getter or button getter,createCompanyButtonregex switched from/create company/ito/create/i,updateCompanyButtonregex switched from/update company/ito/update/i,^…$anchors drop onconfirmDeleteButtonregex,deleteConfirmModalswitched from.fixed.inset-0.z-50to[role="dialog"],/delete company/itext-filter drop,navigate()method drop, file move, class rename,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/admin/companies.spec.ts, future per-row companies-table specs, future status-filter / search-flow validation specs, the admin companies production-source components for the DOM contract,base-page-object.mdfor the inherited surface,admin-bulk-actions-page-object.mdfor the per-source-file template,admin-clients-page-object.mdfor the.fixed.inset-0.z-50form-modal precedent,admin-collections-page-object.mdfor the named-row helper API + per-form fill helper conventions,admin-comments-page-object.mdfor the[role="dialog"]HeroUI Modal-based delete-confirmation modal precedent,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL+adminPagefixture binding) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config / database-seeding drift onto Add-Company-button-rename,.fixed.inset-0.z-50-overlay-primitive-switch,Create Company/Update Companysubmit-button-rename, delete-confirmation-modal-title-rename, confirm-delete-button-rename, additional-<input>-before-name-input, empty-companies-listing seeding regression,adminPagefixture authentication-regression failures,/admin/companiesmiddleware-disabling failures, andplaywright.config.tsbaseURL-change failures; and the 11-stepcompanies.page.ts-change checklist (audit consuming specs, cross-checkbase-page-object.mdfor theBasePageposture, cross-checkadmin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.mdfor the admin-tree page-object template, cross-check the production source for the canonicalAdd Companybutton accessible name and the.fixed.inset-0.z-50Tailwind-overlay primitive on the form modal and the delete-confirmation modal, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound companies driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the companies spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Companies Management"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Dashboard Page Object (
apps/web-e2e/page-objects/admin/dashboard.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin dashboard-landing driver paired withapps/web-e2e/page-objects/admin/dashboard.page.ts, the sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents agetByRole('tablist')-anchored multi-tab navigation surface with a per-tabselectTab(tabName)helper that closes over a case-insensitive substring-match accessible-name regex (distinct from the form-modal / row-action postures the five prior admin-tree drivers document, and distinct from every public-tree driver in the suite which has no tab-based navigation surface today). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,data-export.page.ts,featured-items.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin dashboard-landing driver boundary at/admin-- the smallest possible page object that lets a spec drive the admin dashboard's landing page end-to-end (navigate to/admin, locate the main content region via the#main-contentid-selector, locate the tab navigation via the accessibility-tree-canonicalgetByRole('tablist')locator, locate the refresh trigger via the case-insensitive/refresh/iaccessible-name regex, and select a per-tab navigation target by case-insensitive substring-match accessible-name filter via theselectTab(tabName)helper). Anchored byapps/web-e2e/tests/admin/dashboard.spec.ts(four flows over the admin-shell dashboard-landing surface -- authenticated admin can access admin panel, admin dashboard displays tab navigation, non-admin client is redirected from admin, unauthenticated user cannot access admin); cross-references tobase-page-object.mdfor the inheritance root,admin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.mdfor the prior admin-tree page-object boundaries, and to the consuming spec atapps/web-e2e/tests/admin/dashboard.spec.tsfor the four flows the driver enables. Any future change to the dashboard driver -- adding a per-stat Locator getter, aclickRefresh()flow helper, anassertTabSelected(tabName)invariant assertion, a per-tab Locator getter on top of theselectTab(tabName)helper, or adata-testidmigration of the existing#main-content/getByRole('tablist')postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-dashboard-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/dashboard.spec.tsfor the four-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound dashboard driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the dashboard spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Admin: Dashboard"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Featured Items Page Object (
apps/web-e2e/page-objects/admin/featured-items.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin featured-items management driver paired withapps/web-e2e/page-objects/admin/featured-items.page.ts, the seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents an#active-onlyid-selector toggle (a positional<input id="active-only">checkbox surface, distinct from every other admin-tree driver'sgetByRole('button')orgetByRole('heading')posture) plus a pair of search-input helpers (search(term)/clearSearch()-- composable mutators on the same underlyinggetByRole('textbox').first()Locator, distinct from the form-modal-bound input mutators every other admin-tree driver documents) plus astatsCardsLocator getter that pins to the positional.gridselector (a CSS-utility-class anchor, distinct from the[role="dialog"]/.fixed.inset-0.z-50overlay primitives every other admin-tree driver's modal-Locator getters use). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin featured-items-management driver boundary at/admin/featured-items-- the smallest possible page object that lets a spec drive the admin featured-items page end-to-end (navigate to/admin/featured-items, locate the page heading via the inherited-defaultgetByRole('heading').first()posture, locate the first "Add Featured Item" trigger button by its case-insensitive/add featured item/iaccessible-name regex, locate the search input as the first<textbox>on the page, locate the active-only filter toggle by its#active-onlyid-selector, locate the per-row featured-item modal via the[role="dialog"]accessibility-tree-canonical posture, locate the per-stats grid via the positional.gridCSS-utility selector, and run the search / clear-search mutators that close overfill(term)/.clear()on the shared search-input Locator). Anchored byapps/web-e2e/tests/admin/featured-items.spec.ts(five flows over the admin featured-items management surface -- admin can access featured items page, featured items page displays stats cards, admin can open add featured item modal, search input filters featured items, active-only toggle filters items); cross-references tobase-page-object.mdfor the inheritance root,admin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.md/admin-dashboard-page-object.mdfor the prior admin-tree page-object boundaries, and to the consuming spec atapps/web-e2e/tests/admin/featured-items.spec.tsfor the five flows the driver enables. Any future change to the featured-items driver -- adding a per-row Locator getter, anaddFeaturedItem(...)/editFeaturedItem(...)/deleteFeaturedItem(...)flow helper, anassertActiveOnlyinvariant assertion, agetStatsValue(label)helper, or adata-testidmigration of the existinggetByRole('textbox').first()/#active-only/.grid/[role="dialog"]postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-featured-items-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/featured-items.spec.tsfor the five-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound featured-items driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the featured-items spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Featured Items"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Data Export Page Object (
apps/web-e2e/page-objects/admin/data-export.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin data-export widget driver paired withapps/web-e2e/page-objects/admin/data-export.page.ts, the eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a/adminco-tenant widget posture (the data-export widget is composed into the admin dashboard landing page rather than mounted at a dedicated route -- distinct from every prior admin-tree driver's per-feature route posture), (b) a format-button pair (csvButton/jsonButton) pinned to case-insensitive^CSV$/^JSON$exact-match accessible-name regexes, (c) a#include-metadataid-selector checkbox symmetric with the featured-items driver's#active-onlyposture, (d) a broad-nameexportButtonsLocator that intentionally resolves to a multi-element match via the case-insensitive/export|download/ialternation regex, and (e) aprogressBarLocator with composite-or selectors ([role="progressbar"], .bg-blue-600.rounded-full) -- the first admin-tree driver to document a fallback chain between an accessibility-tree-canonical posture and a positional Tailwind-utility posture. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin data-export widget driver boundary at/admin(composed into the dashboard landing page rather than mounted at a dedicated route) -- the smallest possible page object that lets a spec drive the data-export widget end-to-end (navigate to/admin, locate the widget heading via the inherited-defaultgetByRole('heading').first()posture, locate the first "CSV" format trigger button by its case-insensitive^CSV$exact-match accessible-name regex, locate the first "JSON" format trigger button by its case-insensitive^JSON$exact-match accessible-name regex, locate the include-metadata toggle by its#include-metadataid-selector, locate every export / download trigger button by the case-insensitive/export|download/ialternation regex, and locate the progress indicator via the composite-or[role="progressbar"], .bg-blue-600.rounded-fullselector chain). Anchored byapps/web-e2e/tests/admin/data-export.spec.ts(three flows over the admin data-export widget surface -- admin dashboard has export format buttons, include metadata checkbox is available, export/download buttons are available -- each guarded by atest.skip(true, …)defensive posture so the test remains green when the widget is hidden behind a feature-flag); cross-references tobase-page-object.mdfor the inheritance root,admin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.md/admin-dashboard-page-object.md/admin-featured-items-page-object.mdfor the prior admin-tree page-object boundaries, and to the consuming spec atapps/web-e2e/tests/admin/data-export.spec.tsfor the three flows the driver enables. Any future change to the data-export driver -- adding a per-format download flow helper, anenableMetadata()/disableMetadata()setter helper pair, anassertProgress(percent)invariant assertion, a format-equivalence helper that switches between CSV and JSON, or adata-testidmigration of the existing^CSV$/^JSON$/#include-metadata/[role="progressbar"], .bg-blue-600.rounded-fullpostures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-data-export-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/data-export.spec.tsfor the three-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound data-export driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the data-export spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Data Export"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Item Form Page Object (
apps/web-e2e/page-objects/admin/item-form.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin multi-step item creation / edit form modal driver paired withapps/web-e2e/page-objects/admin/item-form.page.ts, the ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a standalone (noBasePageextension) modal driver posture (theAdminItemFormPageclass is the first admin-tree driver that does not extendBasePagebecause the modal owns no route — it is composed into whichever admin route opens it, conventionally/admin/items), (b) a multi-step wizard surface with four documented steps (Basic Info, Media & Links, Classification, Review & Submit) driven bygoToNextStep()/goToPreviousStep()mutator helpers and three submit buttons (createButton,updateButton,cancelButton), (c) a[role="dialog"][aria-modal="true"]accessibility-tree-canonical modal selector scoped viathis.modal.locator(...)for every per-step input field — the first admin-tree driver to document the explicitaria-modal="true"focus-trapping selector pair (distinct from the comments driver's bare[role="dialog"]posture and the companies driver's positional.fixed.inset-0.z-50Tailwind-overlay posture), (d) a per-step id-selector input field posture (#id,#name,#slug,#description,#icon_url,#source_url) for every step that emits HeroUI form inputs with a stableid, (e) a placeholder-regex input field posture (getByPlaceholder(/enter categories/i)/getByPlaceholder(/enter tags/i)) for the Classification step's autocomplete inputs that the production source does not bind to a stableid, (f) a bareselectHTML-element selector for the status field plus a[role="switch"]accessibility-tree-canonical selector for the featured toggle (distinct from the[role="checkbox"]posture the data-export / featured-items drivers' toggles use because HeroUI'sSwitchis a binary on/off toggle without an indeterminate state), (g) a stratified helper API across three categories (per-step fill helpersfillBasicInfo({...})/fillMediaLinks({...})/addCategory(name)/addTag(name), per-step navigation helpersgoToNextStep()/goToPreviousStep(), per-submit helperssubmitCreate()/submitUpdate()/cancel()), and (h) a per-modal lifecycle helper API (waitForOpen()/waitForClosed()) that wraps thethis.modal.waitFor(...)Playwright primitives in named, intent-revealing methods. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,items.page.ts,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin item-form modal driver boundary — a route-less, modal-bound driver composed into whichever admin route opens the modal (the conventional caller is/admin/items). Anchored byapps/web-e2e/tests/admin/items-crud.spec.ts(a full create-then-edit-then-delete flow over the admin items management surface —nameInput/descriptionInputfill on Step 1,nextButtonvalidity gate,sourceUrlInputfill on Step 2,addCategory(name)/addTag(name)autocomplete commits on Step 3,submitCreate()on the Last Step, then a second flow that opens the modal in edit mode and submits viasubmitUpdate()); cross-references tobase-page-object.mdfor the inheritance root the standalone posture diverges from,signin-page-object.md/item-detail-page-object.md/discover-page-object.mdfor the precedent standalone postures,admin-bulk-actions-page-object.mdfor the/admin/itemsparent route the modal is conventionally composed into,admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.md/admin-dashboard-page-object.md/admin-data-export-page-object.md/admin-featured-items-page-object.mdfor the prior admin-tree page-object boundaries this driver pairs with, and to the consuming spec atapps/web-e2e/tests/admin/items-crud.spec.tsfor the create / edit / delete flows the driver enables. Any future change to the item-form driver -- adding a per-stepassertStep(name)invariant assertion, asubmitAndWaitClosed()composite helper, a per-modalgetValidationError(field)helper, asetStatus(status)/toggleFeatured()setter pair on the Last Step's controls, or adata-testidmigration of the existing[role="dialog"][aria-modal="true"]/#name/#description/#source_url/getByPlaceholder(/enter categories/i)/[role="switch"]postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-item-form-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/items-crud.spec.tsfor the create / edit / delete flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound item-form driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the items-crud spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Item CRUD"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Items Page Object (
apps/web-e2e/page-objects/admin/items.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-management driver paired withapps/web-e2e/page-objects/admin/items.page.ts, the tenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) an eleven-readonly-Locator-field surface -- the largest per-page Locator inventory of any admin-tree driver to date covering page chrome, per-row selection, and the bulk-action toolbar; (b) a nine-method helper API -- the largest per-driver method count of any admin-tree driver to date covering navigation, per-status-tab filtering with a five-element TypeScript union, search-flow mutators, per-row resolution, per-row action-menu interactions, and per-row selection; (c) a two-modal-getter posture (rejectModalandbulkConfirmDialogboth pinned to[role="dialog"][aria-modal="true"]withhasTextfilters); (d) a<input>-id-bound modal-scoped input getter (rejectionReasonInputresolves viathis.rejectModal.locator('#rejectionReason')); (e) a<h4>-tag-anchored named-row resolver (getItemByName(name)) that uses a double-..parent walk to lift the resolution from the per-item heading up to the row container -- the first admin-tree driver posture to document a multi-level DOM-traversal resolution; (f) a multi-attribute composite OR-selector for the pagination Locator (nav[aria-label*="pagination"], nav[aria-label*="Pagination"]); (g) a partial-aria-label-substring-anchored toolbar selector ([role="toolbar"][aria-label*="ulk"]); and (h) exact-match^approve$/^reject$/^delete$regexes for the per-action bulk triggers (distinct from thebulkDeselectButton's substring/deselect/iposture). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,notifications.page.ts,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin items-management driver boundary at/admin/items-- the smallest possible page object that lets a spec drive the admin items page end-to-end. Anchored by four consuming spec files -- the largest spec-fan-out of any admin-tree driver to date:apps/web-e2e/tests/admin/items.spec.ts(three baseline flows),apps/web-e2e/tests/admin/items-crud.spec.ts(per-item CRUD lifecycle),apps/web-e2e/tests/admin/items-filter.spec.ts(status-tab + search filter flows), andapps/web-e2e/tests/admin/items-review.spec.ts(per-row review + bulk-action flows); cross-references tobase-page-object.mdfor the inheritance root,admin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.md/admin-dashboard-page-object.md/admin-data-export-page-object.md/admin-featured-items-page-object.md/admin-item-form-page-object.mdfor the prior admin-tree page-object boundaries, and to the four consuming specs above for the flows the driver enables. Any future change to the items driver -- adding a per-row Locator-factory beyondgetItemByName(name), aclickReject(itemName, reason)composite flow helper, anassertItemPresent(name)/assertItemAbsent(name)invariant assertion, aclickPaginationPage(page)/nextPage()/prevPage()pagination helper, or adata-testidmigration of the existinggetByRole('searchbox')/<h4>/[role="toolbar"][aria-label*="ulk"]/[role="dialog"][aria-modal="true"]postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-items-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the four consuming specs above for the flow envelopes, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound items driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the items spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Items"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Notifications Page Object (
apps/web-e2e/page-objects/admin/notifications.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin notifications dropdown driver paired withapps/web-e2e/page-objects/admin/notifications.page.ts, the eleventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a non-extends-BasePageposture (theAdminNotificationsclass is the first admin-tree driver that does NOT extendBasePage, by design — header-chrome dropdowns do not need the page-navigation helpersBasePageprovides); (b) a plain-class constructor binding all four core Locator fields directly in the constructor body (theBasePagesubclasses passsuper(page)first and bind their per-page Locators afterwards); (c) a four-readonly-Locator-field core surface (bellButton,dropdown,refreshButton,closeButton) plus a five-getter dropdown-content surface (markAllReadButton,unreadBadge,notificationItems,viewAllButton,emptyState); (d) apage.locator(…).first()-anchored bell-button field (page.locator('button[aria-label*="Notifications"]').first()) — the first admin-tree driver field that documents an explicit collection-narrowing call; (e) a#admin-notifications-dropdownid-selector posture for the dropdown panel that scopes every dropdown-content getter viathis.dropdown.getByRole(…)/this.dropdown.locator(…); (f) a two-action surface —open()andclose()— distinct from every prior admin-tree driver's larger method surfaces; (g) a regex-basedgetByRole('button', { name: … })resolution for themarkAllReadButtonandviewAllButtongetters that defends against bilingual capitalisation drift; (h) a.animate-pulse-Tailwind-utility-class-anchored unread-badge getter — the first admin-tree driver getter that documents a Tailwind-utility-class-anchored resolution; (i) a[role="button"]-anchorednotificationItemsgetter (vs.getByRole('button')) that disambiguates the per-row count from the per-action button count because the production-source notification items are rendered as<div role="button">; and (j) agetByText(/no notifications/i)empty-state getter — the first admin-tree driver getter that documents a text-content-anchored resolution. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,reports.page.ts,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root every other admin-tree page object extends, this page documents the suite's admin notifications dropdown driver boundary -- the smallest possible page object that lets a spec drive the admin shell's bell-button + dropdown + per-content surface end-to-end. Anchored by the single consuming spec fileapps/web-e2e/tests/admin/notifications.spec.ts(five test cases covering the bell-button visibility, dropdown open / close, content-or-empty state, and the refresh button); cross-references tobase-page-object.mdfor the inheritance root the standalone posture diverges from,auth-fixture.mdfor theadminPagefixture the consuming spec uses,admin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.md/admin-dashboard-page-object.md/admin-data-export-page-object.md/admin-featured-items-page-object.md/admin-item-form-page-object.md/admin-items-page-object.mdfor the prior admin-tree page-object boundaries, and to the consuming spec atapps/web-e2e/tests/admin/notifications.spec.tsfor the dropdown lifecycle flows the driver enables. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-notifications-query.spec.tswhich covers the route-side two-step gate (session?.user?.id→ 401, thensession.user.isAdmin→ 403). Any future change to the notifications driver -- adding a per-notificationmarkAsRead(text)helper, arefresh()composite that wraps the refresh-button click + reload settle, agetUnreadCount(): Promise<number>accessor, agetNotificationByText(text)named-row resolver, or adata-testidmigration of the existingaria-label*="Notifications"/#admin-notifications-dropdown/aria-label="Refresh notifications"/aria-label="Close notifications panel"postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-notifications-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/notifications.spec.tsfor the dropdown flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound notifications driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the notifications spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Admin: Notifications"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Reports Page Object (
apps/web-e2e/page-objects/admin/reports.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin reports-management driver paired withapps/web-e2e/page-objects/admin/reports.page.ts, the twelfth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a<button>-anchored status-tab navigation surface rather than thegetByRole('tab')-anchored navigation surface every prior status-tab driver uses (the reports page emits the status filter as<button>elements, not[role="tab"]/[role="tablist"]); (b) a five-element status-tab TypeScript union ('All' | 'Pending' | 'Reviewed' | 'Resolved' | 'Dismissed') reflecting the report lifecycle's distinct state machine (Pending → Reviewed → Resolved / Dismissed) rather than the items lifecycle (Draft → Pending → Approved / Rejected); (c) a.border-l-4Tailwind-utility-anchored card-list selector (reportCards) -- the first admin-tree driver to pin per-row resolution to a Tailwind border-utility class rather than a semantic role; (d) a broad/review/isubstringreviewButtonsLocator that intentionally resolves to a multi-element match (symmetric with the data-export driver'sexportButtonsposture); and (e) a bare[role="dialog"]review-dialog getter without the[aria-modal="true"]composite attribute the items driver'srejectModalgetter uses (the bare role posture tolerates HeroUI's per-versionaria-modaldrift). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,notifications.page.ts-- seeadmin-notifications-page-object.md,roles.page.ts,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin reports-management driver boundary at/admin/reports-- the smallest possible page object that lets a spec drive the admin reports page end-to-end. Anchored byapps/web-e2e/tests/admin/reports.spec.ts(five flows over the admin reports management surface -- admin can access reports management page, reports page displays stats cards, status tabs filter reports, admin can open review dialog for a report, reports page shows empty state for non-matching search); cross-references tobase-page-object.mdfor the inheritance root,admin-bulk-actions-page-object.md/admin-clients-page-object.md/admin-collections-page-object.md/admin-comments-page-object.md/admin-companies-page-object.md/admin-dashboard-page-object.md/admin-data-export-page-object.md/admin-featured-items-page-object.md/admin-item-form-page-object.md/admin-items-page-object.md/admin-notifications-page-object.mdfor the prior admin-tree page-object boundaries, and to the consuming spec atapps/web-e2e/tests/admin/reports.spec.tsfor the five flows the driver enables. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-reports-query.spec.tswhich covers the route-side single-step gate (!session?.user?.isAdmin→ 403 'Forbidden', distinct from the notifications route's two-step 401/403 gate). Any future change to the reports driver -- adding a per-card Locator-factory beyond thereviewButtonsmulti-resolution Locator, aclickReview(reportId)/dismissReport(reportId)/resolveReport(reportId, notes)flow helper, anassertCardCount(n)/assertEmptyState()invariant assertion, aclearSearch()reset helper, or adata-testidmigration of the existinggetByRole('button')/getByRole('searchbox')/[role="dialog"]/.border-l-4postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-reports-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/reports.spec.tsfor the five-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound reports driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the reports spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Reports"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Roles Page Object (
apps/web-e2e/page-objects/admin/roles.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin roles-management driver paired withapps/web-e2e/page-objects/admin/roles.page.ts, the thirteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a<select>-anchored dual-filter surface for status and role-type — distinct from every prior admin-tree driver in the rollout which pin status filters via eithergetByRole('tab')(items, clients, comments, companies, collections) orgetByRole('button')(reports). The roles page emits each filter as a native HTML<select>element, and the driver locates them positionally viapage.locator('select').first()andpage.locator('select').nth(1); (b) a modal-overlay-getter triplet (roleFormModal,deleteRoleDialog,permissionsModal) pinned to the.fixed.inset-0.z-50Tailwind-utility-stack selector rather than the[role="dialog"]/[aria-modal="true"]accessibility-tree-canonical selectors every prior admin-tree driver uses (the roles page renders modals as bare Tailwind-utility-stacked<div>elements, NOT[role="dialog"]/[aria-modal="true"]ARIA-tree-canonical surfaces); (c) aLocator.filter({ hasText })chained Locator posture for the two specialised modal getters — the first admin-tree driver in the rollout to useLocator.filter({ hasText })for modal disambiguation; (d) asearchRoles(term)flow helper that does NOT trigger search submission (consumer must wait the debounce window explicitly); (e) a baregetByRole('heading').first()heading resolver and a baregetByRole('button', { name: /add role/i }).first()add-button resolver; and (f) a<input type="text">first-element search resolver (the roles page emits the search input as a bare<input type="text">, NOT a<input type="search">resolvable viagetByRole('searchbox')). Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,notifications.page.ts-- seeadmin-notifications-page-object.md,reports.page.ts-- seeadmin-reports-page-object.md,settings.page.ts,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin roles-management driver boundary at/admin/roles-- the smallest possible page object that lets a spec drive the admin roles page end-to-end. Anchored byapps/web-e2e/tests/admin/roles.spec.ts(four flows over the admin roles management surface -- admin can access roles management page, roles page displays stats cards, admin can search roles, admin can open add role form modal); cross-references tobase-page-object.mdfor the inheritance root, all twelve prior admin-tree page-object docs for the prior admin-tree page-object boundaries, and to the consuming spec atapps/web-e2e/tests/admin/roles.spec.tsfor the four flows the driver enables. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-roles-stats-query.spec.tswhich covers the route-side two-step gate (session?.user→ 401 'Unauthorized', thensession.user.isAdmin→ 403 'Forbidden') for/api/admin/roles/stats— distinct from the reports route's single-step!session?.user?.isAdmin→ 403 'Forbidden' gate AND from the clients / comments / companies / users routes' single-step!session?.user?.isAdmin→ 401 'Unauthorized' gate. Any future change to the roles driver -- adding a per-row Locator-factory beyond the positional<select>resolvers, aclickAddRole()/clickDeleteRole(name)/editRolePermissions(name)flow helper, anassertRolePresent(name)/assertRoleAbsent(name)invariant assertion, a debounce-wait helper for thesearchRoles(term)posture, or adata-testidmigration of the existing positional-<select>/.fixed.inset-0.z-50Tailwind-utility-stack postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-roles-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/roles.spec.tsfor the four-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound roles driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the roles spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Roles"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Settings Page Object (
apps/web-e2e/page-objects/admin/settings.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin settings-management driver paired withapps/web-e2e/page-objects/admin/settings.page.ts, the fourteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a minimal-fields, accordion-section-driven driver posture -- the smallest admin-tree page object surface to date with only ONEreadonlyLocator field (heading), one method (navigate()), one helper (openSection(sectionName)), and two getters (switches,selects) -- justified by the page's content shape (a vertical accordion of collapsed sections, each opened on demand); (b) agetByRole('button', { name: ... }).first()accordion trigger resolver that uses a runtime-builtRegExp(new RegExp(sectionName, 'i')) rather than a static regex literal -- letting the consuming spec drive any accordion section by name via a string parameter; (c) a broad multi-resolutionswitchesLocator (page.locator('[role="switch"]')) that exposes every toggle switch on the page -- the first admin-tree driver to do so; (d) a broad multi-resolutionselectsLocator (page.locator('select')) using the bare HTML element-selector to pin to native<select>elements (distinct from the WAI-ARIA[role="listbox"]posture HeroUI's ReactSelectcomponent emits); and (e) a per-section accordion lifecycle posture with seven canonical sections (General, Homepage, Header, Footer, Monetization, Location, Navigation) -- distinct from every prior admin-tree driver where the page is a flat surface or a single-modal composite. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,notifications.page.ts-- seeadmin-notifications-page-object.md,reports.page.ts-- seeadmin-reports-page-object.md,roles.page.ts-- seeadmin-roles-page-object.md,sponsorships.page.ts,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin settings-management driver boundary at/admin/settings-- the smallest possible page object that lets a spec drive the admin settings page end-to-end. Anchored byapps/web-e2e/tests/admin/settings.spec.ts(six flows over the admin settings management surface -- admin can access settings page, settings page has accordion sections, admin can expand General Settings section, admin can expand Homepage Settings section, admin can expand Header Settings section, admin can expand Monetization Settings section); cross-references tobase-page-object.mdfor the inheritance root, all thirteen prior admin-tree page-object docs, and to the consuming spec atapps/web-e2e/tests/admin/settings.spec.tsfor the six flows the driver enables. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-settings-query.spec.tswhich covers the route-sidegetCachedApiSession(req)cached-session helper (a custom variant ofauth()that caches the session lookup per-request) and the bare{ error: 'Unauthorized' }envelope (NOT{ success: false, error }shape every other admin-tree route emits). Any future change to the settings driver -- adding acloseSection(sectionName)helper, anisOpen(sectionName): Promise<boolean>accessor, per-section convenience methods (openGeneral()/openHomepage()/ etc.), atoggleSwitch(switchName)/selectOption(selectName, value)form-control helper, asubmit()/save()form-submission helper, or adata-testidmigration of the existing[role="switch"]/select/getByRole('button')postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-settings-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/settings.spec.tsfor the six-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound settings driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the settings spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Settings"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Sponsorships Page Object (
apps/web-e2e/page-objects/admin/sponsorships.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsorships-management driver paired withapps/web-e2e/page-objects/admin/sponsorships.page.ts, the fifteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a dual-modal-getter posture that uses two different selector strategies for two semantically distinct modals on the same page —rejectModalpinned to the WAI-ARIA-canonical[role="dialog"][aria-modal="true"]selector with.first()(cheapest resolver because the rejection modal is positionally first),forceApproveModalpinned to the less-strict[role="dialog"]selector chained withLocator.filter({ hasText: /force approve/i })(no positional guarantee because confirmation, error, or info modals may mount between them); (b) a fire-and-forgetsearchSponsorships(term)flow helper that does NOT trigger search submission — symmetric with the roles driver'ssearchRoles(term)posture (consumer must wait the debounce window explicitly viapage.waitForTimeout(…)); (c) a<input>-id-bound modal-scoped input getter (rejectionReasonInput) that resolves at the page-scope viathis.page.locator('#rejectionReason')rather than the modal-scope (this.rejectModal.locator('#rejectionReason')) — defensive against future portal-render refactors that mount the textarea outside the modal subtree; (d) agetByRole('searchbox').first()search input resolver symmetric with the items / clients / comments / companies / collections drivers' search posture (the sponsorships page emits the search input as a native<input type="search">resolvable viagetByRole('searchbox')— distinct from the roles driver's bare<input type="text">first-element posture); and (e) a baregetByRole('heading').first()heading resolver. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,notifications.page.ts-- seeadmin-notifications-page-object.md,reports.page.ts-- seeadmin-reports-page-object.md,roles.page.ts-- seeadmin-roles-page-object.md,settings.page.ts-- seeadmin-settings-page-object.md,surveys.page.ts,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin sponsorships-management driver boundary at/admin/sponsorships-- the smallest possible page object that lets a spec drive the admin sponsorships page end-to-end. Anchored byapps/web-e2e/tests/admin/sponsorships.spec.ts(three flows over the admin sponsorships management surface -- admin can access sponsorships management page, sponsorships page displays stats and content, admin can search sponsorships); cross-references tobase-page-object.mdfor the inheritance root, all fourteen prior admin-tree page-object docs, and to the consuming spec atapps/web-e2e/tests/admin/sponsorships.spec.tsfor the three flows the driver enables. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-sponsor-ads-query.spec.tswhich covers the route-side single-step!session?.user?.isAdmin→ 401'Unauthorized. Admin access required.'gate (the longer-message variant — distinct from the bare'Unauthorized'message every other admin-tree route emits and distinct from the bare'Forbidden'message the reports route's single-step gate emits) for/api/admin/sponsor-adsand pins the AFTER-the-auth-gate ordering ofvalidatePaginationParams(searchParams)andquerySponsorAdsSchema.safeParse(queryParams)(a regression that swaps the order would surface as 400 instead of 401 on the unauth branch). Any future change to the sponsorships driver -- adding aclickReject(itemName, reason)/clickForceApprove(itemName)composite flow helper, anassertSponsorshipPresent(name)/assertSponsorshipAbsent(name)invariant assertion, aselectStatusTab(status)/selectStatusFilter(status)status-filter helper, aclickPaginationPage(page)/nextPage()/prevPage()pagination helper, agetSponsorshipByName(name)/getSponsorshipById(id)per-row Locator-factory, or adata-testidmigration of the existinggetByRole('searchbox')/[role="dialog"][aria-modal="true"]/[role="dialog"]+hasText/#rejectionReasonpostures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-sponsorships-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/sponsorships.spec.tsfor the three-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound sponsorships driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the sponsorships spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Sponsorships"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Surveys Page Object (
apps/web-e2e/page-objects/admin/surveys.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin surveys-management driver paired withapps/web-e2e/page-objects/admin/surveys.page.ts, the sixteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/and the first admin-tree driver in the rollout that documents (a) a barepage.locator('h1').first()heading resolver -- distinct from every other admin-tree driver'spage.getByRole('heading').first()posture (the surveys page emits its top-level heading as a literal<h1>element today, so the bare-tag-name selector is the production-source-stable hook); (b) a literal-union-typedselectFilter(filter)flow helper that takes a'all' | 'global' | 'item'literal-union argument and dispatches on aRecord<string, RegExp>filterMap to agetByRole('button', { name: filterMap[filter] }).first().click()call -- with three case-insensitive regexes (/all surveys/i,/global/i,/items/i) -- the literal-union typing is load-bearing because it enforces the filterMap domain at compile-time (a regression that drops the union type would let consumers pass an arbitrary filter name resolving toundefinedvia thefilterMapindex, surfacing as a runtimeCannot read properties of undefinedfailure rather than a compile-time type error); (c) a dual index-based per-row Locator-factory posture (getEditButton(index)/getDeleteButton(index)) that returnsthis.page.locator('button[title*="Edit"]').nth(index)andthis.page.locator('button[title*="Delete"]').nth(index)-- the first title-attribute substring posture in the admin-tree page-object subtree (the per-row buttons are icon-only buttons with no visible text label, so thetitleattribute substring-match is the next-best production-source-stable hook); and (d) agetByRole('button', { name: /create survey/i }).first()CTA-button resolver that pins to the case-insensitive accessible-name regex match for the page's primary CTA with.first()defence against the empty-state illustration's duplicate CTA. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,notifications.page.ts-- seeadmin-notifications-page-object.md,reports.page.ts-- seeadmin-reports-page-object.md,roles.page.ts-- seeadmin-roles-page-object.md,settings.page.ts-- seeadmin-settings-page-object.md,sponsorships.page.ts-- seeadmin-sponsorships-page-object.md,tags.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin surveys-management driver boundary at/admin/surveys-- the smallest possible page object that lets a spec drive the admin surveys page end-to-end. Anchored byapps/web-e2e/tests/admin/surveys.spec.ts(four flows over the admin surveys management surface -- admin can access surveys management page, surveys page shows create survey button (with a fallback warning-banner check when the surveys feature is flag-disabled), filter buttons switch between All / Global / Item surveys, survey list shows edit and delete actions); cross-references tobase-page-object.mdfor the inheritance root, all fifteen prior admin-tree page-object docs, and to the consuming spec atapps/web-e2e/tests/admin/surveys.spec.tsfor the four flows the driver enables. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-tags-query.spec.tswhich covers the route-side single-step!session?.user?.isAdmin→ 401{ success: false, error: 'Unauthorized' }gate (the bare-message-with-success-key envelope variant -- the only admin-tree route that combines both the bare'Unauthorized'message AND thesuccess: falsediscriminator key, distinct from the longer-message variant'Unauthorized. Admin access required.'that theadmin/categories/admin/sponsor-adsroutes emit and distinct from the bare-key envelope{ error: 'Unauthorized' }(nosuccess: falsediscriminator) that theadmin/clients/admin/comments/admin/companies/admin/usersroutes emit) for/api/admin/tagsand pins the AFTER-the-auth-gate ordering ofvalidatePaginationParams(searchParams)(a regression that swaps the order would surface as a 400'Invalid page parameter. …'instead of a 401 on the unauth branch when the query is malformed). Any future change to the surveys driver -- adding aclickCreateSurvey()flow helper, aclickEditSurvey(index)/clickDeleteSurvey(index)composite flow helper, anassertSurveyPresent(name)/assertSurveyAbsent(name)invariant assertion, awaitForListReady()post-load wait helper to replace the consuming spec's inlinewaitForTimeout(2_000)calls, asurveyForm/surveyEditModalmodal Locator, aclickPaginationPage(page)/nextPage()/prevPage()pagination helper, an additional filter value beyond'all'/'global'/'item'(e.g.'archived','draft') that must extend BOTH the literal-union type AND thefilterMapdispatch, or adata-testidmigration of the existingh1/getByRole('button', { name: /create survey/i })/getByRole('button', { name: filterMap[filter] })/button[title*="Edit"]/button[title*="Delete"]postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-surveys-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/surveys.spec.tsfor the four-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound surveys driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the surveys spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Surveys"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Admin Tags Page Object (
apps/web-e2e/page-objects/admin/tags.page.ts) -- Per-source-file reference for the Playwright e2e suite's admin tags-management driver paired withapps/web-e2e/page-objects/admin/tags.page.ts, the seventeenth and final per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, completing the admin-tree page-object docs rollout (17-of-17). With this page landed, every concrete page-object source file underapps/web-e2e/page-objects/admin/has a paired per-source-file docs anchor that explains the load-bearing reasons each Locator pins to its current selector and the cross-references that any new helper must respect. The tags driver is the first admin-tree driver in the rollout that documents (a) a named-row-resolved CRUD helper trio (getTagByName(name),editTag(name),deleteTag(name)) -- the most direct named-row-driven CRUD posture in the admin tree (distinct from the items driver's resolver-only posture and from the collections driver's single-parent-walk resolver); (b) a<div>-anchored named-row resolver with a^${name}start-anchor regex -- the broadest possible row-anchor in the admin tree plus the first admin-tree driver posture to document a regex-based hasText filter that pins to the row's text content STARTING with the tag name; (c) a#tag-idand#tag-namehyphenated-kebab-case id-selector input field pair -- production-source-stable hooks following the production source's HTML form convention rather than HeroUI's camelCase default; (d) a modal-scoped[role="switch"]status toggle getter scoped through thetagFormModal(distinct from the settings driver's page-levelswitchesmulti-resolution Locator); (e) a.fixed.inset-0.z-50Tailwind-overlay form modal (matching the companies and roles drivers); (f) a per-mode submit-button-pair posture (createTagButton/updateTagButton) mirroring the companies driver; and (g) a two-keydata: { id?: string; name: string }optional-id form-fill helper -- the first admin-tree driver helper to document a conditional-fill posture driven by an optional TypeScript object key. Sits inside theadmin/page-object subtree alongside the sixteen sibling admin-surface page objects (bulk-actions.page.ts-- seeadmin-bulk-actions-page-object.md,clients.page.ts-- seeadmin-clients-page-object.md,collections.page.ts-- seeadmin-collections-page-object.md,comments.page.ts-- seeadmin-comments-page-object.md,companies.page.ts-- seeadmin-companies-page-object.md,dashboard.page.ts-- seeadmin-dashboard-page-object.md,data-export.page.ts-- seeadmin-data-export-page-object.md,featured-items.page.ts-- seeadmin-featured-items-page-object.md,item-form.page.ts-- seeadmin-item-form-page-object.md,items.page.ts-- seeadmin-items-page-object.md,notifications.page.ts-- seeadmin-notifications-page-object.md,reports.page.ts-- seeadmin-reports-page-object.md,roles.page.ts-- seeadmin-roles-page-object.md,settings.page.ts-- seeadmin-settings-page-object.md,sponsorships.page.ts-- seeadmin-sponsorships-page-object.md,surveys.page.ts-- seeadmin-surveys-page-object.md). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's admin tags-management driver boundary at/admin/tags-- the smallest possible page object that lets a spec drive the admin tags page end-to-end. Anchored byapps/web-e2e/tests/admin/tags.spec.ts(five flows over the admin tags management surface -- admin can access tags management page, admin can create a new tag, admin can edit an existing tag, admin can delete a tag using native confirm dialog, tags page shows tag count in stats); cross-references tobase-page-object.mdfor the inheritance root and all sixteen prior admin-tree page-object docs. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-tags-all-query.spec.tswhich covers the route-sidegetCachedItems({ lang })Git-CMS reader (distinct from every other admin-tree route's database-backed posture) and the?locale=query param with type-coercion validation. Any future change to the tags driver -- adding acreateTag(data)/submitCreate(data)/submitEdit(data)composite flow helper, aconfirmDelete()helper for the nativeconfirm()dialog, anassertTagPresent(name)/assertTagAbsent(name)invariant assertion, agetTagsCount(): Promise<number>accessor, or adata-testidmigration of the existing<div>-anchored row resolver /#tag-id/#tag-name/[role="switch"]/.fixed.inset-0.z-50postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateadmin-tags-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/admin/tags.spec.tsfor the five-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandadminPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound tags driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the tags spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Tags"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the admin-tree page-object docs rollout is complete (17-of-17); subsequent rollouts should turn to theapps/web-e2e/page-objects/auth/and remainingapps/web-e2e/page-objects/client/subtrees. - E2E Client Dashboard Page Object (
apps/web-e2e/page-objects/client/dashboard.page.ts) -- Per-source-file reference for the Playwright e2e suite's client dashboard driver paired withapps/web-e2e/page-objects/client/dashboard.page.ts, the first per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/, opening the client-tree page-object docs rollout (1-of-6) that mirrors the seventeen-file admin-tree rollout completed atadmin-tags-page-object.md. The five sibling client page objects this rollout will publish references for in subsequent runs areprofile.page.ts,settings.page.ts,submissions.page.ts,submit.page.ts, andtrash.page.ts(six total underapps/web-e2e/page-objects/client/). The client dashboard driver is the first client-tree driver in the rollout that documents (a) a smallest-possible-surface posture with only anavigate()method and three pre-boundLocatorfields (heading/statsGrid/welcomeText) -- no composite primitives today; consuming specs drive the page directly via inline locators on top of theauth-fixture.mdclientPageauthenticated-page fixture (a posture symmetric withdiscover-page-object.mdandsignin-page-object.md); (b) agetByRole('heading', { name: /dashboard/i })locale-tolerant case-insensitive substring resolver for the dashboard heading -- the highest-stability locator per Playwright's locator-priority guidance and the most resilient against future English-string changes ("My Dashboard" / "Welcome to your Dashboard") and locale switches across EN / FR / ES / DE / AR / ZH; (c) a.grid.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-4Tailwind responsive class chain anchor for the stats grid -- a brittle-by-design structural anchor that pins the responsive 1-column-mobile / 2-column-tablet / 4-column-desktop grid contract that the dashboard's visual hierarchy depends on (nodata-testidis wired up today, and the change checklist anticipates a futuredata-testid="stats-grid"migration); (d) agetByText(/welcome back/i)greeting-string-tolerant resolver for the per-user welcome message -- substring-anchored to survive a future "Welcome back, {name}!" or "Welcome back to {feature}" change without masking a real greeting-rename regression; and (e) a.first()strict-mode-correctness append on every Locator field -- preventing future strict-mode multi-match errors from a second/dashboard/iheading, a secondary stats grid, or a "Welcome back to {feature}" widget. Sits inside theclient/page-object subtree alongside the five sibling client-surface page objects pending docs (profile.page.ts,settings.page.ts,submissions.page.ts,submit.page.ts,trash.page.ts) and is paired one-to-one with routes underapps/web/app/[locale]/client/**, the authenticated client area of the public-facing app (distinct from the admin area at/admin/**which the seventeenadmin-*references cover, and distinct from the public-facing pages at/,/discover,/items/[slug], etc. that thepublic-pages-page-object.mdthroughitem-detail-page-object.mdfourteen public-tree references cover). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's authenticated-client dashboard driver boundary at/client/dashboard-- the smallest possible page object that lets a spec drive the client dashboard end-to-end. Anchored byapps/web-e2e/tests/client/dashboard.spec.ts(three flows over the client dashboard surface -- authenticated client can access dashboard at/client/dashboard, unauthenticated user is redirected to/auth/signinby the[locale]/client/**middleware gate, dashboard displays a/dashboard/iheading); cross-references tobase-page-object.mdfor the inheritance root,auth-fixture.mdfor theclientPageauthenticated-page fixture,signin-page-object.mdfor the auth-tree driver consuming specs depend on for the authenticatedclientPagefixture's setup precondition,admin-dashboard-page-object.mdfor the admin-area dashboard sibling concept,discover-page-object.mdfor another smallest-possible-surface page-object posture this driver mirrors,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURLposture this file's navigation resolves against, andfixtures-index.mdfor the fixture barrel that exposes theclientPageauthenticated-page fixture. Pinned to the co-tenant API smoke spec atapps/web-e2e/tests/api/client-dashboard-stats-query.spec.tswhich covers the route-sidegetCachedApiSession()cached-session helper and pins the unauthenticated GET branch's 401 envelope contract. Any future change to the client dashboard driver -- adding agetStat(name)per-stat-card resolver, aclickFirstActivity()activity-feed primitive, anassertWelcomeMessage(name)invariant assertion, adata-testid="stats-grid"migration of the existing Tailwind class chain, or a fixture-boundclientDashboardPageaccessor -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateclient-dashboard-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/client/dashboard.spec.tsfor the three-flow envelope, cross-checkbase-page-object.mdfor the inheritance root, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandclientPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound client dashboard driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the client dashboard subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client: Dashboard"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring across the client/ subtree, and a reviewer pass. With this entry the client-tree page-object docs rollout opens (1-of-6); subsequent rollouts in this subtree will turn toprofile.page.ts,settings.page.ts,submissions.page.ts,submit.page.ts, andtrash.page.ts. - E2E Client Profile Page Object (
apps/web-e2e/page-objects/client/profile.page.ts) -- Per-source-file reference for the Playwright e2e suite's client profile / settings driver paired withapps/web-e2e/page-objects/client/profile.page.ts, the second per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/(continuing the client-tree page-object docs rollout, 2-of-6) and the first client-tree driver in the rollout that documents (a) a multi-route navigation pair (navigateToSettings()/navigateToBasicInfo()) — the first client-tree driver to expose more than one navigation shortcut, distinct from every prior page-object driver in the suite which exposes a singlenavigate()method; (b) an eight-input form-field cluster (displayNameInput,usernameInput,bioInput,locationInput,companyInput,jobTitleInput,websiteInput,saveButton) -- the largest per-page form-field inventory of any non-modal page-object driver in the suite; (c) a camelCase id-selector input field cluster (#displayName,#bio,#jobTitle) matching the HeroUI<Input>component's defaultidemission for camelCasenameprops (distinct from the tags driver's hyphenated kebab-case#tag-idposture and from the item-form driver's snake_case#icon_urlposture); (d) a.gridTailwind-utility-anchored settings-cards getter (settingsCards); and (e) a page-level form posture -- distinct from every admin-tree driver's modal-bound form posture (the basic-info form is rendered page-level on a dedicated route at/client/settings/profile/basic-info, not inside a modal overlay). Sits inside theclient/page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts-- seeclient-dashboard-page-object.md,settings.page.ts,submissions.page.ts,submit.page.ts,trash.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's client profile / settings driver boundary at/client/settingsand/client/settings/profile/basic-info. Anchored byapps/web-e2e/tests/client/profile.spec.ts(five flows over the client profile / settings surface -- client can access settings page, settings page shows settings cards grid, client can access basic info form, basic info form has save button, display name field accepts input); cross-references tobase-page-object.mdfor the inheritance root,client-dashboard-page-object.mdfor the client-tree rollout-template precedent,signin-page-object.mdfor the auth-tree driver consuming specs depend on for the authenticatedclientPagefixture's setup precondition,admin-tags-page-object.mdandadmin-item-form-page-object.mdfor the admin-tree id-selector-posture variants this driver's camelCase posture intentionally diverges from. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-clients-dashboard-query.spec.tswhich covers the first admin-tree route the smoke layer covers that documents thecheckAdminAuth()three-step guard (from@/lib/auth/admin-guard.ts) — distinct from every other admin-tree route's inline gate posture. Any future change to the profile driver -- adding a per-fieldsetDisplayName(name)/setBio(text)setter helper, afillBasicInfo(data)composite form-fill helper, asubmitBasicInfo()/saveAndAssertSuccess()composite flow helper, agetDisplayName(): Promise<string>/getBio(): Promise<string>accessor, per-tab navigation methods beyondnavigateToBasicInfo()(e.g.navigateToLocation()/navigateToSecurity()), or adata-testidmigration of the existing camelCase id-selector /getByRole('button', { name: /save/i })postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateclient-profile-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/client/profile.spec.tsfor the five-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandclientPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound profile driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the profile spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Profile"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 2-of-6; subsequent rollouts in this subtree will turn tosettings.page.ts,submissions.page.ts,submit.page.ts, andtrash.page.ts. - E2E Client Settings Page Object (
apps/web-e2e/page-objects/client/settings.page.ts) -- Per-source-file reference for the Playwright e2e suite's client settings index driver paired withapps/web-e2e/page-objects/client/settings.page.ts, the third per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/(continuing the client-tree page-object docs rollout, 3-of-6) and the first client-tree driver in the rollout that documents (a) a three-link navigation-shelf cluster (basicInfoLink,securityLink,billingLink) -- the first client-tree driver to expose pre-bound Locators for in-page navigation links, anchored togetByRole('link', { name: /…/i })substring resolvers, distinct from the profile driver's multi-route navigation method posture which routes viagoto(); (b) a.grid.grid-cols-1.md\\:grid-cols-2Tailwind-class-chain settings-grid getter (settingsGrid) -- pinned to the responsive 1-column-mobile / 2-column-tablet+ Tailwind class chain, distinct from the profile driver's bare.gridposture and from the client-dashboard driver's wider four-column-desktop chain; (c) a singlenavigate()method --goto('/client/settings')-- symmetric with every prior page-object driver in the suite that exposes a single navigation shortcut, distinct from the profile driver's multi-routenavigateToSettings()/navigateToBasicInfo()pair because the settings index is a single route whose only purpose is to render the navigation shelf of cards; (d) alevel: 1heading getter --getByRole('heading', { level: 1 }).first()-- the first client-tree driver that pins the heading Locator to the per-page H1 specifically, distinct from the dashboard driver'sname: /dashboard/isubstring pin and from the profile driver's baregetByRole('heading').first()pin; and (e) a navigation-shelf-only posture -- the driver exposes Locators for the shelf of navigation cards (heading + grid + three links) but no form-field Locators because the/client/settingsindex is the per-tenant navigation shelf for the per-tab forms (the forms themselves render under/client/settings/profile/basic-info,/client/settings/security/*, etc., scoped through the profile driver and any future per-tab driver, not through this index driver). Sits inside theclient/page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts-- seeclient-dashboard-page-object.md,profile.page.ts-- seeclient-profile-page-object.md,submissions.page.ts,submit.page.ts,trash.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's client settings index driver boundary at/client/settings-- the smallest possible page object that lets a spec drive the client settings shelf end-to-end. Anchored byapps/web-e2e/tests/client/settings.spec.ts(three flows over the client settings index surface -- authenticated client can access settings page, settings page displays settings cards via agetByRole('link')count assertion, unauthenticated user is redirected from settings via the[locale]/client/**middleware redirect to/auth/signin); cross-references tobase-page-object.mdfor the inheritance root,client-dashboard-page-object.mdfor the rollout-template precedent,client-profile-page-object.mdfor the multi-route navigation pair posture this driver's single-method posture intentionally diverges from,signin-page-object.mdfor the auth-tree driver consuming specs depend on for the authenticatedclientPagefixture's setup precondition,auth-fixture.mdfor theclientPagefixture used in the two authenticated flows. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-location-index-query.spec.tswhich covers the second admin-tree route the smoke layer covers that documents thecheckAdminAuth()three-step guard from@/lib/auth/admin-guard.tsAND is the first admin-tree route covered by the smoke layer that exposes BOTH aGETAND aPOSThandler. Any future change to the settings driver -- adding per-tabclickBasicInfoLink()/navigateToBasicInfoTab()shortcuts, agetCardCount(): Promise<number>accessor, aclickFirstCard()/assertCardOrder(labels)composite flow helper, a fixture-boundclientSettingsPageaccessor, anavigateAndWait()composite navigation helper, or adata-testidmigration of the existing.grid.grid-cols-1.md\\:grid-cols-2Tailwind class chain -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateclient-settings-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/client/settings.spec.tsfor the three-flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandclientPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound settings driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the settings spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client: Settings"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 3-of-6; subsequent rollouts in this subtree will turn tosubmissions.page.ts,submit.page.ts, andtrash.page.ts. - E2E Client Submissions Page Object (
apps/web-e2e/page-objects/client/submissions.page.ts) -- Per-source-file reference for the Playwright e2e suite's client submissions management driver paired withapps/web-e2e/page-objects/client/submissions.page.ts, the fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/(continuing the client-tree page-object docs rollout, 4-of-6) and the first client-tree driver in the rollout that documents (a) a named-row-resolved CRUD helper trio (viewSubmission(title),editSubmission(title),deleteSubmission(title)) -- the first client-tree driver to expose per-row imperative helpers, mirroring the admin-treeadmin-tags-page-object.mddriver'seditTag(name)/deleteTag(name)posture and theadmin-collections-page-object.mddriver'seditCollection(name)/deleteCollection(name)posture but adapted for the client surface; (b) a named-row resolver via two-parent-walk (getSubmissionByTitle(title)) -- the helper resolves a submission row by walkingpage.locator('h3').filter({ hasText: title }).first().locator('..').locator('..'), pinning to the production source's two-deep<h3>→ row card grandparent shape (the deepest parent-walk in the page-object suite, encoding the production source's card-with-header-and-actions layout pattern); (c) abutton[title*="…"]substring-attribute-selector triplet (button[title*="iew"]/button[title*="dit"]/button[title*="elete"]) -- the row-action buttons resolve via the HTMLtitleattribute's substring (intentionally dropping the leading capital so that "View" / "view" / "VIEW" all match), the first client-tree driver to document an HTML-attribute-substring selector posture distinct from the admin-tree drivers'aria-label/getByRolepostures; (d) a status-filter tab navigator (selectStatusFilter(status: 'all' | 'pending' | 'approved' | 'rejected')) -- a literal-union TypeScript parameter that drives status-tab clicks viagetByRole('button', { name: new RegExp('^${status}', 'i') }).first().click()with start-anchor regex defending against future button-text drift like "All submissions" / "Pending review" / "Approved items" / "Rejected by moderator"; (e) a three-modal getter triplet (detailModal,editModal,deleteDialog) -- the first client-tree driver to document multiple[role="dialog"]re-evaluating Locator getters with distinct scoping strategies (detailModaluses bare.first(),editModaluses.filter({ has: this.page.locator('#name') })form-field-presence scope,deleteDialoguses.filter({ hasText: /delete/i })body-text scope); (f) a navigation-shelf header pair (heading,newSubmissionLink,trashLink); and (g) a search-input field (searchInput) pinned viainput[type="text"][placeholder*="earch"]-- the substring-on-placeholderselector drops the leading capital so that "Search" / "search" / "Search submissions" / "search items" all match. Sits inside theclient/page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts-- seeclient-dashboard-page-object.md,profile.page.ts-- seeclient-profile-page-object.md,settings.page.ts-- seeclient-settings-page-object.md,submit.page.ts,trash.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's client submissions management driver boundary at/client/submissions-- the smallest possible page object that lets a spec drive the client submissions list end-to-end (search, filter by status, view / edit / delete a per-row submission, navigate to the new-submission flow or the trash bin). Anchored byapps/web-e2e/tests/client/submissions.spec.ts(three flows -- submit page loads for authenticated client, submissions list page loads for authenticated client, unauthenticated user is redirected from submissions to/auth/signin) ANDapps/web-e2e/tests/client/submit-and-manage.spec.ts(the only client-tree driver consumed by a P0 critical-business-flow spec from PR #621, used for the submit-and-manage flow withgetSubmissionByTitle(title)/viewSubmission(title)/editSubmission(title)/deleteSubmission(title)per-row CRUD helpers); cross-references tobase-page-object.mdfor the inheritance root,client-dashboard-page-object.md/client-profile-page-object.md/client-settings-page-object.mdfor the rollout-template precedents,signin-page-object.mdfor the auth-tree driver consuming specs depend on for the authenticatedclientPagefixture's setup precondition,auth-fixture.mdfor theclientPagefixture used in all authenticated flows,admin-tags-page-object.mdandadmin-collections-page-object.mdfor the admin-tree drivers with the per-row CRUD helper posture this driver mirrors, andadmin-comments-page-object.mdfor the admin-tree driver with the[role="dialog"]delete-confirmation modal posture. Pinned to the co-tenant API smoke spec atapps/web-e2e/tests/api/admin-clients-stats-query.spec.tswhich covers the admin-only enhanced-client-statistics endpoint atapps/web/app/api/admin/clients/stats/route.tsand pins the route's inline two-stepauth()chain with the uniquely shapedif (!session)first-step gate (checking the whole session object rather than the more commonif (!session?.user)pattern the siblingadmin/roles/statsroute uses) -- the unauthenticated branch returns 401 with the bare'Unauthorized'envelope, distinct from the catch's'Failed to fetch client stats'route-specific message and from every other admin-tree stats route's catch envelope. Any future change to the submissions driver -- adding acreateSubmission(data)composite flow helper, aconfirmDelete()helper for the delete-dialog, anassertSubmissionPresent(title)/assertSubmissionAbsent(title)invariant assertion, agetSubmissionsCount(): Promise<number>accessor, per-tab status-filter shortcuts beyond the literal-union typedselectStatusFilter(status), or adata-testidmigration of the existing<h3>-anchored two-parent-walk row resolver /[title*=]row-action triplet /[role="dialog"]modal triplet -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateclient-submissions-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming specs atapps/web-e2e/tests/client/submissions.spec.tsANDapps/web-e2e/tests/client/submit-and-manage.spec.tsfor the three-flow envelope plus per-row CRUD helper consumers, cross-checkbase-page-object.mdfor the inheritance root, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandclientPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound submissions driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the submissions spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client: Submissions") AND the submit-and-manage subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Client Submit"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 4-of-6; subsequent rollouts in this subtree will turn tosubmit.page.tsandtrash.page.ts. - E2E Client Submit Page Object (
apps/web-e2e/page-objects/client/submit.page.ts) -- Per-source-file reference for the Playwright e2e suite's client item-submission three-step form driver paired withapps/web-e2e/page-objects/client/submit.page.ts, the fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/(continuing the client-tree page-object docs rollout, 5-of-6) and the first client-tree driver in the rollout that documents (a) a multi-step wizard surface with three documented steps (Basic Info → Payment / Plan Selection → Review & Submit) driven by thenextStepButton/previousButton/submitButtontriplet -- distinct from every prior client-tree driver (all single-page surfaces) and distinct from the admin item-form driver's modal-bound four-step wizard; (b) a/submitpublic-tree route boundary -- the only client-tree page object the docs rollout covers that targets a route OUTSIDE/client/**; (c) a mixed selector-anchor posture combining id-selectors (#name,#description,#categories) for production-source-stable form fields, bare HTML element type-selectors (input[type="url"]) for the LinkInput component which has no stable id today, and accessible-name regex-anchored buttons for wizard navigation triggers; (d) a per-stepfillBasicInfo({ name, url, description })composite helper with a load-bearing fill order (URL first, then name, then description -- to let the LinkInput's OG-metadata fetch fire before the explicit name / description fills); (e) a per-stepselectCategory(categoryName)/selectTag(tagName)autocomplete commit helper pair -- the first client-tree driver to document combobox / tag-selection autocomplete helpers; and (f) aselectFreePlan()plan-selection helper with an OR-of-two-substring regex matching eitherGet Started FreeorSelect Freebutton labels. Sits inside theclient/page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts-- seeclient-dashboard-page-object.md,profile.page.ts-- seeclient-profile-page-object.md,settings.page.ts-- seeclient-settings-page-object.md,submissions.page.ts-- seeclient-submissions-page-object.md,trash.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's client item-submission three-step form driver boundary at/submit. Anchored byapps/web-e2e/tests/client/submit-and-manage.spec.ts(the full three-step submit flow runs inserialmode because the subsequent flows depend on the just-submitted item being visible in the submissions list); cross-references tobase-page-object.mdfor the inheritance root, all four prior client-tree page-object docs for the rollout-template precedents, andadmin-item-form-page-object.mdfor the modal-bound counterpart that this driver's per-route three-step form is the public-facing client equivalent of. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-categories-git-query.spec.tswhich covers the admin-only Git-repository-status endpoint atapps/web/app/api/admin/categories/git/route.ts-- the first admin-tree route the smoke layer covers that documents a unique combination of FOUR distinct contracts (zero-argumentGET()handler signature, the BARE{ error: '...' }envelope WITH the role-context-specific'Unauthorized. Admin access required.'message, GitHub-API-backed service viacreateCategoryGitService(gitConfig), and three distinct configuration-error 500 envelopes after the gate). Any future change to the submit driver -- adding asubmitFullFlow(data)composite that drives all three steps, paid-plan selection helpers (selectProPlan()/selectEnterprisePlan()), anassertStep(step)invariant assertion, agetCurrentStep(): Promise<number>accessor, or adata-testidmigration of the existing camelCase id-selector /input[type="url"]/getByRole('button', { name: /next step/i })postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateclient-submit-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-check the consuming spec atapps/web-e2e/tests/client/submit-and-manage.spec.tsfor the three-step flow envelope, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandclientPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound submit driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the submit subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Submit"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 5-of-6; the final entry will turn totrash.page.tsto close the rollout. - E2E Client Trash Page Object (
apps/web-e2e/page-objects/client/trash.page.ts) -- Per-source-file reference for the Playwright e2e suite's client submissions trash bin driver paired withapps/web-e2e/page-objects/client/trash.page.ts, the sixth and final per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/-- closing the client-tree page-object docs rollout at 6-of-6 -- and the first client-tree driver in the rollout that documents (a) a soft-deleted-row recovery surface at/client/submissions/trash-- the only client-tree page object the docs rollout covers that targets a derived sub-route of an existing client-tree page (the/client/submissions/trashroute is a child of the/client/submissionsroute theclient-submissions-page-object.mddriver covers); (b) a breadcrumb back-navigation Locator (backLink) pinned via thea[href*="/client/submissions"]substring-attribute selector -- the first client-tree driver to document anhref*=substring-attribute selector for back-link navigation, where the*=substring posture defends against future production-sourcehrefdrift between/client/submissions(the bare-ancestor route) and/client/submissions?status=…(a query-param-augmented variant the production source might add for "go back to the filtered view") and a future locale-prefixed/en/client/submissionsshape that Next.js's middleware-based i18n posture sometimes emits server-side; (c) a filter-by-text-content row collection Locator (trashItems) pinned viapage.locator('button').filter({ hasText: /restore/i })-- the first client-tree driver to document a text-content filter on a bare HTML element-type Locator that resolves to every restore button on the trash page (one per soft-deleted row), letting a consuming spec count restorable items viaawait trashPage.trashItems.count()and act on the first viarestoreFirst(); (d) an empty-state-affordance Locator (emptyState) pinned viapage.getByText(/trash.*empty|no.*deleted/i).first()-- the first client-tree driver to document an OR-of-two-substring regex on agetByTextLocator, where the OR-regex matches either theYour trash is empty/Trash is empty/Trash bin emptyrendering OR theNo deleted items/No items deletedrendering and the.*between the two substrings allows arbitrary intermediate words; and (e) a bare imperativerestoreFirst()mutator -- the first client-tree driver to document a named-action helper that does NOT take a row-key parameter (in contrast to the submissions driver'sviewSubmission(title)/editSubmission(title)/deleteSubmission(title)trio) acting on the first matching restore button in DOM order, reflecting the trash bin's intentionally minimal surface where the consuming spec only needs to prove that at least one soft-deleted item can be restored, not that a specific named item can be restored. Sits inside theclient/page-object subtree alongside the five sibling client-surface page objects (dashboard.page.ts-- seeclient-dashboard-page-object.md,profile.page.ts-- seeclient-profile-page-object.md,settings.page.ts-- seeclient-settings-page-object.md,submissions.page.ts-- seeclient-submissions-page-object.md,submit.page.ts-- seeclient-submit-page-object.md). Wherebase-page-object.mddocuments the page-object inheritance root, this page documents the suite's client submissions trash bin driver boundary at/client/submissions/trash. Documents the at-a-glance summary table of every load-bearing element (the type-onlyPage, Locatorimport; theimport { BasePage } from '../base.page'runtime import; theexport class ClientTrashPage extends BasePagesingle named export; the fourreadonlyLocator fields coveringheading/backLink/trashItems/emptyState; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass; thenavigate()method that calls the inheritedgoto('/client/submissions/trash'); therestoreFirst()minimal-surface restore mutator); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 -- E2E Test Coverage; cross-references tobase-page-object.mdfor the inheritance root, all five prior client-tree page-object docs for the rollout-template precedents, andsignin-page-object.mdfor the auth-tree driver consuming specs depend on for the authenticatedclientPagefixture's setup precondition. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-categories-all-query.spec.tswhich covers the admin-only Git-CMS categories-listing endpoint atapps/web/app/api/admin/categories/all/route.ts-- the first admin-tree route the smoke layer covers that documents the unique combination of THREE distinct contracts (getCachedItems({ lang })Git-based CMS reader for categories, a?locale=query param read AFTER the gate WITHOUT any defensivetypeof locale !== 'string'narrowing distinct from the sibling tags-all route's dead-branch narrow, and the paired categories-data-route posture as the read-only Git-CMS variant of the database-backed/api/admin/categorieslisting route distinct from both the database-backed listing posture and the/api/admin/categories/gitGitHub-API-backed sibling route). Any future change to the trash driver -- adding arestoreByTitle(title)per-row helper, apermanentlyDelete(title)mutator, anassertEmpty()invariant assertion, agetRestorableCount(): Promise<number>accessor, bulk mutators (restoreAll()/permanentlyDeleteAll()/emptyTrash()), or adata-testidmigration of the existinggetByRole('heading')/a[href*="/client/submissions"]/button+hasTextfilter /getByTextOR-regex postures -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each Locator pins to its current selector, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updateclient-trash-page-object.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor thebaseURLandclientPagefixture binding, cross-checkfixtures-index.mdfor a future fixture-bound trash driver, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the trash subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Trash"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the client-tree page-object docs rollout reaches 6-of-6 -- closing the rollout; subsequent rollouts will turn to the public-tree page objects (e.g.home.page.ts,browse.page.ts) or to per-spec docs covering the client / admin / api / public / smoke / i18n / auth test trees. - E2E Smoke Health Spec (
apps/web-e2e/tests/smoke/health.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public-pages health-check spec paired withapps/web-e2e/tests/smoke/health.spec.ts, the first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/, opening the per-spec-file docs rollout that complements the now-closed page-object docs rollout (the admin-tree at 17-of-17, the public-tree at 14-of-14, the client-tree at 6-of-6, plus theauth/signinandbase.page.tsroots). Where the page-object docs rollout documented the driver layer (the*.page.tsfiles that encapsulate per-page Locator and helper APIs), this page opens the consumer layer rollout -- the*.spec.tsfiles that import drivers / fixtures / helpers and turn them into assertion-bearing scenarios. The smoke health spec is the first consumer-layer reference in the rollout that documents (a) a session-agnostic posture -- the spec imports the runtime test directly from@playwright/testrather than the project's auth-aware fixture fromfixtures-index.md, with the load-bearing reasons being session agnosticism (the smoke layer must prove the public surface is reachable to a fresh, unauthenticated browser context), independence fromglobal-setup.md(the smoke layer must run successfully even before global-setup runs), and a smaller import graph (the runtime test has zero transitive imports beyond Playwright); (b) a data-driven test generation posture -- a singlefor (const route of PUBLIC_ROUTES)loop generates one Playwrighttest()per route in the sharedPUBLIC_ROUTESconstant frome2e-test-data.md, giving a single source of truth for the public-route surface, stable per-route test IDs that survivePUBLIC_ROUTESreordering, and isolated failure where a regression on one route does not cascade to others; (c) awaitUntil: 'domcontentloaded'trade-off -- the second-earliest of Playwright's four wait conditions ('commit' / 'domcontentloaded' / 'load' / 'networkidle'), trading full-page-load wait time for smoke-suite speed while still letting the body-visibility assertion succeed; (d) a< 400HTTP status threshold that deliberately includes the 3xx redirect class to accept locale-prefix injection 307s (theapps/web/middleware.tsmiddleware), trailing-slash normalisation 308s, auth-redirect 302s for already-authenticated visitors hitting/auth/signin, andCache-Control: max-age304s -- distinct from a=== 200strict pin which would emit spurious failures on every locale-redirect; and (e) a most-universalbodyLocator pin for the rendered-DOM assertion -- distinct from amain/[role="main"]pin (which would fail on routes that wrap content in non-<main>semantic roots), aheaderpin (which would fail on auth routes that opt out of the global header), or apage.title()non-empty pin (which would fail on routes that emit metadata-driven empty titles). Sits inside thetests/smoke/test subtree alongside the sibling smoke spec attests/smoke/navigation.spec.ts(pending docs). Wherebase-page-object.mddocuments the page-object inheritance root andfixtures-index.mddocuments the fixture-export boundary, this page documents the suite's smoke layer health-check entry point -- the smallest possible "did the page render" assertion-bearing spec the e2e suite publishes. Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-clients-advanced-search-query.spec.tswhich covers the admin-only advanced-client-search endpoint atapps/web/app/api/admin/clients/advanced-search/route.ts-- the first admin-tree route the smoke layer covers that documents the unique combination of FOUR distinct contracts (the bare{ error: 'Unauthorized' }envelope on the unauth 401 branch with NOsuccesskey, the largest documented query-param surface in the admin tree at 13+ keys plus pagination, the inlineNumber()/Number.isFinite()/Math.floor()/Math.min(Math.max(…, 1), 100)pagination clamp distinct from the sharedvalidatePaginationParams()helper, and four distinct date-range filters via the sharedparseDate(v)helper that silently ignores NaN-valued Date objects). Documents the at-a-glance summary table of every load-bearing element (theimport { test, expect }runtime import; the singleimport { PUBLIC_ROUTES } from '../../helpers/test-data'shared-data import; the singletest.describe('Smoke: Public pages health check', …)block; thefor (const route of PUBLIC_ROUTES)data-driven generator; the per-test title combiningnameandpath; thepage.goto()withwaitUntil: 'domcontentloaded'; theexpect(response, …).not.toBeNull()defensive non-null pin; theexpect(response!.status()).toBeLessThan(400)HTTP-status threshold; theexpect(page.locator('body')).toBeVisible()rendered-DOM pin); the whywaitUntil: 'domcontentloaded'four-condition comparison; the why< 400four-branch redirect-class enumeration; the whybodyuniversal-pin rationale comparing againstmain/[role="main"]/header/page.title()alternatives; a "What it does not contain" five-bullet enumeration of the deliberate omissions (nodata-testidselectors, no accessibility assertions, no screenshot / visual-regression assertions, no per-route content assertions, no locale enumeration, no authenticated-route entries); and the why'@playwright/test'three-reason rationale (session agnosticism, independence from global-setup, smaller import graph). Cross-references tobase-page-object.mdfor the inheritance root the smoke spec deliberately does NOT use,e2e-test-data.mdfor the shared-data boundary,fixtures-index.mdfor the fixture-export boundary the smoke spec deliberately does NOT use,playwright-config.mdfor the project-level config,global-setup.mdfor the per-suite global setup the smoke spec runs successfully without, and to Spec 010 -- E2E Test Coverage for the spec governing the e2e suite's coverage goals. Any future change to the smoke health spec -- adding per-localePUBLIC_ROUTESenumeration, addingdata-testidselectors, adding accessibility assertions, adding screenshot / visual-regression assertions, adding per-route content assertions, adding authenticated-route entries, switching towaitUntil: 'load'/'networkidle', switching to a=== 200strict status pin, switching frombodytomain/[role="main"], switching from'@playwright/test'to the project's auth-aware fixture, or generalising thefor (const route of PUBLIC_ROUTES)posture to aforEach/.test.eachposture -- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each design choice pins to its current shape, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updatesmoke-health-spec.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor the smoke-project filter, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the smoke spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Smoke"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the per-spec-file docs rollout opens (1-of-N); subsequent rollouts in this rollout will turn totests/smoke/navigation.spec.ts, then to per-tree spec rollouts undertests/admin//tests/client//tests/public//tests/api//tests/auth//tests/i18n/. - E2E Smoke Navigation Spec (
apps/web-e2e/tests/smoke/navigation.spec.ts) -- Per-source-file reference for the Playwright e2e suite's core navigation smoke spec paired withapps/web-e2e/tests/smoke/navigation.spec.ts, the second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/, continuing the per-spec-file docs rollout opened bysmoke-health-spec.md. Where the siblingsmoke-health-spec.mddocuments a data-driven, breadth-first smoke posture (onetest()per route in a sharedPUBLIC_ROUTESconstant frome2e-test-data.md, body-visibility-only assertions), this page documents the hand-crafted, depth-first smoke posture -- four hand-writtentest()blocks that exercise specific user-flow primitives (home → item-detail click-through, home → sign-in click-through, an item-grid count assertion, a categories-page heading pin) the per-route health spec cannot exercise. With this entry the per-spec-file docs rollout closes its coverage of the smoke tree (thetests/smoke/directory has exactly two*.spec.tsfiles; both now have docs anchors). Documents (a) the four hand-writtentest()blocks that pin distinct user-flow primitives --'home page displays directory items'(item-grid populated invariant viaa[href*="/items/"]count > 0),'can navigate from home to an item detail page'(click-through navigation vialocator + click + waitForURL(/\/items\//)+h1visibility pin),'can navigate to categories page'(categories-page heading invariant viagoto('/categories')+h1visibility),'can navigate to sign in from home'(sign-in CTA discoverability viagetByRole('link', { name: /sign in/i })+ URL pin to/auth/signin); (b) the why hand-writtentest()blocks (not a data-driven loop) three-reason rationale -- per-scenario assertion divergence (each scenario pins a structurally different invariant: count > 0 /h1visibility / URL match / link-click), per-scenario navigation primitive divergence (goto,goto + locator + click + waitForURL,goto + locator,goto + role-locator + click + URL pin), per-scenario test-ID granularity (hand-written, descriptive titles that survive reordering); (c) the whya[href*="/items/"]attribute-substring CSS selector rationale (resilience to the page-object refactor surface, cross-route coverage matching every list / grid / card variant, selector simplicity self-documenting "any anchor whosehrefcontains/items/"); (d) the whygetByRole('link', { name: /sign in/i })accessibility-first locator rationale (tests the user-visible primitive resolved via the accessibility tree exactly the way an assistive-technology user reaches the link, resilience to URL refactor where renaming/auth/signin→/loginwould only require updating the URL pin, cross-locale coverage with thelocale: 'en-US'use-flag fromplaywright-config.md); (e) the why 30-secondexpect.toBeVisible({ timeout: 30_000 })override rationale (the explicit override is a deliberate self-documenting pin against future contributors who lower the global default, cold-cache resilience for the home-page render that involves fetching every Git CMS item / locale resource bundle / theme CSS / SSR-hydrated React tree, distinct from the navigation timeout governinggoto/waitForURLwaits); (f) a "What it does not contain" six-bullet enumeration of the deliberate omissions (no authenticated flows, no form interactions beyond CTA discoverability, no screenshot / visual-regression assertions, no per-locale enumeration, no per-content-warning enumeration, no accessibility-tree assertions beyond the sign-in CTA). Pinned to the co-tenant smoke spec atapps/web-e2e/tests/api/admin-reports-stats-query.spec.tswhich covers the admin-only report-statistics endpoint atapps/web/app/api/admin/reports/stats/route.ts-- the first admin-tree route the smoke layer covers that documents the single-step 403 'Forbidden' gate combined with the bareGET()handler signature, an intersection no other admin-tree route documents (the siblingadmin-reports-query.spec.tsdocuments the 403 gate with aGET(request: Request)signature; theadmin-roles-stats-query.spec.tsdocuments the bareGET()signature with a two-step 401/403 gate; this spec documents the intersection -- the single-step 403 gate with no request parameter at all). Cross-references tosmoke-health-spec.md(the sibling smoke spec, paired withtests/smoke/health.spec.ts, documenting the data-driven posture this spec deliberately contrasts with),base-page-object.md(the page-object inheritance root the smoke spec deliberately does NOT use),e2e-test-data.md(the shared-data boundary the smoke spec deliberately does NOT import from -- distinct from the sibling health spec which importsPUBLIC_ROUTESfromhelpers/test-data.ts),fixtures-index.md(the fixture-export boundary the smoke spec intentionally does NOT use, same session-agnostic posture as the sibling health spec),playwright-config.md(the project-level config from which the smoke spec inheritsfullyParallel: true/retries: process.env.CI ? 2 : 0/expect.timeout: 30_000/navigationTimeout: 60_000/reportersettings),global-setup.md(the per-suite global setup the smoke spec runs successfully without -- deliberate independence, same posture as the sibling health spec),signin-page-object.md(the sign-in page-object driver the smoke spec does NOT use because the smoke layer must run before any driver is exercised), and to Spec 010 -- E2E Test Coverage for the spec governing the e2e suite's coverage goals. Any future change to the smoke navigation spec -- adding a fifth scenario (e.g.'can navigate to about page'), generalising the four hand-written blocks into a data-driven loop, replacing the substring-CSSa[href*="/items/"]selector with adata-testidlocator, replacing the role-based sign-in locator with a CSS attribute selector, replacing theh1heading pin with abody-visibility pin (the sibling-spec's posture), adding a per-locale enumeration, adding form-interaction coverage beyond the CTA discoverability, or switching from'@playwright/test'to the project's auth-aware fixture fromfixtures-index.md-- now has a docs anchor that explains the trade-offs of the existing posture, the load-bearing reasons each design choice pins to its current shape, and the cross-references that any new helper must respect. The expected change protocol for a refactor: updatesmoke-navigation-spec.mdin the same PR that touches the source file, updatedocs/log.mdwith a one-line summary, cross-checke2e-tsconfig.mdfor theincludeglob, cross-checkplaywright-config.mdfor the smoke-project filter, run dualpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the smoke spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Smoke"), adocs/log.mdentry, a Spec 010 -- E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. With this entry the per-spec-file docs rollout extends to 2-of-N and the smoke tree closes at 2-of-2; subsequent rollouts will turn to per-tree spec rollouts undertests/admin//tests/client//tests/public//tests/api//tests/auth//tests/i18n/. - E2E Admin Map-Status Query Spec (
apps/web-e2e/tests/api/admin-settings-map-status-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin map-provider configuration-status query-param smoke spec paired withapps/web-e2e/tests/api/admin-settings-map-status-query.spec.ts, the third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/, opening the per-admin-API-query-smoke-spec docs sub-rollout that follows the now-closed (2-of-2)tests/smoke/rollout opened bysmoke-health-spec.mdandsmoke-navigation-spec.md. The route under test (apps/web/app/api/admin/settings/map-status/route.ts) is the first admin-tree route the smoke layer covers that documents three distinct postures the prior sibling specs do not -- (a) thegetCachedApiSession(req)request-scoped session resolver (the only admin-tree route the smoke layer covers that uses the wrapper fromapps/web/lib/auth/cached-session.tsrather than the bareauth()call every other admin-tree route uses, with the request-bearingGET(req: NextRequest)handler signature necessary because the wrapper requires theNextRequestto key its per-request cache); (b) the single-step gate-collapse posture with the bare{ error }envelope (if (!session?.user?.isAdmin)collapses both unauth and auth-non-admin into the same 401 envelope, deliberately lacking thesuccess: falsekey every other admin-tree route smoke-covered to date emits -- a regression that adopts the canonical envelope would change the client-side error-handling contract for every consumer of the map-status admin dashboard widget); (c) the per-environment publishable-key disclosure surface (the success branch returns booleans derived fromBoolean(process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN)andBoolean(process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY), never the actual values; the spec includes a deliberate negative-string assertion that the unauth response body does NOT contain a Mapbox public access token (pk.*) or Google Maps API key (AIza*) substring or either env-var name -- the first admin-tree query smoke that pins a per-env-disclosure contract on the unauth branch). Documents the at-a-glance scenario tree (a 49-path bulk-loop walk of every plausible query-param shape asserting< 500; a bare-{ error }envelope assertion withbody.successundefined check; eight per-key isolation walks for?provider=/?include=/?isAdmin=/?userId=/?token=/?bypass=/?reveal=/Acceptheader; a per-env-key non-disclosure assertion). Cross-references tosmoke-health-spec.mdandsmoke-navigation-spec.md(the sibling per-spec-file references the rollout opened with),admin-settings-page-object.md(the admin settings page-object driver paired with the same route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 011 -- Maps Providers for the governing specs. With this entry the per-spec-file docs rollout extends to 3-of-N and opens thetests/api/per-spec-file sub-rollout at 1-of-many. - E2E Admin Twenty CRM Config Query Spec (
apps/web-e2e/tests/api/admin-twenty-crm-config-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Twenty CRM configuration query-param smoke spec paired withapps/web-e2e/tests/api/admin-twenty-crm-config-query.spec.ts, the fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the second underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/twenty-crm/config/route.ts-- the first admin-tree route the smoke layer covers that documents three distinct postures the prior sibling specs do not (or do partially): (a) the route-specific'Unauthorized. Admin access required.'error string (distinct from the bare'Unauthorized'string every other admin-tree route smoke-covered to date emits, and distinct from the'Forbidden'string theadmin/reportsandadmin/clients/statsroutes' second-step gates emit; the only sibling that uses the same purpose-built string isadmin/sponsor-ads); (b) the bareGET()handler signature combined with single-step canonical-envelope gate (the handler signature isGET()with norequestparameter, so there is nosearchParamssurface inside the handler at all; combined with the single-stepif (!session?.user?.isAdmin)gate and the canonical{ success: false, error }envelope, this is the first admin-tree route the smoke layer covers that documents this exact intersection -- distinct from the immediately-precedingadmin-settings-map-status-query-spec.mdwhich uses the request-bearingGET(req: NextRequest)signature with the bare{ error }envelope); (c) the per-tenant credential-disclosure contract (the success branch returns a Twenty CRM config object whoseapiKeyfield is masked by theTwentyCrmConfigRepository.getConfig()helper to****<last4>; the spec includes a deliberate negative-string assertion that the unauth response body does NOT contain the masked form via the/\*{4}[A-Za-z0-9]{4}/regex, theTWENTY_CRM_API_KEY/TWENTY_CRM_BASE_URLenv-var names, or any of the config sub-field namesapiKey/baseUrl/syncMode-- the first admin-tree query smoke that pins a per-tenant CRM-credential-disclosure contract on the unauth branch). Documents the at-a-glance scenario tree (a 52-path bulk-loop walk asserting< 500; a route-specific Unauthorized-envelope assertion; seven per-key isolation walks for?syncMode=/?reveal=/?isAdmin=/?userId=/?token=/?bypass=/Acceptheader; a per-tenant CRM-credential non-disclosure assertion; a cross-route error-string isolation assertion that the GET response error does NOT echo any of'Unauthorized'/'Forbidden'/'Failed to retrieve configuration'/'User ID not found'/'Insufficient permissions'). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md(the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 4-of-N and thetests/api/per-spec-file sub-rollout extends to 2-of-many. - E2E Admin Sponsor-Ads Query Spec (
apps/web-e2e/tests/api/admin-sponsor-ads-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ads listing query-param smoke spec paired withapps/web-e2e/tests/api/admin-sponsor-ads-query.spec.ts, the fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the third underapps/web-e2e/tests/api/. Pairs with the existing spec coveringapps/web/app/api/admin/sponsor-ads/route.ts-- the first admin-tree route the smoke layer covers that documents three distinct postures the prior sibling specs do not (or do partially): (a) the route-specific'Unauthorized. Admin access required.'error string with a request-bearingGET(request: NextRequest)handler signature (distinct from the immediately-precedingadmin-twenty-crm-config-query-spec.mdwhich uses the same purpose-built error string but pairs it with a bareGET()handler signature -- the sponsor-ads route is the first admin-tree route the smoke layer covers that documents the longer-message variant combined with a wide query-param surface read vianew URL(request.url).searchParamsAFTER the auth gate); (b) the widest documented query-param surface in the admin tree paired with deferred Zod validation (the route reads pagination?page=/?limit=, enum filters?status=/?interval=, free-text search?search=, and order-targeting keys?sortBy=/?sortOrder=, then runsquerySponsorAdsSchema.safeParse(queryParams)for shape validation andvalidatePaginationParams(searchParams)for pagination clamping -- but all of these reads run AFTER the auth gate; the spec includes a deliberate'Unauthorized. Admin access required.' != 'Invalid query parameters'assertion that pins the auth-gate-before-Zod-validation order and would surface any future re-ordering as a 400 instead of a 401 on the unauth branch); (c) the deepest per-spec isolation walk-set in the admin tree (eight independent query-key isolations?status=/?interval=/?page=&limit=/?isAdmin=/?userId=/?token=/?bypass=/Acceptheader, plus repeated-key walks?as=admin&as=user/?token=foo&token=bar/?bypass=1&bypass=0/?status=active&status=pending, plus a cookie /X-*header side-channel walk asserting fabricatednext-auth.session-token/authjs.session-tokencookies andX-Forwarded-For/X-Real-IPheaders do NOT bypassauth()). Documents the at-a-glance scenario tree (a 78-path bulk-loop walk asserting< 500; a longer-message envelope assertion with the canonicalsuccess: falsekey; a parameterised-vs-baseline status-stability comparison; six per-key isolation walks plus anAcceptheader walk; an auth-gate-before-Zod-validation assertion that invalid Zod query params return 401 'Unauthorized. Admin access required.' rather than 400 'Invalid query parameters'; a side-channel cookie /X-*header walk; a cross-route error-string isolation assertion that the GET response error does NOT echo'Unauthorized'or'Forbidden'; a repeated-key invariance walk). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md(the sibling per-spec-file references),admin-sponsorships-page-object.md(the admin sponsorships page-object driver paired with the same route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 5-of-N and thetests/api/per-spec-file sub-rollout extends to 3-of-many. - E2E Admin Roles Query Spec (
apps/web-e2e/tests/api/admin-roles-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin roles listing query-param smoke spec paired withapps/web-e2e/tests/api/admin-roles-query.spec.ts, the sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fourth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/roles/route.ts-- the first admin-tree route the smoke layer covers that documents an auth-gate-divergence finding: unlike every other admin-tree GET route smoke-covered by the siblingadmin-sponsor-ads-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-settings-map-status-query-spec.md, and the other admin-tree query smoke specs (admin-categories-query.spec.ts,admin-clients-query.spec.ts,admin-comments-query.spec.ts,admin-companies-query.spec.ts,admin-dashboard-stats-query.spec.ts,admin-featured-items-query.spec.ts,admin-geo-analytics-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-location-index-query.spec.ts,admin-navigation-query.spec.ts,admin-notifications-query.spec.ts,admin-reports-query.spec.ts,admin-reports-stats-query.spec.ts,admin-roles-stats-query.spec.ts,admin-settings-query.spec.ts,admin-tags-query.spec.ts,admin-tags-all-query.spec.ts,admin-twenty-crm-test-connection-body.spec.ts,admin-users-query.spec.ts,admin-users-stats-query.spec.ts), theapps/web/app/api/admin/roles/route.tsGET handler does NOT callauth()and does NOT checksession?.user?.isAdminbefore delegating toroleRepository.findAllPaginated(...); the same absence holds for the siblingapps/web/app/api/admin/roles/active/route.tsGET handler. The spec is INVARIANT to the resolution of the auth-gate question (logged indocs/questions.mdunder Q-010b, recommended default "yes, add the same two-step gate as the sibling/api/admin/roles/statsroute") -- every assertion uses either the< 500envelope or the baseline-equality envelope so the spec stays green whether the route remains unauthenticated OR a future contributor adds anauth()gate. Documents three smaller postures the prior sibling specs do not document at this intersection: (a) pagination via the sharedvalidatePaginationParams(searchParams)helper (the same helper used by every paginated admin surface; the helper short-circuits the handler with its{ error, status }envelope BEFORE the repository call); (b) narrow inline enum coercion for?status=(accepts ONLY the literal strings'active'/'inactive'and coerces every other value including'pending'/'archived'/'deleted'toundefinedvia an inline ternary, NOT via a Zod schema); (c) narrow inline enum coercion for?sortBy=/?sortOrder=via theascast on thesearchParams.get(...)result with'name'/'asc'defaults. Documents the at-a-glance scenario tree (a ~80-path bulk-loop walk asserting< 500; a parameterised-vs-baseline status-stability comparison; a per-helper short-circuit assertion for?page=invalid/?limit=invalid; per-key isolation walks for?status=/?sortBy=/?sortOrder=covering the beyond-the-enum values; per-key isolation walks for?userId=/?token=/?bypass=/?includeDeleted=; anAcceptheader walk; a side-channel cookie /X-*header walk; a repeated-key walk). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md(the sibling per-spec-file references),admin-roles-page-object.md(the admin roles page-object driver paired with the same route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 6-of-N and thetests/api/per-spec-file sub-rollout extends to 4-of-many, and surfaces the first auth-gate-divergence finding the docs tree publishes via the question register. - E2E Admin Roles Active Query Spec (
apps/web-e2e/tests/api/admin-roles-active-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin active-roles query-param smoke spec paired withapps/web-e2e/tests/api/admin-roles-active-query.spec.ts, the seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/roles/active/route.ts-- the second admin-tree route the smoke layer covers that documents the auth-gate-divergence finding opened by the immediately-preceding siblingadmin-roles-query-spec.md: the handler does NOT callauth()and does NOT checksession?.user?.isAdminbefore delegating toroleRepository.findActive(), so the route is effectively public today (the e2e harness hits it without an authenticated session and receives the same 200-with-roles payload an authenticated admin would). The same Q-010b migration-path note indocs/questions.mdapplies (recommended default "yes, add the same two-step gate as the sibling/api/admin/roles/statsroute"). Documents three postures distinct from the sibling listing route: (a) the bare zero-argumentGET()Next 16 handler signature (the handler does NOT take arequestparameter at all, so every?…=…permutation is silently discarded by the Next.js routing layer before the handler body runs -- the route is INVARIANT to its query string and every permutation rounds-trips to the same status as the no-arg baseline; a regression that switches the signature toGET(request)and starts readingsearchParams.get(...)would change the observable behavior on at least one of the permutations the spec walks); (b) the zero-argumentroleRepository.findActive()repository call (the repository is invoked with NOoptionsbag at all -- distinct from the siblingroleRepository.findAllPaginated(options)call; a regression that threads any of the query keys into a new options bag would change the auth-branch payload); (c) the active-roles-specific?includeInactive=…per-key isolation walk (the route's whole purpose is to return only active roles; a regression that wires?includeInactive=trueinto a new options bag would defeat that purpose -- the per-key isolation walk pins the baseline-equality envelope so any such regression surfaces via the auth-branch behavioral test out of scope for this spec). Documents the at-a-glance scenario tree (a ~85-path bulk-loop walk asserting< 500; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for?status=/?isAdmin=/?sortBy=/?sortOrder=/?page=/?userId=/?token=/?bypass=/?includeInactive=; anAcceptheader walk; a side-channel cookie /X-*header walk; a repeated-key walk). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md(the sibling per-spec-file references),admin-roles-page-object.md(the admin roles page-object driver paired with the same admin route's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 7-of-N and thetests/api/per-spec-file sub-rollout extends to 5-of-many, and the second admin-tree route flagged by Q-010b picks up its own per-source-file reference (the first beingadmin-roles-query-spec.md). - E2E Admin Items Import Body Spec (
apps/web-e2e/tests/api/admin-items-import-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-import-execute body / header / method smoke spec paired withapps/web-e2e/tests/api/admin-items-import-body.spec.ts, the seventeenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifteenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/items/import/route.ts-- the first admin-tree route the smoke layer covers that combines a static-pathPOSThandler with a two-step body validation chain AFTER the gate AND AFTER the body parse, distinct from the single-step body validation ofadmin/items/[id]/review, the three-step body validation ofadmin/categories/reorder, and the six-step body validation ofadmin/items/bulk. Documents the unique combination of: (1)POSThandler with a static path distinct from the dynamic-segment[id]routes covered byadmin-items-id-review-body-spec.mdandadmin-items-id-history-query-spec.md; (2) single-stepauth()chain matching the canonical longer message family (if (!session?.user?.isAdmin)), distinct from the two-step gates ofadmin/users/check-email/admin/users/check-username/admin/notifications/mark-all-read/admin/clients/bulk; (3) canonical longer'Unauthorized. Admin access required.'401 message matching theadmin/items/bulk,admin/categories/reorder,admin/items/[id]/review, andadmin/twenty-crm/*family; (4)success: falseenvelope key matching the same family, distinct from the bare{ error: 'Unauthorized' }envelope of the two-step-gated routes; (5) body parse viaawait request.json()AFTER the gate -- the gate-then-parse-then-validate-then-service order is the load-bearing invariant of this route; (6) two-step body validation chain AFTER the gate AND AFTER the body parse with two distinct 400 messages ('Missing or invalid rows array.'on!body.rows || !Array.isArray(body.rows),'Missing import options.'on!body.options) -- the first two-step body-validation admin-tree smoke the docs tree publishes, complementary to the single-step / three-step / six-step body-validation smokes the sub-rollout previously published; (7) service-call surface AFTER the gate AND AFTER both validation steps -- the handler instantiatesnew ItemImportService()and callsexecuteImport(rows, options)with the body'srowsand the body'soptionsmerged with three defaults (duplicateStrategy ||= 'skip',defaultStatus ||= 'draft',submittedBy = session.user.email || 'admin'), with success-branch payload{ success: true, result }whereresultis theImportExecutionResultreturned by the service; (8)safeErrorResponse(error, 'Failed to execute import')catch matching theadmin/items/bulkandadmin/items/[id]/historycatch family; (9) method-resolution surface withPOST-only export, so every other method (GET/PUT/PATCH/DELETE) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~21-header bulk-loop walk + a ~25-body bulk-loop walk both asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a negative-property assertion that the unauth response does NOT echo aresultkey orsuccess: true; a gate-before-body-validation invariant pinning that BOTH 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the'Failed to execute import'message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk; a service-not-entered invariance walk pinning that the unauth response does NOT echo aresultobject; a duplicate-strategy / default-status enum-shape invariance walk pinning that the gate fires before the default-fallback; a large-rows-array invariance walk pinning that 10-row and 100-row bodies round-trip to the same status as the empty-rows baseline). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md,admin-clients-bulk-method-spec.md, andadmin-items-id-history-query-spec.md(the sibling per-spec-file references),admin-items-page-object.md(the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 17-of-N and thetests/api/per-spec-file sub-rollout extends to 15-of-many, and the first two-step-body-validation admin-tree smoke lands as a complementary surface to the single-step / three-step / six-step body-validation smokes the sub-rollout previously published. - E2E Admin Items Import Validate Body Spec (
apps/web-e2e/tests/api/admin-items-import-validate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-import-validate (dry-run) multipart-body / header / method smoke spec paired withapps/web-e2e/tests/api/admin-items-import-validate-body.spec.ts, the eighteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixteenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/items/import/validate/route.ts-- the first admin-tree route the smoke layer covers that combines a static-pathPOSThandler with a multipart/form-data body parsed viaawait request.formData()AFTER the gate, distinct from every prior admin-tree smoke (which all parse JSON viaawait request.json()). Documents the unique combination of: (1)POSThandler with a static path (sibling of the JSON-bodyadmin-items-import-body-spec.mdroute); (2) single-stepauth()chain matching the canonical longer message family (if (!session?.user?.isAdmin)), distinct from the two-step gates ofadmin/users/check-email/admin/users/check-username/admin/notifications/mark-all-read/admin/clients/bulk; (3) canonical longer'Unauthorized. Admin access required.'401 message matching theadmin/items/import,admin/items/bulk,admin/categories/reorder,admin/items/[id]/review, andadmin/twenty-crm/*family; (4)success: falseenvelope key matching the same family; (5) body parse viaawait request.formData()AFTER the gate -- the first admin-tree smoke spec that documents aformData()-based body parse, complementary to the JSON-based body parses of every prior admin-tree smoke; (6) five-step file / mapping validation chain AFTER the gate AND AFTER the formData parse with five distinct 400 messages ('No file provided.','Invalid file type. Only CSV and XLSX files are supported.','File too large. Maximum size is 10 MB.','Invalid column mapping JSON.','File contains no data rows.') -- the first five-step file/mapping-validation admin-tree smoke the docs tree publishes, complementary to the single-step / two-step / three-step / six-step body-validation smokes the sub-rollout previously published; (7) service-call surface AFTER the gate AND AFTER every validation step -- the handler instantiatesnew ItemImportService()and callsparseCSV(...)/parseXLSX(...)followed byvalidateRows(...), with success-branch payload{ success: true, headers, suggestedMapping, validationResults, summary }(four success-branch keys plus thesuccess: trueflag); (8)safeErrorResponse(error, 'Failed to validate import file')catch matching theadmin/items/import,admin/items/bulk, andadmin/items/[id]/historycatch family; (9) method-resolution surface withPOST-only export, so every other method (GET/PUT/PATCH/DELETE) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~22-header bulk-loop walk + a ~24-multipart-body bulk-loop walk both asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a negative-property assertion that the unauth response does NOT echo any of the four success-branch keys orsuccess: true; a gate-before-validation invariant pinning that ALL FIVE 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the'Failed to validate import file'message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a malformed-multipart-body invariance walk pinning that the gate fires before the formData parse; a service-not-entered invariance walk pinning that the unauth response does NOT echo any of the four success-branch keys; a file-extension shape invariance walk (whitelisted.csv/.xlsx/.xlsplus non-whitelisted.txt/.json/.pdf/ extensionless) pinning that the extension whitelist is NOT evaluated on the unauth branch; a mapping-JSON shape invariance walk (valid + invalid + broken + empty + missing) pinning thatJSON.parse(mapping)is NOT evaluated on the unauth branch; a duplicate-strategy / default-status enum-shape invariance walk pinning that the gate fires before the default-fallback). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md,admin-clients-bulk-method-spec.md,admin-items-id-history-query-spec.md, andadmin-items-import-body-spec.md(the sibling per-spec-file references),admin-items-page-object.md(the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 18-of-N and thetests/api/per-spec-file sub-rollout extends to 16-of-many, and the first multipart/form-data admin-tree smoke lands as a complementary surface to the JSON-body smokes the sub-rollout previously published. - E2E Admin Notifications [id] Read Method Spec (
apps/web-e2e/tests/api/admin-notifications-id-read-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-notification mark-as-read method / id / header smoke spec paired withapps/web-e2e/tests/api/admin-notifications-id-read-method.spec.ts, the nineteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventeenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/notifications/[id]/read/route.ts-- the first admin-tree route the smoke layer covers that combines a dynamic-segment[id]PATCHhandler with the two-step!session?.user?.id→!tenantIdgate envelope. Documents the unique combination of: (1) dynamic-segment[id]PATCHhandler -- the first dynamic-segmentPATCHhandler the admin-tree smoke layer pins, distinct from the static-pathPATCHofadmin/notifications/mark-all-read, the dynamic-segmentPOSTofadmin/items/[id]/review, and the dynamic-segmentGETofadmin/items/[id]/history; (2) two-step gate (!session?.user?.id→ 401'Unauthorized', then AFTER params and AFTER 400 missing-id branch:!tenantId→ 403'Tenant not found') -- SAME envelope as the siblingadmin/notifications/mark-all-read, distinct from the single-step!session?.user?.isAdmingate of the canonical-longer-message admin routes; (3) bare'Unauthorized'401 message matching the siblingadmin/notifications/mark-all-read, distinct from the canonical longer'Unauthorized. Admin access required.'of the single-step-gated routes; (4) bare{ error: ... }envelope with NOsuccesskey on either the 401 or 403 branch -- matching the siblingadmin/notifications/mark-all-read, distinct from the{ success: false, error: ... }envelope of the canonical-longer-message routes; (5) path-id surface -- the handler readsidfromawait paramsAFTER the auth gate, and the unauth branch must NEVER reach the params resolution; (6) tenant-resolution surface AFTER params and AFTER the 400 missing-id branch (!tenantId→ 403'Tenant not found'), unreachable on the unauth branch; (7) DB-update surface AFTER both gates -- the handler issues a Drizzledb.update(notifications)withset({ isRead: true, readAt: ..., updatedAt: ... })and a three-clausewhere(id + userId + tenantId), then.returning(), with success-branch payload{ success: true, notification: <row> }; (8)console.error+ bare'Internal server error'catch matching theadmin/users/check-email/admin/users/check-usernamefamily, distinct from thesafeErrorResponse(...)catch of the canonical-longer-message routes; (9) method-resolution surface withPATCH-only export, so every other method (GET/POST/PUT/DELETE) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~20-header bulk-loop walk + a ~9-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion pinning{ error: 'Unauthorized' }exactly; a strict envelope-shape assertionObject.keys(body) === ['error']; a negative-property assertion that the unauth response does NOT echo anotificationkey orsuccess: true; a gate-before-post-auth invariant pinning that NONE of the four post-auth messages ('Notification ID is required','Tenant not found','Notification not found','Internal server error') must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison pinning that the params resolution does NOT happen on the unauth branch; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / POST / PUT / DELETE round-trip to< 500; a malformed-JSON-body invariance walk; a DB-update-not-entered invariance walk pinning that the unauth response does NOT echo anotificationkey from the Drizzle.returning()payload). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md,admin-clients-bulk-method-spec.md,admin-items-id-history-query-spec.md,admin-items-import-body-spec.md, andadmin-items-import-validate-body-spec.md(the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 19-of-N and thetests/api/per-spec-file sub-rollout extends to 17-of-many, and the first dynamic-segment[id]PATCHadmin-tree smoke lands as the dynamic-segment sibling ofadmin/notifications/mark-all-read. - E2E Admin Sponsor Ads [id] Approve Method Spec (
apps/web-e2e/tests/api/admin-sponsor-ads-id-approve-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ad approval method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-sponsor-ads-id-approve-method.spec.ts, the twentieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighteenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/sponsor-ads/[id]/approve/route.ts-- the first admin-tree route the smoke layer covers that combines a dynamic-segment[id]POSThandler with a forgiving body parse inside its own try/catch AFTER the gate AND AFTER params resolution, AND a multi-error-code catch chain that maps three distinct service-thrownError.messagevalues to three distinct HTTP envelopes. Documents the unique combination of: (1) dynamic-segment[id]POSThandler (sibling ofadmin/items/[id]/reviewbut with a different gate / body / catch posture); (2) compound single-ifgate!session?.user?.isAdmin || !session.user.id-- a single-step gate that ANDs the canonicalisAdminpredicate with a!session.user.idfalsity probe, observably equivalent to the single-step gate from the unauth client's perspective; (3) canonical longer'Unauthorized. Admin access required.'401 message matching the canonical-longer-envelope family; (4)success: falseenvelope key matching the same family; (5) params resolution AFTER the gate -- every id shape round-trips to the same 401 status as the canonical id baseline; (6) body parse inside its own try/catch AFTER params AND AFTER the gate -- theforceApproveflag defaults tofalseif the body is missing, malformed, or omits the key; (7) service-call surface AFTER both the gate AND the body parse withsponsorAdService.approveSponsorAd(id, session.user.id, forceApprove), success-branch payload{ success: true, data: <ad>, message: 'Sponsor ad approved and activated successfully' }; (8) multi-error-code catch chain mapping'Sponsor ad not found'→ 404,'PAYMENT_NOT_RECEIVED'→ 400,error.message.includes('Cannot approve')→ 400, withsafeErrorResponse(error, 'Failed to approve sponsor ad')fallback -- the first multi-error-code catch chain admin-tree smoke the docs tree publishes; (9) service-zero-rows fallback returning 500{ success: false, error: 'Failed to approve sponsor ad' }if the service returns falsy; (10) method-resolution surface withPOST-only export. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~21-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a negative-property assertion that the unauth response does NOT echo adatakey, amessagekey, orsuccess: true; a gate-before-post-auth invariant pinning that NONE of the four post-gate messages ('Sponsor ad not found','PAYMENT_NOT_RECEIVED','Failed to approve sponsor ad','Sponsor ad approved and activated successfully') must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk pinning that the inner try/catch never surfaces a 400; a service-not-entered invariance walk pinning that the unauth response does NOT echo adatakey from the service payload; a forceApprove enum-shape invariance walk pinning that the gate fires before the flag evaluation). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md,admin-clients-bulk-method-spec.md,admin-items-id-history-query-spec.md,admin-items-import-body-spec.md,admin-items-import-validate-body-spec.md, andadmin-notifications-id-read-method-spec.md(the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 20-of-N and thetests/api/per-spec-file sub-rollout extends to 18-of-many, and the first multi-error-code catch chain admin-tree smoke lands as a complementary surface to the single-message catch families the sub-rollout previously published. - E2E Admin Sponsor Ads [id] Reject Method Spec (
apps/web-e2e/tests/api/admin-sponsor-ads-id-reject-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ad rejection method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-sponsor-ads-id-reject-method.spec.ts, the twenty-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the nineteenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/sponsor-ads/[id]/reject/route.ts-- the first admin-tree route the smoke layer covers that combines a dynamic-segment[id]POSThandler with a Zod-safeParse(...)body validation AFTER the gate AND AFTER params resolution AND AFTER the body parse, AND a two-branch catch chain that maps two distinct service-thrownError.messagevalues to two distinct HTTP envelopes. Sibling ofadmin-sponsor-ads-id-approve-method-spec.mdsharing the SAME compound single-ifgate (!session?.user?.isAdmin || !session.user.id), the SAME canonical longer 401 envelope, and the SAME{ success: false, error: ... }envelope shape. Documents the unique combination of: (1) dynamic-segment[id]POSThandler; (2) compound single-ifgate; (3) canonical longer'Unauthorized. Admin access required.'401 message andsuccess: falseenvelope key matching the canonical-longer-envelope family; (4) body parse via.catch(() => ({}))Promise-chain catch -- a single-expression catch that returns an empty object on parse failure, distinct from the inner try/catch block of theapproveroute; (5) Zod-safeParse(...)body validation AFTER the gate and AFTER params resolution and AFTER the body parse, with a 400 response that echoesvalidationResult.error.issues[0]?.message || 'Invalid request body'-- a dynamic error message drawn from the Zod schema, distinct from the hand-rolled string literals of every prior admin-tree smoke -- the first Zod-safeParse(...)admin-tree smoke the docs tree publishes; (6) schema constraints --rejectionReasonis required withminLength: 10andmaxLength: 500, withidfromparamsco-validated through the schema ({ id, rejectionReason: body.rejectionReason }), distinct from theapproveroute which validates only theforceApproveflag; (7) service-call surface AFTER both the gate AND the Zod validation withsponsorAdService.rejectSponsorAd(id, session.user.id, validationResult.data.rejectionReason), success-branch payload{ success: true, data: <ad>, message: 'Sponsor ad rejected successfully' }; (8) two-branch catch chain mappingerror.message.includes('Cannot reject')→ 400,'Sponsor ad not found'→ 404, withsafeErrorResponse(error, 'Failed to reject sponsor ad')fallback -- a complementary surface to the three-branch catch chain of the siblingapproveroute; (9) service-zero-rows fallback returning 500{ success: false, error: 'Failed to reject sponsor ad' }; (10) method-resolution surface withPOST-only export. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~21-header bulk-loop walk + a ~15-body bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four post-gate messages ('Sponsor ad not found','Failed to reject sponsor ad','Sponsor ad rejected successfully','Invalid request body') must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe; a malformed-JSON-body invariance walk; a service-not-entered invariance walk; arejectionReasonlength / shape invariance walk pinning that therejectSponsorAdSchema.safeParse(...)is NOT evaluated on the unauth branch -- across everyrejectionReasonshape (valid 70-char + 10-char-min boundary + 5-char-too-short + empty + null + numeric + 501-char-too-long + missing)). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md,admin-clients-bulk-method-spec.md,admin-items-id-history-query-spec.md,admin-items-import-body-spec.md,admin-items-import-validate-body-spec.md,admin-notifications-id-read-method-spec.md, andadmin-sponsor-ads-id-approve-method-spec.md(the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 21-of-N and thetests/api/per-spec-file sub-rollout extends to 19-of-many, and the first Zod-safeParse(...)admin-tree smoke lands as a complementary surface to the manual-validation smokes the sub-rollout previously published. - E2E Admin Sponsor Ads [id] Cancel Method Spec (
apps/web-e2e/tests/api/admin-sponsor-ads-id-cancel-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin sponsor-ad cancellation method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-sponsor-ads-id-cancel-method.spec.ts, the twenty-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twentieth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/sponsor-ads/[id]/cancel/route.ts-- the first admin-tree route the smoke layer covers that combines a dynamic-segment[id]POSThandler with a pure single-step!session?.user?.isAdmingate (NOT the compound!isAdmin || !idgate of the siblingapprove/rejectroutes), AND a Zod-safeParse(...)body validation against an optional-only schema (cancelReasonhas onlymaxLength: 500and is NOT required), AND a reverse-ordered two-branch catch chain that puts the not-found 404 branch BEFORE the'Cannot cancel'400 branch -- distinct from the siblingrejectroute which puts'Cannot reject'400 BEFORE'Sponsor ad not found'404. Documents the unique combination of: (1) dynamic-segment[id]POSThandler; (2) pure single-step!session?.user?.isAdmingate -- a single-step gate that ONLY checksisAdmin, distinct from the compound!isAdmin || !idgate of the siblingapprove/rejectroutes; (3) canonical longer'Unauthorized. Admin access required.'401 message andsuccess: falseenvelope key matching the canonical-longer-envelope family; (4) body parse via.catch(() => ({}))Promise-chain catch; (5) Zod-safeParse(...)body validation AFTER the gate and AFTER params resolution and AFTER the body parse, withcancelSponsorAdSchemaco-validatingidfromparamswithcancelReasonfrom body; (6) optional-onlycancelReasonwithmaxLength: 500constraint -- a missing / undefined / nullcancelReasonwould pass validation on the auth branch (whereas the siblingrejectroute requiresrejectionReasonwithminLength: 10) -- the first optional-Zod-field admin-tree smoke the docs tree publishes; (7) service-call surface withsponsorAdService.cancelSponsorAd(id, validationResult.data.cancelReason)(NOTE: nosession.user.idaudit-user threaded through, distinct from the siblingapprove/rejectroutes), success-branch payload{ success: true, data: <ad>, message: 'Sponsor ad cancelled successfully' }; (8) reverse-ordered two-branch catch chain mapping'Sponsor ad not found'→ 404 FIRST, thenerror.message.includes('Cannot cancel')→ 400, withsafeErrorResponse(error, 'Failed to cancel sponsor ad')fallback -- distinct from the siblingrejectroute's order; (9) service-zero-rows fallback returning 500; (10) method-resolution surface withPOST-only export. Documents the at-a-glance scenario tree (a ~6-id bulk-loop walk + a ~21-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four post-gate messages must appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a per-id-shape status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe; a malformed-JSON-body invariance walk; a service-not-entered invariance walk; acancelReasonlength / shape invariance walk pinning that thecancelSponsorAdSchema.safeParse(...)is NOT evaluated on the unauth branch -- across everycancelReasonshape (missing + empty + null + valid + 500-char-boundary + 501-char-too-long + numeric)). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md,admin-clients-bulk-method-spec.md,admin-items-id-history-query-spec.md,admin-items-import-body-spec.md,admin-items-import-validate-body-spec.md,admin-notifications-id-read-method-spec.md,admin-sponsor-ads-id-approve-method-spec.md, andadmin-sponsor-ads-id-reject-method-spec.md(the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 22-of-N and thetests/api/per-spec-file sub-rollout extends to 20-of-many, and the first optional-Zod-field admin-tree smoke lands as a complementary surface to the required-Zod-field and manual-validation smokes the sub-rollout previously published. - E2E Admin Roles [id] Permissions Method Spec (
apps/web-e2e/tests/api/admin-roles-id-permissions-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin role-permissions dual-method (GET + PUT) / dynamic-id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-roles-id-permissions-method.spec.ts, the twenty-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-first underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/roles/[id]/permissions/route.ts— the first admin-tree route the smoke layer covers that combines (1) a dynamic-segment[id]handler exporting BOTH aGETand aPUT(a true dual-method surface, distinct from every prior single-method admin-id smoke), AND (2) an auth gate that delegates to thecheckAdminAuth()helper atapps/web/lib/auth/admin-guard.ts(NOT inline!session?.user?.isAdmin), AND (3) a shorter'Unauthorized'401 envelope (NOT the canonical longer'Unauthorized. Admin access required.'envelope that every prior admin-id smoke pins), AND (4) an imperative permissions-array validation againstisValidPermission(permission)fromapps/web/lib/permissions/definitions.ts(NOT a ZodsafeParse(...)schema, NOT a manual['approved', 'rejected'].includes(...)allowlist). Documents thecheckAdminAuth()helper's three-branch envelope (401'Unauthorized'no-session / 401'User ID not found'no-id / 403'Insufficient permissions'non-admin-auth — distinct from every prior admin-tree route which returns 401 for both unauth AND non-admin-auth), the side-channelinvalidPermissionsarray echoed in the auth-branch 400 envelope (a UNIQUE envelope key no prior admin-tree smoke pins), and pins the gate-before-post-auth, gate-before-params-resolution, gate-before-body-parse, gate-before-service, gate-before-validation, cross-method envelope-equality, side-channelinvalidPermissionsnon-disclosure, and first-branch landing invariants — thetests/per-spec-file docs rollout extends to 23-of-N and thetests/api/per-spec-file sub-rollout extends to 21-of-many, and the first dual-methodcheckAdminAuth()-helper admin-tree smoke lands as a complementary surface to the canonical-longer-envelope, inline-!session?.user?.isAdmin-gated, Zod / manual-validation smokes the sub-rollout previously published. - E2E Admin Items [id] Method Spec (
apps/web-e2e/tests/api/admin-items-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-item CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-items-id-method.spec.ts, the twenty-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-second underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/items/[id]/route.ts— the first triple-method admin-tree smoke the docs tree publishes (every prior dynamic-segment admin-id smoke pins a single-method export; theadmin-roles-id-permissions-method-spec.mdsmoke pins a dual-methodGET+PUTexport; this route ships THREE distinct HTTP-verb handlersGET+PUT+DELETEfrom a single file). All three handlers share the SAME inline!session?.user?.isAdmingate, the SAME canonical longer 401 envelope ('Unauthorized. Admin access required.'), and the SAME{ success: false, error: ... }envelope shape — but each diverges on its post-gate surface: GET has no body parse, callsitemRepository.findById(id)with a 404'Item not found'branch, returns{ success: true, data: <item> }, and catches withsafeErrorResponse(error, 'Failed to fetch item'); PUT parses JSON viaawait request.json()(NOT wrapped in a per-call try/catch — a malformed body would 500 via the outer catch on the auth branch), spreads the body into anUpdateItemRequest, builds an audit-user fromsession.user.id/name/email, callsitemRepository.update(id, updateData, auditUser), optionally syncs to Twenty CRM (gated byprocess.env.TWENTY_CRM_ENABLED !== 'false'and a bodybrandfield) and to the Location Index (gated bygetLocationEnabled()), returns{ success: true, data: <item>, message: 'Item updated successfully' }, and catches withsafeErrorResponse(error, 'Failed to update item'); DELETE has no body parse, builds the same audit-user, callsitemRepository.delete(id, auditUser), optionally removes from the Location Index (gated bygetLocationEnabled()), returns{ success: true, message: 'Item deleted successfully' }(NOTE: NOdatakey — distinct from GET / PUT success payloads which both includedata), and catches withsafeErrorResponse(error, 'Failed to delete item'). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~20-header × 3-method bulk-loop walk + a ~17-PUT-body bulk-loop walk all asserting< 500; per-method canonical-longer 401-envelope assertions across GET / PUT / DELETE; a cross-method envelope-equality assertion pinning that all three handlers emit byte-identical 401 envelopes; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the six post-auth messages ('Item not found','Failed to fetch item','Failed to update item','Failed to delete item','Item updated successfully','Item deleted successfully') must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT pinning that the gate fires before the body parse; a service-not-entered invariance walk pinning that NONE of the three repository calls is entered on the unauth branch; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the full set of sibling per-spec-file references undertests/api/and the dual-methodadmin-roles-id-permissions-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 24-of-N and thetests/api/per-spec-file sub-rollout extends to 22-of-many, and the first triple-method admin-tree smoke lands as a complementary surface to the single-method and dual-method smokes the sub-rollout previously published. - E2E Admin Clients [clientId] Method Spec (
apps/web-e2e/tests/api/admin-clients-clientid-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-client profile CRUD GET / PUT / DELETE method / clientId / body / header smoke spec paired withapps/web-e2e/tests/api/admin-clients-clientid-method.spec.ts, the twenty-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-third underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/clients/[clientId]/route.ts— the second triple-method admin-tree smoke the docs tree publishes (afteradmin-items-id-method-spec.md) but the first that exposes the bare{ error: 'Unauthorized' }envelope (NOsuccess: falsekey) AND uses a non-canonical[clientId]dynamic-segment param name (every prior dynamic-segment admin smoke uses[id]). All three handlers share the SAME single-step inline!session?.user?.isAdmingate but the 401 envelope is the bare{ error: 'Unauthorized' }(NOsuccess: falsekey) — distinct from every prior dynamic-segment-[id]admin smoke. Documents the unique combination of: (1)[clientId]dynamic-segment param name --await paramsresolves to{ clientId: string }AFTER the gate; (2) single-step inline!session?.user?.isAdmingate with a bare 401 envelope; (3) bare{ error: 'Unauthorized' }envelope with NOsuccesskey on the 401 branch -- distinct from the canonical-longer envelope of the sibling triple-methodadmin/items/[id]route; (4) direct query-function calls (getClientProfileById/updateClientProfile/deleteClientProfilefrom@/lib/db/queries) instead of a repository class -- distinct from theItemRepositoryof the sibling route; (5)console.error+ bare{ error: '<msg>' }500 catch with handler-specific messages ('Failed to fetch client'/'Failed to update client'/'Failed to delete client') -- distinct from thesafeErrorResponse(...)catch of the sibling route; (6) GET success payload{ success: true, data: <client> }; (7) PUT success payload{ success: true, data: <client> }(NOmessagekey -- distinct from the siblingadmin/items/[id]PUT which includes'Item updated successfully'); (8) DELETE success payload{ success: true, message: 'Client deleted successfully' }(NOdatakey); (9) PUT CRM-sync side effect -- two-step (company → person) chain wrapped in its own try/catch, gated byprocess.env.TWENTY_CRM_ENABLED !== 'false', distinct from the sibling route's single-step (company-only) sync; (10) method-resolution surface withGET/PUT/DELETE-only export. Documents the at-a-glance scenario tree (a ~6-clientId × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~17-PUT-body bulk-loop walk all asserting< 500; per-method bare 401-envelope assertions across GET / PUT / DELETE pinning the divergence vs the canonical longer envelope; a strict envelope-shape assertionObject.keys(body) === ['error']withbody.successundefined; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the five post-auth messages must appear in any unauth response; a per-clientId-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all three query-function calls; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the full set of sibling per-spec-file references undertests/api/, the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the dual-methodadmin-roles-id-permissions-method-spec.md, and the bare-envelope dynamic-segmentadmin-notifications-id-read-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 25-of-N and thetests/api/per-spec-file sub-rollout extends to 23-of-many, and the first bare-envelope-no-success-key triple-method admin-tree smoke lands as a complementary surface to the canonical-longer-envelope triple-method smoke the sub-rollout previously published. - E2E Admin Users [id] Method Spec (
apps/web-e2e/tests/api/admin-users-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-user CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-users-id-method.spec.ts, the twenty-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-fourth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/users/[id]/route.ts— the third triple-method admin-tree smoke the docs tree publishes (afteradmin-items-id-method-spec.mdandadmin-clients-clientid-method-spec.md) but the first that combines a two-step gate (!session?.user→ 401'Unauthorized', then!session.user.isAdmin→ 403'Forbidden'), a hybrid 401 envelope{ success: false, error: 'Unauthorized' }(bare message PLUSsuccess: falsekey — distinct from both the canonical-longer envelope ofadmin/items/[id]AND the no-success-key bare envelope ofadmin/clients/[clientId]), an eight-step PUT body-validation chain with handler-specific error messages for each branch (a-h: object-shape / email / username / name / title / avatar / role-DB-lookup / status-enum), a DELETE self-deletion guard (session.user.id === id→ 400'Cannot delete your own account'— a unique safety check no other admin-tree route enforces), and anerror.message-pass-through catch posture on PUT and DELETE that returns 400 with the raw error message instead of a fixed 500 string when the error is anErrorinstance. Documents the unique combination of: (1)[id]dynamic-segment param name with two-step gate; (2) two-step gate with bare-message +success: falseenvelope key; (3) hybrid 401 envelope{ success: false, error: 'Unauthorized' }matching theadmin/roles/[id]/permissionsenvelope; (4) eight-step PUT body-validation chain AFTER the gate AND AFTER params resolution AND AFTER the body parse, with the role-validation step issuing aroleRepository.findById(body.role)DB lookup that returns 400'Invalid role'if not found -- the first DB-lookup body-validation step admin-tree smoke the docs tree publishes; (5) DELETE self-deletion guard -- a unique safety check no other admin-tree route enforces; (6)error.message-pass-through catch on PUT and DELETE -- distinct from every prior smoke's fixed-message catch; (7) GET success payload{ success: true, data: <user> }; (8) PUT success payload{ success: true, data: <updatedUser> }(NOmessagekey); (9) DELETE success payload{ success: true, message: 'User deleted successfully' }(NOdatakey); (10) method-resolution surface withGET/PUT/DELETE-only export. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~28-PUT-body bulk-loop walk all asserting< 500; per-method hybrid 401-envelope assertions across GET / PUT / DELETE; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a cross-method envelope-equality assertion; an unauth-lands-on-401-not-403 invariant pinning that the response must NEVER be 403 and must NEVER echo'Forbidden'; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the twelve post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all three repository calls; a gate-before-eight-step-validation invariance walk pinning that EVERY step-(a)–(h) probe (~13 probes covering all eight validation branches) round-trips to the same 401 status; a DELETE self-deletion-guard invariance walk pinning that every id shape -- including session-shaped ids that would trigger the guard on the auth branch -- round-trips to the same 401 status, and the unauth response must NEVER echo'Cannot delete your own account'). Cross-references the full set of sibling per-spec-file references undertests/api/, the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the bare-envelope-no-success-key triple-methodadmin-clients-clientid-method-spec.md, and the dual-methodadmin-roles-id-permissions-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 26-of-N and thetests/api/per-spec-file sub-rollout extends to 24-of-many, and the first hybrid-envelope two-step-gated triple-method admin-tree smoke lands as a complementary surface to the canonical-longer-envelope and bare-envelope triple-method smokes the sub-rollout previously published. - E2E Admin Categories [id] Method Spec (
apps/web-e2e/tests/api/admin-categories-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-category CRUD GET / PUT / DELETE method / id / body / query / header smoke spec paired withapps/web-e2e/tests/api/admin-categories-id-method.spec.ts, the twenty-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-sixth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/categories/[id]/route.ts— the second triple-method admin-tree smoke the docs tree publishes (afteradmin-items-id-method-spec.md) and the first triple-method admin smoke with a DELETE-only?hard=truequery-parameter branch that flips the service call fromcategoryRepository.delete(id)(soft delete / deactivation) tocategoryRepository.hardDelete(id)(permanent removal), AND the first DELETE smoke with a query-flag-driven success-message dichotomy ('Category deactivated successfully'vs'Category permanently deleted'). All three handlers share the SAME single-step inline!session?.user?.isAdmingate and the SAME canonical longer 401 envelope ('Unauthorized. Admin access required.') and the SAME{ success: false, error: ... }envelope shape, but each diverges on its post-gate surface: (a) GET — no body parse, no query parse, callscategoryRepository.findById(id)with a 404'Category not found'branch, returns{ success: true, data: <category> }, catches withsafeErrorResponse(error, 'Failed to fetch category'); (b) PUT — JSON body parse viaawait request.json()AFTER the gate (NOT wrapped in a per-call try/catch — a malformed body would 500 via the outer catch on the auth branch), spreadsbody.nameinto anUpdateCategoryRequestwithidfrom params, callscategoryRepository.update(updateData), runsawait invalidateContentCaches()on the success branch, returns{ success: true, data: <category>, message: 'Category updated successfully' }, has THREE message-pattern catch branches BEFORE the outersafeErrorResponse(error, 'Failed to update category')catch ('not found'→ 404,'already exists'→ 409,'must be'→ 400, each echoing the rawerror.message); (c) DELETE — no body parse, parsessearchParams.get('hard') === 'true'AFTER the gate, callscategoryRepository.hardDelete(id)ifhard === trueelsecategoryRepository.delete(id), runsawait invalidateContentCaches()on the success branch, returns{ success: true, message: 'Category permanently deleted' }forhard === trueor{ success: true, message: 'Category deactivated successfully' }otherwise (NOdatakey — distinct from GET / PUT), has ONE message-pattern catch branch ('not found'→ 404 echoingerror.message) BEFORE the outersafeErrorResponse(error, 'Failed to delete category')catch. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~20-header × 3-method bulk-loop walk + a ~14-PUT-body bulk-loop walk + a ~9-DELETE-?hard=…-query bulk-loop walk all asserting< 500; per-method canonical-longer 401-envelope assertions across GET / PUT / DELETE; a cross-method envelope-equality assertion; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a DELETE-query envelope-invariance assertion pinning that all?hard=…permutations round-trip to the SAME 401 envelope as the no-query baseline; a success-branch-key non-disclosure assertion across all three methods (and across both DELETE query branches); a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages -- including BOTH DELETE success messages -- must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk pinning that NONE of the four repository calls (findById/update/delete/hardDelete) is entered on the unauth branch; a side-effects-not-entered invariance walk pinning that theinvalidateContentCaches()call is unreachable on the unauth branch for both PUT and DELETE; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the full set of sibling per-spec-file references undertests/api/, including the first triple-methodadmin-items-id-method-spec.md, the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.md, the dual-methodadmin-roles-id-permissions-method-spec.md, and the nested dual-methodadmin-collections-id-items-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 28-of-N and thetests/api/per-spec-file sub-rollout extends to 26-of-many, and the first triple-method admin smoke with a DELETE-only query-parameter branch and a query-flag-driven success-message dichotomy lands as a complementary surface to the prior triple-method smokes the sub-rollout previously published. - E2E Admin Reports [id] Method Spec (
apps/web-e2e/tests/api/admin-reports-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-report CRUD GET / PUT method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-reports-id-method.spec.ts, the twenty-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-seventh underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/reports/[id]/route.ts— the first 403-on-unauth admin-tree smoke the docs tree publishes (every prior admin-tree route returns 401 on the unauth branch with one of three envelope shapes: canonical-longer / bare-Unauthorized/ hybrid-success: false-bare; this route returns 403'Forbidden'on the unauth branch instead). Both handlers share several unique characteristics: (1)checkDatabaseAvailability()pre-gate that runs BEFORE the auth gate -- distinct from every prior admin-tree smoke whereauth()is the FIRST guard; (2) single-step!session?.user?.isAdmingate that returns 403{ success: false, error: 'Forbidden' }on the unauth branch -- distinct from every prior admin-tree route which returns 401; (3)success: falseenvelope key on the 403 branch with strict envelope-shapeObject.keys(body).sort() === ['error', 'success']; (4) dynamic[id]segment resolved AFTER both gates; (5) dev-gatedconsole.errorcatch that only logs whenprocess.env.NODE_ENV === 'development'. Each handler also has its own divergent post-gate surface: GET runsgetReportById(id)→ 404'Report not found', returns{ success: true, data: <report> }; PUT runs the existence check FIRST (getReportById(id)BEFORE the body parse — distinct from every prior PUT smoke), then parses body and validatesstatus/resolutionagainst theReportStatus/ReportResolutionenums (with dynamically-interpolated 400 messages prefixed'Invalid status\|resolution. Must be one of: ...'), then callsupdateReport(...)followed by a conditional moderation-action chain that runsremoveContent/warnUser/suspendUser/banUserfrom the moderation service based onresolution(withgetContentOwner(...)lookup for user-action resolutions), then a finalgetReportById(id)for the full updated report, and returns{ success: true, message: moderationResult?.message || 'Report updated successfully', data: <report>, moderationResult }(FOUR success-branch keys). Documents the at-a-glance scenario tree (a ~6-id × 2-method bulk-loop walk + a ~17-header × 2-method bulk-loop walk + a ~22-PUT-body bulk-loop walk all asserting< 500; per-method 403-envelope assertions across GET and PUT; a NEVER-401 invariant pinning that the unauth client lands on 403, not 401; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion that NONE of the route-specificdataandmoderationResultkeys plusmessageandsuccess: truemust appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the five static post-auth messages AND none of the dynamic'Invalid status\|resolution. Must be one of: ...'400 messages (matched via regex prefix) must appear in any unauth response; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across the entire moderation chain; a status / resolution enum-shape invariance walk; a moderation-chain-not-entered invariance walk pinning that everyresolutionvalue that would trigger a moderation action --content_removed/user_warned/user_suspended/user_banned-- round-trips to the same 403 status with NOmoderationResultkey in the response). Cross-references the full set of sibling per-spec-file references undertests/api/, including the 401-on-unauth dual-methodadmin-collections-id-items-method-spec.mdand the 401-on-unauth triple-methodadmin-items-id-method-spec.md,admin-clients-clientid-method-spec.md,admin-users-id-method-spec.md, andadmin-categories-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 29-of-N and thetests/api/per-spec-file sub-rollout extends to 27-of-many, and the first 403-on-unauth admin-tree smoke lands as a complementary surface to the 401-on-unauth smokes the sub-rollout previously published. - E2E Admin Sponsor Ads [id] Method Spec (
apps/web-e2e/tests/api/admin-sponsor-ads-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-sponsor-ad GET / DELETE method / id / header smoke spec paired withapps/web-e2e/tests/api/admin-sponsor-ads-id-method.spec.ts, the thirtieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-eighth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/sponsor-ads/[id]/route.ts— the first GET + DELETE-only dual-method admin-tree smoke the docs tree publishes (the sponsor-ad write path is split across the siblingadmin-sponsor-ads-id-approve-method-spec.md,admin-sponsor-ads-id-reject-method-spec.md, andadmin-sponsor-ads-id-cancel-method-spec.mdaction routes which the smoke layer already covers separately; with this entry the sponsor-ad area smoke coverage is complete). Both handlers share the SAME single-step inline!session?.user?.isAdmingate, the SAME canonical longer 401 envelope, and the SAME{ success: false, error: ... }envelope shape, but each diverges on its catch posture -- the load-bearing divergence: GET uses aconsole.error+ 500'Failed to fetch sponsor ad'catch, while DELETE uses a narrow-matcherror.message === 'Sponsor ad not found'→ 404 catch followed by asafeErrorResponse(error, 'Failed to delete sponsor ad')fallback. The DELETE handler is the first admin DELETE smoke where the catch chain begins with a narrow-match equality check on a service-thrown sentinel string (rather than a.includes(...)substring match or a fixed fallback). Documents the at-a-glance scenario tree (a ~6-id × 2-method bulk-loop walk + a ~17-header × 2-method bulk-loop walk all asserting< 500; per-method canonical-longer 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting POST / PUT / PATCH round-trip to< 500; a service-not-entered invariance walk; a DELETE narrow-match-catch-not-entered invariance walk pinning that the unauth response must NEVER echo the service-thrown sentinel'Sponsor ad not found'; a per-handler catch-message-divergence walk pinning that NONE of the two distinct catch messages must appear in any unauth response). Cross-references the three sponsor-ad action smokesadmin-sponsor-ads-id-approve-method-spec.md,admin-sponsor-ads-id-reject-method-spec.md, andadmin-sponsor-ads-id-cancel-method-spec.md, the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, and the dual-methodadmin-roles-id-permissions-method-spec.mdandadmin-reports-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 30-of-N and thetests/api/per-spec-file sub-rollout extends to 28-of-many, and the first GET + DELETE-only dual-method admin-tree smoke lands -- closing the sponsor-ad-area smoke coverage at four routes (the leaf-[id]root withGET+DELETEplus the three nested-[id]/<action>POST routes forapprove/reject/cancel). - E2E Admin Comments [id] Method Spec (
apps/web-e2e/tests/api/admin-comments-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-comment CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-comments-id-method.spec.ts, the thirty-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-ninth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/comments/[id]/route.ts— the first 403-on-unauth triple-method admin-tree smoke the docs tree publishes (every prior triple-method admin smoke returns 401 on the unauth branch; the siblingadmin-reports-id-method-spec.md403-on-unauth route is dual-methodGET+PUT; this is the first triple-method 403 admin smoke). All three handlers share the SAME single-step inline!session?.user?.isAdmingate that returns 403{ success: false, error: 'Forbidden' }, the SAME{ success: false, error: ... }envelope shape, and the SAMEconsole.error('Failed to <verb> comment:', error)+ 500'Internal Server Error'catch posture. Each handler diverges on its post-gate surface: GET runsgetTenantId()AFTERawait params→ 403'Tenant not found'if missing, then issues an inline Drizzle query withleftJointoclientProfilesand tenant scoping, returning 404'Comment not found'or{ success: true, data: <comment-with-user> }; PUT runsgetTenantId()BEFOREawait params(NOTE: ordering distinct from GET) → 403'Tenant not found', parses JSON body, validatescontent?.trim()→ 400'Content is required'if falsy, runs a soft-delete-awaregetCommentById(id)existence check (existingComment.deletedAt→ 404'Comment not found'), then issues an inline Drizzle re-query (NOTE: the actualupdateCommentcall is commented out in the source — the route currently re-fetches the comment without updating it; a regression-sensitive note), returning{ success: true, data: <comment-with-user>, message: 'Comment updated successfully' }; DELETE has NOgetTenantId()call (distinct from GET / PUT), runs a soft-delete-awaregetCommentById(id)existence check, callsdeleteComment(id)(soft delete via settingdeletedAt), and returns{ success: true, message: 'Comment deleted successfully' }(NOdatakey). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~12-PUT-body bulk-loop walk all asserting< 500; per-method 403-envelope assertions across GET / PUT / DELETE; a NEVER-401 invariant pinning that the unauth client lands on 403 across all three methods; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the six post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison across all three methods; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across the inline Drizzle queries plusgetCommentById(...)/deleteComment(...)/getTenantId()calls; a tenant-resolution-not-entered invariance walk pinning that the unauth response must NEVER echo'Tenant not found'for GET / PUT). Cross-references the dual-method 403-on-unauthadmin-reports-id-method-spec.md, the GET + DELETE-only dual-methodadmin-sponsor-ads-id-method-spec.md, and the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 31-of-N and thetests/api/per-spec-file sub-rollout extends to 29-of-many, and the first 403-on-unauth triple-method admin-tree smoke lands as a complementary surface to the dual-method 403-on-unauth and the various 401-on-unauth triple-method smokes the sub-rollout previously published. - E2E Admin Companies [id] Method Spec (
apps/web-e2e/tests/api/admin-companies-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-company CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-companies-id-method.spec.ts, the thirty-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirtieth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/companies/[id]/route.ts— the first triple-method admin-tree smoke the docs tree publishes that combines the bare{ error: 'Unauthorized' }401 envelope (NOsuccesskey — matchingadmin/clients/[clientId]) with a Zodparse()(NOTsafeParse()) body-validation step that emits adetails: [{field, message}]400 envelope (a UNIQUE envelope key no prior admin-tree smoke pins) AND two 409 Conflict pre-update uniqueness checks (getCompanyByDomain(...)andgetCompanyBySlug(...)) with dynamically-interpolated messages ('Company with domain '<domain>' already exists'/'Company with slug '<slug>' already exists') AND an outer-catch unique-constraint translation chain that mapserror.message.includes('unique constraint' \| 'duplicate key')to one of three 409 envelope variants based ondomain/slugsubstring detection. All three handlers share the SAME single-step inline!session?.user?.isAdmingate that returns 401{ error: 'Unauthorized' }, the SAME bare envelope shape, and the SAMEconsole.error+ bare{ error: '<msg>' }500 catch posture (with handler-specific messages'Failed to fetch\|update\|delete company'). Each handler diverges on its post-gate surface: GET callsgetCompanyById(id)→ 404 / 200{ success: true, data: <company> }; PUT runs the existence check FIRST (getCompanyById(id)BEFORE the body parse — distinct from every prior PUT smoke), then parses JSON body and runs Zodparse()(NOTsafeParse()) wrapped in inline try/catch (catchesZodErrorand returns 400 with customdetails: [{field, message}]array), then runs TWO 409 pre-update uniqueness checks (getCompanyByDomain/getCompanyBySlug) with dynamically-interpolated messages, thenupdateCompany(id, ...)returning 404 if falsy, then optional CRM sync (gated byprocess.env.TWENTY_CRM_ENABLED !== 'false', wrapped in its own try/catch), returning{ success: true, data: <company> }; PUT also has an outer-catch unique-constraint translation chain that maps DB error messages to one of three 409 envelopes; DELETE callsdeleteCompany(id)returning boolean → 404 if false / 200{ success: true, message: 'Company deleted successfully' }. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~16-PUT-body bulk-loop walk all asserting< 500; per-method bare 401-envelope assertions; a strict envelope-shape assertionObject.keys(body) === ['error']with nosuccesskey; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion that NONE of the route-specificdata,details,messagekeys plussuccess: truemust appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the nine static post-auth messages plus none of the dynamic'Company with domain\|slug '<...>' already exists'409 messages (matched via regex prefix) must appear in any unauth response; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all five DB-query calls; a Zod-validation-not-entered invariance walk pinning that every Zod-invalid body shape round-trips to the same 401 status with NOdetailskey in the response; a uniqueness-check-409-not-entered invariance walk pinning that the unauth response must NEVER match the dynamic/^Company with (domain\|slug) '/regex prefixes; a unique-constraint-outer-catch-not-entered invariance walk pinning that the unauth response must NEVER echo any of the three static unique-constraint translation messages; a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response). Cross-references the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.md, the 403-on-unauth triple-methodadmin-comments-id-method-spec.md, the categories-CRUD triple-methodadmin-categories-id-method-spec.md, and the Zod-safeParse(...)single-methodadmin-sponsor-ads-id-reject-method-spec.mdandadmin-sponsor-ads-id-cancel-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 32-of-N and thetests/api/per-spec-file sub-rollout extends to 30-of-many, and the first Zod-parse()-with-details-envelope admin-tree smoke lands as a complementary surface to the prior Zod-safeParse(...)and manual-validation triple-method smokes the sub-rollout previously published. - E2E Admin Collections [id] Method Spec (
apps/web-e2e/tests/api/admin-collections-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-collection CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-collections-id-method.spec.ts, the thirty-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-second underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/collections/[id]/route.ts— the first triple-method admin-tree smoke the docs tree publishes that combines a canonical longer'Unauthorized. Admin access required.'401 envelope with a ZodsafeParse(...).error.flatten()400 envelope posture (distinct from every prior triple-method admin smoke which use either an inline-untyped destructure with no Zod, an inline-untyped destructure with a DELETE-only?hard=truequery branch, a Zodparse()(THROWS) withdetails: ZodError.errors-style catch envelope, or a validation-less PUT with seven body fields shoved straight intodb.update(...)). All three handlers share the SAME inline!session?.user?.isAdmingate (NOT delegated to acheckAdminAuth(...)helper), the SAME canonical longer 401 envelope, the SAMEsafeErrorResponse(...)outer-catch fallback (with handler-specific messages'Failed to fetch\|update\|delete collection'), the SAMEfindByIdpre-action 404 check on PUT + DELETE (distinct fromadmin/categories/[id]PUT which lets the service throw, and distinct fromadmin/featured-items/[id]PUT which uses the.returning()length-zero check), and the SAMErevalidatePath(...)cache invalidation pattern AFTER the repository call (withinvalidateContentCaches()called in addition). Each handler diverges on its post-gate surface: GET callscollectionRepository.findById(id)returning 404'Collection not found'if missing or{ success: true, data: <collection> }; PUT parses JSON body AFTER the gate, runs ZodsafeParse(updateCollectionSchema)→ 400{ success: false, error: 'Invalid collection payload', details: parsed.error.flatten() }(UNIQUEflatten()-shapeddetails: { formErrors: string[], fieldErrors: Record<string, string[]> }envelope — DIFFERENT from theerror.errorsarray aparse()-then-catch route would emit), then runs the pre-updatefindById, thencollectionRepository.update(updateData), has THREE distinct catch branches (already exists→ 409 with bare-message echo,must→ 400 with bare-message echo,safeErrorResponse(...)fallback), then runs the conditional slug-revalidation branch (revalidates the OLD slug's path if it changed) plus the always-emitted new-slug + index revalidation, returning{ success: true, data: <updated>, message: 'Collection updated successfully' }; DELETE runs the pre-deletefindById, callscollectionRepository.delete(id), has TWO distinct catch branches (not found→ 404 with bare-message echo,safeErrorResponse(...)fallback), then runsinvalidateContentCaches()+ tworevalidatePath(...)calls, returning{ success: true, message: 'Collection deleted successfully' }(NOdatakey — distinct from the GET / PUT success payloads which both includedata). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~16-header × 3-method bulk-loop walk + a ~21-PUT-body bulk-loop walk all asserting< 500; per-method canonical-401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages must appear in any unauth response — including the Zod safeParse 400 envelope's fixed'Invalid collection payload'error string; a gate-before-Zod-safeParseinvariant pinning that the unauth response NEVER carries thedetails/formErrors/fieldErrorskeys even when the body would have failed Zod validation; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a repository-call-not-entered invariance walk across all three handlers; a cache-invalidation-side-effect-not-entered invariance walk across PUT + DELETE; a per-handler-catch-message-non-disclosure walk pinning that NONE of the three distinctsafeErrorResponse(...)fallback strings must appear in any unauth response; a gate-before-409-/-'must'-400-catch invariance walk pinning that the unauth status MUST be 401 — NOT 400 / 409 — and the envelope MUST be the canonical 401 envelope). Cross-references the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.md, the categories-CRUD triple-methodadmin-categories-id-method-spec.md, the 403-on-unauth triple-methodadmin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-methodadmin-companies-id-method-spec.md, the validation-less / non-admin-gated / soft-delete-DELETE triple-methodadmin-featured-items-id-method-spec.md, and the companion nested-dual-methodadmin-collections-id-items-method-spec.md, and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, anddocs/questions.mdfor the governing specs. With this entry the per-spec-file docs rollout extends to 34-of-N and thetests/api/per-spec-file sub-rollout extends to 32-of-many, and the first Zod-safeParse(...)-with-flatten()-envelope admin-tree smoke lands as a complementary surface to the prior Zod-parse()and validation-less and inline-untyped triple-method smokes the sub-rollout previously published. - E2E Admin Tags [id] Method Spec (
apps/web-e2e/tests/api/admin-tags-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-tag CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-tags-id-method.spec.ts, the thirty-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-third underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/tags/[id]/route.ts— the first triple-method admin-tree smoke the docs tree publishes that combines the hybrid bare-Unauthorized+success: false401 envelope (matchingadmin/users/[id],admin/featured-items/[id],admin/roles/[id]/permissions) with a single-step inline!session?.user?.isAdmingate AND a PUT outer-catch three-brancherror.message.includes(...)chain that maps'not found'→ 404,'already exists'→ 409,'required' \| 'must be'→ 400 (each echoing the rawerror.message). All three handlers share the SAME single-step inline!session?.user?.isAdmingate that returns 401{ success: false, error: 'Unauthorized' }, the SAME hybrid envelope shape, and the SAMEconsole.error+ 500 catch posture (with handler-specific messages'Failed to fetch\|update\|delete tag'). Each handler diverges on its post-gate surface: GET callstagRepository.findById(id)returning 404'Tag not found'or{ success: true, data: <tag> }; PUT parses JSON body, runsif (!name)→ 400'Tag name is required', callstagRepository.update(id, { name, isActive }), runsawait invalidateContentCaches()on the success branch, returns{ success: true, data: <tag>, message: 'Tag updated successfully' }, has THREE catch branches ('not found'→ 404 echoing raw message,'already exists'→ 409,'required' \| 'must be'→ 400) plus 500'Failed to update tag'fallback; DELETE callstagRepository.delete(id), runsawait invalidateContentCaches(), returns{ success: true, message: 'Tag deleted successfully' }(NOdatakey), has ONE catch branch ('not found'→ 404 echoing raw message) plus 500'Failed to delete tag'fallback. Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~14-PUT-body bulk-loop walk all asserting< 500; per-method hybrid 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages must appear in any unauth response; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk; a cache-invalidation-side-effect-not-entered invariance walk pinning that the unauth response must NEVER echo'Tag updated successfully'or'Tag deleted successfully'; a three-branch-catch-chain-not-entered invariance walk pinning that the PUT outer-catch'serror.message.includes(...)branches are unreachable on the unauth branch). Cross-references the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.mdandadmin-featured-items-id-method-spec.md, the categories-CRUD triple-methodadmin-categories-id-method-spec.md, the 403-on-unauth triple-methodadmin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-methodadmin-companies-id-method-spec.md, and the Zod-safeParse(...)-with-flatten()-envelope triple-methodadmin-collections-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 35-of-N and thetests/api/per-spec-file sub-rollout extends to 33-of-many, and the first hybrid-envelope-with-3-branch-error.message.includes(...)-catch admin-tree smoke lands as a complementary surface to the canonical-longer-envelope and bare-envelope and 403-on-unauth triple-method smokes the sub-rollout previously published. - E2E Admin Roles [id] Method Spec (
apps/web-e2e/tests/api/admin-roles-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-role CRUD GET / PUT / DELETE method / id / body / query / header smoke spec paired withapps/web-e2e/tests/api/admin-roles-id-method.spec.ts, the thirty-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-fourth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/roles/[id]/route.ts— the first triple-method admin-tree smoke the docs tree publishes that combines the two-step!session?.user→!session.user.isAdmingate (matchingadmin/users/[id]andadmin/reports/[id], distinct from the single-step gates of every other prior triple-method admin smoke) with a DELETE?hard=truequery-parameter branch that flips the soft-delete ('Role deleted (marked as inactive)') and hard-delete ('Role permanently deleted') messages — matching theadmin/categories/[id]DELETE-?hard=truepattern but with a distinct soft-delete success message — AND a three-step manual PUT body validation chain with FIXED error messages (NOT dynamically interpolated): (a)'Role name cannot be empty'on!updateData.name.trim(); (b)'Role name must be between 3 and 100 characters'on length out-of-range; (c)'Role description must be at most 500 characters'on description > 500. PUT also does an explicit existence check AFTER the body parse (NOT before, distinct fromadmin/reports/[id]andadmin/companies/[id]which check BEFORE). All three handlers share the SAME two-step gate, the SAME hybrid bare-message +success: false401 envelope ({ success: false, error: 'Unauthorized' }), and the SAMEconsole.error+ 500 catch posture (with handler-specific messages'Failed to fetch\|update\|delete role'). Each handler diverges on its post-gate surface: GET callsroleRepository.findById(id)returning 404'Role not found'or{ success: true, data: <role> }; PUT parses JSON body AFTER both gate steps, runs the existence check, then the three-step validation, thenroleRepository.update(id, ...), returns{ success: true, data: <role>, message: 'Role updated successfully' }; DELETE parsessearchParams.get('hard') === 'true'query AFTER both gate steps, runs the existence check, branches onhardDeleteboolean (hardDelete === true→roleRepository.hardDelete(id); elseroleRepository.delete(id)), returns{ success: true, message: 'Role permanently deleted' }forhard === trueor{ success: true, message: 'Role deleted (marked as inactive)' }otherwise (NOdatakey). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~15-PUT-body bulk-loop walk + an ~8-DELETE-?hard=...-query bulk-loop walk all asserting< 500; per-method hybrid 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; an unauth-lands-on-401-not-403 invariant pinning that the FIRST gate step fires for unauth clients (the response is 401 not 403, and must NEVER echo'Forbidden'); a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the eleven post-auth messages must appear in any unauth response — including the three PUT validation messages and both DELETE success messages; a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a DELETE?hard=...query-shape invariance walk pinning that every query shape (true / false / TRUE uppercase / 1 numeric / empty / unrelated key / extra key) round-trips to the same 401 status; a cross-method side-channel walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a service-not-entered invariance walk across all four repository calls (findById/update/delete/hardDelete); a three-step-validation invariance walk pinning that EVERY step-(a)/(b)/(c) probe round-trips to the same 401 status; a hard-delete-branch-not-entered invariance walk pinning that the unauth response must NEVER echo'Role deleted (marked as inactive)'or'Role permanently deleted'). Cross-references the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.mdandadmin-featured-items-id-method-spec.mdandadmin-tags-id-method-spec.md, the categories-CRUD triple-methodadmin-categories-id-method-spec.md, the 403-on-unauth triple-methodadmin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-methodadmin-companies-id-method-spec.md, and the Zod-safeParse(...)-with-flatten()-envelope triple-methodadmin-collections-id-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 36-of-N and thetests/api/per-spec-file sub-rollout extends to 34-of-many, and the first two-step-gate-with-DELETE-?hard-query admin-tree smoke lands as a complementary surface to the prior single-step-gate triple-method smokes the sub-rollout previously published. - E2E Admin Items Create Body Spec (
apps/web-e2e/tests/api/admin-items-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level item-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-items-create-body.spec.ts, the thirty-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-fifth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/items/route.ts(thePOSTexport) — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines a five-field required-validation chain (!id || !name || !slug || !description || !source_urlin a single guard expression returning 400'Item ID, name, slug, description, and source URL are required') with TWO 409 Conflict pre-create duplicate checks (itemRepository.checkDuplicateId(...)anditemRepository.checkDuplicateSlug(...)) using dynamically-interpolated messages ('Item with ID '<id>' already exists'/'Item with slug '<slug>' already exists') AND an audit-user-threading + CRM-company-link + Location-Index side-effect chain on the success branch. The companionadmin-items-query.spec.tscovers the GET (paginated list) surface of the same route. The POST handler shares the SAME single-step inline!session?.user?.isAdmingate with its GET sibling and returns the canonical longer 401 envelope{ success: false, error: 'Unauthorized. Admin access required.' }with strict envelope-shape preservation. CRM sync is gated byprocess.env.TWENTY_CRM_ENABLED === 'true'(NOTE: strict-equals comparison, distinct fromadmin/items/[id]/route.tsPUT which uses!== 'false'), wrapped in its own try/catch, walking a four-step chain (getOrCreateCompanyFromBrand→linkItemToCompany→ conditional CRM sync viaupsertCompanyif newly linked). Location Index side effect is gated bygetLocationEnabled(). Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~19-body bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofdata,item,id,slug,success: truekeys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that the two static post-auth messages plus the dynamic'Item with (ID\|slug) '<...>' already exists'409 messages (matched via regex prefix) must NEVER appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk; a required-field-validation-not-entered invariance walk; a duplicate-id-/-duplicate-slug-409-not-entered invariance walk; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201; a CRM-sync-side-effect-not-entered invariance walk pinning that a body withbranddoes NOT change the unauth status; a Location-Index-side-effect-not-entered invariance walk pinning that a body withlocationdoes NOT change the unauth status). Cross-references the companionadmin-items-query.spec.ts, the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, and the body-validation single-methodadmin-items-import-body-spec.md/admin-items-import-validate-body-spec.md/admin-items-bulk-body-spec.md/admin-items-id-review-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 37-of-N and thetests/api/per-spec-file sub-rollout extends to 35-of-many, and the first POST-only collection-level admin-tree smoke lands -- complementing the existing query-surface coverage of the same items-collection route. - E2E Admin Users Create Body Spec (
apps/web-e2e/tests/api/admin-users-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level user-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-users-create-body.spec.ts, the thirty-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-sixth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/users/route.ts— the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the two-step!session?.user→!session.user.isAdmingate (matchingadmin/users/[id],admin/roles/[id],admin/reports/[id]) with an eight-step body validation chain (object-shape / 5-required-fields / email-format / username-regex / name-length / password-Zod-safeParse/ title-length / avatar-length / role-DB-lookup) AND ZodpasswordSchema.safeParse(body.password)for password-only validation (returning a dynamically-interpolatedpasswordResult.error.issues[0]?.messageon failure — distinct from prior smokes that use Zod for the body-as-a-whole) AND a username regex validation (/^[a-zA-Z0-9_-]{3,30}$/— the first regex-based username validation in admin smoke) AND theerror.message-pass-through outer catch (matchingadmin/users/[id]PUT/DELETE) that returns 400 with the raw error message instead of a fixed 500 string when the error is anErrorinstance. The companionadmin-users-query.spec.tscovers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~32-body bulk-loop walk all asserting< 500; a hybrid 401-envelope assertion{ success: false, error: 'Unauthorized' }; a strict envelope-shape assertion; an unauth-lands-on-401-not-403 invariant; a success-branch-key non-disclosure assertion that NONE ofdata,user,id,success: truekeys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the thirteen post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk; an eight-step-validation invariance walk pinning that EVERY step-(a)-(i) probe round-trips to the same 401 status; a role-DB-lookup-not-entered invariance walk pinning that the unauth response must NEVER echo'Invalid role'; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201). Cross-references the companionadmin-users-query.spec.ts, the leaf-[id]triple-methodadmin-users-id-method-spec.mdcovering the same eight-step validation pattern on PUT updates, the collection-level POST companionadmin-items-create-body-spec.md, and the body-validation single-methodadmin-users-check-email-body-spec.mdandadmin-users-check-username-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 38-of-N and thetests/api/per-spec-file sub-rollout extends to 36-of-many, and the first eight-step-validation collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same users-collection route. - E2E Admin Categories Create Body Spec (
apps/web-e2e/tests/api/admin-categories-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level category-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-categories-create-body.spec.ts, the thirty-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-seventh underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/categories/route.ts— the first POST-only collection-level admin-tree smoke the docs tree publishes that uses acategorysuccess-payload key (NOTdata) plus a single-field required validation plus a three-branch outer catch chain ('already exists'→ 409,'must be'→ 400,safeErrorResponse(...)fallback). Thecategorysuccess-key matches the siblingPOST /api/admin/collectionswhich usescollection(notdata), suggesting a convention divergence between create endpoints that serve a single resource (categories / collections / tags) vs those that serve nested or composite payloads (items / users — which usedata). The companionadmin-categories-query.spec.tscovers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofcategory,data,message,success: truekeys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk; a required-field-check-not-entered invariance walk pinning that the unauth response must NEVER echo'Category name is required'; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201, must NOT contain acategorykey, and must NOT echo'Category created successfully'; a three-branch-outer-catch-not-entered invariance walk pinning that the unauth response must echo the canonical 401 envelope, not any branch of the outer catch chain). Cross-references the companionadmin-categories-query.spec.ts, the leaf-[id]triple-methodadmin-categories-id-method-spec.mdcovering the same three-branch outer catch on PUT updates, the collection-level POST companionsadmin-items-create-body-spec.mdandadmin-users-create-body-spec.md, and the body-validation single-methodadmin-categories-reorder-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 39-of-N and thetests/api/per-spec-file sub-rollout extends to 37-of-many, and the firstcategory-key collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same categories-collection route. - E2E Admin Tags Create Body Spec (
apps/web-e2e/tests/api/admin-tags-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level tag-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-tags-create-body.spec.ts, the fortieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/tags/route.ts— the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the hybrid bare-Unauthorized+success: false401 envelope (matchingadmin/tags/[id]GET/PUT/DELETE,admin/users/[id],admin/featured-items/[id],admin/roles/[id]/permissions) with atagsuccess-payload key (NOTdata) — distinct from the canonical-longer-envelopeadmin/categoriesandadmin/collectionsPOST smokes (which also use single-resource success-keys but with the canonical longer 401 envelope). The POST handler runs a two-field required check (if (!id || !name)→ 400'Tag ID and name are required'), callstagRepository.create({ id, name, isActive: isActive ?? true })(defaultsisActivetotrueif not provided — distinct from prior POST smokes that don't default a boolean field), runsawait invalidateContentCaches()on success, and returns{ success: true, tag: <tag> }with status 201 (NOmessagekey — distinct fromadmin/categoriesPOST'Category created successfully'andadmin/collectionsPOST'Collection created successfully'). The outer catch uses a three-branch chain matchingadmin/tags/[id]PUT:'already exists'→ 409,'required' \| 'must be'→ 400, else fixed-message 500'Failed to create tag'fallback (NOTsafeErrorResponse(...)— distinct from theadmin/categoriesPOST). The companionadmin-tags-query.spec.tscovers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a hybrid 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE oftag,data,success: truekeys plus the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the two static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk; a required-field-check-not-entered invariance walk; a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201 and must NOT contain atagkey; a three-branch-outer-catch-not-entered invariance walk pinning that the unauth response must echo the canonical 401 envelope, not any branch of the outer catch chain — including the fixed-message 500 fallback). Cross-references the companionadmin-tags-query.spec.ts, the leaf-[id]triple-methodadmin-tags-id-method-spec.mdcovering the same hybrid envelope and three-branch outer catch on PUT updates, the collection-level POST companionsadmin-items-create-body-spec.md,admin-users-create-body-spec.md, andadmin-categories-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 40-of-N and thetests/api/per-spec-file sub-rollout extends to 38-of-many, and the first hybrid-envelopetag-key collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same tags-collection route. - E2E Admin Clients Create Body Spec (
apps/web-e2e/tests/api/admin-clients-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level client-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-clients-create-body.spec.ts, the forty-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-ninth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/clients/route.ts— the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the bare{ error: 'Unauthorized' }envelope (NOsuccesskey — matching theadmin/clients/[clientId]smoke andadmin/companies/[id]) with a get-or-create user side-effect chain that usescrypto.randomBytes(6)to generate a temporary password for newly-created users (Temp<hex>!) AND a status-200 success branch (NOT 201, distinct from every prior collection-level POST smoke). The POST handler runs an email-or-userId fallback (const email = raw.email ?? raw.userId— distinct from prior POST smokes that have a single named required field), a single-field required check (if (!email)→ 400{ error: 'Email is required' }bare envelope),getUserByEmail(email)lookup, an inner-try/catch user-create branch that on failure returns 400 with dynamically-interpolated'Failed to create user: <err.message>'message, a get-or-create fallback validation (if (!user || !user.id)→ 400'Failed to create or find user for client'), thencreateClientProfile(clientData)with defaults (status='active',plan='free',accountType='individual'), an optional CRM sync side-effect gated byprocess.env.TWENTY_CRM_ENABLED !== 'false'wrapped in its own try/catch, and returns{ success: true, data: <client>, message: 'Client created successfully' }with status 200 (NOT 201). The outer catch returns 500{ error: 'Failed to create client' }(BARE envelope, NOsuccesskey). The companionadmin-clients-query.spec.tscovers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion pinning the divergence vs the canonical longer envelope; a strict envelope-shape assertionObject.keys(body) === ['error']withbody.successundefined; a success-branch-key non-disclosure assertion that NONE ofdata,success,messagekeys must appear in any unauth response and the unauth response status must NOT be 200; a gate-before-post-auth invariant pinning that the four static post-auth messages plus the dynamic'Failed to create user: ...'regex prefix must NOT appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-email-check-not-entered invariance walk; a get-or-create-user-side-effect-not-entered invariance walk pinning that the unauth response must NEVER match the dynamic'Failed to create user: ...'regex prefix and must NEVER echo'Failed to create or find user for client'; a createClientProfile-call-not-entered invariance walk pinning that the unauth response status must NOT be 200 and must NOT contain adatakey). Cross-references the companionadmin-clients-query.spec.ts, the leaf-[clientId]triple-methodadmin-clients-clientid-method-spec.mdcovering the same bare-envelope shape on GET / PUT / DELETE, the collection-level POST companionsadmin-items-create-body-spec.md,admin-users-create-body-spec.md,admin-categories-create-body-spec.md, andadmin-tags-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 41-of-N and thetests/api/per-spec-file sub-rollout extends to 39-of-many, and the first bare-envelope-with-get-or-create-user-side-effect collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same clients-collection route. - E2E Admin Companies Create Body Spec (
apps/web-e2e/tests/api/admin-companies-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level company-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-companies-create-body.spec.ts, the forty-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fortieth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/companies/route.ts— the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the bare{ error: 'Unauthorized' }envelope (NOsuccesskey) with a Zodparse()(NOTsafeParse()) body validation emitting adetails: [{field, message}]400 envelope AND two dynamically-interpolated 409 pre-create uniqueness checks (getCompanyByDomain/getCompanyBySlug) AND an outer-catch unique-constraint translation chain that maps DB errors to one of three 409 envelope variants. Sibling ofadmin-companies-id-method-spec.mdPUT — they share the SAME bare envelope, the SAME Zod-parse()-with-details-envelope validation chain, the SAME TWO 409 pre-create/-update uniqueness checks (with dynamically-interpolated messages), and the SAME outer-catch unique-constraint translation chain. The POST diverges from the PUT on: NO existence check (distinct from the PUT which checks the existing company FIRST),createCompany(validatedData)call instead ofupdateCompany(id, ...), and status-201 success branch with{ success: true, data: <company> }. The companionadmin-companies-query.spec.tscovers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofdata,details,successkeys must appear in any unauth response; a gate-before-post-auth invariant pinning that the five static post-auth messages plus the dynamic'Company with (domain\|slug) '<...>' already exists'409 messages (matched via regex prefix) must NEVER appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a Zod-validation-not-entered invariance walk pinning that every Zod-invalid body shape round-trips to the same 401 status with NOdetailskey; a uniqueness-check-409-not-entered invariance walk pinning that the unauth response must NEVER match the dynamic/^Company with (domain\|slug) '/regex prefixes; a createCompany-call-not-entered invariance walk pinning that the unauth response status must NOT be 201 and must NOT contain adatakey; a unique-constraint-outer-catch-not-entered invariance walk pinning that the unauth response must NEVER echo any of the three static unique-constraint translation messages). Cross-references the companionadmin-companies-query.spec.ts, the leaf-[id]triple-methodadmin-companies-id-method-spec.mdcovering the same Zod-parse()-with-details-envelope validation chain on PUT updates, the collection-level POST companionsadmin-items-create-body-spec.md,admin-users-create-body-spec.md,admin-categories-create-body-spec.md,admin-tags-create-body-spec.md, andadmin-clients-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 42-of-N and thetests/api/per-spec-file sub-rollout extends to 40-of-many, and the first bare-envelope-Zod-parse()-with-details-envelope collection-level POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same companies-collection route. - E2E Admin Collections Create Body Spec (
apps/web-e2e/tests/api/admin-collections-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-level collection-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-collections-create-body.spec.ts, the forty-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-first underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/collections/route.ts— the first POST-only collection-level admin-tree smoke the docs tree publishes that combines a per-call inlinetry { body = await request.json() } catch (jsonError) { ... }wrapper emitting an'Invalid JSON in request body'400 envelope (the FIRST collection-level admin POST route the smoke layer covers that wraps therequest.json()call in its own try/catch — every prior collection-level POST smoke uses the bareawait request.json()form) with a manual TWO-field required check (!createData.id || !createData.name→ 400'Collection ID and name are required') plus a two-revalidatePathcache-invalidation chain on the success branch (revalidatePath('/collections')PLUSrevalidatePath(\/collections/${newCollection.slug}`)slug-aware, in addition toawait invalidateContentCaches()). Sibling of [admin-categories-create-body-spec.md](./plugins/admin-categories-create-body-spec.md) — they share the SAME canonical-longer 401 envelope ({ success: false, error: 'Unauthorized. Admin access required.' }), the SAME three-branch outer catch chain ('already exists'→ 409 /'must'→ 400 /safeErrorResponse(...)fallback), and the SAME non-datasuccess-payload key (collectionhere,categorythere). The collections POST diverges from the categories POST on: per-callrequest.json()try/catch (categories uses bareawait request.json()), TWO-field required check (categories has only ONE), and TWOrevalidatePathcalls on success (categories has none). Returns{ success: true, collection:, message: 'Collection created successfully' } with status 201. The companion [admin-collections-query.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/admin-collections-query.spec.ts) covers the GET (paginated list) surface of the same route. Documents the at-a-glance scenario tree (a ~17-header bulk-loop walk + a ~15-body bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion pinning the divergence vs the bare-envelope sibling routes (admin/companies,admin/clients); a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']ANDbody.success === false; a success-branch-key non-disclosure assertion that NONE ofcollection,data,messagekeys plussuccess: trueand the 201 status must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the four static post-auth messages ('Invalid JSON in request body','Collection ID and name are required','Failed to create collection','Collection created successfully') must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a per-call-request.json-try/catch-not-entered invariance walk pinning that the'Invalid JSON in request body'400 envelope must NEVER appear on the unauth branch — distinct from prior POST smokes which use the bareawait request.json()form; a required-field-check-not-entered invariance walk; a create-call-+-cache-invalidation-not-entered invariance walk pinning that the unauth response status must NOT be 201, must NOT contain acollectionkey, and must NOT echo'Collection created successfully'— and pinning the gate-before-revalidatePath-side-effect order; a three-branch-outer-catch-not-entered invariance walk pinning that the unauth response must echo the canonical 401 envelope, not any branch of the'already exists'/'must'/safeErrorResponse(...)outer catch chain). Cross-references the companion [admin-collections-query.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/admin-collections-query.spec.ts), the leaf-[id]triple-method [admin-collections-id-method-spec.md](./plugins/admin-collections-id-method-spec.md) covering the GET / PUT / DELETE surface on the/api/admin/collections/[id]/route.tsroute (it uses the same canonical-longer 401 envelope and the samecollectionsuccess-payload key but a different validation chain — ZodsafeParse(...).error.flatten()400 envelope on PUT), the leaf-[id]/itemsdual-method [admin-collections-id-items-method-spec.md](./plugins/admin-collections-id-items-method-spec.md) covering the GET / POST surface on the/api/admin/collections/[id]/items/route.tsnested route, the closest-sibling collection-level POST companion [admin-categories-create-body-spec.md](./plugins/admin-categories-create-body-spec.md), and the other collection-level POST companions [admin-tags-create-body-spec.md](./plugins/admin-tags-create-body-spec.md), [admin-companies-create-body-spec.md](./plugins/admin-companies-create-body-spec.md), [admin-clients-create-body-spec.md](./plugins/admin-clients-create-body-spec.md), [admin-items-create-body-spec.md](./plugins/admin-items-create-body-spec.md), and [admin-users-create-body-spec.md](./plugins/admin-users-create-body-spec.md), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) and [Spec 009 -- Admin Dashboard](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/009-admin-dashboard) for the governing specs. With this entry the **per-spec-file docs rollout extends to 43-of-N** and the **tests/api/per-spec-file sub-rollout extends to 41-of-many**, and the **first per-call-request.json-try/catch + two-revalidatePath` collection-level POST admin-tree smoke** lands -- complementing the existing query-surface coverage of the same collections-collection route. - E2E Admin Roles Create Body Spec (
apps/web-e2e/tests/api/admin-roles-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's non-admin-gated collection-level role-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-roles-create-body.spec.ts, the forty-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-second underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/roles/route.ts— documenting the fifth Q-010b-style auth-gate-divergence finding in the admin-tree smoke layer: the route'sPOSThandler does NOT callauth()at all, so any unauthenticated client can create roles (including admin-flagged roles by sending{ name: 'X', description: 'Y', isAdmin: true }). The companionadmin-roles-query.spec.tsalready documents the same Q-010b finding for the GET surface. With no gate, the unauth client receives the same response an authenticated client would: 400'Missing required fields: name, description'on no body, 400'Unable to derive a valid role ID from name'on names that normalize to empty, 400'Role name must be between 3 and 100 characters'on length out-of-range, 400'Role description must be at most 500 characters'on description > 500, 409'Role with similar name already exists'on duplicate ID, or 201{ success: true, data: <role>, message: 'Role created successfully' }on valid bodies. The POST handler additionally has a stable-ID-derivation step (thenameis normalized via.normalize('NFKD'), diacritic stripping, lowercasing, and slug-style hyphen collapsing — the first POST smoke that walks a slug-derivation step), a soft-delete-aware uniqueness check (roleRepository.exists(id, { includeDeleted: true })), and an outer-catch translation that maps'already exists' \| 'unique constraint' \| 'duplicate key'to a single fixed 409 message. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a NEVER-401-or-403 invariant pinning the auth-gate-divergence finding; asuccess-key envelope-shape assertion; a per-header-permutation status-stability comparison for the same body; a side-channel walk pinning that fabricated session cookies andX-*headers do NOT escalate privilege; a cross-method probe; a malformed-JSON-body invariance walk; a required-field-check-first-validation-fires invariant pinning the route's only "protection"; a length-validation deterministic-fire invariant). Cross-references the companionadmin-roles-query.spec.ts, the leaf-[id]triple-methodadmin-roles-id-method-spec.mdwhich DOES have a two-step gate (so the gate is on the[id]sub-resources but NOT on the collection root), the dual-methodadmin-roles-id-permissions-method-spec.md, and the other Q-010b findingadmin-featured-items-id-method-spec.md, and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, anddocs/questions.md(the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 44-of-N and thetests/api/per-spec-file sub-rollout extends to 42-of-many, and the fifth Q-010b auth-gate-divergence finding lands -- documenting that the admin/roles POST endpoint is publicly accessible. - E2E Admin Notifications Create Body Spec (
apps/web-e2e/tests/api/admin-notifications-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's tenant-only-gated collection-level notification-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-notifications-create-body.spec.ts, the forty-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-third underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/notifications/route.ts— documenting the sixth Q-010b-style auth-gate-divergence finding in the admin-tree smoke layer: the route'sPOSThandler does NOT call!isAdminat any point. It DOES require an authenticated user (!session?.user?.id→ 401), so the route is tenant-scoped to authenticated users but is effectively non-admin-restricted. The POST handler combines a two-step gate (!session?.user?.id→ 401{ success: false, error: 'Unauthorized' }, then!tenantIdaftergetTenantId()AFTER body parse + required-fields check → 403{ success: false, error: 'Tenant not found' }) — distinct from prior two-step gates which rungetTenantId()BEFORE body parse — this route's tenant resolution is INTERLEAVED with body validation. Hybrid bare-Unauthorized+success: falseenvelope (matchingadmin/users/[id],admin/featured-items/[id],admin/roles/[id]/permissions). Four-field required check (type,title,message,userId) → 400'Missing required fields'BEFORE tenant resolution.getTenantId()AFTER required-fields check — the first collection-level POST smoke that runs the tenant-resolution check AFTER body validation. Inline Drizzle insert withnotificationsschema + JSON-stringifieddatafield — distinct from prior POST smokes which delegate to a repository class. Success payload withnotificationsuccess-key (NOTdata) —{ success: true, notification: <newNotification[0]> }with status 200 (NOT 201).console.error+ 500'Internal server error'catch — distinct fromsafeErrorResponse(...)-using POST smokes. The companionadmin-notifications-query.spec.tscovers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~16-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a hybrid 401-envelope assertion; a strict envelope-shape assertion; an unauth-lands-on-401-not-403 invariant; a success-branch-key non-disclosure assertion that NONE ofnotification,data,success: truekeys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-fields-check-not-entered invariance walk; a tenant-resolution-check-not-entered invariance walk pinning that the unauth response must NEVER echo'Tenant not found'; a Drizzle-insert-not-entered invariance walk pinning that the unauth response must NEVER echo anotificationkey from the inserted row). Cross-references the companionadmin-notifications-query.spec.ts, the leaf-[id]PATCHadmin-notifications-id-read-method-spec.md, the static-path PATCHadmin-notifications-mark-all-read-method-spec.md, and the other Q-010b findingsadmin-roles-create-body-spec.md(no auth at all) andadmin-featured-items-id-method-spec.md(also tenant-only-gated, but on a leaf-[id]route), and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, anddocs/questions.md(the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 45-of-N and thetests/api/per-spec-file sub-rollout extends to 43-of-many, and the sixth Q-010b auth-gate-divergence finding lands -- documenting that the admin/notifications POST endpoint is tenant-scoped but NOT admin-gated. - E2E Admin Featured Items Create Body Spec (
apps/web-e2e/tests/api/admin-featured-items-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's non-admin-gated collection-level featured-items-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-featured-items-create-body.spec.ts, the forty-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-fourth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/featured-items/route.ts— documenting the seventh Q-010b-style auth-gate-divergence finding in the admin-tree smoke layer. The route'sPOSThandler does NOT call!isAdminat any point. It DOES require an authenticated user (!session?.user?.id→ 401) and a tenant (!tenantId→ 403 BEFORE body parse — a "tenant-first" two-step gate ordering distinct fromadmin/notificationsPOST which runsgetTenantId()AFTER body parse). The POST handler runs a two-field required check (!itemSlug || !itemName→ 400'Item slug and name are required'), an already-featured check via inline DrizzleselectfromfeaturedItemswitheq(itemSlug)+eq(isActive, true)+ tenant scoping → 400'Item is already featured'if a row exists (the first POST smoke that uses a 400 -- not 409 -- for an already-exists check), then an inline Drizzle insert withfeaturedUntilparsed asnew Date()if provided,featuredBy = session.user.id,featuredOrderdefaults to0via destructure default. Returns{ success: true, data: <featuredItem>, message: 'Item featured successfully' }with status 200. Outer catch isconsole.error+ 500'Failed to create featured item'. Documents the at-a-glance scenario tree (a ~16-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a hybrid 401-envelope assertion; a strict envelope-shape assertion; an unauth-lands-on-401-not-403 invariant; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the five static post-auth messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-fields-check-not-entered invariance walk; an already-featured-check-not-entered invariance walk pinning that the unauth response must NEVER echo'Item is already featured'; a Drizzle-insert-not-entered invariance walk). Cross-references the leaf-[id]triple-methodadmin-featured-items-id-method-spec.mdcovering the same tenant-only-gated route on GET / PUT / DELETE, the other Q-010b findingsadmin-roles-create-body-spec.md(no auth at all) andadmin-notifications-create-body-spec.md(also tenant-only-gated, but with interleaved tenant resolution), and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, anddocs/questions.md(the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 46-of-N and thetests/api/per-spec-file sub-rollout extends to 44-of-many, and the seventh Q-010b auth-gate-divergence finding lands -- documenting that the admin/featured-items POST endpoint is tenant-scoped but NOT admin-gated, with a tenant-first two-step gate ordering distinct from the sibling notifications POST. - E2E Admin Categories Git Create Body Spec (
apps/web-e2e/tests/api/admin-categories-git-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-CMS-write category-create POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-categories-git-create-body.spec.ts, the forty-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-fifth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/categories/git/route.ts— the first POST-only Git-CMS-write admin-tree smoke the docs tree publishes (distinct from the regularadmin/categoriesPOST which writes to the DB; this Git POST commits a new category file to the configuredDATA_REPOSITORYGitHub repository viacreateCategoryGitService). The POST handler combines a single-step inline!session?.user?.isAdmingate that returns 401{ error: 'Unauthorized. Admin access required.' }— NOTE: canonical longer message but WITHOUTsuccess: falseenvelope key, a UNIQUE envelope shape that mixes the canonical longer message ofadmin/items/[id]etc. WITH the bare envelope ofadmin/clients/[clientId]/admin/companies/[id]etc. (no other admin-tree route combines these two). The POST handler runs a two-field required check (!id || !name→ 400{ success: false, error: 'Category ID and name are required' }— NOTE: includessuccess: falsekey, distinct from the 401 envelope which lacks it), then a DATA_REPOSITORY env-var validation chain (missing → 500'DATA_REPOSITORY not configured. Please set DATA_REPOSITORY environment variable.', malformed → 500'Invalid DATA_REPOSITORY format. Expected: https://github.com/owner/repo'), then a GH_TOKEN env-var validation (missing → 500'GitHub token not configured. Please set GH_TOKEN environment variable.'), thencreateCategoryGitService(gitConfig).createCategory({ id, name })— the load-bearing Git-service call. Returns{ success: true, category: <newCategory>, message: 'Category created and committed to Git repository' }with status 200 (NOT 201). Outer catch is two-branch:error.message.includes('already exists')→ 409 echoing rawerror.message, elsesafeErrorResponse(error, 'Failed to create category via Git'). The companionadmin-categories-git-query.spec.tscovers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~12-body bulk-loop walk all asserting< 500; a canonical-longer-bare-envelope 401 assertion; a strict envelope-shape assertionObject.keys(body) === ['error']withbody.successundefined; a success-branch-key non-disclosure assertion that NONE ofcategory,data,success,messagekeys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the six static post-auth messages (including the three env-var validation messages) must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-field-check-not-entered invariance walk; an env-var-validation-chain-not-entered invariance walk pinning that the unauth response must NEVER echo any of the three env-var error messages; a Git-service-call-not-entered invariance walk pinning that the unauth response must NEVER echo acategorykey from the Git-committed payload). Cross-references the companionadmin-categories-git-query.spec.tsand the DB-write companionadmin-categories-create-body-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 47-of-N and thetests/api/per-spec-file sub-rollout extends to 45-of-many, and the first Git-CMS-write POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same Git-CMS route. - E2E Admin Settings Update Method Spec (
apps/web-e2e/tests/api/admin-settings-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin settings-update PATCH body / header smoke spec paired withapps/web-e2e/tests/api/admin-settings-update-method.spec.ts, the forty-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-sixth underapps/web-e2e/tests/api/. Pairs with thePATCHexport ofapps/web/app/api/admin/settings/route.ts— the first PATCH-only collection-level config-write admin-tree smoke the docs tree publishes. It is also the first admin-tree smoke that usesgetCachedApiSession(req)instead ofauth()— a cached-session-lookup variant. The PATCH handler combines a single-step!session?.user?.isAdmingate that returns 401{ error: 'Unauthorized' }(BARE envelope, NOsuccesskey, SHORT message), a JSON body parse, a single-field required check (if (!key)→ 400{ error: 'Key is required' }),configManager.updateNestedKey('settings.${key}', value)for the load-bearing works.yml write, an update-failed branch (500'Failed to update setting'if falsy), success payload{ success: true, key, value }with status 200 (UNIQUE: echoes the inputkeyandvalue), and outer catchconsole.error+ 500'Failed to update settings'. The companionadmin-settings-query.spec.tscovers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~16-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertionObject.keys(body) === ['error']; a success-branch-key non-disclosure assertion that NONE ofsuccess,key,valuekeys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a required-key-check-not-entered invariance walk; a configManager-update-not-entered invariance walk pinning that the unauth response must NEVER echo akeyorvaluefrom the input). Cross-references the companionadmin-settings-query.spec.tsand the map-status sub-routeadmin-settings-map-status-query.spec.ts, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 48-of-N and thetests/api/per-spec-file sub-rollout extends to 46-of-many, and the first cached-session-lookup config-write PATCH admin-tree smoke lands -- complementing the existing query-surface coverage of the same settings-collection route. - E2E Admin Categories Git Query Spec (
apps/web-e2e/tests/api/admin-categories-git-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-repository-status / categories GET smoke spec paired withapps/web-e2e/tests/api/admin-categories-git-query.spec.ts, the fiftieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-eighth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/admin/categories/git/route.ts-- the GET-companion of the recently-landedadmin-categories-git-create-body-spec.md(POST). Where the POST handler commits a new category file to the configuredDATA_REPOSITORYGitHub repository, the GET handler reads Git repository status and categories via the GitHub API. The route combines a unique combination of FOUR distinct contracts: (1) a zero-argumentGET()handler signature that does not take aNextRequestargument and reads nosearchParamsat all (same posture as the notifications route), (2) a bare{ error: 'Unauthorized. Admin access required.' }envelope without thesuccessdiscriminant key -- the ONLY admin-tree GET route that combines the bare-envelope shape with the canonical longer role-context-specific message, (3) a GitHub-API-backed service viacreateCategoryGitService(gitConfig)that makes live HTTPS calls to the GitHub API using the configuredGITHUB_TOKEN/DATA_REPOSITORYenvironment variables -- distinct from every other admin-tree route's drizzle / DB posture and from the file-system Git-CMS reader of thecategories/allandtags/allroutes, and (4) three distinct configuration-error 500 envelopes after the gate (canonical envelope withsuccess: false, NOT bare -- a deliberate inconsistency between the unauth-branch and post-auth configuration-error branches). Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting< 500; a bare 401-envelope assertion with the canonical longer message; a strict envelope-shape assertionbody.success === undefined; a negative-message assertionbody.error !== 'Unauthorized'andbody.error !== 'Forbidden'; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for?userId=/?token=/?bypass=/?repo=&branch=&owner=/?path=key families; side-channel walks forAcceptheader / cookie / IP headers; a gate-before-config-validation invariant pinning that the three configuration-error 500 envelopes (DATA_REPOSITORYnot set, invalid format,GITHUB_TOKENnot set) must NEVER fire on the unauth branch; a gate-before-Git-service invariant pinning that thecreateCategoryGitService(gitConfig)GitHub-API service must NEVER be entered on the unauth branch). Cross-references the POST companionadmin-categories-git-create-body-spec.md, the DB-backed siblingadmin-categories-query.spec.ts, the Git-CMS file-system siblingadmin-categories-all-query.spec.ts, the Git-CMS file-system siblingadmin-tags-all-query-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 50-of-N and thetests/api/per-spec-file sub-rollout extends to 48-of-many, and the first GitHub-API-backed admin-tree GET smoke lands -- complementing the existing POST coverage of the same Git-CMS route. - E2E Sponsor Ads User Method Spec (
apps/web-e2e/tests/api/sponsor-ads-user-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-scoped sponsor-ads GET + POST body / header smoke spec paired withapps/web-e2e/tests/api/sponsor-ads-user-method.spec.ts, the one-hundred-and-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-third underapps/web-e2e/tests/api/. Pairs with theGETANDPOSTexports ofapps/web/app/api/sponsor-ads/user/route.ts— the first per-source-file dual-method smoke the docs tree publishes that pins Zod-safeParsevalidation on BOTH a query-parameter surface AND a body surface (GET validates query viaquerySponsorAdsSchema.safeParse; POST validates body viacreateSponsorAdSchema.safeParse). UNIQUE: the FIRST per-source-file dual-method smoke pinning Zod schema validation across both query and body. Distinct from EVERY prior dual-method smoke: Zod-safeParse on BOTH query AND body (UNIQUE); dynamic environment-based payment provider (ACTIVE_PAYMENT_PROVIDER = process.env.NEXT_PUBLIC_PAYMENT_PROVIDER || PaymentProvider.STRIPEis a module-level constant -- UNIQUE: FIRST per-source-file dual-method smoke pinning a module-level env-based provider constant where the handler ALWAYS uses this provider regardless of body); POST returns 201 status (NOT 200 -- UNIQUE among sponsor-ads POST smokes); POST 400 for invalid JSON with'Invalid JSON in request body'distinct from body-validation 400 (FIRST per-source-file POST smoke pinning a try/catch aroundawait request.json()with a distinct message); conditional already-exists 400 catch branch --error.message.includes('already have')→ 400'You already have an active sponsor ad'(UNIQUE: FIRST per-source-file POST smoke pinning a message-substring catch dispatcher with a status override viasafeErrorResponse(error, message, 400)); pagination success payload on GET withhasNext/hasPrevcomputed booleans (UNIQUE: FIRST per-source-file GET smoke pinning a hasNext/hasPrev computed-pagination contract); approval-workflow success message on POST --'Sponsor ad submission created successfully. Pending admin approval.'(UNIQUE: FIRST per-source-file POST smoke pinning an approval-workflow success message); TWO-key 401 envelope{ success: false, error: 'Unauthorized' }on both methods. The handlers combine: GET handler withauth()session lookup,searchParamsextraction, build queryParams withuserId: session.user.id,querySponsorAdsSchema.safeParse(queryParams),getSponsorAdsPaginated(...)load-bearing DB read, success returns paginated payload; POST handler withauth(), JSON body parse with try/catch,createSponsorAdSchema.safeParse({ ...body, paymentProvider: ACTIVE_PAYMENT_PROVIDER }),createSponsorAd(userId, validated)load-bearing DB write, success returns 201 with{ data, message }, conditional 400 catch-branch on'already have'message substring; method-resolution surface where the route exports GET + POST (PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~6 headers × 2 methods + ~8 POST bodies all asserting< 500; canonical TWO-key 401-envelope assertions on GET AND POST; a cross-method 401-envelope-equality assertion; a strict TWO-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear including the conditional already-exists 400 message; a createSponsorAd-not-entered invariance walk on POST -- CRITICAL: pinning that XSS markers in the body are NEVER echoed back; a gate-before-Zod-query-validation invariance walk on GET pinning that invalid query values still produce 401 NOT 400; a gate-before-body-parse-and-Zod-body-validation invariance walk on POST pinning that malformed JSON / invalid body all produce 401 NOT 400; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk on POST). Cross-references the companion sponsor-ads checkout siblingsponsor-ads-checkout-body-spec.md, the companion sponsor-ads cancel siblingsponsor-ads-user-id-cancel-body-spec.md, the companion sponsor-ads renew siblingsponsor-ads-user-id-renew-body-spec.md, the companion public-sponsor-ads specsponsor-ads-public.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 105-of-N and thetests/api/per-spec-file sub-rollout extends to 103-of-many, and the first per-source-file dual-method smoke pinning Zod-safeParsevalidation across BOTH query AND body lands -- pinning a module-level env-based provider constant contract, a message-substring catch dispatcher with status override, a hasNext/hasPrev computed-pagination contract, and an approval-workflow success message that no prior dual-method smoke covers. - E2E Client Items [id] Method Spec (
apps/web-e2e/tests/api/client-items-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client per-id item GET + PUT + DELETE dynamic-segment / body / header smoke spec paired withapps/web-e2e/tests/api/client-items-id-method.spec.ts, the one-hundred-and-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-seventh underapps/web-e2e/tests/api/. Pairs with theGET,PUT, ANDDELETEexports ofapps/web/app/api/client/items/[id]/route.ts— the first per-source-file triple-method smoke the docs tree publishes that pins FIVE distinct helper imports from the@/lib/utils/client-authutility module (requireClientAuth+serverErrorResponse+notFoundResponse+forbiddenResponse+badRequestResponse) on a single source file. UNIQUE: every prior per-source-file smoke imports between 1 and 3 helpers from this utility; this is the FIRST to pin a 5-helper-import contract from a single utility module. Extends theclient-items-method-spec.mdsibling (which pins therequireClientAuthhelper on the COLLECTION-level GET + POST surface) into the PER-ID GET + PUT + DELETE dynamic-segment surface. Distinct from EVERY prior triple-method smoke: 5-helper-import contract (UNIQUE -- FIRST per-source-file smoke pinning a 5-helper import from a single utility);itemIdParamSchema.safeParse({ id })Zod validation on a path param (UNIQUE: FIRST per-source-file triple-method smoke pinning Zod validation on a dynamic-segment parameter vs query / body); GET success payload withengagementsub-object --{ success, item, engagement: { views, likes } }(UNIQUE: FIRST per-source-file GET smoke pinning a nested engagement-metrics sub-object derived from the item entity viaviews ?? 0,likes ?? 0); PUT empty-update guard --Object.keys(updateData).length === 0→ 400'No fields to update'(UNIQUE: FIRST per-source-file PUT smoke pinning a no-op-update guard that triggers AFTER successful Zod validation but BEFORE the repository write); PUTstatusChangeddynamic message -- success message changes based onresult.statusChanged('Item updated successfully'vs'Item updated successfully. Since it was previously approved, it has been moved to pending for re-review.'-- UNIQUE: FIRST per-source-file PUT smoke pinning a dynamic-by-result-flag success message); PUT FOUR-branch nested catch dispatcher --error.message === 'Item not found'→ 404,error.message.includes('permission')→ 403,error.message.includes('deleted')→ 400, default → outerserverErrorResponse(UNIQUE: FIRST per-source-file PUT smoke pinning a four-branch message-substring catch dispatcher layered inside an outer try/catch); DELETE THREE-branch nested catch dispatcher --error.message === 'Item not found'→ 404,error.message.includes('permission')→ 403,error.message.includes('already deleted')→ 400 (UNIQUE: FIRST per-source-file DELETE smoke pinning a three-branch message-substring catch dispatcher);'Unauthorized. Please sign in to continue.'longer-message TWO-key envelope (matchesclient-items-method-spec.mdandclient-items-stats-query-spec.md);notFoundResponse(message)404-helper +forbiddenResponse(message)403-helper -- UNIQUE: FIRST per-source-file smoke pinning dedicated builder helpers for 404 and 403 responses (vs rawNextResponse.json(..., { status: 404 })). The handlers combine: GET handler withrequireClientAuth(),itemIdParamSchema.safeParse({ id }),clientItemRepository.findByIdForUser(id, userId)ownership-checked load,!item→notFoundResponse('Item not found or you do not have permission to view it'), success returns{ success, item, engagement: { views, likes } }, outer catchserverErrorResponse(error, 'Failed to fetch item'); PUT handler withrequireClientAuth(), param Zod,clientUpdateItemSchema.safeParse(body)→ 400 with issues-joined message on failure, empty-update guard → 400,clientItemRepository.updateAsClient(id, userId, updateData)load-bearing DB write, success returns{ success, item, statusChanged, previousStatus, message }with dynamic message based onstatusChanged, FOUR-branch nested catch then outerserverErrorResponse(error, 'Failed to update item'); DELETE handler withrequireClientAuth(), param Zod,clientItemRepository.softDeleteForUser(id, userId)load-bearing DB write, success returns{ success, message: 'Item deleted successfully' }, THREE-branch nested catch then outerserverErrorResponse(error, 'Failed to delete item'); method-resolution surface where the route exports GET + PUT + DELETE (POST / PATCH must round-trip to< 500). Documents the at-a-glance scenario tree (four bulk-loop walks -- ~6 headers × 3 methods + ~7 PUT bodies all asserting< 500; longer-message TWO-key 401-envelope assertions on GET, PUT, AND DELETE; a cross-method 401-envelope-equality assertion across all three methods; a strict TWO-key envelope-shape assertion with noitemorengagementleak; a gate-before-post-auth invariant pinning that NONE of EIGHT candidate post-auth messages -- spanning GET / PUT / DELETE branches -- must appear; an updateAsClient-not-entered invariance walk on PUT -- CRITICAL: pinning that XSS markers in the PUT body are NEVER echoed back; a softDeleteForUser-not-entered invariance walk on DELETE -- CRITICAL: pinning that the URL itemId marker is NEVER echoed back; a gate-before-FOUR-branch-catch invariance walk on PUT pinning that NONE of'Item not found'/'permission'/'deleted'leak; a gate-before-Zod-body-validation invariance walk on PUT pinning that invalid body shapes still produce 401 NOT 400; a cross-method probe (POST / PATCH); a side-channel walk on PUT; a cross-id invariance walk pinning that the auth gate fires BEFORE any per-id branch -- thenotFoundResponse/forbiddenResponsepaths are unreachable on unauth). Cross-references the companion client-items collection siblingclient-items-method-spec.md(pins therequireClientAuthhelper on the COLLECTION-level GET + POST surface; this spec extends it into the PER-ID dynamic-segment surface), the companion client-items-stats siblingclient-items-stats-query-spec.md(uses the samerequireClientAuth()helper on a single GET surface for the stats endpoint), the companion client-protected siblingclient-protected.spec.ts, the admin per-id siblingadmin-items-id-method-spec.md(admin-gated GET + PUT + DELETE surface for the same item entity -- admin-scoped instead of client-scoped), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 109-of-N and thetests/api/per-spec-file sub-rollout extends to 107-of-many, and the first per-source-file triple-method smoke pinning a 5-helper-import contract from a single utility lands -- pinning a Zod-on-path-param contract, an engagement-metrics nested sub-object GET payload, an empty-update guard, a dynamic-by-result-flag PUT success message, a FOUR-branch PUT catch dispatcher, a THREE-branch DELETE catch dispatcher, and dedicatednotFoundResponse/forbiddenResponsebuilder helpers that no prior triple-method smoke covers. - E2E Collections Exists Query Spec (
apps/web-e2e/tests/api/collections-exists-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public collections-existence probe query-param smoke spec paired withapps/web-e2e/tests/api/collections-exists-query.spec.ts. Pairs with theGETexport ofapps/web/app/api/collections/exists/route.ts-- the second member of the public-existence-probe trio to receive a per-source-file landing page (after the previously-landedcategories-exists-query-spec.mdGit-CMS catch-and-200 sibling and the previously-landedsurveys-exists-query-spec.mdDB-service catch-and-200 sibling). With this entry the three-member existence-probe trio is now fully documented per-source-file. The collections-exists route is uniquely the catch-and-500 member of the trio: every thrown error insidecollectionRepository.findAllis caught and the route returns a500status with the extraerror: 'Failed to check collections existence'field -- distinct from both siblings whose catch branches return200 OK. Distinct from every other public-route per-source-file GET smoke the docs tree has published to date -- the route reads zero query parameters today (the_requestparameter is underscored to mark it deliberately unused), runs above the DB-repository backing store viacollectionRepository.findAll(NOT a Git-CMS reader and NOT a service-layer wrapper), returns the{ exists, count }envelope on the happy path AND the{ exists: false, count: 0, error: 'Failed to check collections existence' }envelope on the catch path, and logs every catch-branch error toconsole.errorunconditionally (NOT only in development mode like the categories-exists sibling, and NOT silently like the surveys-exists sibling). The handler combines: no auth gate (intentionally public); zero query-param read (the_requestparameter is underscored to mark it deliberately unused -- UNIQUE within the existence-probe trio); hard-coded{ includeInactive: false }repository flag (load-bearing -- a future contributor who wires?includeInactive=trueinto the call would also need to flip the response envelope shape or add a separateinactiveCountfield); DB-repository-backedcollectionRepository.findAllread (distinct from the categories-exists sibling's Git-CMSfetchItemsreader and from the surveys-exists sibling'ssurveyService.getManyservice-layer reader -- the collections-exists route talks directly to a DB repository without a service wrapper); happy-path success payload{ exists: <bool>, count: <number> }with status200; catch-and-500fallback returns{ exists: false, count: 0, error: 'Failed to check collections existence' }with status500(UNIQUE within the public-existence trio); unconditionalconsole.errorlogging (the catch branch fires on every environment);GET(_request: NextRequest)Next-specific handler signature with the underscored parameter (UNIQUE within the public-existence trio: the only handler whose parameter is underscored to mark it deliberately unused). Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting>= 200 && < 600covering?locale=permutations across all eight illustrated locales, i18n aliases, cache-busting, strict / validate flags, content projection,?includeInactive=repository-flag flips, content negotiation, status / active filters, multi-tenancy, empty values, repeated keys, special-character payloads, long values, and bogus / typo'd keys; a canonical{ exists: boolean, count: number }envelope assertion viaexpect([200, 500]).toContain(...); a parameterised-vs-baseline status-stability comparison; a three-way shape-stability walk; a UNIQUE zero-query-input contract walk pinning that the explicit-locale, empty-locale, and absent-locale cases all return the same status because the route reads no query input; a UNIQUE?includeInactive=invariance walk pinning that the hard-codedincludeInactive: falserepository flag is NOT flipped by a caller-supplied?includeInactive=truequery param). Cross-references the catch-and-200 Git-CMS-backed siblingcategories-exists-query-spec.md, the catch-and-200 DB-service-backed siblingsurveys-exists-query-spec.md, the cross-cuttingfeature-existence.spec.ts(also probesGET /api/collections/existsBUT only the no-arg baseline), the DB-backed admin sibling at/api/admin/collections(covered byadmin-collections-query.spec.ts), the collection-detail GET / PUT / DELETE siblingadmin-collections-id-method-spec.md, the collection-create POST siblingadmin-collections-create-body-spec.md, and to Spec 010 -- End-to-End Test Coverage for the governing spec. With this entry the public-existence-probe trio is now fully documented per-source-file (categories-exists Git-CMS catch-and-200 + surveys-exists DB-service catch-and-200 + collections-exists DB-repo catch-and-500), and the first per-source-file GET smoke pinning a catch-and-500 DB-repository-backed public existence probe lands -- pinning a zero-query-input contract, a hard-codedincludeInactive: falseflag invariance, an unconditionalconsole.errorlogging contract, and a generic-error-message-with-no-information-disclosure contract that no prior public-existence-probe smoke covers. - E2E Surveys Exists Query Spec (
apps/web-e2e/tests/api/surveys-exists-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public surveys-existence probe query-param smoke spec paired withapps/web-e2e/tests/api/surveys-exists-query.spec.ts. Pairs with theGETexport ofapps/web/app/api/surveys/exists/route.ts-- the third member of the public-existence-probe trio (after thecategories-exists-query-spec.mdcatch-and-200 Git-CMS sibling and the still-undocumented DB-backedcollections-exists-query.spec.tscatch-and-500 sibling) -- the first per-source-file GET smoke the docs tree publishes that pins a DB-backed public existence probe whose catch branch ALSO returns200 OKAND whose response envelope is the leaner{ exists }shape with NOcountfield. Distinct from every other public-route per-source-file GET smoke the docs tree has published to date -- both the categories-exists and the collections-exists siblings live above the Git-CMS / DB-repository backing stores and emit the{ exists, count }envelope; this route lives above a DB-backedsurveyService.getManythat selects published surveys from the configured database, returns the leaner{ exists }shape (since thelimit: 1short-circuit makes the count uninformative anyway), and is the catch-and-no-count sibling of the categories-exists route -- same catch-and-200 posture but lean envelope. The handler combines: no auth gate (intentionally public);?type=query-param read viasearchParams.get('type')(the route reads exactly ONE query param; every other key is silently ignored -- distinct from the categories-exists sibling which reads?locale=and from the collections-exists sibling which reads zero query params via the underscored_requestparameter); strict byte-for-byte type coerciontypeParam === SurveyTypeEnum.ITEM ? SurveyTypeEnum.ITEM : SurveyTypeEnum.GLOBAL(every non-'item'value --nullfor the absent key,''for the empty value,'global'for the explicit value, every typo / unknown / case-variant -- maps to the same GLOBAL branch); DB-backedsurveyService.getMany({ type, status: PUBLISHED, limit: 1 })read with the load-bearinglimit: 1short-circuit (the route only needs to know whether AT LEAST ONE published survey exists for the requested type); happy-path success payload{ exists: <bool> }with status200(existscomputed as(result.surveys?.length || 0) > 0); catch-and-empty fallback returns{ exists: false }with status200(NOT500) -- same catch-and-200 posture as the categories-exists sibling; no development-mode logging (the catch branch is silent on every environment -- distinct from the categories-exists sibling which logs in development mode and from the collections-exists sibling which logs unconditionally);GET(request: NextRequest)Next-specific handler signature withnew URL(request.url)rather thanrequest?.nextUrl?.searchParams?.get(...)optional-chaining triple. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting< 500covering?type=permutations acrossitem/global, case-variants (ITEM/Item/iTeM/GLOBAL/Global), unknown values (location/tag/category/unknown/null), empty / whitespace values (?type=/?type=%20), future filter keys (?status=/?published=), pagination keys (?limit=), i18n keys (?locale=/?lang=), cache-busting (?refresh=/?force=/?fresh=/?nocache=), strict / validate flags, content projection (?include=/?fields=/?select=/?expand=), content negotiation (?format=), multi-tenancy (?tenant=/?tenantId=), combined keys, repeated keys, special-character payloads, long values, and bogus / typo'd keys; a canonical leaner{ exists: boolean }envelope assertion; a parameterised-vs-baseline status-stability comparison; a three-way shape-stability walk; a UNIQUEtypeParam === SurveyTypeEnum.ITEMfallback-semantics walk pinning that the no-arg, the explicit?type=global, the unknown?type=unknown, the case-variant?type=ITEM, and the empty?type=paths all land in the same GLOBAL branch and return the same status; a branch-split shape-invariance walk pinning that the ITEM branch (?type=item) and the GLOBAL branch (no-arg) land in differentsurveyService.getManycalls but both return the same{ exists }envelope and both return200). Cross-references the catch-and-200 Git-CMS-backed siblingcategories-exists-query-spec.md(same catch-and-200 posture but distinct query-param surface (?locale=vs?type=) and distinct envelope shape ({ exists, count }vs{ exists })), the catch-and-500 DB-backed siblingcollections-exists-query.spec.ts, the cross-cuttingfeature-existence.spec.ts(also probesGET /api/surveys/existsBUT only the no-arg baseline; this per-source-file spec adds the query-param surface so a regression that introduces a?status=filter, a?lang=filter, a?refresh=cache-bust, or a non-200 status on an unknown?type=value is caught immediately as a status divergence between the no-arg and parameter-laden branches), the DB-backed siblingsurveys-id-method-spec.md, the DB-backed siblingsurveys-id-responses-method-spec.md, the DB-backed siblingsurveys-responses-id-query-spec.md, the public-route per-source-fileitems-popularity-scores-query-spec.md, Spec 010 -- End-to-End Test Coverage, and Spec 005 -- Internationalisation (the navigation shell that consumes all three existence probes inherits the locale-fallback semantics, even though this route does NOT read?locale=itself). - E2E Surveys [surveyId] Responses Method Spec (
apps/web-e2e/tests/api/surveys-id-responses-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-survey-responses GET / POST dynamic-segment / body / header smoke spec paired withapps/web-e2e/tests/api/surveys-id-responses-method.spec.ts, the one-hundred-and-twelfth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-tenth underapps/web-e2e/tests/api/. Pairs with theGETandPOSTexports ofapps/web/app/api/surveys/[surveyId]/responses/route.ts-- the first per-source-file dual-method smoke the docs tree publishes that pins a SPLIT-auth gate contract on a single per-source-file route —GETis admin-gated (returns401 'Unauthorized'for non-admin callers) whilePOSTis PUBLIC (any caller may submit a response, with optional session capture for theuserIdfield). Distinct from the siblingsurveys/[surveyId]/route.tsMIXED-auth gate (public-GET + admin-PUT + admin-DELETE) covered bysurveys-id-method-spec.md. Distinct from EVERY prior dual-method smoke: SPLIT-auth gate (UNIQUE: FIRST per-source-file dual-method smoke pinning an admin-GET + public-POST contract on the SAME dynamic-segment route -- most dual-method siblings either gate both methods or leave both public); POST is public + 404-survey-existence guard -- the POST handler does NOT callauth()for the gate; it callssurveyService.getOne(surveyId)after body validation and returns 404'Survey not found'if the survey does not exist (UNIQUE: FIRST per-source-file POST smoke pinning a 404-existence guard BEFORE submission rather than as a 401 gate);body.dataJSON-object guard -- POST requiresbody.datato be a non-null object, 400'Invalid request body: "data" is required'otherwise (UNIQUE: a manualtypeof body.data === 'object' && body.data != nullguard, NOT a ZodsafeParse); IP / user-agent header capture -- POST capturesx-forwarded-for(first comma-segment), falls back tox-real-ip, then to'unknown'; capturesuser-agentwith'unknown'fallback (UNIQUE: FIRST per-source-file POST smoke pinning an IP / user-agent header-capture contract);itemIdsourced from the SURVEY -- the POST handler setsresponseData.itemId = survey.itemId(NOTbody.itemId) -- UNIQUE: the handler IGNORES any caller-provideditemIdand sources it from the survey row; 201 Created on success POST (UNIQUE: FIRST per-source-file POST smoke pinning a201success status);{ success: true, data: <responses> }GET payload + paginated filter shape -- GET acceptsitemId/userId/startDate/endDate/page/limitquery parameters with a strict/^\d+$/regex onpage/limit(anything else falls back toundefined); distinct catch-helper messages --safeErrorResponse(error, 'Failed to fetch responses')outer-catch on GET vssafeErrorResponse(error, 'Failed to submit response')outer-catch on POST. The handlers combine: GET handler with outer try/catch aroundauth()session lookup (!session?.user?.isAdmin→ 401 TWO-key envelope), query-param parsing with/^\d+$/regex onpage/limit,surveyService.getResponses(surveyId, filters)load-bearing service call, success returns{ success: true, data: <responses> }, outer catchsafeErrorResponse(error, 'Failed to fetch responses'); POST handler with outer try/catch around JSON body parse,body.dataJSON-object guard 400,surveyService.getOne(surveyId)existence guard 404, OPTIONALauth()session capture for theuserIdfield (NOT a gate -- public callers submit anonymously), IP / user-agent header capture,surveyService.submitResponse(responseData)load-bearing service call, success returns{ success: true, data: <response>, message: 'Response submitted successfully' }with status 201, outer catchsafeErrorResponse(error, 'Failed to submit response'); method-resolution surface where the route exportsGETANDPOST(PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~9 headers × 2 methods + ~7 query-string permutations + ~10 POST bodies all asserting< 500; canonical TWO-key 401 envelope on GET; canonical TWO-key 404 envelope on POST; canonical TWO-key 400 envelope on POST; strict TWO-key envelope-shape preservation across every error envelope; gate-before-post-auth invariant on GET; validation-before-existence invariant on POST; gate-before-service-delegation invariant on GET (CRITICAL); survey-existence-before-service-delegation invariant on POST (CRITICAL); cross-method probe (PUT / PATCH / DELETE); side-channel walk on both GET and POST; IP / user-agent header isolation on the 404 branch; SPLIT-auth distinct-envelope contract pinning that GET 401 and POST 404 envelopes are NOT byte-identical). Cross-references the companion survey detail siblingsurveys-id-method-spec.md, the companion per-response siblingsurveys-responses-id-query-spec.md, the companion survey collection siblingsurveys.spec.ts, the companion survey-existence siblingsurveys-exists-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 112-of-N and thetests/api/per-spec-file sub-rollout extends to 110-of-many, and the first per-source-file dual-method smoke pinning a SPLIT-auth gate contract (admin-GET + public-POST) lands -- pinning a 404-survey-existence guard, abody.dataJSON-object guard, an IP / user-agent header capture, a survey-deriveditemIdcontract, a201 Createdsuccess status, and distinct GET vs POST catch-helper messages that no prior dual-method smoke covers. - E2E Client Items Import Sample Query Spec (
apps/web-e2e/tests/api/client-items-import-sample-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped sample-template GET / query-param / header smoke spec paired withapps/web-e2e/tests/api/client-items-import-sample-query.spec.ts, the one-hundred-and-fourteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-twelfth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/client/items/import/sample/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins arequireClientAuth()-gated binary-stream sample-template handler that delegates toItemExportService.generateSampleCSV()/generateSampleXLSX(). UNIQUE: every prior per-source-fileclient-items*GET smoke (client-items-method,client-items-id-method,client-items-stats-query) returns a JSON envelope; this is the FIRSTrequireClientAuth()-gated GET smoke that returns a binary stream with aContent-Disposition: attachment; filename="..."header on the happy path. It also pins theexportQuerySchema.parse(...)Zod-enum query parse AFTER the gate (matches the admin siblingadmin/items/export/sampleschema BUT with the longer-message client-auth envelope on the unauth branch), thesafeErrorResponse(error, 'Failed to generate sample template')outer-catch helper (BYTE-IDENTICALLY matches the admin sibling 500-message), thenew ItemExportService()direct-instantiation pattern with the per-formatgenerateSampleCSV()/generateSampleXLSX()service entry points, the binary-stream success contract with aContent-Disposition: attachment; filename="..."header (UNIQUE: every priorclient-items*GET smoke pins a JSON envelope), and'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope (matches the priorclient-items*siblings). Distinct from EVERY prior per-source-file GET smoke:requireClientAuth()+exportQuerySchemapair (UNIQUE: FIRST per-source-file GET smoke that gates a ZodexportQuerySchema.parse(...)query parse with therequireClientAuthdiscriminated-union helper -- the siblingclient-items-method/client-items-id-method/client-items-stats-queryparse no query schema; the admin siblingadmin-items-export-sample-queryuses the SAMEexportQuerySchemabut gates with bareauth()+session.user.isAdmininstead ofrequireClientAuth()); binary-stream success contract (UNIQUE: FIRSTrequireClientAuth()-gated GET smoke pinning aNextResponse(new Uint8Array(…), { headers: { 'Content-Type': …, 'Content-Disposition': 'attachment; filename="…"' } })binary-stream success contract -- distinct from JSON-envelope shape every priorclient-items*GET smoke pins; the unauth branch is invariant to this distinction (still a 401 JSON envelope), but the post-auth contract is fundamentally different);safeErrorResponse(error, 'Failed to generate sample template')outer-catch (UNIQUE: FIRSTrequireClientAuth()-gated GET smoke pinning asafeErrorResponsecross-utility helper, NOTserverErrorResponselike theclient-items-stats-querysibling -- the catch message BYTE-IDENTICALLY matches the admin siblingadmin/items/export/sampleroute's catch message);ItemExportServicedirect service-class posture (UNIQUE: FIRSTrequireClientAuth()-gated GET smoke pinning anew ItemExportService()direct-instantiation pattern, NOT a repository factory -- distinct from thegetClientItemRepository()factory pattern of theclient-items-stats-querysibling and theItemImportServiceinstantiation of theclient-items-import-method/client-items-import-validate-methodsiblings);format=Zod-enum case-sensitivity -- Zod enums match exactly, soformat=CSV/format=XLSXround-trip to 4xx on the auth branch via thesafeErrorResponse(...)catch (the unauth branch is invariant to this distinction);format=enum default -- the schema has a.default('csv'), so omittingformatyields CSV on the auth branch (the unauth branch is invariant to this distinction). The GET handler combines: outer try/catch aroundrequireClientAuth()discriminated-union check,searchParamsextraction,exportQuerySchema.parse(Object.fromEntries(searchParams))Zod-validatedformatenum ('csv' | 'xlsx'with a'csv'default),new ItemExportService()instantiation, per-format dispatch (generateSampleXLSX()for'xlsx'elsegenerateSampleCSV()), success returns aNextResponse(<bytes / string>, { headers: { 'Content-Type': ..., 'Content-Disposition': 'attachment; filename="..."' } }), outer catchsafeErrorResponse(error, 'Failed to generate sample template'); method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (two bulk-loop walks -- ~50 query permutations + ~11 headers all asserting< 500; a longer-message TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion with nodata/format/filenameleak; a gate-before-catch invariance walk pinning that the 500-catch message never fires on unauth; a gate-before-binary-stream-header invariance walk -- CRITICAL: pinning that theContent-Disposition: attachment; …header NEVER appears on the unauth branch; a gate-before-binary-stream-content-type invariance walk -- CRITICAL: pinning that the unauth branch emitsapplication/json, NOTtext/csvor the XLSX spreadsheetml MIME type; a gate-before-Zod-parse invariance walk pinning everyformat=value (valid / empty / invalid / case-variant) collapses to the no-arg baseline status; an impersonation / token / bypass / filename-traversal invariance walk pinning every dangerous-passthrough query key collapses to the no-arg baseline status; an Accept-header invariance walk pinning the route does not negotiate content-types; a side-channel walk on GET (Cookie / Authorization / X-User-Id / X-Forwarded-For / X-Real-IP); a repeated-key invariance walk pinningsearchParams.get(name)'s first-value semantics; a cross-method probe (POST / PUT / PATCH / DELETE); a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every parameter combination). Cross-references the companion client-items-import siblingclient-items-import-method-spec.md(pins therequireClientAuth()helper on the COMMIT-mode batch-import POST surface; this spec extends the family into the SAMPLE-TEMPLATE GET surface), the companion client-items-import-validate siblingclient-items-import-validate-method-spec.md(pins therequireClientAuth()helper on the DRY-RUN VALIDATE-mode multipart POST surface; this spec is the GET sibling that emits the sample template the user fills before calling the validate / import POST endpoints), the companion client-items collection siblingclient-items-method-spec.md, the companion client-items per-id siblingclient-items-id-method-spec.md, the companion client-items-stats siblingclient-items-stats-query-spec.md(uses the samerequireClientAuth()helper on a single GET surface for the stats endpoint --serverErrorResponsecatch, distinct from this spec'ssafeErrorResponsecatch), the companion client-protected siblingclient-protected.spec.ts, the admin-tree counterpart atapps/web/app/api/admin/items/export/sample/route.ts(admin-gated equivalent covered separately byadmin-items-export-sample-query.spec.ts-- uses bareauth()+isAdmininstead ofrequireClientAuth(), and the SAMEexportQuerySchemaZod parse + the SAME'Failed to generate sample template'catch message + the SAMEItemExportService.generateSample*service calls), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 114-of-N and thetests/api/per-spec-file sub-rollout extends to 112-of-many, and the first per-source-file GET smoke pinning arequireClientAuth()-gated binary-stream sample-template handler lands -- pinning a Zod-exportQuerySchemaquery-parse contract gated byrequireClientAuth(), aContent-Disposition: attachment; filename="..."binary-stream success contract, asafeErrorResponse(error, 'Failed to generate sample template')cross-utility outer-catch helper that BYTE-IDENTICALLY matches the admin sibling, anItemExportServicedirect service-class instantiation pattern, aformat=Zod-enum case-sensitivity contract, and aformat=enum-default contract that no priorrequireClientAuth()-gated GET smoke covers. - E2E Featured Items Query Spec (
apps/web-e2e/tests/api/featured-items-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public featured-items GET query-param smoke spec paired withapps/web-e2e/tests/api/featured-items-query.spec.ts, the one-hundred-and-seventeenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fourteenth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/featured-items/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins a public (no-auth-gate) tenant-resolving listing endpoint combining aNumber.parseInt(searchParams.get('limit') ?? '6', 10)default-6parse path, aMath.min(Math.max(rawLimit, 1), 50)two-sided silent clamp, aNumber.isFinite(rawLimit)non-finite fallback, a strict-stringsearchParams.get('includeExpired') === 'true'boolean-from-string parse (anything other than the literal string'true'keepsincludeExpiredfalse), anawait getTenantId()tenant-resolution short-circuit (anulltenant returns{ success: true, data: [], count: 0 }without ever touching the DB), and a try / catch empty-list fallback that swallows internal errors and returns{ success: true, data: [], count: 0 }rather than 500ing. UNIQUE: every prior per-source-file public-route GET smoke (sponsor-ads-public,items-popularity-scores,agent-discovery) pins a route whose error / null-state branch returns either a distinct envelope or NO envelope (a 4xx); this is the FIRST per-source-file GET smoke that pins a route whose null-tenant branch, DB-unavailable branch, AND outer-catch branch ALL collapse onto the SAME{ success: true, data: [], count: 0 }empty-list envelope. Distinct from EVERY prior public-route GET smoke: tenant-resolution short-circuit (UNIQUE -- the FIRST per-source-file GET smoke pinning a route whose null-tenant branch returns a 200-empty envelope rather than a 401 / 403);Number.parseInt(value ?? '6', 10)default-6parse path with explicit radix-10 second argument (UNIQUE -- distinct fromitems-popularity-scores's implicit-radixparseInt(...); the explicit radix-10 makes decimal interpretation load-bearing for any caller submitting a leading-0value that someparseIntimplementations would treat as octal);Math.min(Math.max(rawLimit, 1), 50)two-sided silent clamp (UNIQUE -- distinct fromitems-popularity-scores's one-sidedMath.min(parseInt(limit), 100)upper-clamp-only andsponsor-ads-public'sMath.min(Math.max(1, Math.floor(rawLimit)), 50)clamp; this route's clamp covers BOTH endpoints of the[1, 50]range -- values below1are silently raised to1, values above50are silently lowered to50); strict-string=== 'true'boolean-from-string parse (UNIQUE -- the FIRST per-source-file GET smoke pinning a strict-string boolean parser on a query parameter -- only the literal lowercase'true'flips the default;?includeExpired=TRUE,?includeExpired=1,?includeExpired=false,?includeExpired=all keepfalse);isActive: true+tenantIdtwo-condition WHERE clause (UNIQUE -- the FIRST per-source-file GET smoke pinning a public listing route that combines anisActiveflag check with a tenant-scoping check inside the sameand(...)clause); multi-key composite ORDER BY (UNIQUE -- the FIRST per-source-file GET smoke pinning a Drizzle two-key composite orderingdesc(featuredItems.featuredOrder), desc(featuredItems.featuredAt));{ success, data, count }three-key envelope (UNIQUE -- the FIRST per-source-file GET smoke pinning a public-route success envelope that adds acount: numbercardinality key alongsidesuccess/data); try / catch empty-list fallback (NOT 500) (UNIQUE -- the FIRST per-source-file GET smoke pinning a route that catches every internal error and returns the same empty-list envelope as the null-tenant branch and thecheckDatabaseAvailability()short-circuit); public (no-auth-gate) route (distinct from the auth-gatedadmin/featured-itemsandadmin/featured-items/[id]siblings); method-resolution surface -- the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (one query-string bulk-loop walk covering ~30 permutations -- no-arg baseline, validlimit1/6/10/50, out-of-range upperlimit51/999/10000 admit-clamped to 50, out-of-range lowerlimit0/-5 admit-clamped to 1, empty /abc/NaNlimitNumber.parseInt-default fallback to'6'andNumber.isFinite(NaN) === falsenon-finite branch, floatlimit6.5/49.9Number.parseIntinteger-truncation, whitespace /+limit%2010/%2B10Number.parseInttolerance, strict-stringincludeExpiredtrue/false/1/0/empty/TRUE pinning the=== 'true'strict-equality check, combinedlimit + includeExpired, unknown query keys silently ignored -- all asserting< 500). Cross-references the cross-cuttingitems.spec.ts(also probesGET /api/featured-itemsBUT only the no-arg baseline; this per-source-file spec adds the query-param surface so a regression inNumber.parseInt, theMath.min/Math.maxclamp, theNumber.isFinitefallback, the=== 'true'strict-equality check, thegetTenantId() === nullshort-circuit, or the try / catch empty-list fallback is caught explicitly), the neighbouring auth-gated admin siblingadmin-featured-items-id-method-spec.md(auth-gated single-featured-item CRUDGET/PUT/DELETEon/api/admin/featured-items/[id]-- the two routes share thefeaturedItemsDrizzle table but diverge entirely on auth posture and method surface), the neighbouring admin listing siblingadmin-featured-items-create-body-spec.md, the neighbouring sponsor-ads siblingsponsor-ads-checkout-body-spec.md, the neighbouring popularity-scores siblingitems-popularity-scores-query-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 117-of-N and thetests/api/per-spec-file sub-rollout extends to 114-of-many, and the first per-source-file GET smoke pinning a public (no-auth-gate) tenant-resolving featured-items listing handler lands -- pinning aNumber.parseInt(value ?? '6', 10)default-6parse path with explicit radix-10, aMath.min(Math.max(rawLimit, 1), 50)two-sided silent clamp, aNumber.isFinite(rawLimit)non-finite fallback, a strict-string=== 'true'boolean-from-string parse, a tenant-resolution null-short-circuit, anisActive: true+tenantIdtwo-condition WHERE clause, adesc(featuredOrder), desc(featuredAt)multi-key composite ORDER BY, a{ success, data, count }three-key envelope, and a try / catch empty-list fallback that no prior per-source-file public-route GET smoke covers. - E2E Health Database Query Spec (
apps/web-e2e/tests/api/health-database-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public database-health GET query-param smoke spec paired withapps/web-e2e/tests/api/health-database-query.spec.ts, the one-hundred-and-eighteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fifteenth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/health/database/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins a public (no-auth-gate) zero-argument health-probe endpoint combining a hard-codeddb.execute(sql\SELECT 1 as test`)round-trip (no parameter binding, no URL-driven SQL), a two-branch (200-healthy / 500-unhealthy) status envelope determined by the database's reachability NOT the URL, a shared{ status, database, timestamp }envelope shape across both branches with a branch-specific fourth key (resulton success,erroron failure), and a bare zero-argumentGET()Next 16 handler signature that NEVER reads the request URL. UNIQUE: every prior per-source-file public-route GET smoke (featured-items-query,items-popularity-scores,sponsor-ads-public,agent-discovery) asserts a generic< 500contract because their500is a regression signal; this is the FIRST per-source-file GET smoke that asserts the tighter[200, 500]two-valid-status contract because the route's500is an EXPECTED outcome (catch branch when the configured database is unreachable, which the e2e environment does not guarantee). Distinct from EVERY prior public-route GET smoke: **two-valid-status[200, 500]contract** (UNIQUE -- the FIRST per-source-file GET smoke pinning a route whose500status is an EXPECTED outcome rather than a regression signal -- the bulk-loop assertion isexpect([200, 500]).toContain(response.status()), surfacing 502 / 503 / 504 gateway failures or any 4xx parameter-rejection regression as a test failure); **status invariance under URL changes** (UNIQUE -- the FIRST per-source-file GET smoke pinning that the response branch never drifts from the baseline regardless of how many bogus / typo'd / SQL-injection-shaped query params the caller appends -- a regression that introduces arequest.nextUrl.searchParams.get(...)call would be caught by this branch-equality assertion); **bare zero-argumentGET()handler signature for a PUBLIC route** (the FIRST per-source-file GET smoke pinning a public route that uses theexport async function GET()signature with NOrequestparameter at all -- the siblingadmin-roles-active-query-spec.mdALSO pins a bare zero-argument GET signature but that route is admin-tree); **SQL-injection invariance contract** (UNIQUE -- the FIRST per-source-file GET smoke pinning that SQL-injection-shaped values in?schema=/?table=do NOT reach the SQL layer -- the route runs a hard-codedsql`SELECT 1 as test`with no parameter binding, so injected payloads cannot alter the executed statement); **canonical health-envelope contract** (UNIQUE -- the FIRST per-source-file GET smoke pinning a Kubernetes-style health-probe envelope with the three-key{ status, database, timestamp }shape enumerable across both branches;status ∈ {'healthy', 'unhealthy'},database ∈ {'connected', 'disconnected'},timestampDate.parse-able ISO-8601 string); **non-JSONformatinvariance** (the route always respondsapplication/jsonregardless of anyAcceptheader or?format=text/?format=prometheusparameter); **public (no-auth-gate) route** (distinct from the auth-gatedclient/dashboard/stats,client/geo-stats,client/items/coordinatesclient-tree health-probe siblings); **method-resolution surface** -- the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (one query-string bulk-loop walk covering ~50 permutations -- no-arg baseline,?refresh=/?force=cache-bypass keys,?schema=/?database=/?table=scoping keys,?timeout=fail-fast keys,?check=/?probe=different-check keys including K8s liveness / readiness / startup,?format=content-negotiation keys includingtext/prometheus,?verbose=/?debug=diagnostics keys,?locale=/?lang=i18n keys, empty values, repeated keys, SQL-injection-shaped values including'OR'1'='1/';+DROP+TABLE+users;+--/%27/%22/%3B/%2D%2D, 500-character long values, bogus / typo'd query keys -- all asserting the[200, 500]two-valid-status contract -- plus three hand-written tests: canonical-envelope shape assertion, status-invariance test pinning baseline-equality of both status and branch, SQL-injection invariance test). Cross-references the cross-cutting [health.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/health.spec.ts) (also probesGET /api/health/databaseBUT only the no-arg baseline; this per-source-file spec adds the **query-param surface** + the **SQL-injection invariance contract** + the **canonical-envelope shape assertion**), the neighbouring [internal-db-init-query-spec.md](./plugins/internal-db-init-query-spec.md) (documents the related/api/internal/db-initsurface that complements the database-health endpoint -- init-time vs probe-time database surfaces), the neighbouring [cron-sync-query-spec.md](./plugins/cron-sync-query-spec.md) (another zero-argument GET handler that mirrors this spec's bareGET()signature posture), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) for the governing spec. With this entry the **per-spec-file docs rollout extends to 118-of-N** and the **tests/api/per-spec-file sub-rollout extends to 115-of-many**, and the **first per-source-file GET smoke pinning a public (no-auth-gate) zero-argument health-probe endpoint** lands -- pinning a tighter[200, 500]two-valid-status contract that no prior per-source-file public-route GET smoke covers (every prior smoke asserts a generic< 500contract because their500is a regression signal; this route's500` is an EXPECTED outcome), a status-invariance under URL changes contract, a SQL-injection invariance contract, a canonical Kubernetes-style health-envelope shape contract, and a non-JSON content-negotiation invariance contract that no prior per-source-file public-route GET smoke covers. - E2E Client Items Coordinates Query Spec (
apps/web-e2e/tests/api/client-items-coordinates-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite'srequireClientAuth()-gated client items-coordinates GET / query-param surface smoke spec paired withapps/web-e2e/tests/api/client-items-coordinates-query.spec.ts, pinning the third per-source-filerequireClientAuth()-gated zero-argument GET handler (after the siblingclient-dashboard-stats-queryandclient-geo-stats-queryspecs) combining agetClientItemRepository().getCoordinatesByUser(userId)repository-delegation pattern (NOTgetStatsByUserlike theclient-items-stats-querysibling, NOTgetGeoStatsByUserlike theclient-geo-stats-querysibling, NOTgetStats(userId)on the dashboard repository like theclient-dashboard-stats-querysibling), a nested-coordinates-keyed success envelope{ success: true, coordinates: Array<{ slug, name, latitude, longitude }> }(the FIRST per-source-file GET smoke pinning acoordinates-keyed nested-array success envelope -- distinct from BOTH the spread-into-envelope shape pinned byclient-dashboard-stats-queryandclient-geo-stats-query, AND thestats-keyed nested-object shape pinned byclient-items-stats-query), aserverErrorResponse(error, 'Failed to fetch item coordinates')outer catch, and a nine-bypass-prevention assertion battery extending the six-test battery of the siblingclient-geo-stats-queryspec with a single-item-lookup bypass-prevention contract (?slug=…/?itemId=…/?itemSlug=…invariance), a content-negotiation bypass-prevention contract (?format=geojson/?format=kml/?format=xml/?format=csvinvariance), and an Accept-header invariance contract (Accept: application/geo+json/application/xml/text/html/*/*round-trip to the same 401 asAccept: application/json). UNIQUE: this is the THIRDrequireClientAuth()-gated GET smoke and the THIRD zero-argument handler in therequireClientAuth()family. Distinct from EVERY prior per-source-file GET smoke:getClientItemRepository().getCoordinatesByUser(userId)repository-delegation (UNIQUE -- the FIRST per-source-file GET smoke pinning agetClientItemRepository()-singleton-factory delegation to thegetCoordinatesByUser(userId)method); nested-coordinates-keyed success envelope (UNIQUE -- the FIRST per-source-file GET smoke pinning acoordinates-keyed nested-array success contract -- a regression that switches the route to a{ success: true, ...coordinates }spread-into-envelope shape would break consumer code that readsbody.coordinates); nine-bypass-prevention assertion battery (UNIQUE -- the FIRST per-source-file GET smoke pinning a nine-test bypass-prevention battery extending the six-test battery of the siblingclient-geo-stats-queryspec with three additional contracts -- single-item lookup, content negotiation, Accept-header -- that no prior per-source-file GET smoke covers); single-item-lookup bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a?slug=…/?itemId=…/?itemSlug=…query-parameter invariance contract on a collection endpoint -- a regression that readssearchParams.get('slug')/searchParams.get('itemId')before the gate would change the response payload shape on the auth branch from a collectionArray<…>to a single-item lookup); content-negotiation bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a content-negotiation?format=geojson/?format=kml/?format=xml/?format=csvquery-parameter invariance contract that pins ALL non-default format keys to the same 401 as the no-arg baseline); Accept-header invariance (UNIQUE -- the FIRST per-source-file GET smoke pinning anAccept-header invariance contract --Accept: application/json/application/geo+json/application/xml/text/html/*/*round-trip to the same 401);?lat=NaN/?lat=Infinitydefensive spatial-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning defensive spatial-filter valuesNaN/Infinityon the unauth branch -- a regression that readsparseFloat(searchParams.get('lat'))before the gate could trigger aNaN-comparison bug in a future spatial-filter implementation; this spec pins that the gate fires first, neutralising the bug);?zoom=…/?center=lat,lngmap-control bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning map-control query keys?zoom=12/?center=40.7128,-74.0060-- the obvious extensions for a future "items inside the current viewport" feature);serverErrorResponse(error, 'Failed to fetch item coordinates')outer-catch (matches the discriminated-union helper-contract shared with theclient-dashboard-stats-query,client-geo-stats-query, andclient-items-stats-querysiblings -- NOTsafeErrorResponselikeclient-items-import-sample-query); admin-allowed-on-client-endpoints note (matches the siblingclient-dashboard-stats-queryandclient-geo-stats-queryspecs' contract for a parallelrequireClientAuth()-gated client-tree endpoint -- the spec pins that the admin-status read happens viasession.user.isAdmin, NEVER via?admin=…/?asAdmin=…/?bypass=…/?impersonate=…query parameters). The GET handler combines:requireClientAuth()discriminated-union auth helper (returns{ success: false, response: <401 NextResponse> }on failure -- the route returnsauthResult.responsedirectly via the early-return idiom -- or{ success: true, userId: string }on success),getClientItemRepository()singleton-factory pattern (returns the client-item repository instance -- SHARED with the siblingclient-items-stats-queryandclient-geo-stats-queryroutes, but invoked for a different method),clientItemRepository.getCoordinatesByUser(userId)load-bearing repository-delegation call (returns the per-user coordinate list asArray<{ slug: string, name: string, latitude: number, longitude: number }>), nested-coordinates-keyed success envelopeNextResponse.json({ success: true, coordinates })(the repository result is nested under acoordinateskey -- NOT spread into the envelope like theclient-dashboard-stats-query/client-geo-stats-querysiblings, NOT under astatskey like theclient-items-stats-querysibling), outer catchserverErrorResponse(error, 'Failed to fetch item coordinates')(maps any thrown error to a 500 with the documented message -- the catch can ONLY fire AFTER the auth gate has already let the call through, so the unauth branch is invariant to it); method-resolution surface where the route exports ONLYGET(POST/PUT/PATCH/DELETEround-trip to a 405 -- Next.js missing-method response). Documents the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~110 permutations -- no-arg baseline, admin-impersonation keys, client-terminology variants, single-item-lookup keys (?itemId=/?slug=/?itemSlug=), magic-auth keys, geographic-filter keys, spatial / map-control filter keys (including?zoom=12/?center=40.7128,-74.0060/?lat=NaN/?lat=Infinity), item-status filter keys, pagination keys (including?cursor=), projection keys, cache-busting keys, content-negotiation (including?format=geojson/?format=kml), i18n keys, sort-override keys, multi-tenancy keys, admin-override keys, empty values, repeated keys (including?lat=40&lat=50), special-character values, 500-character long values, bogus / typo'd query keys, all asserting< 500, plus NINE hand-written tests pinning the canonical 401 envelope shape, the bogus-parameter status invariance, the?userId=…session-gate-bypass-prevention, the?token=…query-token-auth-bypass-prevention, the?admin=…query-admin-override-prevention, the?bbox=…spatial-filter-bypass-prevention (nine spatial keys), the?slug=…single-item-lookup-bypass-prevention (three slug keys), the?format=geojsoncontent-negotiation-bypass-prevention (five format keys), the multi-permutation shape stability across three different parameter sets, and the Accept-header invariance across five Accept-header values). Cross-references the neighbouringrequireClientAuth()-gated GET siblingclient-geo-stats-query-spec.md(pairs withapps/web-e2e/tests/api/client-geo-stats-query.spec.tsand pins the spread-geo-stats success envelope shape on a stats-only payload vs the nested-coordinates-keyed array shape this spec pins on a coordinate-list payload -- shares thegetClientItemRepository()singleton-factory with this route, but diverges on which repository method it invokesgetGeoStatsByUservsgetCoordinatesByUser), the neighbouringrequireClientAuth()-gated GET siblingclient-dashboard-stats-query-spec.md(pairs withapps/web-e2e/tests/api/client-dashboard-stats-query.spec.tsand pins the spread-stats success envelope shape vs the nested-coordinates-keyed array shape this spec pins -- both specs share the samerequireClientAuth()discriminated-union auth-helper return contract and the same'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope), the neighbouringrequireClientAuth()-gated GET siblingclient-items-stats-query-spec.md(pairs withapps/web-e2e/tests/api/client-items-stats-query.spec.tsand pins the{ success: true, stats: ... }nested-stats success envelope shape on the auth branch vs the nested-coordinates-keyed array shape this spec pins -- shares thegetClientItemRepository()singleton-factory with this route, but diverges on which repository method it invokesgetStatsByUservsgetCoordinatesByUser), the neighbouringrequireClientAuth()-gated client family specsclient-items-method-spec.md,client-items-id-method-spec.md,client-items-import-method-spec.md,client-items-import-validate-method-spec.md, andclient-items-import-sample-query-spec.md, the cross-cuttingclient-protected.spec.ts(covers the broader auth-protected client surface that this coordinates endpoint sits within), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 119-of-N and thetests/api/per-spec-file sub-rollout extends to 116-of-many, and the third per-source-filerequireClientAuth()-gated zero-argument GET smoke lands -- pinning a discriminated-union auth-gate contract, a nested-coordinates-keyed success envelope shape, agetClientItemRepository().getCoordinatesByUser(userId)singleton-factory repository-delegation, aserverErrorResponse('Failed to fetch item coordinates')outer-catch, and a nine-bypass-prevention assertion battery (?userId=…/?token=…/?admin=…/?bbox=…/?slug=…/?format=geojsoninvariance + multi-permutation shape stability + Accept-header invariance) that no prior per-source-file GET smoke covers. - E2E Client Geo Stats Query Spec (
apps/web-e2e/tests/api/client-geo-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite'srequireClientAuth()-gated client geo-stats GET / query-param surface smoke spec paired withapps/web-e2e/tests/api/client-geo-stats-query.spec.ts, the one-hundred-and-eighteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fifteenth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/client/geo-stats/route.ts-- the second per-source-file GET smoke the docs tree publishes that pins arequireClientAuth()-gated zero-argument handler (the FIRST being the siblingclient-dashboard-stats-queryspec) combining agetClientItemRepository().getGeoStatsByUser(userId)repository-delegation pattern (NOTgetClientDashboardRepository().getStats(userId)like the siblingclient-dashboard-stats-queryspec, NOTgetClientItemRepository().getStatsByUser(userId)like the siblingclient-items-stats-queryspec), a spread-geo-stats success envelope{ success: true, ...geoStats }(matches the spread-into-envelope shape pinned byclient-dashboard-stats-query-- NOT the{ success: true, stats: <statsObject> }nested shape pinned byclient-items-stats-query), aserverErrorResponse(error, 'Failed to fetch geographic statistics')outer catch, and a six-bypass-prevention assertion battery (?userId=…admin-impersonation,?token=…magic-token bypass,?admin=…query-admin-override,?country=…/?city=…/?lat=…geographic-filter bypass, multi-permutation shape stability) on top of the standard query-string bulk-loop walk. UNIQUE: this is the THIRDrequireClientAuth()-gated GET smoke afterclient-items-stats-queryandclient-dashboard-stats-query, and the SECOND zero-argument handler in therequireClientAuth()family. Distinct from EVERY prior per-source-file GET smoke:getClientItemRepository().getGeoStatsByUser(userId)repository-delegation (UNIQUE -- the FIRST per-source-file GET smoke pinning agetClientItemRepository()-singleton-factory delegation to thegetGeoStatsByUser(userId)method; the route shares thegetClientItemRepository()singleton-factory with theclient-items-stats-queryroute but diverges on which repository method it invokes); spread-geo-stats success envelope{ success: true, ...geoStats }(UNIQUE within the spread-stats family -- the FIRST per-source-file GET smoke pinning a spread-stats envelope on a geographic dataset where the geo-stats fieldstotal_items/items_with_location/items_remote/service_area_breakdown/top_cities/top_countriesbecome top-level keys of the response envelope alongsidesuccess); six-bypass-prevention assertion battery (UNIQUE -- the FIRST per-source-file GET smoke pinning a six-test bypass-prevention battery -- extends the five-test battery of the siblingclient-dashboard-stats-queryspec with a geographic-filter bypass-prevention contract that no prior per-source-file GET smoke covers); geographic-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a geographic-filter query-parameter invariance contract on a stats endpoint -- the route returns the full per-user payload today; a regression that readssearchParams.get('country')/searchParams.get('city')/searchParams.get('lat')/searchParams.get('lng')/searchParams.get('bbox')/searchParams.get('radius')before the gate would change the response payload shape on the auth branch);?lat=…/?lng=…/?bbox=…/?radius=…spatial-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a spatial-filter query-parameter invariance contract -- a regression that adds spatial filtering "items near a point" or "items inside a bounding box" before the gate would change the response payload shape on the auth branch);?serviceArea=…/?service_area=…/?coverage=…service-area-filter bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a service-area-filter query-parameter invariance contract -- the service-area filter keys are particularly relevant to theservice_area_breakdownarray inside the response payload);?topN=…/?fields=top_citiesper-bucket pagination/projection bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a per-bucket pagination key?topN=…and per-bucket projection keys?fields=top_cities/?fields=top_countries,service_area_breakdown/?select=items_with_location/?include=items_remoteinvariance contract -- these keys are particularly tempting on a geo-stats endpoint where thetop_citiesandtop_countriesarrays could be paginated or filtered);?format=geojson/?format=kmlcontent-negotiation bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a geographic-format content-negotiation invariance contract -- the route returns JSON exclusively today; a regression that adds GeoJSON or KML output formats would need to respect the gate);serverErrorResponse(error, 'Failed to fetch geographic statistics')outer-catch (matches the discriminated-union helper-contract shared with theclient-dashboard-stats-queryandclient-items-stats-querysiblings -- NOTsafeErrorResponselikeclient-items-import-sample-query); admin-allowed-on-client-endpoints note (matches the siblingclient-dashboard-stats-queryspec's contract for a parallelrequireClientAuth()-gated client-tree endpoint -- the spec pins that the admin-status read happens viasession.user.isAdmin, NEVER via?admin=…/?asAdmin=…/?bypass=…/?impersonate=…query parameters). The GET handler combines:requireClientAuth()discriminated-union auth helper (returns{ success: false, response: <401 NextResponse> }on failure -- the route returnsauthResult.responsedirectly via the early-return idiom -- or{ success: true, userId: string }on success),getClientItemRepository()singleton-factory pattern (returns the client-item repository instance -- SHARED with the siblingclient-items-stats-queryroute, but invoked for a different method),clientItemRepository.getGeoStatsByUser(userId)load-bearing repository-delegation call (returns the geo-stats payload withtotal_items/items_with_location/items_remote/service_area_breakdown/top_cities/top_countrieskeys), spread-geo-stats success envelopeNextResponse.json({ success: true, ...geoStats })(the spread merges the geo-stats fields into the top level of the response envelope alongsidesuccess-- SHARED-SHAPE with the siblingclient-dashboard-stats-queryspec), outer catchserverErrorResponse(error, 'Failed to fetch geographic statistics')(maps any thrown error to a 500 with the documented message -- the catch can ONLY fire AFTER the auth gate has already let the call through, so the unauth branch is invariant to it); method-resolution surface where the route exports ONLYGET(POST/PUT/PATCH/DELETEround-trip to a 405 -- Next.js missing-method response). Documents the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~95 permutations -- no-arg baseline,?userId=/?user_id=/?uid=/?id=admin-impersonation keys,?clientId=/?client_id=/?clientID=client-terminology variants,?token=/?secret=/?api_key=/?authorization=/?session=magic-auth keys,?country=/?city=/?region=/?area=/?countryCode=geographic-filter keys,?serviceArea=/?service_area=/?coverage=service-area filter keys,?lat=/?lng=/?bbox=/?radius=spatial-filter keys,?period=/?range=/?window=time-window keys,?limit=/?offset=/?page=/?topN=pagination keys,?fields=/?select=/?include=projection keys,?refresh=/?force=/?fresh=/?cache=/?nocache=cache-busting keys,?format=json/?format=xml/?format=csv/?format=geojson/?format=kmlcontent-negotiation,?locale=/?lang=/?currency=i18n keys,?status=/?type=filter keys,?sort=/?order=/?direction=sort-override keys,?tenant=/?tenantId=/?org=multi-tenancy keys,?admin=/?asAdmin=/?bypass=/?impersonate=admin-override keys, empty values, repeated keys, special-character values, 500-character long values, bogus / typo'd query keys, all asserting< 500, plus EIGHT hand-written tests pinning the canonical 401 envelope shape, the bogus-parameter status invariance, the?userId=…session-gate-bypass-prevention, the?token=…query-token-auth-bypass-prevention, the?admin=…query-admin-override-prevention, the?country=…/?city=…/?lat=…geographic-filter-bypass-prevention, and the multi-permutation shape stability across three different parameter sets). Cross-references the neighbouringrequireClientAuth()-gated GET siblingclient-dashboard-stats-query-spec.md(pairs withapps/web-e2e/tests/api/client-dashboard-stats-query.spec.tsand pins the spread-stats{ success: true, ...stats }shape this spec mirrors -- both specs share the samerequireClientAuth()discriminated-union auth-helper return contract, the same'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope, the same zero-argument handler signature, and the same spread-into-envelope success-payload shape -- but diverge on which repository they delegate togetClientDashboardRepository()vsgetClientItemRepository()and on which bypass-prevention assertions they pin date-range vs geographic-filter), the neighbouringrequireClientAuth()-gated GET siblingclient-items-stats-query-spec.md(pairs withapps/web-e2e/tests/api/client-items-stats-query.spec.tsand pins the{ success: true, stats: ... }nested-stats success envelope shape on the auth branch vs the spread-stats shape this spec pins -- shares thegetClientItemRepository()singleton-factory with this route, but diverges on which repository method it invokesgetStatsByUservsgetGeoStatsByUser), the neighbouringrequireClientAuth()-gated client family specsclient-items-method-spec.md,client-items-id-method-spec.md,client-items-import-method-spec.md,client-items-import-validate-method-spec.md, andclient-items-import-sample-query-spec.md, the cross-cuttingclient-protected.spec.ts(covers the broader auth-protected client surface that this geo-stats endpoint sits within), the neighbouring siblingclient-items-coordinates-query.spec.ts(covers another geographic-data endpoint/api/client/items/coordinatesunder the client-tree -- no per-source-file landing page yet for the coordinates sibling), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 118-of-N and thetests/api/per-spec-file sub-rollout extends to 115-of-many, and the first per-source-file GET smoke pinning arequireClientAuth()-gated zero-argument geo-stats handler lands -- pinning a discriminated-union auth-gate contract, a spread-geo-stats success envelope, agetClientItemRepository().getGeoStatsByUser(userId)singleton-factory repository-delegation, aserverErrorResponse('Failed to fetch geographic statistics')outer-catch, and a six-bypass-prevention assertion battery (?userId=…/?token=…/?admin=…/?country=…/?city=…/?lat=…invariance + multi-permutation shape stability) that no prior per-source-file GET smoke covers. - E2E Client Dashboard Stats Query Spec (
apps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite'srequireClientAuth()-gated client dashboard-stats GET / query-param surface smoke spec paired withapps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts, the one-hundred-and-seventeenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fourteenth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/client/dashboard/stats/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins arequireClientAuth()-gated zero-argument handler combining agetClientDashboardRepository().getStats(userId)repository-delegation pattern, a spread-stats success envelope{ success: true, ...stats }(NOT the{ success: true, stats: <statsObject> }nested shape used by the siblingclient-items-stats-queryspec), aserverErrorResponse(error, 'Failed to fetch dashboard statistics')outer catch, and a five-bypass-prevention assertion battery (?userId=…admin-impersonation,?token=…magic-token bypass,?admin=…query-admin-override,?from=…date-range bypass, multi-permutation shape stability) on top of the standard query-string bulk-loop walk. UNIQUE: every priorrequireClientAuth()-gated GET smoke (client-items-stats-query,client-items-method,client-items-id-method,client-items-import-sample-query) takes arequest: NextRequestargument; this is the SECONDrequireClientAuth()gate afterclient-items-stats-queryand the SECOND zero-argument handler in therequireClientAuth()family, AND the FIRST per-source-file GET smoke pinning the spread-stats success envelope shape. Distinct from EVERY prior per-source-file GET smoke:requireClientAuth()discriminated-union auth gate (zero-argument handler) (UNIQUE -- the auth helper returns{ success: false, response: <401 NextResponse> }on failure or{ success: true, userId: string }on success -- the discriminated-union shape that the spec validates by assertingresponse.status() === 401); spread-stats success envelope{ success: true, ...stats }(UNIQUE -- the FIRST per-source-file GET smoke pinning a...statsspread-into-envelope success contract where the dashboard fieldstotalSubmissions/totalViews/totalVotesReceived/totalCommentsReceived/viewsAvailable/recentActivity/uniqueItemsInteracted/totalActivity/activityChartData/engagementChartData/submissionTimeline/engagementOverview/statusBreakdown/topItemsbecome top-level keys of the response envelope alongsidesuccess); five-bypass-prevention assertion battery (UNIQUE -- the FIRST per-source-file GET smoke pinning a five-test bypass-prevention battery --?userId=…does NOT bypass the session gate,?token=…does NOT introduce a query-token bypass,?admin=…does NOT introduce a query-admin-override,?from=…date-range params do NOT change the unauth branch, multi-permutation shape stability across three different parameter sets);serverErrorResponse(error, 'Failed to fetch dashboard statistics')outer-catch (matches theclient-items-stats-querysibling helper contract -- NOTsafeErrorResponselikeclient-items-import-sample-query);getClientDashboardRepository().getStats(userId)repository-delegation (UNIQUE -- the FIRST per-source-file GET smoke pinning agetClientDashboardRepository()-singleton-factory delegation vs thenew ItemExportService()direct-instantiation pattern ofclient-items-import-sample-query); admin-allowed-on-client-endpoints note (UNIQUE -- the route'srequireClientAuth()helper notes that admins are allowed to use client endpoints today; the spec pins that the admin-status read happens viasession.user.isAdmin, NEVER via?admin=…/?asAdmin=…/?bypass=…/?impersonate=…query parameters);?from=…/?to=…date-range bypass-prevention (UNIQUE -- the FIRST per-source-file GET smoke pinning a date-range query-parameter invariance contract on a stats endpoint -- the route returns the full all-time payload today; a regression that readssearchParams.get('from')/searchParams.get('to')before the gate would change the response payload shape on the auth branch). The GET handler combines:requireClientAuth()discriminated-union auth helper (returns{ success: false, response: <401 NextResponse> }on failure -- the route returnsauthResult.responsedirectly via the early-return idiom -- or{ success: true, userId: string }on success),getClientDashboardRepository()singleton-factory pattern (returns the dashboard repository instance),dashboardRepository.getStats(userId)load-bearing repository-delegation call (returns the dashboard payload withtotalSubmissions/totalViews/totalVotesReceived/totalCommentsReceived/viewsAvailable/recentActivity/uniqueItemsInteracted/totalActivity/activityChartData/engagementChartData/submissionTimeline/engagementOverview/statusBreakdown/topItemskeys), spread-stats success envelopeNextResponse.json({ success: true, ...stats })(the spread merges the dashboard fields into the top level of the response envelope alongsidesuccess), outer catchserverErrorResponse(error, 'Failed to fetch dashboard statistics')(maps any thrown error to a 500 with the documented message -- the catch can ONLY fire AFTER the auth gate has already let the call through, so the unauth branch is invariant to it); method-resolution surface where the route exports ONLYGET(POST/PUT/PATCH/DELETEround-trip to a 405 -- Next.js missing-method response). Documents the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~60 permutations -- no-arg baseline,?userId=/?user_id=/?uid=/?id=admin-impersonation keys,?clientId=/?client_id=/?clientID=client-terminology variants,?token=/?secret=/?api_key=/?authorization=/?session=magic-auth keys,?from=/?to=/?startDate=/?endDate=date-range filter keys,?period=/?range=/?window=time-window keys,?limit=/?offset=/?page=pagination keys,?fields=/?select=/?include=projection keys,?refresh=/?force=/?fresh=/?cache=/?nocache=cache-busting keys,?format=content-negotiation,?locale=/?lang=/?currency=i18n keys,?status=/?type=filter keys,?sort=/?order=/?direction=sort-override keys,?tenant=/?tenantId=/?org=multi-tenancy keys,?admin=/?asAdmin=/?bypass=/?impersonate=admin-override keys, empty values, repeated keys, special-character values, 500-character long values, bogus / typo'd query keys, all asserting< 500, plus EIGHT hand-written tests pinning the canonical 401 envelope shape, the bogus-parameter status invariance, the?userId=…session-gate-bypass-prevention, the?token=…query-token-auth-bypass-prevention, the?admin=…query-admin-override-prevention, the?from=…date-range-bypass-prevention, and the multi-permutation shape stability across three different parameter sets). Cross-references the neighbouringrequireClientAuth()-gated GET siblingclient-items-stats-query-spec.md(pairs withapps/web-e2e/tests/api/client-items-stats-query.spec.tsand pins the{ success: true, stats: ... }nested-stats success envelope on the auth branch vs the spread-stats{ success: true, ...stats }shape this spec pins; both specs share the samerequireClientAuth()discriminated-union auth-helper return contract and the same'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope), the neighbouringrequireClientAuth()-gated client family specsclient-items-method-spec.md,client-items-id-method-spec.md,client-items-import-method-spec.md,client-items-import-validate-method-spec.md, andclient-items-import-sample-query-spec.md, the cross-cuttingclient-protected.spec.ts(covers the broader auth-protected client surface that this dashboard-stats endpoint sits within), the neighbouring siblingclient-geo-stats-query.spec.ts(covers the/api/client/geo-statscompanion endpoint that returns geographic-distribution stats with a parallelrequireClientAuth()gate -- no per-source-file landing page yet for the geo-stats sibling), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 117-of-N and thetests/api/per-spec-file sub-rollout extends to 114-of-many, and the first per-source-file GET smoke pinning arequireClientAuth()-gated zero-argument dashboard-stats handler lands -- pinning a discriminated-union auth-gate contract, a spread-stats success envelope, agetClientDashboardRepository()singleton-factory repository-delegation, aserverErrorResponse('Failed to fetch dashboard statistics')outer-catch, and a five-bypass-prevention assertion battery (?userId=…/?token=…/?admin=…/?from=…invariance + multi-permutation shape stability) that no prior per-source-file GET smoke covers. - E2E Auth Change-Password Spec (
apps/web-e2e/tests/api/auth-change-password.spec.ts) -- Per-source-file reference for the Playwright e2e suite's bare two-test no-server-error password-change POST smoke spec paired withapps/web-e2e/tests/api/auth-change-password.spec.ts-- the bare-baseline companion to the already-documentedauth-change-password-body-spec.mdlanding page (paired withauth-change-password-body.spec.ts). The body sibling drills into the rich body / header permutation surface (rate-limit-FIRST gate posture, Zod validation, multi-stage post-auth chain, OAuth-account guard, bcrypt-current / bcrypt-duplicate gates); this sibling is the two-test minimal< 500no-server-error contract baseline that pins the basic guarantee that the route never blows up regardless of whether a session is attached or the body is well-formed. UNIQUE within the auth-change-password spec pair: this is the bare-baseline member of the pair. Every prior per-source-file landing page in the docs tree pairs to a SINGLE source spec; this is the first per-source-file landing page that documents one HALF of a two-spec pair covering the same route (the body sibling pairs with the richauth-change-password-body.spec.tsand this sibling pairs with the bareauth-change-password.spec.ts). Distinct from the body sibling: bare two-test contract -- this spec emits exactly TWO tests (POST /api/auth/change-password without a session does not 5xxwith a fully-shaped body andPOST /api/auth/change-password with empty body does not 5xxwith{}), each assertingexpect(response.status()).toBeLessThan(500); no envelope-shape assertions -- the spec does NOT pin the canonical 401 / 400 / 429 envelopes (the body sibling does); no gate-ordering invariants -- the spec does NOT pin the rate-limit → auth → JSON-parse → Zod → tenant → user-DB → OAuth-guard → bcrypt-current → bcrypt-duplicate → bcrypt-hash → DB-update → email gate-ordering chain (the body sibling does); no bulk-loop walks -- the spec emits only two hand-written tests (the body sibling emits header / body bulk-loop walks); two-body coverage -- the spec hits a well-shaped body and an empty body, both must round-trip< 500; no method-resolution surface walk -- the spec does NOT emit a cross-method probe (GET/PUT/PATCH/DELETE); no side-channel walk -- the spec does NOT emit a side-channel walk (fabricated session cookies / Authorization headers). The route under test (apps/web/app/api/auth/change-password/route.ts) exports onlyPOST. The POST handler combines: client-IP extraction;ratelimit('change-password:<clientIP>', 5, 15 minutes)FIRST gate (exhausted → 429{ success: false, error: 'Too many password change attempts. Please try again later.', retryAfter });auth()session lookup (!session?.user?.id→ 401{ success: false, error: 'Unauthorized. Please sign in.' });await request.json()(no per-call try/catch);changePasswordSchema.safeParse(body)Zod validation (currentPassword non-empty + newPasswordpasswordSchema+ confirmPassword equal-via-.refine(...));getTenantId()(null → 403); user-DB select (null → 404);!user.passwordHashOAuth-account guard → 400;bcrypt.compare(currentPassword, user.passwordHash)(false→ 400);bcrypt.compare(newPassword, user.passwordHash)(true→ 400 same-password);bcrypt.hash(newPassword, 12);db.update(users); fire-and-forgetsendPasswordChangeConfirmationEmail(...); success 200{ success: true, message: 'Password changed successfully' }; outer catch 500{ success: false, error: 'Internal server error' }. Documents the at-a-glance scenario tree (two hand-written tests covering the well-shaped-body and empty-body shapes -- both asserting< 500and both expected to land on the unauth 401 branch under the rate-limit-not-tripped-yet posture). Cross-references the companion rich-permutation siblingauth-change-password-body-spec.md(pairs withauth-change-password-body.spec.tsand pins the rate-limit-FIRST gate posture, the canonical 401 / 400 / 429 envelopes, the bulk-loop header / body walks, and the gate-before-Zod / gate-before-tenant / gate-before-user-DB / gate-before-OAuth-guard / gate-before-bcrypt-current / gate-before-bcrypt-duplicate / gate-before-DB-update invariants), the companion auth-route smoke pairauth/forgot-password.spec.tsandauth/new-password.spec.ts(page-level forgot / reset password flows), Spec 003 -- Auth Providers (governs the auth-related routes at large), and Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 117-of-N and thetests/api/per-spec-file sub-rollout extends to 114-of-many, and the first per-source-file landing page that documents one HALF of a two-spec pair covering the same route lands -- pinning the bare-baseline< 500contract on the bare two-test smoke companion to an already-documented rich-permutation body smoke that no prior landing page covers (every prior landing page pairs to a SINGLE source spec). - E2E Categories Exists Query Spec (
apps/web-e2e/tests/api/categories-exists-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public categories-existence probe query-param smoke spec paired withapps/web-e2e/tests/api/categories-exists-query.spec.ts. Pairs with theGETexport ofapps/web/app/api/categories/exists/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins a fully public Git-CMS-backed existence probe whose catch branch ALSO returns200 OK(NOT500). The route is the catch-and-200 sibling of the DB-backedcollections-exists-query.spec.tscompanion: same{ exists, count }envelope, same nav-shell degradation contract, but the catch branch maps every thrown error to a200with{ exists: false, count: 0 }rather than the500the collections-exists sibling emits. Distinction is load-bearing: the navigation shell hits both probes on every render and must degrade quietly (NOT block the whole page) when the content layer is unavailable. The handler combines: no auth gate (intentionally public);?locale=query-param read viarequest?.nextUrl?.searchParams?.get('locale') || 'en'(the route reads exactly ONE query param; every other key is silently ignored — distinct from the collections-exists sibling which reads zero query params via the underscored_requestparameter);fetchItems({ lang: locale })Git-CMS read against the.content/mirror cloned fromDATA_REPOSITORY; happy-path success payload{ exists: <bool>, count: <number> }with status200(existscomputed asArray.isArray(categories) && categories.length > 0,countascategories?.length || 0); catch-and-empty fallback returns{ exists: false, count: 0 }with status200(NOT500); conditional development-mode logging —console.errorfires inside the catch only whenprocess.env.NODE_ENV === 'development'(distinct from the collections-exists sibling which logs unconditionally);GET(request: NextRequest)Next-specific handler signature with optional-chaining triplerequest?.nextUrl?.searchParams?.get(...)to keep the read safe under any futurerequest === undefinedrefactor. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting< 500covering?locale=permutations across all eight illustrated locales (en/fr/es/de/ar/zh/pt/ja), i18n aliases (?lang=/ combined?locale=&lang=), cache-busting (?refresh=/?force=/?fresh=/?nocache=), strict / validate flags, content projection (?include=/?fields=/?select=/?expand=), content negotiation (?format=json/?format=xml/?format=csv), status / active filters, multi-tenancy (?tenant=/?tenantId=), empty values (?locale=/?lang=/?refresh=), repeated keys (?locale=en&locale=fr), special-character payloads (%25/%2F/%5C/%27/%3Cscript%3E/%22%3Eoops), long values (500-char?locale=/?include=), and bogus / typo'd keys; a canonical{ exists: boolean, count: number }envelope assertion; a parameterised-vs-baseline status-stability comparison; a three-way shape-stability walk across the no-arg baseline + a?locale=&include=combined tuple + a?refresh=&format=&unknown=cache-bust + content-negotiation tuple; and a UNIQUEsearchParams.get('locale') || 'en'fallback-semantics walk pinning that the no-arg, the empty-string?locale=, and the explicit-?locale=enpaths all land in the same branch and return the same status). Cross-references the catch-and-500 DB-backed siblingcollections-exists-query.spec.ts, the surveys existence probesurveys-exists-query.spec.ts, the Git-CMS-backed admin siblingadmin-categories-all-query-spec.md, the DB-backed admin sibling at/api/admin/categories, the public-route per-source-fileitems-popularity-scores-query-spec.md, Spec 010 — End-to-End Test Coverage, and Spec 005 — Internationalisation which governs the?locale=fallback semantics this route inherits. - E2E Admin Categories All Query Spec (
apps/web-e2e/tests/api/admin-categories-all-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-CMS categories-listing query-param smoke spec paired withapps/web-e2e/tests/api/admin-categories-all-query.spec.ts. Pairs with theGETexport ofapps/web/app/api/admin/categories/all/route.ts-- the dedicated landing page for the first Git-CMS-backed admin-tree query smoke the docs tree ever published, previously covered indirectly via theclient-trash-page-object.mdco-tenant cross-link and called out repeatedly from the siblingadmin-tags-all-query-spec.mdwithout a dedicated landing page of its own. The categories-all route is the no-defensive-narrowing Git-CMS sibling of the tags-all route: same admin gate, samegetCachedItems({ lang })Git-CMS reader, same bare{ success: false, error: 'Unauthorized' }401 envelope, but NO defensivetypeof locale !== 'string'narrowing (the only Git-CMS-backed admin-tree route that omits the dead-branch validator). The handler combines:auth()session lookup; single-step!session?.user?.isAdmingate -> 401 with the bare canonical envelope (NOT'Unauthorized. Admin access required.'/'Forbidden');?locale=query-param read AFTER the gate viasearchParams.get('locale') || 'en'(no narrowing);getCachedItems({ lang: locale })Git-CMS read against the.content/mirror cloned fromDATA_REPOSITORY; success payload{ success: true, data: categories }200; outer catchconsole.error+ 500'Failed to fetch categories';GET(request: NextRequest)Next-specific handler signature. UNIQUE: every other admin-tree GET route uses a drizzle / Postgres backend EXCEPT the sibling tags-all route; this is the no-narrowing Git-CMS sibling that documents the simpler posture. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting< 500covering?locale=permutations across all six supported locales plusinvalidplus the empty string, locale variations (?lang=/?language=/?l=), pagination keys, status / active filters, content projection, cache-busting, impersonation, magic-token bypass, admin-override, multi-tenancy, Git-CMS-targeting, path-traversal payloads (../../etc/passwd/%00malicious), repeated keys, and bogus / typo'd keys; a bare 401-envelope assertion; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for?locale=/?userId=/?token=/?bypass=/?repo=&branch=&commit=/?path=/?refresh=key families; anAcceptheader walk; a side-channel cookie / IP header walk; a repeated-key walk; a negative-message assertion pinning the bare'Unauthorized'message and explicitly rejecting both the'Forbidden'and'Unauthorized. Admin access required.'alternatives; a path-traversal-resistance invariant pinning that even with../../etc/passwdor%00maliciouspayloads the unauth branch returns the identical 401 envelope; a cache-bust-resistance invariant pinning that the in-memory Git-CMS cache is NEVER invalidated on the unauth branch). Cross-references the Git-CMS-siblingadmin-tags-all-query-spec.md(same posture WITH a dead-branch defensivetypeof locale !== 'string'narrowing -- the only Git-CMS-backed admin-tree route that carries the defensive validator), the DB-backed siblingadmin-categories-query.spec.ts(database-backed/api/admin/categorieslisting route with pagination), the GitHub-API-backed siblingadmin-categories-git-query-spec.md(live HTTPS calls to the GitHub API usingGITHUB_TOKEN/DATA_REPOSITORYrather than the local.content/mirror), the POST-companion of the GitHub-API-backed siblingadmin-categories-git-create-body-spec.md(matching commit-a-new-category POST surface), the co-tenant page-object driverclient-trash-page-object.md(the cross-link that originally introduced this route to the docs tree before this dedicated landing page was published), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 117-of-N and the dedicated landing page for the original Git-CMS-backed admin-tree query smoke lands -- closing a long-standing docs-tree gap where the categories-all route was referenced indirectly across several sibling docs without a per-source-file landing page. - E2E Client Items [id] Restore Method Spec (
apps/web-e2e/tests/api/client-items-id-restore-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped per-id soft-delete restore POST body / header smoke spec paired withapps/web-e2e/tests/api/client-items-id-restore-method.spec.ts. Pairs with thePOSTexport ofapps/web/app/api/client/items/[id]/restore/route.ts-- the first per-source-file POST smoke the docs tree publishes that pins arequireClientAuth()-gated soft-delete restore action that delegates to aclientItemRepository.restoreForUser(id, userId)repository entry point with a THREE-branch nested catch dispatcher mapping repo-thrown error messages ('Item not found'exact match -> 404,'permission'substring -> 403,'not deleted'substring -> 400) to the@/lib/utils/client-authbuilder helpers (notFoundResponse,forbiddenResponse,badRequestResponse) with a'Failed to restore item'outer-catch default and the canonical'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope. UNIQUE: every priorclient-items*POST smoke covers a CRUD-style mutation or a batch-import service entry point -- this is the FIRST that pins a soft-delete restore action with therequireClientAuthhelper. Companion to the broader smoke atclient-item-restore.spec.tswhich stays as the single-test "did it 5xx?" canary; this spec is the detailed per-source-file invariant-pinning sibling. Cross-references the parent-route siblingclient-items-id-method-spec.md(5-helper-import contract on the parent[id]triple-methodGET + PUT + DELETEsurface; this spec extends it into the single-methodPOST /restoreaction sub-route), the broaderclient-items*family (client-items-method-spec.md,client-items-stats-query-spec.md,client-items-import-method-spec.md), and to Spec 010 -- E2E Test Coverage for the governing spec. - E2E Agent Discovery Spec (
apps/web-e2e/tests/public/agent-discovery.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public agent-discovery surface smoke spec paired withapps/web-e2e/tests/public/agent-discovery.spec.ts, the one-hundred-and-sixteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the first underapps/web-e2e/tests/public/to pin the agent-targeted discovery surface (vs the existingseo-manifests.spec.tssibling that pins the crawler-targeted discovery surface). Pairs with theGETexports ofapps/web/app/llms.txt/route.tsANDapps/web/app/items.json/route.ts-- the first per-source-file public-route smoke the docs tree publishes that pins two coupled routes in a single spec covering the llms.txt convention advertisement (/llms.txt) and the paired canonical-data JSON dump (/items.json) so downstream LLM agents can consume the directory without scraping HTML. UNIQUE: every prior public-route smoke covers a crawler-targeted SEO discovery surface (robots.txt,sitemap.xml,opengraph-image,favicon.ico); this is the FIRST that pins the agent-targeted discovery surface. Distinct from EVERY prior public-route smoke: two-route paired contract (UNIQUE -- the FIRST per-source-file smoke that pins two coupled routes in a single spec where/llms.txtadvertises/items.jsonas the canonical-data anchor and the spec validates BOTH the advertisement (text/plain body contains/items.json) AND the data contract (JSON envelope shape andcount === items.lengthinvariant));Cache-Control: public, max-age=300, s-maxage=900(UNIQUE -- the FIRST per-source-file public-route smoke pinning the shared 5-minute / 15-minute browser / CDN cache-tiering);Access-Control-Allow-Origin: *CORS-open (UNIQUE -- the FIRST per-source-file smoke pinning a CORS-open public JSON endpoint -- agents must be able to fetch/items.jsonfrom any origin without a preflight gate); stable JSON envelope shape --{ site, generatedAt, count, items }is the documented downstream contract pinningcount === items.length(load-bearing invariant),generatedAtISO-8601 parseable, each item carryingslug+categories(array) +tags(array) at minimum; no side-channel branching (UNIQUE -- the FIRST per-source-file public-route smoke pinning that fabricated session cookies / Authorization headers do NOT alter the dispatch on EITHER route -- both routes are fully public); method-resolution surface -- both routes export ONLYGET(cross-method probesPOST/PUT/PATCH/DELETEMUST round-trip< 500-- Next.js returns 405). Documents the at-a-glance scenario tree (eight tests covering:/llms.txttext/plain body shape including the leading#,/items.jsonadvertisement, sitemap + atom anchors;/llms.txtCache-Control sanity;/items.jsonapplication/json + envelope shape +count === items.lengthinvariant + ISO-8601 generatedAt parse;/items.jsonCache-Control + CORS-open headers;/items.jsonper-item shapeslug+ array-typedcategories+tags; cross-route side-channel invariance walk pinning fabricated cookies / Authorization headers do NOT alter dispatch on EITHER route;/items.jsoncross-method probe;/llms.txtcross-method probe -- all asserting< 500). Cross-references the neighbouring crawler-targeted SEO siblingseo-manifests.spec.ts(covers the closely related/robots.txt,/sitemap.xml,/opengraph-image, and/favicon.icodiscovery surface; this sibling pins the agent-targeted discovery surface -- the/llms.txtconvention isagent-targeted, NOT crawler-targeted), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 116-of-N, thetests/public/per-spec-file sub-rollout extends from one (seo-manifests) to two, and the first per-source-file public-route smoke pinning the agent-targeted discovery surface lands -- pinning the llms.txt convention advertisement, a paired canonical-data/items.jsonJSON envelope, aCache-Control: public, max-age=300, s-maxage=900shared cache-tiering, anAccess-Control-Allow-Origin: *CORS-open contract, acount === items.lengthenvelope-shape invariant, and a no-side-channel public-dispatch contract that no prior per-source-file public-route smoke covers. - E2E Admin Clients Query Spec (
apps/web-e2e/tests/api/admin-clients-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin client-profiles listing query-param smoke spec paired withapps/web-e2e/tests/api/admin-clients-query.spec.ts, the one-hundred-and-eighteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fifteenth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/admin/clients/route.ts-- the first per-source-file admin-tree query smoke the docs tree publishes that pins a route whose single-stepauth()+session.user.isAdmingate returns the bare'Unauthorized'message (no'. Admin access required.'suffix) AND fires BEFORE the sharedvalidatePaginationParams(searchParams)helper, BEFORE the six optional?search=/?status=/?plan=/?accountType=/?provider=query-param reads, AND BEFORE the legacygetClientProfiles({…})query helper delegated to from@/lib/db/queries. UNIQUE: every prior admin-tree query smoke pins one of three different gate postures -- the canonical-longer-message'Unauthorized. Admin access required.'family (admin/categories,admin/items,admin/items/import,admin/items/import/validate); the two-step!session?.user?.id→ 401 then!session.user.isAdmin→ 403 family (admin/notifications/[id]/read,admin/notifications/mark-all-read,admin/users/check-email,admin/users/check-username,admin/clients/bulk); OR the auth-gate-divergence-finding posture of the un-gatedadmin/roles/admin/roles/activefamily. This spec is the FIRST per-source-file admin-tree GET smoke pinning the bare-message single-step-collapse posture (matches the siblingadmin/comments/admin/companies/admin/usersroutes). Distinct from EVERY prior admin-tree query smoke: bare'Unauthorized'401 message (NO suffix) (UNIQUE -- the FIRST per-source-file admin-tree GET smoke pinning the single-step-collapse bare-message envelope, distinct from the canonical-longer-message family AND the two-step-split family); single-step gate AHEAD ofvalidatePaginationParams(...)helper (UNIQUE -- the FIRST per-source-file admin-tree GET smoke pinning a route whose single-stepsession?.user?.isAdmingate fires BEFORE the shared pagination helper -- the helper short-circuits with its{ error, status }400 envelope on?page=invalid/?limit=invalid/?page=-1/?limit=0/?limit=200, but only on the AUTH branch); six optional query-param reads, all AFTER the gate (UNIQUE -- the FIRST per-source-file admin-tree GET smoke pinning a route whose six query-param reads carry NO inline enum coercion or Zod schema validation -- distinct from theadmin/rolesroute's narrow inline ternary enum coercion for?status=and?sortBy=/?sortOrder=); legacygetClientProfiles({…})query helper (distinct from theadmin/categoriesroute'scategoryRepository.findAllPaginated(...)repository-pattern posture -- the handler importsgetClientProfilesdirectly from@/lib/db/queriesand passes a singleoptionsbag with the seven parsed fields; this spec stays green if a future contributor refactors the route to aclientRepositoryabstraction); three-key{ success, data: { clients }, meta }success envelope -- thedatakey carries a singleclients: []sub-key, distinct from theadmin/usersroute's bare{ success, data: [...], pagination: {…} }shape;POSTbranch with environment-flag-gated CRM sync -- out of scope for this GET-only spec, but documented in the per-source-file landing page so future contributors who add aPOSTsmoke must defend against the synchronouscreateTwentyCrmSyncServiceFromEnv()upsert viaTWENTY_CRM_ENABLED=falseenvironment override. The route under test combines: outertry / catcharoundauth()session lookup (!session?.user?.isAdmin→ 401{ error: 'Unauthorized' }),URL(request.url).searchParamsextraction,validatePaginationParams(searchParams)helper short-circuit on invalid pagination, six optionalsearchParams.get('…') || undefinedreads (search,status,plan,accountType,provider),getClientProfiles({…})legacy paginated query helper call, success returns{ success: true, data: { clients: result.profiles }, meta: { page, totalPages, total, limit } }, outer catch withconsole.error+{ error: 'Failed to fetch clients' }(status 500); method-resolution surface where the route exportsGETANDPOST(PUT/PATCH/DELETEmust round-trip to< 500). Documents the at-a-glance scenario tree (one bulk-loop walk over ~60 paths + eleven hand-written scenarios pinning: the strict 401-on-no-arg-baseline + bare{ error: 'Unauthorized' }envelope; status invariance across stacked-key permutations; per-key isolation walks for?asAdmin=/?as=/?asUser=/?impersonate=admin-impersonation,?token=/?secret=/?api_key=/?authorization=/?session=/?adminToken=magic-token,?bypass=/?admin=/?override=/?force=admin-override,?status=and?provider=filter-bypass;Acceptheader isolation; repeated-key walk; the bare-message envelope assertion pinningbody.error === 'Unauthorized'ANDbody.error !== 'Unauthorized. Admin access required.'ANDbody.error !== 'Forbidden'). Cross-references the neighbouring per-id siblingadmin-clients-clientid-method-spec.md, the neighbouring bulk siblingadmin-clients-bulk-method-spec.md, the neighbouring create siblingadmin-clients-create-body-spec.md(partitions thePOSTbody surface on the SAME route file -- the two per-source-file specs together pin both thePOSTbody surface and theGETquery surface), the shared admin-clients page-object driveradmin-clients-page-object.md, the prior per-source-file admin-tree GET smokesadmin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-sponsor-ads-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-settings-map-status-query-spec.md,admin-tags-all-query-spec.md, the admin-protected coverage specadmin-protected-extra.spec.ts(covers this route at the broad< 500level; this per-source-file spec adds the deep query-surface walk on top), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 118-of-N and thetests/api/per-spec-file sub-rollout extends to 115-of-many, and the first per-source-file admin-tree GET smoke pinning the bare-message single-step-collapse{ error: 'Unauthorized' }401 envelope lands -- pinning a single-stepsession?.user?.isAdmingate ahead of thevalidatePaginationParams(...)helper, six gate-protected optional query-param reads with no inline enum coercion or Zod validation, the legacygetClientProfiles({…})query helper posture, and the bare 401 envelope shape distinct from both the canonical-longer-message family and the two-step-split family. - E2E Items Popularity Scores Query Spec (
apps/web-e2e/tests/api/items-popularity-scores.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public popularity-scores debug-endpoint GET query-param smoke spec paired withapps/web-e2e/tests/api/items-popularity-scores.spec.ts, the one-hundred-and-fifteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-thirteenth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/items/popularity-scores/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins a public (no-auth-gate) debug endpoint combining aMath.min(parseInt(limit), 100)admit-clamp invariant, alocaledefault-'en'fallback, agetCachedItems({ lang })cache-aware fetch path, an empty-items short-circuit envelope{ items: [], message: 'No items found' }, a logarithmic-scaling score formulaMath.log10(value + 1) * weight, a featured-boost score cap (+10000), a three-tier recency-decay schedule (<30d→1000,<90d→500,<180d→250), and a stable rank-after-sort mutation (sort byscoredesc +name.localeCompareasc, then mutaterankto the 1-based sort index). UNIQUE: every prior per-source-fileitems*GET smoke (items-engagement-query,items-export-query,items-export-settings-query) gates either withauth()or a feature-flag check; this is the FIRST per-source-file GET smoke that pins a route that is intentionally public -- exposing a debug-only sort breakdown for any caller that hits the URL. Distinct from EVERY prior items GET smoke: public (no-auth-gate) route (UNIQUE -- the FIRST per-source-file GET smoke pinning a fully publicitems*route -- noauth()/requireClientAuth()/isAdmin/ feature-flag gate guards the handler);Math.min(parseInt(limit), 100)admit-clamp (UNIQUE -- the FIRST per-source-file GET smoke pinning a silent integer-clamp on a query parameter --limitvalues above 100 are clamped to 100;parseIntof an empty / non-numeric string falls back to the default'20'; the route NEVER 4xxs on a malformedlimit); logarithmic-scaling score formula (UNIQUE -- the FIRST per-source-file GET smoke pinning aMath.log10(value + 1) * weightengagement-scoring formula); featured boost (+10000) (UNIQUE -- the FIRST per-source-file GET smoke pinning a featured-item flat score boost as the load-bearing tie-breaker between featured and non-featured items); three-tier recency-decay schedule (UNIQUE -- the FIRST per-source-file GET smoke pinning a piecewise-linear recency-decay schedule); empty-items short-circuit envelope{ items: [], message: 'No items found' }(UNIQUE -- the FIRST per-source-file GET smoke pinning a non-error early-return envelope on agetCachedItems({ lang })cache miss -- themessagekey is ONLY emitted on the empty-items branch); stable rank-after-sort mutation (UNIQUE -- the FIRST per-source-file GET smoke pinning a sort-then-mutate-rankpattern); score-breakdown surface (UNIQUE -- the FIRST per-source-file GET smoke pinning ascoreBreakdownsub-object with seven labeled components --featured,views,votes,rating,favorites,comments,recency); locale-fallback semantics --localedefaults to'en'; unknown locales return an empty items list (NOT an error). The GET handler combines:searchParamsextraction (limit = Math.min(parseInt(searchParams.get('limit') || '20'), 100);locale = searchParams.get('locale') || 'en'),getCachedItems({ lang: locale })cache-aware items fetch (empty array short-circuits to{ items: [], message: 'No items found' }),getEngagementMetricsPerItem(slugs)per-item engagement metrics map, per-item score (featured boost + logarithmic engagement scoring +avgRating * 500+ per-tier recency-decay; engagement-missing fallback heuristic seeds score fromtags.length * 10capped at 100, name-length tiers 50 / 25,icon_url50,promo_code75), sort byscoredesc +name.localeCompareasc + mutaterankto the 1-based sort index, slice + envelope{ totalItems, showing: Math.min(limit, itemsWithScores.length), items: itemsWithScores.slice(0, limit) }, outer catch 500{ error: 'Failed to fetch popularity scores' }; method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (one query-string bulk-loop walk covering 15 permutations -- no-arg baseline, validlimit5/20, out-of-rangelimit999/10000 admit-clamped to 100, empty /abc/ negative / zerolimitparseInt-default fallback, knownlocaleen/fr/zh, unknownlocaleempty-items short-circuit, combinedlimit + locale, combined out-of-range + locale clamp-then-locale order -- all asserting< 500). Cross-references the cross-cuttingdiscovery.spec.ts(also probesGET /api/items/popularity-scoresBUT only the no-arg baseline; this per-source-file spec adds the query-param surface so a regression inparseInt, theMath.minclamp, thelocaledefault, or the empty-items branch is caught explicitly), the neighbouring engagement endpoint siblingitems-engagement-query-spec.md(when published), the neighbouring item-detail public specitem-public.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 115-of-N and thetests/api/per-spec-file sub-rollout extends to 113-of-many, and the first per-source-file GET smoke pinning a public (no-auth-gate) popularity-scores debug-endpoint handler lands -- pinning aMath.min(parseInt(limit), 100)admit-clamp invariant, alocaledefault-'en'fallback, an empty-items short-circuit envelope, a logarithmic-scaling score formula, a featured-boost score cap, a three-tier recency-decay schedule, and a stable rank-after-sort mutation that no prior per-source-file GET smoke covers. - E2E Surveys Responses [responseId] Query Spec (
apps/web-e2e/tests/api/surveys-responses-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-response-detail GET dynamic-segment / header smoke spec paired withapps/web-e2e/tests/api/surveys-responses-id-query.spec.ts, the one-hundred-and-thirteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-eleventh underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/surveys/responses/[responseId]/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins an admin-gated survey-response-by-id lookup delegating tosurveyService.getResponseById(responseId)with a404 'Response not found'non-existence guard AFTER the auth gate. Distinct from EVERY prior per-source-file GET smoke:auth() + isAdmingate BEFORE the lookup -- non-admin callers see 401'Unauthorized'and the load-bearingsurveyService.getResponseById(responseId)call is NEVER entered; single-route GET-only export -- the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500);{ success: true, data: <response> }success payload +{ success: false, error: 'Response not found' }404 envelope (UNIQUE: FIRST per-source-file GET smoke pinning aResponse not found404 envelope -- distinct from the siblingsurveys/[surveyId]route which usesSurvey not found);safeErrorResponse(error, 'Failed to fetch response')outer-catch (UNIQUE: FIRST per-source-file GET smoke pinning a'Failed to fetch response'(singular) 500-catch helper vs'Failed to fetch responses'(plural) on the plural-collection siblingsurveys-id-responses-method-spec.md). The GET handler combines: outer try/catch aroundauth()session lookup (!session?.user?.isAdmin→ 401 TWO-key envelope),surveyService.getResponseById(responseId)load-bearing service call, 404{ success: false, error: 'Response not found' }if the result is null, success returns{ success: true, data: <response> }, outer catchsafeErrorResponse(error, 'Failed to fetch response'); method-resolution surface where the route exports ONLYGET. Documents the at-a-glance scenario tree (one bulk-loop walk -- ~7 headers asserting< 500; canonical TWO-key 401 envelope; strict TWO-key envelope-shape preservation; gate-before-post-auth invariant; gate-before-service-delegation invariant pinning that XSS markers in the path-segment are NEVER echoed ANDsurveyService.getResponseByIdNEVER executes on unauth (CRITICAL); side-channel isolation walk; cross-permutation status invariance walk pinning byte-identical 401 envelopes across every header permutation; cross-method probe (POST / PUT / PATCH / DELETE); catch-helper non-leak walk pinning'Failed to fetch response'does NOT fire on the unauth branch). Cross-references the companion plural-collection siblingsurveys-id-responses-method-spec.md(pins a SPLIT-auth gate on the parentapps/web/app/api/surveys/[surveyId]/responses/route.tsroute -- the plural-collection sibling uses'Failed to fetch responses'for its 500-catch helper, this spec pins'Failed to fetch response'for the per-id helper), the companion survey detail siblingsurveys-id-method-spec.md(pins a MIXED-auth gate onapps/web/app/api/surveys/[surveyId]/route.ts), the companion survey collection siblingsurveys.spec.ts, the companion survey-existence siblingsurveys-exists-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 113-of-N and thetests/api/per-spec-file sub-rollout extends to 111-of-many, and the first per-source-file GET smoke pinning an admin-gated survey-response-by-id detail lookup lands -- pinning a'Response not found'404 envelope, a'Failed to fetch response'(singular) 500-catch helper, and a single-route GET-only export contract that no prior per-source-file GET smoke covers. - E2E Client Items Import Validate Method Spec (
apps/web-e2e/tests/api/client-items-import-validate-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped item-import-validate (dry-run) POST multipart / body / header smoke spec paired withapps/web-e2e/tests/api/client-items-import-validate-method.spec.ts, the one-hundred-and-eleventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-ninth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/client/items/import/validate/route.ts-- the first per-source-file POST smoke the docs tree publishes that pins arequireClientAuth()-gated multipart/form-data validate-only handler that delegates toItemImportService.validateRows(a dry-run service entry point -- distinct from the siblingclient/items/importroute which callsexecuteImport). UNIQUE: every prior per-source-fileclient-items*smoke (client-items-method,client-items-id-method,client-items-stats-query,client-items-import-method) parses JSON viaawait request.json(); this is the FIRST that pins arequireClientAuth()-gated handler that parsesmultipart/form-dataviaawait request.formData(). It also pins the 5-step file/mapping validation chain AFTER the gate (matches the admin siblingadmin/items/import/validatechain BUT with the longer-message client-auth envelope on the unauth branch), thesafeErrorResponse(error, 'Failed to validate import file')outer-catch helper (matches the admin sibling 500-message), the{ success: true, headers, suggestedMapping, validationResults, summary }success payload with the service-derived validation result aggregate, hard-codedduplicateStrategy: 'skip'+defaultStatus: 'pending'validation options (UNIQUE: client requests CANNOT override either via the form data -- distinct from the admin sibling which DOES accept these as form fields), and'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope (matches the priorclient-items*siblings). Distinct from EVERY prior per-source-file POST smoke:requireClientAuth()+ multipart/form-data pair (UNIQUE: the FIRST per-source-file POST smoke that gates amultipart/form-databody parse with therequireClientAuthdiscriminated-union helper -- the siblingclient-items-import-methodparses JSON, the siblingadmin-items-import-validate-bodyparses multipart but usesauth()+isAdmininstead ofrequireClientAuth()); 5-step file/mapping validation chain AFTER the gate AND AFTER the formData parse with the five 400 envelopes'No file provided.'/'Invalid file type. Only CSV and XLSX files are supported.'/'File too large. Maximum size is 10 MB.'/'Invalid column mapping JSON.'/'File contains no data rows.'(the unauth branch must NEVER reach ANY of the five steps);validateRows-not-executeImportservice call -- the load-bearing call isimportService.validateRows(parsed.rows, { ..., duplicateStrategy: 'skip', defaultStatus: 'pending' })(UNIQUE: FIRSTrequireClientAuth()-gated POST smoke pinning avalidateRows(dry-run) service entry point);{ success: true, headers, suggestedMapping, validationResults, summary }success payload (UNIQUE: FIRSTrequireClientAuth()-gated POST smoke pinning a FOUR-key success payload vsresult-keyed two-key payload ofclient-items-import-method);safeErrorResponse(error, 'Failed to validate import file')outer-catch (UNIQUE: shares thesafeErrorResponsecross-utility helper with the siblingclient-items-import-methodand the admin siblingadmin/items/import/validate-- FIRST per-source-filerequireClientAuth()-gated POST smoke pinning a multipart-form-data validate-mode catch message that BYTE-IDENTICALLY matches the admin sibling); hard-coded validation options -- the handler hard-codesduplicateStrategy: 'skip'anddefaultStatus: 'pending'when callingvalidateRows-- client requests CANNOT override either via the form data (UNIQUE: FIRSTrequireClientAuth()-gated POST smoke pinning a hard-coded validation-options contract distinct from the admin sibling which DOES acceptduplicateStrategy+defaultStatusas form fields). The POST handler combines: outer try/catch aroundrequireClientAuth()discriminated-union check,await request.formData()body parse,formData.get('file')!fileguard 400, filename whitelist 400,file.size > 10 * 1024 * 1024400,formData.get('mapping')non-null +JSON.parsefailure 400,parseCSV(...)/parseXLSX(...)thenparsed.rows.length === 0400,validateRows(parsed.rows, { columnMapping: effectiveMapping, duplicateStrategy: 'skip', defaultStatus: 'pending' })load-bearing service call, success returns{ success: true, headers, suggestedMapping, validationResults, summary }, outer catchsafeErrorResponse(error, 'Failed to validate import file'); method-resolution surface where the route exports ONLYPOST(GET / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (two bulk-loop walks -- ~9 headers + ~12 multipart bodies all asserting< 500; a longer-message TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion with noheaders/suggestedMapping/validationResults/summaryleak; a gate-before-validation-chain invariant pinning the five 400-branch messages must NEVER appear; a gate-before-catch invariant pinning that the 500-catch message never fires on unauth; a validateRows-not-entered CRITICAL invariance walk pinning that XSS markers in the multipart body are NEVER echoed back AND that the load-bearing service call NEVER executes; a success-branch-key non-disclosure walk pinning none of the four success-branch JSON keys leak; a malformed-multipart invariance walk; a file-extension invariance walk; a cross-method probe (GET / PUT / PATCH / DELETE); a side-channel walk on POST; a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every multipart permutation). Cross-references the companion client-items-import siblingclient-items-import-method-spec.md(pins therequireClientAuth()helper on the COMMIT-mode batch-import POST surface; this spec extends it into the DRY-RUN VALIDATE-mode batch-import POST surface), the companion client-items collection siblingclient-items-method-spec.md, the companion client-items per-id siblingclient-items-id-method-spec.md, the companion client-items-stats siblingclient-items-stats-query-spec.md, the companion client-protected siblingclient-protected.spec.ts, the admin-tree validate counterpart atapps/web/app/api/admin/items/import/validate/route.ts(admin-gated equivalent covered separately byadmin-items-import-validate-body.spec.ts-- usesauth()+isAdmininstead ofrequireClientAuth(), and DOES acceptduplicateStrategy+defaultStatusas form fields; the client variant does NOT), the companion client-items-import-sample sibling atapps/web/app/api/client/items/import/sample/route.ts(emits a sample CSV -- covered separately), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 111-of-N and thetests/api/per-spec-file sub-rollout extends to 109-of-many, and the first per-source-file POST smoke pinning arequireClientAuth()-gated multipart/form-data validate-only handler lands -- pinning a 5-step file/mapping validation chain, avalidateRows(dry-run) service entry point delegation, a FOUR-key{ success, headers, suggestedMapping, validationResults, summary }success payload, asafeErrorResponse(error, 'Failed to validate import file')cross-utility outer-catch helper that BYTE-IDENTICALLY matches the admin sibling, and hard-coded{ duplicateStrategy: 'skip', defaultStatus: 'pending' }validation options that client requests CANNOT override via the form data. - E2E Client Items Import Method Spec (
apps/web-e2e/tests/api/client-items-import-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client-scoped item-import POST body / header smoke spec paired withapps/web-e2e/tests/api/client-items-import-method.spec.ts, the one-hundred-and-tenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/client/items/import/route.ts-- the first per-source-file POST smoke the docs tree publishes that pins arequireClientAuth()-gated batch-import handler that delegates to anItemImportService.executeImportservice entry point. UNIQUE: every prior per-source-fileclient-items*smoke pins a CRUD-style handler (collection GET + POST list, per-id GET + PUT + DELETE, stats GET); this is the FIRST that pins a batch-import handler that fans out to a service layer. It also pins the NESTEDbody.rowsarray contract, the'Missing or invalid rows array.'Zod-free 400 message (UNIQUE: a manualArray.isArrayguard, NOT a ZodsafeParse), thesafeErrorResponse(error, 'Failed to execute import')outer-catch helper (UNIQUE -- sourced from@/lib/utils/api-error, NOTclient-auth.serverErrorResponse), and the{ success, result }success payload with the service-derived result aggregate ({ total, created, updated, skipped, errors }). Distinct from EVERY prior per-source-file POST smoke:requireClientAuth()+ service-layer delegation pair (UNIQUE: the FIRST per-source-file POST smoke that gates a service-layer batch entry point with therequireClientAuthdiscriminated-union helper); nestedbody.rowsarray contract --body.rowsMUST be anArray.isArraynon-null array, otherwise 400'Missing or invalid rows array.'(UNIQUE: FIRST per-source-file POST smoke pinning a manualArray.isArrayguard vs ZodsafeParse);safeErrorResponse(error, 'Failed to execute import')outer-catch (UNIQUE: this helper comes from@/lib/utils/api-errorNOTclient-auth.serverErrorResponse-- FIRST per-source-file POST smoke pinning thesafeErrorResponsecross-utility helper for a client-auth-gated handler);{ success, result }success payload with service-derived result aggregate --resulthas the shape{ total, created, updated, skipped, errors }(UNIQUE: FIRST per-source-file POST smoke pinning aresult-keyed success payload vsitem-keyed,subscription-keyed,data-keyed,stats-keyed prior siblings);'Unauthorized. Please sign in to continue.'longer-message TWO-key 401 envelope (matchesclient-items-method-spec.md,client-items-id-method-spec.md, andclient-items-stats-query-spec.md); hard-coded import options -- the handler hard-codesduplicateStrategy: 'skip',defaultStatus: 'pending', andsubmittedBy: userIdwhen callingexecuteImport(client requests CANNOT override any of these via the request body -- a bypass-attempt probe in the body bulk-loop walk pins this). The POST handler combines:requireClientAuth()discriminated-union check, JSON body parse (request.json()cast asClientImportRequestBody),Array.isArrayguard onbody.rows-- 400'Missing or invalid rows array.'on miss,new ItemImportService().executeImport(rows, { duplicateStrategy: 'skip', defaultStatus: 'pending', submittedBy: userId })load-bearing service call, success returns{ success: true, result }with the service-derived{ total, created, updated, skipped, errors }aggregate, outer catchsafeErrorResponse(error, 'Failed to execute import'); method-resolution surface where the route exports ONLYPOST(GET / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (two bulk-loop walks -- ~6 headers + ~10 POST bodies all asserting< 500; a longer-message TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion with noresult/total/created/updated/skipped/errorsleak; a gate-before-post-auth invariant pinning the 400-branch message, the 500-catch message, and the result-aggregate keys must NEVER appear; an executeImport-not-entered invariance walk -- CRITICAL: pinning that XSS markers in the rows array body are NEVER echoed back AND that the load-bearing service call NEVER executes; a gate-before-Array.isArray-guard invariance walk pinning that even with rows missing or non-array, response is 401 NOT 400; a cross-method probe (GET / PUT / PATCH / DELETE); a side-channel walk on POST; a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every body permutation). Cross-references the companion client-items collection siblingclient-items-method-spec.md(pins therequireClientAuthhelper on the COLLECTION-level GET + POST surface; this spec extends it into the BATCH-IMPORT POST surface), the companion client-items per-id siblingclient-items-id-method-spec.md(pins therequireClientAuthhelper on the PER-ID GET + PUT + DELETE surface), the companion client-items-stats siblingclient-items-stats-query-spec.md(uses the samerequireClientAuth()helper on a single GET surface), the companion client-protected siblingclient-protected.spec.ts, the admin-tree import counterpart atapps/web/app/api/admin/items/import/route.ts(admin-gated equivalent covered separately byadmin-items-import-body.spec.ts), the companion client-items-import-validate sibling atapps/web/app/api/client/items/import/validate/route.ts(validates rows pre-execute -- covered separately), the companion client-items-import-sample sibling atapps/web/app/api/client/items/import/sample/route.ts(emits a sample CSV -- covered separately), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 110-of-N and thetests/api/per-spec-file sub-rollout extends to 108-of-many, and the first per-source-file POST smoke pinning arequireClientAuth()-gated batch-import handler that delegates to a service-layer entry point lands -- pinning a nestedbody.rowsArray.isArrayguard contract, asafeErrorResponse(error, 'Failed to execute import')cross-utility outer-catch helper, a{ success, result }service-derived result-aggregate success payload, and hard-coded{ duplicateStrategy, defaultStatus, submittedBy }import options that no prior POST smoke covers. - E2E Client Items Method Spec (
apps/web-e2e/tests/api/client-items-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client items collection-level GET + POST body / header smoke spec paired withapps/web-e2e/tests/api/client-items-method.spec.ts, the one-hundred-and-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-sixth underapps/web-e2e/tests/api/. Pairs with theGETANDPOSTexports ofapps/web/app/api/client/items/route.ts— the first per-source-file dual-method smoke the docs tree publishes that pins therequireClientAuth()helper-based auth gate on BOTH GET AND POST (theclient-items-stats-query-spec.mdsibling pins the helper on a single GET surface; this spec extends to the dual-method usage). Also pins thebadRequestResponse(message)400-helper and the issues-joined Zod error message contract. Distinct from EVERY prior dual-method smoke:requireClientAuth()helper on BOTH methods (FIRST per-source-file dual-method smoke pinning the discriminated-union auth-helper return contract on both GET AND POST exports);badRequestResponse(message)400-helper (UNIQUE -- a NEW helper distinct fromsafeErrorResponseandserverErrorResponse-- FIRST per-source-file smoke pinning a dedicated 400-builder helper); issues-joined Zod error message --validationResult.error.issues.map((issue) => issue.message).join(', ')(UNIQUE: FIRST per-source-file smoke pinning a comma-joined Zod-issues 400 message, vs taking only the first issue like in the sponsor-ads-user sibling); GET success payload with FLAT keys at top level --{ success, items, total, page, limit, totalPages, stats }(UNIQUE: nodatawrapper, nopaginationwrapper, flat shape -- FIRST per-source-file GET smoke pinning a flat-pagination success payload); POST returns 201 status (NOT 200) with a review-workflow success message'Item submitted successfully. It will be reviewed by our team before being published.';?deleted=truequery branches to a different repo method (findDeletedByUservsfindByUserPaginated-- UNIQUE: FIRST per-source-file GET smoke pinning a query-driven repo-method dispatch contract);'Unauthorized. Please sign in to continue.'longer-message TWO-key envelope (matches theclient-items-stats-querysibling). The handlers combine: GET handler withrequireClientAuth(),clientItemsListQuerySchema.safeParse(query),?deleted=truebranch tofindDeletedByUserelsefindByUserPaginated, success returns flat payload; POST handler withrequireClientAuth(), JSON body parse,clientCreateItemSchema.safeParse(body),clientItemRepository.createAsClient(userId, validated)load-bearing DB write, success returns 201 with{ success, item, message }; method-resolution surface where the route exports GET + POST (PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~6 headers × 2 methods + ~9 POST bodies all asserting< 500; longer-message TWO-key 401-envelope assertions on GET AND POST; a cross-method 401-envelope-equality assertion; a strict TWO-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear including the review-workflow success message; a createAsClient-not-entered invariance walk on POST -- CRITICAL: pinning that XSS markers in the body are NEVER echoed back; a gate-before-Zod-query-validation invariance walk on GET pinning that invalid query values still produce 401 NOT 400; a gate-before-Zod-body-validation invariance walk on POST pinning that invalid body shapes still produce 401 NOT 400; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk on POST). Cross-references the companion client items-stats siblingclient-items-stats-query-spec.md(uses the samerequireClientAuth()helper on a single GET surface), the client per-id sibling atapps/web/app/api/client/items/[id]/route.ts(per-item resource -- covered separately), the companion client-protected siblingclient-protected.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 108-of-N and thetests/api/per-spec-file sub-rollout extends to 106-of-many, and the first per-source-file dual-method smoke pinning therequireClientAuth()helper on BOTH methods lands -- pinning abadRequestResponse(message)400-helper contract, an issues-joined Zod error message contract, a flat-pagination GET success payload, a query-driven repo-method dispatch contract, and a review-workflow POST success message that no prior dual-method smoke covers. - E2E Sponsor Ads User [id] Query Spec (
apps/web-e2e/tests/api/sponsor-ads-user-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-id user-scoped sponsor-ad lookup GET dynamic-segment / header smoke spec paired withapps/web-e2e/tests/api/sponsor-ads-user-id-query.spec.ts, the one-hundred-and-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fifth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/sponsor-ads/user/[id]/route.ts— the first per-source-file dynamic-segment GET smoke the docs tree publishes that pins a 404-mask user-scoped IDOR -- whensponsorAd.userId !== session.user.id, the handler returns 404'Sponsor ad not found'(NOT 403 'Forbidden') with the SAME envelope as the genuine not-found branch. UNIQUE: the FIRST per-source-file dynamic-segment GET smoke pinning a 404-mask security pattern on a USER-OWNED resource (the surveys-id sibling pins a 404-mask on STATUS-gated admin resources; this sponsor-ads-user-id sibling pins the pattern on a per-user-ownership resource). Distinct from EVERY prior dynamic-segment GET smoke: 404-mask user-scoped IDOR (UNIQUE -- the 404 envelope for cross-user access is BYTE-IDENTICAL to the 404 envelope for genuinely-non-existent IDs); TWO-key 401 envelope{ success: false, error: 'Unauthorized' }; TWO-key 404 envelope{ success: false, error: 'Sponsor ad not found' }used for BOTH not-found AND IDOR violations (intentional masking); TWO-key success payload{ success: true, data: <sponsorAd> }; TWO-key 500 envelope{ success: false, error: 'Failed to fetch sponsor ad' }. The GET handler combinesauth()session lookup (!session?.user?.id→ 401 TWO-key),{ id } = await paramsdynamic-segment resolution, the load-bearinggetSponsorAdById(id)DB read,!sponsorAdcheck (→ 404 TWO-key{ success: false, error: 'Sponsor ad not found' }), 404-mask user-scoped IDOR check (sponsorAd.userId !== session.user.id→ 404 with the SAME envelope -- intentional masking of cross-user access), success payload{ success: true, data: <sponsorAd> }with status 200, outer catch 500 TWO-key{ success: false, error: 'Failed to fetch sponsor ad' }, and method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk asserting< 500; a canonical TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion viaObject.keys(body).sort(); a gate-before-post-auth invariant pinning that'Sponsor ad not found'and'Failed to fetch sponsor ad'must NEVER appear; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a getSponsorAdById-not-entered invariance walk -- CRITICAL: pinning that nodata.userId/data.itemSlug/data.itemName/data.paymentProviderfields from a sponsor-ad row are leaked; a cross-id invariance walk pinning that the auth gate fires BEFORE any per-id branch -- the 404-mask is unreachable on unauth; a catch-branch-not-entered invariance walk pinning that the 500 catch dispatcher never fires on unauth). Cross-references the companion sponsor-ads cancel siblingsponsor-ads-user-id-cancel-body-spec.md(POST verb on the same[id]segment), the companion sponsor-ads renew siblingsponsor-ads-user-id-renew-body-spec.md, the collection-level GET + POST siblingsponsor-ads-user-method-spec.md(Zod-safeParse on both query and body), the surveys-id 404-mask siblingsurveys-id-method-spec.md(404-mask pattern on STATUS-gated admin resources -- vs this user-ownership pattern), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 107-of-N and thetests/api/per-spec-file sub-rollout extends to 105-of-many, and the first per-source-file dynamic-segment GET smoke pinning a 404-mask user-scoped IDOR on a per-user-ownership resource lands -- pinning a 404-mask security pattern that BYTE-IDENTICALLY masks cross-user access as not-found (vs the surveys-id sibling's status-based 404-mask) that no prior dynamic-segment GET smoke covers. - E2E Sponsor Ads User Stats Query Spec (
apps/web-e2e/tests/api/sponsor-ads-user-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-scoped sponsor-ads stats GET header smoke spec paired withapps/web-e2e/tests/api/sponsor-ads-user-stats-query.spec.ts, the one-hundred-and-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-fourth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/sponsor-ads/user/stats/route.ts-- the first per-source-file GET smoke the docs tree publishes that pins a THREE-bucket nested-stats success payload --{ success: true, stats: { overview, byInterval, revenue } }where each bucket has its own required-keys contract:overviewhas SEVEN status-bucket counts (total,pendingPayment,pending,active,rejected,expired,cancelled);byIntervalhas TWO billing-interval counts (weekly,monthly);revenuehas THREE revenue rollups in minor currency units (totalRevenue,weeklyRevenue,monthlyRevenue). UNIQUE -- every prior per-source-file GET stats smoke pins a flat shallow stats key set; this is the FIRST that pins a THREE-bucket nested-stats invariant where thestatsobject is a triple-nested aggregate. Distinct from EVERY prior session-gated GET smoke: THREE-bucket nested-stats success payload (UNIQUE); bareauth()session lookup distinct from therequireClientAuth()discriminated-union helper used byclient-items-stats-query-spec.md; TWO-key 401 envelope{ success: false, error: 'Unauthorized' }(same shape as thesponsor-ads/userparent route, distinct from the bare ONE-key envelope used byuser-paymentsandsubscriptionsiblings); TWO-key success payload{ success: true, stats }(usesstatskey NOTdata); service-call delegation --sponsorAdService.getSponsorAdStatsByUser(session.user.id)is the ONLY post-auth load-bearing call (no DB-helper layer); TWO-key 500 catch envelope{ success: false, error: 'Failed to fetch sponsor ad stats' }(distinct from parent route's'Failed to fetch sponsor ads'and'Failed to create sponsor ad'); zero-arg GET signatureexport async function GET()with NOrequest/contextarguments. The handler combines:auth()session lookup → 401 TWO-key envelope on miss;sponsorAdService.getSponsorAdStatsByUser(userId)load-bearing service call returning the THREE-bucket aggregate; success payload{ success: true, stats: { overview, byInterval, revenue } }; outer catch returns 500{ success: false, error: 'Failed to fetch sponsor ad stats' }; method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (a single header bulk-loop walk -- ~6 headers all asserting< 500; a canonical TWO-key 401-envelope assertion; a strict TWO-key envelope-shape assertion; a gate-before-post-auth invariant pinning the'Failed to fetch sponsor ad stats'message must NEVER appear; a sponsorAdService-not-entered CRITICAL invariance walk pinning that NEITHER the bucket namesoverview/byInterval/revenueNOR the inner status / interval / revenue keys leak on unauth; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a catch-branch isolation walk pinning the 500-catch never fires on unauth; a cross-permutation status invariance walk). Cross-references the companion sponsor-ads parent siblingsponsor-ads-user-method-spec.md(covers the GET + POST surface of the parent/sponsor-ads/userroute), the companion sponsor-ads cancel siblingsponsor-ads-user-id-cancel-body-spec.md, the companion sponsor-ads renew siblingsponsor-ads-user-id-renew-body-spec.md, the companion client-items-stats siblingclient-items-stats-query-spec.md(another per-source-file stats GET smoke that uses therequireClientAuthhelper instead of bareauth()), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 106-of-N and thetests/api/per-spec-file sub-rollout extends to 104-of-many, and the first per-source-file GET smoke pinning a THREE-bucket nested-stats payload lands -- pinning a triple-nested aggregate{ overview, byInterval, revenue }invariant where prior stats smokes only ever pinned flat shallow stats key sets. - E2E Stripe Subscriptions Method Spec (
apps/web-e2e/tests/api/stripe-subscriptions-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe collection-level subscriptions (plural) GET + POST + PUT + DELETE body / header smoke spec paired withapps/web-e2e/tests/api/stripe-subscriptions-method.spec.ts, the one-hundred-and-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-second underapps/web-e2e/tests/api/. Pairs with theGET,POST,PUT, ANDDELETEexports ofapps/web/app/api/stripe/subscriptions/route.ts— the first per-source-file QUAD-method smoke the docs tree publishes (every prior smoke covers 1, 2, or 3 methods). Distinct from the singular sibling at/api/stripe/subscription: PROPER USER-SCOPED IDOR on PUT AND DELETE --subscription.userId !== session.user.id→ 404'Subscription not found'(CONTRAST withstripe-subscription-method-spec.mdwhich has NO IDOR -- Q-010 finding -- this plural sibling does it correctly). Distinct from EVERY prior method-method smoke: FOUR-method export (GET + POST + PUT + DELETE -- UNIQUE: FIRST per-source-file QUAD-method smoke); GET conditional response shape (?active=truereturns{ data, plan, limits }; default returns{ data, history, meta }-- UNIQUE: FIRST per-source-file GET smoke pinning a conditional response shape based on query); POST returns 201 status (NOT 200 -- UNIQUE among Stripe POST smokes); POST 409 Conflict for existing active subscription (UNIQUE: FIRST per-source-file POST smoke pinning a 409 Conflict status code); query-string DELETE -- DELETE uses query parameters (?id=&reason=&cancelAtPeriodEnd=) NOT body (UNIQUE: FIRST per-source-file DELETE smoke pinning a query-driven mutating DELETE -- vs body-driven DELETE in every other sibling); DYNAMIC success message on DELETE based on?cancelAtPeriodEnd=trueflag; bare ONE-key 401 envelope consistent across ALL FOUR methods; three-field required-check on POST with comma-joined-field-list 400 message'Missing required fields: planId, paymentProvider, subscriptionId'(UNIQUE: FIRST per-source-file POST smoke pinning a comma-joined-field-list 400 message). The handlers combine: GET handler withauth(), query parsing (?active=true,?history=true), branch on activeOnly vs all, success returns conditional shape; POST handler withauth(), JSON body parse, THREE-required-field check,hasActiveSubscriptioncheck → 409,createSubscription(...)load-bearing call, success returns 201 with{ data, message: 'Subscription created successfully' }; PUT handler withauth(), JSON body parse,!subscriptionId→ 400,getSubscriptionById+ USER-SCOPED IDOR (!subscription || subscription.userId !== session.user.id→ 404),updateSubscription(...)load-bearing call, success returns{ data, message: 'Subscription updated successfully' }; DELETE handler withauth(), query parsing,!id→ 400,getSubscriptionById+ USER-SCOPED IDOR → 404,cancelSubscription(...)load-bearing call, success returns DYNAMIC message based oncancelAtPeriodEndflag; method-resolution surface where the route exports GET + POST + PUT + DELETE (PATCH must round-trip to< 500). Documents the at-a-glance scenario tree (four header bulk-loop walks -- ~6 headers × 4 methods asserting< 500; canonical bare ONE-key 401-envelope assertions on GET AND POST; a cross-method 401-envelope-equality assertion across all four methods; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear including dynamic success messages and the 409 conflict message; an updateSubscription-not-entered invariance walk on PUT -- CRITICAL: pinning that XSS markers in the PUT body are NEVER echoed back; a cancelSubscription-not-entered invariance walk on DELETE with query-string ID -- CRITICAL: pinning that the query-string id is NEVER echoed back; a cross-query invariance walk on GET pinning that different?active=/?history=query permutations produce IDENTICAL unauth envelopes; a cross-method probe (PATCH); a side-channel walk on POST; a required-field-check-not-entered invariance walk on POST pinning that even empty-body POST returns 401 NOT 400). Cross-references the singular siblingstripe-subscription-method-spec.md(NO IDOR -- Q-010 finding -- this plural sibling DOES have proper user-scoped IDOR), the per-id update siblingstripe-subscription-id-update-body-spec.md(different IDOR pattern viagetSubscriptionByProviderSubscriptionId), the per-id cancel siblingstripe-subscription-id-cancel-body-spec.md(uses POST verb -- not DELETE), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 104-of-N and thetests/api/per-spec-file sub-rollout extends to 102-of-many, and the first per-source-file QUAD-method smoke lands -- pinning a conditional-GET-response-shape contract, a 201-status POST contract, a 409-Conflict contract, a query-driven DELETE contract, a dynamic-success-message-on-DELETE contract, and a comma-joined-field-list 400 message contract that no prior multi-method smoke covers. - E2E Stripe Subscription Method Spec (
apps/web-e2e/tests/api/stripe-subscription-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe collection-level subscription POST + PUT + DELETE body / header smoke spec paired withapps/web-e2e/tests/api/stripe-subscription-method.spec.ts, the one-hundred-and-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundred-and-first underapps/web-e2e/tests/api/. Pairs with thePOST,PUT, ANDDELETEexports ofapps/web/app/api/stripe/subscription/route.ts— the first per-source-file triple-method smoke the docs tree publishes for a Stripe subscription-management endpoint that documents a Q-010-style NO-IDOR finding on PUT AND DELETE -- the handlers authenticate the session but DO NOT verify that thesubscriptionIdfrom the body actually belongs to the calling user. ANY authenticated user can update or cancel ANY Stripe subscription by ID, bypassing the IDOR checks of the per-id siblings ([subscriptionId]/update,[subscriptionId]/cancel). Distinct from EVERY prior triple-method smoke: NO IDOR check on PUT or DELETE (UNIQUE -- FIRST per-source-file triple-method smoke pinning a Q-010-style NO-IDOR finding on mutating subscriptionId-keyed methods); different body-required field on POST vs PUT/DELETE -- POST requirespriceId+paymentMethodId; PUT requiressubscriptionId; DELETE requiressubscriptionId(UNIQUE: FIRST per-source-file triple-method smoke pinning three DIFFERENT required-field shapes on the three methods); POST 400'Failed to create customer'branch (UNIQUE -- only POST has the!customerIdcheck; PUT and DELETE skip the customer-id resolution); returns RAW Stripe subscription object verbatim on ALL THREE methods (UNIQUE: no wrapper envelope on success);metadata: { userId: session.user.id }OVERWRITE on PUT (UNIQUE -- PUT writes the CALLER'S userId into the subscription's metadata regardless of who actually owns the subscription -- compounds the Q-010 finding by enabling ownership-record laundering); bare ONE-key 401 envelope{ error: 'Unauthorized' }consistent across all three methods. The handlers combine: POST handler with!session?.user→ 401, JSON body parse,getOrCreateStripeProvider,getCustomerId(session.user),!customerId→ 400'Failed to create customer',createSubscription(...)load-bearing call, success returns raw Stripe subscription object; PUT handler with!session?.user→ 401, JSON body parse,getOrCreateStripeProvider,updateSubscription({ subscriptionId, priceId, cancelAtPeriodEnd, metadata })-- NO IDOR CHECK, success returns raw Stripe subscription object; DELETE handler with!session?.user→ 401, JSON body parse,getOrCreateStripeProvider,cancelSubscription(subscriptionId, cancelAtPeriodEnd)-- NO IDOR CHECK, success returns raw Stripe subscription object; method-resolution surface where the route exports POST + PUT + DELETE (GET / PATCH must round-trip to< 500). Documents the at-a-glance scenario tree (three header bulk-loop walks -- ~6 headers × 3 methods asserting< 500; canonical bare ONE-key 401-envelope assertions on POST, PUT, AND DELETE; a cross-method 401-envelope-equality assertion pinning byte-identical envelopes across all three methods; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear; an updateSubscription-not-entered invariance walk on PUT -- CRITICAL: even though PUT has NO IDOR check post-auth, the auth gate itself must fire BEFORE updateSubscription (no XSS-marker leak); a cancelSubscription-not-entered invariance walk on DELETE -- CRITICAL: same gate-before invariant; a cross-method probe (GET / PATCH); a side-channel walk on POST). Cross-references the per-id update siblingstripe-subscription-id-update-body-spec.md(DOES enforce a user-scoped IDOR check -- the proper way to update a subscription), the per-id cancel siblingstripe-subscription-id-cancel-body-spec.md(NO IDOR -- already a known finding; this collection-level route extends the no-IDOR surface to PUT), the per-id reactivate siblingstripe-subscription-id-reactivate-body-spec.md(tenant-only IDOR), the Stripe billing-portal siblingstripe-subscription-portal-body-spec.md,docs/questions.mdfor the Q-### entry tracking the no-IDOR finding, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 103-of-N and thetests/api/per-spec-file sub-rollout extends to 101-of-many, and the first per-source-file triple-method smoke pinning a Q-010-style NO-IDOR finding on PUT AND DELETE lands -- pinning a no-IDOR-on-mutating-method contract (compounding by metadata-userId overwrite), three-different-required-field-shapes contract on POST/PUT/DELETE, and a raw-Stripe-object success contract that no prior triple-method smoke covers. - E2E Client Items Stats Query Spec (
apps/web-e2e/tests/api/client-items-stats-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's client items-stats GET header smoke spec paired withapps/web-e2e/tests/api/client-items-stats-query.spec.ts, the one-hundred-and-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the one-hundredth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/client/items/stats/route.ts— the first per-source-file GET smoke the docs tree publishes that pins therequireClientAuth()helper-based auth gate with the'Unauthorized. Please sign in to continue.'longer-message TWO-key envelope. UNIQUE: a different auth-helper abstraction than the bareauth()session lookup used in every other per-source-file smoke; uses the explicitclient-authutility helpers (requireClientAuth,serverErrorResponse). Distinct from EVERY prior GET smoke:requireClientAuth()helper-based auth gate (UNIQUE -- returns a discriminated union{ success: false, response: NextResponse }on failure or{ success: true, userId: string }on success -- FIRST per-source-file GET smoke pinning a discriminated-union auth-helper return contract);'Unauthorized. Please sign in to continue.'401 envelope message (UNIQUE -- longer specific message naming the action -- distinct from bare'Unauthorized','Unauthorized. Admin access required.'admin-tree, and'Authentication required'Stripe siblings); TWO-key 401 envelope{ success: false, error: 'Unauthorized. Please sign in to continue.' }; TWO-key success payload{ success: true, stats: <statsObject> }(UNIQUE: usesstatskey NOTdatalike most success payloads);serverErrorResponse(error, 'Failed to fetch statistics')outer catch (UNIQUE helper distinct fromsafeErrorResponse); zero-arg GET signature (export async function GET()with NOrequest/contextarguments). The GET handler combinesrequireClientAuth()discriminated-union auth-helper,getClientItemRepository()repository factory, the load-bearingclientItemRepository.getStatsByUser(userId)DB read, success payload{ success: true, stats: { total, draft, pending, approved, rejected, deleted } }, outer catchserverErrorResponse(error, 'Failed to fetch statistics'), and method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk asserting< 500; a longer-message TWO-key 401-envelope assertion pinningsuccess: false,error: 'Unauthorized. Please sign in to continue.'; a strict envelope-shape assertion viaObject.keys(body).sort() === ['error', 'success']; a gate-before-post-auth invariant pinning that NEITHER'Failed to fetch statistics'NOR the success-branch stats keys leak; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a clientItemRepository-getStatsByUser-not-entered invariance walk -- CRITICAL: pinning that the load-bearing DB read NEVER runs on unauth; a cross-permutation status invariance walk). Cross-references the companion client-dashboard-stats siblingclient-dashboard-stats-query.spec.ts(anotherrequireClientAuth-gated stats endpoint), the companion client-protected siblingclient-protected.spec.ts, the companion client-geo-stats siblingclient-geo-stats-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 102-of-N and thetests/api/per-spec-file sub-rollout extends to 100-of-many (a centennial milestone for thetests/api/sub-rollout), and the first per-source-file GET smoke pinning therequireClientAuth()helper-based auth-gate contract with a discriminated-union return type lands -- pinning a longer-message TWO-key 401 envelope, astats-keyed success payload (vsdata-keyed), and aserverErrorResponseerror helper that no prior GET smoke covers. - E2E Payment Account [userId] Query Spec (
apps/web-e2e/tests/api/payment-account-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-user payment-account-lookup GET dynamic-segment / query / header smoke spec paired withapps/web-e2e/tests/api/payment-account-id-query.spec.ts, the one-hundred-and-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-ninth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/payment/account/[userId]/route.ts— the first per-source-file dynamic-segment GET smoke the docs tree publishes that pins a strict user-id-IDOR check (session.user.id !== params.userId→ 403 bare{ error: 'Forbidden' }with NO message specifying ownership) on a per-user resource lookup endpoint. CRITICAL: provides the auth-gated counterpart to thepayment-account-method-spec.mdsibling which has NO auth gate (Q-010 finding) -- documenting a security-asymmetry where the GET on[userId]IS auth-gated while the POST/PUT on the parent route are NOT. Distinct from EVERY prior dynamic-segment GET smoke: strict user-id-IDOR check with bare'Forbidden'message (UNIQUE -- distinct frompayment-id-method's'Forbidden: You do not own this subscription'); userId-then-IDOR-then-provider validation order (UNIQUE -- the IDOR check is INTERLEAVED between two validation checks, the FIRST per-source-file GET smoke pinning an IDOR check placed mid-validation-cascade);?provider=query parameter required (consistent with the POST/PUT siblings' body-required check; this GET reads it from the query string); 404 with bare envelope{ error: 'Payment account not found' }(UNIQUE: FIRST per-source-file GET smoke pinning a 404 with this literal message); returns raw paymentAccount fields in success (matches POST/PUT siblings); DOES haveauth()gate (CONTRAST with the no-auth-gate POST/PUT siblings on the same parent route -- Q-010 finding -- the FIRST per-source-file GET smoke documenting an auth-gated GET sibling of an unguarded POST/PUT pair). The GET handler combinesauth()session lookup (!session?.user?.id→ 401 bare ONE-key),{ userId } = await paramsdynamic-segment resolution,searchParams.get('provider')query extraction,!userId400 check (impossible from dynamic segment but pinned), IDOR check (session.user.id !== userId→ 403 bare{ error: 'Forbidden' }),!provider400 check, the load-bearinggetUserPaymentAccountByProvider(userId, provider)DB read, 404 if null, success payload as raw paymentAccount fields, outer catch 500, and method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk + a ~8-query bulk-loop walk all asserting< 500; a canonical bare ONE-key 401-envelope assertion; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear including'Forbidden','Provider is required','Payment account not found'; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a getUserPaymentAccountByProvider-not-entered invariance walk -- CRITICAL: pinning that the load-bearing DB read NEVER runs on unauth and noproviderId/customerId/createdAtis leaked; a cross-userId invariance walk pinning that different user IDs produce IDENTICAL unauth envelopes -- the auth gate fires BEFORE the IDOR check; a cross-provider invariance walk pinning that different?provider=values produce IDENTICAL unauth envelopes). Cross-references the companion POST + PUT siblingpayment-account-method-spec.md(NO auth gate -- Q-010 finding -- this GET sibling IS auth-gated and does have an IDOR check), the companion subscription-update payment siblingpayment-id-method-spec.md(uses a different IDOR message'Forbidden: You do not own this subscription'),docs/questions.mdfor the Q-### entry tracking the security-asymmetry finding (auth-gated GET vs no-auth-gate POST/PUT on the same parent route), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 101-of-N and thetests/api/per-spec-file sub-rollout extends to 99-of-many, and the first per-source-file dynamic-segment GET smoke pinning an IDOR-mid-validation-cascade contract lands -- pinning a strict user-id-IDOR check with bare'Forbidden'message, an interleaved validation-order contract, and a security-asymmetry finding (auth-gated GET vs no-auth-gate POST/PUT on the same parent) that no prior dynamic-segment smoke covers; completing thepayment/accounttriplet (POST + PUT no-auth + GET-by-userId auth-gated). - E2E Payment Account Method Spec (
apps/web-e2e/tests/api/payment-account-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's payment-account create / update POST + PUT body / header smoke spec paired withapps/web-e2e/tests/api/payment-account-method.spec.ts, the one-hundredth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTANDPUTexports ofapps/web/app/api/payment/account/route.ts— the first per-source-file dual-method smoke the docs tree publishes that documents a Q-010-style NO-AUTH-GATE finding for a non-admin mutating route on BOTH POST AND PUT exports — the handler has NOauth()call, NO ownership check; ANY caller can create a payment account for ANYuserId+customerId(POST) OR update any payment account byid(PUT). The smoke spec pins this finding as the CURRENT contract -- a future PR that adds auth would explicitly break this spec, prompting an update. Distinct from EVERY prior dual-method smoke: NOauth()gate on EITHER method (FIRST per-source-file dual-method smoke pinning a Q-010-style no-auth-gate finding on BOTH POST AND PUT exports); NO ownership check (POST trusts the caller-supplieduserId+customerIddirectly; PUT trusts the caller-suppliedid);setupUserPaymentAccount(provider, userId, customerId)runs UNCONDITIONALLY on both POST and PUT (UNIQUE: PUT does NOT check that theidmatches an existing record; it just callssetupUserPaymentAccountwith the body fields -- effectively the same logic as POST plus anidgate); THREE-required-field cascade on POST (provider, userId, customerId) and FOUR-required-field cascade on PUT (id, provider, userId, customerId) with each emitting a distinct 400 message via individualif (!field)checks (UNIQUE: FIRST per-source-file dual-method smoke pinning a per-field individual-required-check chain); bare ONE-key 400 envelope{ error: 'Field X is required' }(NOsuccesskey); bare ONE-key 500 envelope{ error: 'Internal server error' }(NOsuccesskey); returns raw paymentAccount fields in success payload{ id, userId, providerId, customerId, createdAt, updatedAt }(NO wrapper envelope -- UNIQUE: most success responses use{ success: true, data: {...} }). The handlers combine: POST handler with NO auth gate, JSON body parse, required-field cascade (provider → userId → customerId, each individually checked with distinct 400 message), load-bearingsetupUserPaymentAccount(...)UNCONDITIONAL DB write, success payload as raw paymentAccount fields, outer catch 500'Internal server error'; PUT handler with NO auth gate, JSON body parse, required-field cascade (id → provider → userId → customerId, each individually checked with distinct 400 message), load-bearingsetupUserPaymentAccount(...)UNCONDITIONAL DB write (NOT an actual update byid), success payload as raw paymentAccount fields, outer catch 500'Internal server error'; method-resolution surface where the route exports POST AND PUT (GET / PATCH / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (four bulk-loop walks -- ~6 headers × 2 methods + ~7 POST bodies + ~6 PUT bodies all asserting< 500; a NO-401 contract assertion on BOTH POST and PUT pinning that the response is NEVER 401 nor 403; an auth-signal-ignored contract walk pinning that fabricated session cookies / Authorization headers / X-User-Id headers produce the SAME status as bare requests; a required-field cascade canonical-messages assertion pinning the three distinct 400 messages on POST --'Provider is required','User ID is required','Customer ID is required'; a PUT FOUR-required-field cascade assertion pinning that'Account ID is required'fires FIRST when all fields are missing; a strict ONE-key 400 envelope-shape assertion; a cross-method probe (GET / PATCH / DELETE); a no-catch-on-valid-body contract pinning that valid-shape requests do NOT 5xx). Cross-references the companion subscription-update payment siblingpayment-id-method-spec.md(DOES enforce auth + ownership -- distinct from this no-auth-gate sibling), the provider-specific payment-methods siblingstripe-payment-methods-create-body-spec.md(auth-gated),docs/questions.mdfor the Q-### entry tracking the no-auth-gate finding, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 100-of-N (a centennial milestone) and thetests/api/per-spec-file sub-rollout extends to 98-of-many, and the first Q-010-style no-auth-gate finding for a dual-method (POST + PUT) non-admin mutating route lands -- pinning the no-auth-gate contract on BOTH methods, a per-field individual-required-check cascade contract, and a raw-paymentAccount-fields success-payload contract that no prior dual-method smoke covers. - E2E Payment [subscriptionId] Method Spec (
apps/web-e2e/tests/api/payment-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-subscription auto-renewal GET + PATCH dynamic-segment / body / header smoke spec paired withapps/web-e2e/tests/api/payment-id-method.spec.ts, the ninety-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-seventh underapps/web-e2e/tests/api/. Pairs with theGETANDPATCHexports ofapps/web/app/api/payment/[subscriptionId]/route.ts— the first per-source-file dual-method smoke the docs tree publishes that pins a provider-agnostic auto-renewal toggle (the handler acceptsprovider/paymentProvidervalues from thePaymentProviderenum and routes the sync viagetOrCreateProvider(provider), working with Stripe, LemonSqueezy, Polar, Solidgate). Distinct from EVERY prior dual-method smoke: provider-agnostic dual-method (FIRST per-source-file smoke pinning agetOrCreateProvider(provider)dispatch contract on a per-subscription endpoint -- vs the per-provider Stripe / LemonSqueezy / Polar siblings which hardcode their provider); provider-source split -- GET readsproviderfrom the QUERY STRING (?provider=), PATCH readspaymentProviderfrom the BODY (UNIQUE: FIRST per-source-file dual-method smoke pinning a SAME-NAMED-FIELD-from-DIFFERENT-SOURCES contract); dynamic enum-validation 400 message --'Invalid payment provider. Must be one of: stripe, lemonsqueezy, polar, solidgate'(UNIQUE: FIRST per-source-file smoke pinning a 400 message that DYNAMICALLY lists valid enum values viavalidProviders.join(', ')); TWO distinct body-validation 400 messages on PATCH --'Invalid JSON in request body'(catch aroundawait request.json()) vs'Invalid request body. Expected a JSON object.'(post-parse non-object check) -- FIRST per-source-file PATCH smoke pinning a two-tier body-validation chain; explicittypeof enabled !== 'boolean'type-check --'Invalid request body. "enabled" must be a boolean.'(UNIQUE: pre-Zod boolean type-validation, vs Zod's opaque error messages); user-scoped IDOR with explicit message --'Forbidden: You do not own this subscription'(UNIQUE: FIRST per-source-file smoke pinning a user-scoped 403 message that names ownership); best-effort provider sync (if the provider sync call throws, the local DB update is preserved -- handler logs and returns success; UNIQUE: FIRST per-source-file PATCH smoke pinning a best-effort provider sync after a successful local DB write); dynamic success message (responsemessagefield is one of TWO distinct strings based on theenabledtoggle -- 'Auto-renewal has been enabled...' / 'disabled...'). The handlers combine GET handler withauth()session lookup (!session?.user?.id→ 401 ONE-key bare envelope),{ subscriptionId } = await params,searchParams.get('provider') || 'stripe'provider extraction, enum validation, subscription lookup, user-scoped IDOR check (subscription.userId !== session.user.id→ 403), success payload{ subscriptionId, autoRenewal, cancelAtPeriodEnd, endDate }; PATCH handler withauth()session lookup, JSON body parse with try/catch (400 on malformed), non-object check (400 on array / null), typeof-enabled boolean check (400 on non-bool), enum validation, subscription lookup with same user-scoped IDOR,subscriptionService.setAutoRenewal(subscription.id, enabled)load-bearing DB write, best-effort provider-sync viagetOrCreateProvider(paymentProvider).updateSubscription(...), success payload{ success: true, subscription: <updatedSubscription>, message: <dynamic> }; method-resolution surface where the route exports GET AND PATCH (POST / PUT / DELETE must round-trip to< 500). Documents the at-a-glance scenario tree (three bulk-loop walks -- ~6 headers × 2 methods + ~13 PATCH bodies all asserting< 500; canonical bare ONE-key 401-envelope assertions on GET AND PATCH; a cross-method 401-envelope-equality assertion pinning byte-identical envelopes; a strict ONE-key envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of nine candidate messages must appear including the dynamic success messages; a setAutoRenewal-not-entered invariance walk on PATCH -- CRITICAL: pinning that XSS markers in the body are NEVER echoed back; a gate-before-enum-validation invariance walk on GET pinning that?provider=fake-providerdoes NOT trigger the dynamic 400 message on unauth; a cross-method probe (POST / PUT / DELETE); a side-channel walk on PATCH; a gate-before-body-validation invariance walk pinning that malformed JSON / array body / non-bool enabled all produce 401 NOT 400; a cross-subscription-ID invariance walk pinning that different IDs produce IDENTICAL unauth envelopes -- the auth gate fires BEFORE any per-subscription-id branch). Cross-references the Stripe-specific subscription-update siblingstripe-subscription-id-update-body-spec.md(hardcodes the Stripe provider vs this provider-agnostic dispatch), the Stripe-specific subscription-cancel siblingstripe-subscription-id-cancel-body-spec.md, the Stripe-specific subscription-reactivate siblingstripe-subscription-id-reactivate-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 99-of-N and thetests/api/per-spec-file sub-rollout extends to 97-of-many, and the first per-source-file dual-method smoke pinning a provider-agnostic auto-renewal toggle lands -- pinning agetOrCreateProviderdispatch contract, a SAME-NAMED-FIELD-from-DIFFERENT-SOURCES contract (provider in query for GET, paymentProvider in body for PATCH), a dynamic enum-validation 400 message contract, a two-tier body-validation chain contract, an explicit boolean type-check contract, and a best-effort provider-sync contract that no prior dual-method smoke covers. - E2E Verify ReCAPTCHA Body Spec (
apps/web-e2e/tests/api/verify-recaptcha-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's ReCAPTCHA verification proxy POST body / header smoke spec paired withapps/web-e2e/tests/api/verify-recaptcha-body.spec.ts, the ninety-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-sixth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/verify-recaptcha/route.ts— the first per-source-file smoke the docs tree publishes that pins a dev-mode bypass envelope with status 200{ success: true, score: 1.0, action: 'bypass' }(whenRECAPTCHA_SECRET_KEYis missing ANDNODE_ENV === 'development'). It is also the first smoke the docs tree publishes that pins a route built on top of theexternalClient.postForm<T>(url, body)helper (form-encoded outbound POST to Google'shttps://www.google.com/recaptcha/api/siteverifyendpoint) AND the first smoke that pins theerror_codesunderscore-rename invariant (Google returnserror-codeswith a hyphen; the handler renames it toerror_codeswith an underscore in the response envelope). The cross-cuttingmethod-guards.spec.tsALSO probesPOST /api/verify-recaptchaBUT only checks that an empty-object body produces a non-5xx response; this per-source-file spec drills into the four-branch dispatcher (token-required-400 / dev-bypass-200 / not-configured-500 / Google-proxy-pass-through). Distinct from EVERY prior POST smoke: form-encoded outbound POST viaexternalClient.postForm(UNIQUE -- every other proxy POST in the docs tree usesfetch/externalClient.postJSON body);error_codesunderscore-rename invariant (UNIQUE -- no other proxy in the docs tree performs this hyphen-to-underscore rename); score / action surface (UNIQUE -- no other smoke exercises ReCAPTCHA scoring fields); dev-mode bypass branch (UNIQUE -- NO other smoke pins a 200 dev-bypass envelope withaction: 'bypass'); not-configured 500 branch (status 500 with NO stack trace / sensitive content). The POST handler combines a JSON body parse viaawait request.json()(wrapped in outertry / catchso malformed JSON falls through to the 500 catch), anif (!token)token-required gate (→ 400{ success: false, error: 'ReCAPTCHA token is required' }), an!secretKeydev-bypass / not-configured branch (bifurcates oncoreConfig.NODE_ENV === 'development'), anexternalClient.postFormGoogle siteverify proxy (secret: secretKey, response: tokenform-encoded body), anapiUtils.isSuccess(response)check (on failure → 500{ success: false, error: 'Failed to verify ReCAPTCHA' }), a renamed-envelope success pass-through ({ success: data.success, score: data.score, action: data.action, hostname: data.hostname, challenge_ts: data.challenge_ts, error_codes: data['error-codes'] }-- HYPHEN → UNDERSCORE rename oferror-codes), an outer catch (→ 500{ success: false, error: 'Verification failed' }), and method-resolution surface where the route exports ONLYPOST(GET / PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (a ~12-header bulk-loop walk + a ~16-body bulk-loop walk asserting non-crashing status; the load-bearing 400 token-required envelope assertion{ success: false, error: 'ReCAPTCHA token is required' }; a strict envelope-shape assertion with NOfeatureDisabledkey -- DIFFERENT from extract-body sibling; a falsy-token uniformity assertion; a no-token-echo invariant; a side-channel walk; a cross-method probe; a bypass-attempt-body-keys invariance assertion -- user-suppliedRECAPTCHA_SECRET_KEY/secret/NODE_ENVbody fields are IGNORED; an env-driven dispatch invariant assertion -- response MUST match exactly ONE of dev-bypass / not-configured / Google-proxy envelopes; the HYPHEN → UNDERSCORE rename invariant assertion; a 500-envelope no-leak assertion -- CRITICAL: NEVER leaksstack/cause/RECAPTCHA_SECRET_KEY/secretKey/siteverify/google.comfragments; an outer-catch malformed-JSON fallback assertion; a truthy non-string token gate-semantics assertion; a gate-before-post-validation order assertion). Cross-references the cross-cuttingmethod-guards.spec.ts, the neighbouringextract-body-spec.md(also covers a POST-only proxy endpoint BUT uses Zod validation AND afeatureDisabledenvelope -- 200 status,featureDisabled: truekey; this verify-recaptcha sibling uses hand-rolledif (!token)validation AND aerror: 'ReCAPTCHA token is required'envelope -- 400 status, NOfeatureDisabledkey), and to Spec 010 -- E2E Test Coverage for the governing spec and Spec 008 -- Analytics Providers for the analytics-providers spec the verify-recaptcha route sits inside. With this entry the per-spec-file docs rollout extends to 98-of-N and thetests/api/per-spec-file sub-rollout extends to 96-of-many, and the first per-source-file POST smoke pinning a dev-mode bypass envelope + form-encoded outbound POST +error_codesunderscore-rename invariant lands -- pinning a four-branch env-driven dispatcher contract, a Google-siteverify proxy contract, and a hyphen-to-underscore rename invariant that no prior POST smoke covers. - E2E Cron Subscription Reminders Method Spec (
apps/web-e2e/tests/api/cron-subscription-reminders-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's subscription-reminders cron GET + POST header smoke spec paired withapps/web-e2e/tests/api/cron-subscription-reminders-method.spec.ts, the ninety-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-fifth underapps/web-e2e/tests/api/. Pairs with theGETANDPOSTexports ofapps/web/app/api/cron/subscription-reminders/route.ts— the first per-source-file smoke the docs tree publishes that pins a 207 Multi-Status partial-success response (the handler returns 207 whenresult.success === false). The existing multi-cron siblingcron-jobs.spec.tscovers OTHER cron routes; this spec drills into the subscription-reminders handler specifically. Distinct from EVERY prior cron smoke: timing-safe comparison on the FULLAuthorizationheader --Buffer.from(authHeader)is compared toBuffer.from(\Bearer ${cronSecret}`)(UNIQUE: every other cron handler compares ONLY the secret portion after strippingBearer); **BARE ONE-key 401 envelope**{ error: 'Unauthorized' }(NOsuccesskey, NOmessagefield -- DIFFERENT from the cron-subscription-expiration sibling's TWO-key envelope); **207 Multi-Status response** (UNIQUE -- the FIRST per-source-file smoke pinning a 207 partial-success status code); **spread-result success / error pattern** (both branches spread the entire result object into the response --{ message: 'Subscription reminder job completed', ...result }and{ error: 'Job completed with errors', ...result }-- UNIQUE: distinct from subscription-expiration which constructs an explicitdataenvelope); **GET + POST dual-method-delegate exports** (POST simply doesreturn GET(request), matches subscription-expiration sibling); **outer catch viasafeErrorResponse(error, 'Cron job failed')** (distinct message vs subscription-expiration's'Failed to process expired subscriptions'). The handler combines theverifyCronSecret(request)helper (Bearer-token check with timing-safe comparison on the FULL authHeader), the dev-mode short-circuit, the load-bearingsubscriptionRenewalReminderJob()reminder-job call, the conditional 207 branch (if (!result.success)→ 207 Multi-Status with spread-result envelope), success payload{ message: 'Subscription reminder job completed', ...result }with status 200, outer catch withsafeErrorResponse(error, 'Cron job failed'), and method-resolution surface where the route exports GET AND POST (POST delegates to GET; PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (two header bulk-loop walks -- ~9 headers × 2 methods asserting< 500; a BARE ONE-key 401 envelope assertion when no Authorization header is present pinningerror: 'Unauthorized'with NOsuccess/messagekeys; a strict envelope-shape assertion when 401 is reached; a no-Bearer-secret-echo invariant; a timing-safe length-mismatch handling assertion on the FULL header pinning that BOTH a too-short AND a too-long Authorization header produce< 500; a POST-delegates-to-GET assertion pinning byte-identical envelopes on POST and GET unauth branches; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk; a subscriptionRenewalReminderJob-not-entered invariance walk -- CRITICAL: the load-bearing reminder-job call NEVER runs on unauth and no spread-result key likeprocessed/remindedis leaked; a gate-before-post-auth invariant; a no-207-on-unauth invariant pinning that the 207 partial-success status code is NEVER reached on the unauth branch). Cross-references the subscription-expiration cron sibling [cron-subscription-expiration-method-spec.md](./plugins/cron-subscription-expiration-method-spec.md) (uses ALSO timing-safe comparison BUT compares ONLY the secret portion afterBearerstripped and emits a TWO-key 401 envelope; this spec compares the FULLAuthorizationheader and emits a BARE ONE-key envelope), the cron/sync GET sibling [cron-sync-query-spec.md](./plugins/cron-sync-query-spec.md) (uses a DIFFERENT cron-auth contract -- exact string match, NOT timing-safe), the multi-cron sibling [cron-jobs.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/cron-jobs.spec.ts), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) for the governing spec. With this entry the **per-spec-file docs rollout extends to 97-of-N** and the **tests/api/` per-spec-file sub-rollout extends to 95-of-many**, and the first per-source-file smoke pinning a 207 Multi-Status partial-success response + timing-safe comparison on the FULL Authorization header + BARE ONE-key cron 401 envelope lands -- pinning a partial-success status-code contract, a full-header constant-time comparison contract, and a spread-result success/error envelope pattern that no prior cron smoke covers; completing the cron triplet (sync + expiration + reminders) on per-source-file coverage. - E2E Cron Subscription Expiration Method Spec (
apps/web-e2e/tests/api/cron-subscription-expiration-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's subscription-expiration cron GET + POST header smoke spec paired withapps/web-e2e/tests/api/cron-subscription-expiration-method.spec.ts, the ninety-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-fourth underapps/web-e2e/tests/api/. Pairs with theGETANDPOSTexports ofapps/web/app/api/cron/subscription-expiration/route.ts— the first per-source-file smoke the docs tree publishes that pins a timing-safe Bearer-token comparison viacrypto.timingSafeEqual. The existing multi-cron siblingcron-jobs.spec.tscovers the OTHER cron routes; this spec drills into the subscription-expiration handler specifically AND its GET + POST dual-method-delegate export pattern. Distinct from EVERY prior cron smoke: timing-safe Bearer-token comparison viacrypto.timingSafeEqual(Buffer.from(provided), Buffer.from(cronSecret))-- the FIRST per-source-file smoke pinning a constant-time comparison contract on a Bearer-token-gated endpoint; length-equality short-circuit --providedSecret.length !== cronSecret.length→ false (avoidstimingSafeEquallength-mismatch throw -- UNIQUE);authHeader.replace('Bearer ', '')parsing -- extracts the token via.replace(...)rather than exact-match comparison like the cron/sync sibling; TWO-key 401 envelope{ success: false, message: 'Unauthorized - Invalid or missing cron secret' }(UNIQUE: longer specific message naming the failure mode -- vs cron/sync's'Unauthorized'-- usesmessagenoterror); GET + POST dual-method-delegate exports -- POST simply doesreturn GET(request)(UNIQUE: FIRST per-source-file smoke pinning a method-delegate POST that re-routes to GET verbatim); email-service best-effort side-effect -- if the email service is unavailable, the cron does NOT fail; PII-strippedaffectedUsers-- the response includes{ subscriptionId, userId, planId }per affected user but NEVERemail(intentional PII protection). The handler combines theverifyCronSecret(request)helper (Bearer-token check with timing-safe comparison), the dev-mode short-circuit (if (!cronSecret && process.env.NODE_ENV === 'development')→ bypass), the load-bearingsubscriptionService.processExpiredSubscriptions()DB-write call, the PII-strip transformation, the email-service side-effect (best-effort, does NOT fail), success payload{ success: true, message: 'Processed X expired subscriptions', data: { processed, affectedUsers, errors, timestamp } }with status 200, outer catch withsafeErrorResponse(error, 'Failed to process expired subscriptions'), and method-resolution surface where the route exports GET AND POST (POST delegates to GET; PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (two header bulk-loop walks -- ~9 headers × 2 methods including various Authorization probes -- wrong Bearer, empty Bearer, non-Bearer scheme, Basic auth, Bearer with double-space -- plus side-channels asserting< 500; a TWO-key 401 envelope assertion when no Authorization header is present pinningsuccess: false,message: 'Unauthorized - Invalid or missing cron secret', and NOerrorkey; a strict envelope-shape assertion viaObject.keys(body).sort()when 401 is reached; a no-Bearer-secret-echo invariant; a timing-safe length-mismatch handling assertion pinning that BOTH a too-short AND a too-long Bearer token produce< 500; a POST-delegates-to-GET assertion pinning that POST returns the SAME envelope as GET on the unauth branch; a cross-method probe (PUT / PATCH / DELETE); a side-channel walk; a processExpiredSubscriptions-not-entered invariance walk -- CRITICAL: the load-bearing DB-write call NEVER runs on unauth and noaffectedUsers/processed/subscriptionIdis leaked; a gate-before-post-auth invariant). Cross-references the cron/sync GET siblingcron-sync-query-spec.md(uses a DIFFERENT cron-auth contract -- exactBearer ${CRON_SECRET}string match, NOT timing-safe; cron/sync emits a 4-key 401 envelope vs this spec's TWO-key envelope), the multi-cron siblingcron-jobs.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 96-of-N and thetests/api/per-spec-file sub-rollout extends to 94-of-many, and the first per-source-file smoke pinning a timing-safe Bearer-token comparison + GET + POST dual-method-delegate lands -- pinning the constant-time comparison contract, the length-equality short-circuit contract, and the POST-delegates-to-GET contract that no prior smoke covers. - E2E Subscription Query Spec (
apps/web-e2e/tests/api/subscription-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's authenticated user-subscription GET query-param smoke spec paired withapps/web-e2e/tests/api/subscription-query.spec.ts, the ninety-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-third underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/user/subscription/route.ts— the first per-source-file GET smoke the docs tree publishes that pins an OBJECT-wrapped success response for a Stripe-customer-session endpoint where the no-customer-found branch ALSO returns an OBJECT (withhasActiveSubscription: false+ amessagefield). UNIQUE — the direct siblinguser-payments-query.spec.tsreturns a top-level ARRAY for the same Stripe-customer-session pattern; together the two specs pin the divergence between the two response shapes that share an identical auth-gate + customer-resolution prologue. Distinct from EVERY prior session-gated GET smoke: OBJECT-wrapped success response (UNIQUE -- the FIRST per-source-file GET smoke pinning an OBJECT-wrapped Stripe-customer-derived response where the no-customer-found 200 branch ALSO returns an OBJECT, vs the empty-array fallback in the user-payments sibling); no-customer-found 200 OBJECT{ hasActiveSubscription: false, message: 'No Stripe customer found' }(distinct from the user-payments sibling's[]empty-array fallback -- sameauth()+getCustomerId(...)prologue, different fallback shape); bare ONE-key{ error: 'Unauthorized' }401 envelope (NOsuccesskey, NOmessagekey -- same shape as user-payments sibling); two-tier 500 catch dispatcher -- inner Stripe-error → 500'Failed to fetch subscription data from Stripe'; outer → 500'Failed to fetch subscription data'(matches the user-payments sibling's two-tier dispatcher pattern but with subscription-specific messages); zero-arg GET signature (export async function GET()with NOrequest/contextarguments -- matches user-payments sibling); Stripe Subscriptions list withexpand: ['data.default_payment_method'](UNIQUE -- the FIRST per-source-file GET smoke pinning a Stripe expansion-list invariant; the user-payments sibling pins a DUAL-listinvoices.list+subscriptions.listbut neither is expanded); active-subscription discriminator --sub.status === 'active' || sub.status === 'trialing'isolates acurrentSubscriptionfrom history (UNIQUE -- the FIRST per-source-file GET smoke pinning an active-or-trialing-only current-subscription discriminator); cents-to-major-units transform -- everyamountis divided by 100 to convert from Stripe's cents representation to major currency units (UNIQUE -- the FIRST per-source-file GET smoke pinning a cents-to-major-units transform on the success branch); currency uppercase invariant --sub.currency.toUpperCase()is applied to every subscription (UNIQUE -- the FIRST per-source-file GET smoke pinning a currency-case-normalisation invariant); caller-supplied-Stripe-key bypass attempt walked -- the spec walks?stripeKey=/?stripe_key=query parameters to pin that the handler does NOT forward a caller-supplied Stripe API key (CRITICAL -- prevents a future regression that would let a caller substitute their own Stripe key and read another customer's data). The GET handler combinesauth()session lookup (!session?.user?.id→ 401 ONE-key{ error: 'Unauthorized' }),initializeStripeProvider()+getStripeInstance()AFTER the auth gate, the load-bearing customer-id lookupstripeProvider.getCustomerId(session.user)(null → 200 OBJECT{ hasActiveSubscription: false, message: 'No Stripe customer found' }), the load-bearingstripe.subscriptions.list({ customer, limit: 100, expand: ['data.default_payment_method'] })Stripe SDK call, the active-subscription discriminator, the transform (cents-to-major-units, currency-uppercase, ISO-format timestamps), inner catch 500'Failed to fetch subscription data from Stripe', outer catch 500'Failed to fetch subscription data', and method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (a query-string bulk-loop walk over many parameter permutations -- no-arg baseline, admin-impersonation candidates?userId=/?customerId=/?stripeCustomerId=, magic-token candidates?token=, dangerous-passthrough candidates?stripeKey=, status-claim candidates?status=active, plus structural permutations -- all asserting< 500; a canonical ONE-key 401-envelope assertion on the unauth branch; a cross-query envelope-byte-equality assertion pinning that the 401 envelope is byte-identical across query permutations; a?userId=/?customerId=/?stripeCustomerId=walk pinning that no admin-impersonation key short-circuits the session gate or customer-resolution step (CRITICAL); a?stripeKey=/?stripe_key=walk pinning that no caller-supplied Stripe key is forwarded to the Stripe SDK (CRITICAL); a?token=walk pinning that no magic-token query parameter short-circuits the session gate (CRITICAL); a cross-permutation status-and-shape invariance assertion pinning that every query permutation produces the same status and the same envelope shape). Cross-references the companion payment-history GET siblinguser-payments-query.spec.ts(SAME pattern but ARRAY-wrapped response), the Stripe billing-portal POST siblingstripe-subscription-portal-body-spec.md(SAMEauth()+ Stripe customer lookup but on a different HTTP method), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 95-of-N and thetests/api/per-spec-file sub-rollout extends to 93-of-many, and the first per-source-file GET smoke pinning an OBJECT-wrapped Stripe-customer-derived response lands -- pinning a no-customer-found 200 OBJECT fallback contract (vs the user-payments sibling's empty-array fallback), a Stripe expansion-list invariant, an active-or-trialing-only current-subscription discriminator, a cents-to-major-units transform invariant, a currency-uppercase invariant, and a caller-supplied-Stripe-key forwarding-prevention invariant that no prior GET smoke covers. - E2E Favorites [itemSlug] Method Spec (
apps/web-e2e/tests/api/favorites-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-favorite remove DELETE dynamic-segment / header smoke spec paired withapps/web-e2e/tests/api/favorites-id-method.spec.ts, the ninety-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-second underapps/web-e2e/tests/api/. Pairs with theDELETEexport ofapps/web/app/api/favorites/[itemSlug]/route.ts— the first per-source-file DELETE smoke the docs tree publishes that pins a THREE-field tenant-scoped IDOR check + SELECT-then-DELETE pattern on a non-admin per-item DELETE route. The companion collection-level POST + GET siblingfavorites.spec.tscoversapps/web/app/api/favorites/route.ts. Distinct from EVERY prior DELETE smoke:checkDatabaseAvailability()as the FIRST gate (returns 503 with the DATABASE_UNAVAILABLE envelope whenDATABASE_URLis missing -- the auth check fires AFTER the DB-availability check); TWO-key{ success: false, error: 'Unauthorized' }401 envelope + TWO-key{ success: false, error: 'Tenant not found' }403 envelope (TWO distinct gate-failure statuses with the SAME envelope shape but DIFFERENT messages -- UNIQUE: the FIRST per-source-file DELETE smoke pinning a 401 → 403 → 404 cascade with three distinct messages on the same TWO-key envelope shape); THREE-field tenant-scoped IDOR check -- the SELECT + DELETE WHERE clauses BOTH match onuserId === session.user.idANDitemSlug === path.itemSlugANDtenantId === currentTenantId(UNIQUE: the FIRST per-source-file DELETE smoke pinning a three-field tenant-scoped IDOR check); SELECT-then-DELETE pattern -- the handler runs an inlinedb.select().from(favorites).where(...).limit(1)BEFORE the DELETE to surface a 404 if not found (distinct from single-step DELETE WHERE which would silently no-op); TWO-key success payload{ success: true, message: 'Favorite removed successfully' }with NOdatafield (UNIQUE: most DELETE handlers returndata: { ... }with deletion details). The DELETE handler combinescheckDatabaseAvailability()(FIRST gate; 503 if DB unavailable),auth()session lookup (!session?.user?.id→ 401 TWO-key),getTenantId()(null → 403 TWO-key{ success: false, error: 'Tenant not found' }),{ itemSlug } = await paramsdynamic-segment resolution, the SELECT pre-check viadb.select().from(favorites).where(userId + itemSlug + tenantId).limit(1)(empty → 404 TWO-key{ success: false, error: 'Favorite not found' }), the DELETE WHERE with the same three-field clause, success payload{ success: true, message: 'Favorite removed successfully' }with status 200, outer catch withsafeErrorResponse(error, 'Failed to remove favorite'), and method-resolution surface where the route exports ONLYDELETE(GET / POST / PUT / PATCH must round-trip to< 500). Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk includingX-Tenant-IdandX-User-Idside-channel probes asserting< 500; a canonical TWO-key 401-envelope assertion accepting either 401 OR 503 (both pre-IDOR); a strict envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear on the unauth branch; a side-channel walk including X-Tenant-Id; a cross-method probe (GET / POST / PUT / PATCH); a SELECT-pre-check-and-DELETE-WHERE-not-entered invariance walk -- CRITICAL: pinning that the load-bearing SELECT pre-check AND the DELETE mutation NEVER run on unauth and that an XSS marker in the itemSlug URL is NEVER echoed back; a catch-branch-dispatcher-not-entered invariance walk pinning that'Failed to remove favorite'is NOT echoed on the unauth branch; a cross-permutation status invariance walk; a cross-itemSlug invariance walk pinning that different slugs produce IDENTICAL unauth envelopes -- the auth gate fires BEFORE any per-item-slug branch). Cross-references the companion collection-level POST + GET siblingfavorites.spec.ts, the companion engagement / favorites combination specitems-engagement-and-favorites.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 94-of-N and thetests/api/per-spec-file sub-rollout extends to 92-of-many, and the first per-source-file DELETE smoke pinning a three-field tenant-scoped IDOR check + SELECT-then-DELETE pattern lands -- pinning a 401 → 403 → 404 cascade contract with three distinct messages on the same TWO-key envelope shape that no prior DELETE smoke covers. - E2E Surveys [surveyId] Method Spec (
apps/web-e2e/tests/api/surveys-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-survey detail GET / PUT / DELETE dynamic-segment / body / header smoke spec paired withapps/web-e2e/tests/api/surveys-id-method.spec.ts, the ninety-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninety-first underapps/web-e2e/tests/api/. Pairs with theGET,PUT, ANDDELETEexports ofapps/web/app/api/surveys/[surveyId]/route.ts— the first per-source-file triple-method smoke the docs tree publishes that pins a MIXED-auth gate contract -- GET is publicly accessible for published surveys but admin-gated for unpublished surveys (with a UNIQUE 404-mask: non-admin callers see404 'Survey not found'INSTEAD of403 'Forbidden'); PUT and DELETE are admin-only. Distinct from EVERY prior triple-method smoke: MIXED-auth gate (FIRST per-source-file triple-method smoke pinning a public-GET + admin-PUT + admin-DELETE pattern, vs admin-collections-[id] which is admin-gated on ALL three methods); 404-mask on GET for non-published surveys (UNIQUE -- the FIRST per-source-file GET smoke pinning a 404-mask security pattern that hides the existence of unpublished resources from non-admins, with non-admin callers seeing404 'Survey not found'for both not-found-at-all AND not-published-and-not-admin); ID-or-slug fallback lookup (UNIQUE -- the FIRST per-source-file dynamic-segment GET smoke pinning a dual-lookup-by-id-or-slug contract, the handler triessurveyService.getOne(surveyId)first then falls back tosurveyService.getBySlug(surveyId));error.message === 'Survey not found'catch-branch dispatch on PUT and DELETE (UNIQUE -- the FIRST per-source-file PUT/DELETE smoke pinning anError.messageequality-match catch-dispatcher that re-emits 404); TWO-key{ success: false, error: 'Unauthorized' }401 envelope on PUT and DELETE;data: nullin DELETE success payload (UNUSUAL -- most DELETE handlers omitdataor returndata: { ... }). The handlers combine: GET handler withsurveyService.getOne(surveyId)→ fallback tosurveyService.getBySlug(surveyId), 404 if both null,auth()gate AFTER the lookup only for non-published surveys, 404-mask if not admin, success payload{ success: true, data: <survey> }; PUT handler withauth()session lookup (!session?.user?.isAdmin→ 401 TWO-key), JSON body parse, ID-or-slug fallback lookup, 404 if both null,surveyService.update(survey.id, body), success payload{ success: true, data: <updatedSurvey>, message: 'Survey updated successfully' }, outer catch withError.message === 'Survey not found'→ re-emit 404 elsesafeErrorResponse(error, 'Failed to update survey'); DELETE handler withauth()session lookup (!session?.user?.isAdmin→ 401 TWO-key), ID-or-slug fallback lookup, 404 if both null,surveyService.delete(survey.id), success payload{ success: true, data: null, message: 'Survey deleted successfully' }, same catch-dispatcher as PUT; method-resolution surface where the route exports GET / PUT / DELETE (POST / PATCH must round-trip to< 500). Documents the at-a-glance scenario tree (four bulk-loop walks -- ~6 headers × 3 methods + ~9 PUT bodies all asserting< 500; a canonical 404 envelope assertion for non-existent surveys on GET; canonical 401-envelope assertions on PUT AND DELETE; a cross-method 401-envelope-equality assertion pinning byte-identical envelopes; a strict envelope-shape assertion; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear on PUT unauth; a surveyService-update-not-entered invariance walk on PUT -- CRITICAL: pinning that XSS markers are NEVER echoed back; a surveyService-delete-not-entered invariance walk on DELETE -- CRITICAL; a cross-method probe (POST / PATCH); side-channel walks on PUT and DELETE; a catch-branch-dispatcher-not-entered invariance walk pinning that'Survey not found'is NOT echoed on the PUT unauth branch). Cross-references the companion surveys list / collection-level POST siblingsurveys.spec.ts, the companion surveys-exists query siblingsurveys-exists-query.spec.ts, the triple-method admin siblingadmin-collections-id-method-spec.md(uses ALL-admin-gated GET / PUT / DELETE), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 93-of-N and thetests/api/per-spec-file sub-rollout extends to 91-of-many, and the first per-source-file triple-method smoke pinning a MIXED-auth gate contract lands -- pinning a 404-mask security pattern, an ID-or-slug fallback-lookup contract, and anError.messageequality-match catch-dispatcher that no prior triple-method smoke covers. - E2E User Payments Query Spec (
apps/web-e2e/tests/api/user-payments-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's authenticated user-payment-history GET header smoke spec paired withapps/web-e2e/tests/api/user-payments-query.spec.ts, the ninety-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninetieth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/user/payments/route.ts— the first per-source-file GET smoke the docs tree publishes that pins a top-level-ARRAY success response (NOT an object wrapper). The handler returns either[](when the caller has no Stripe customer) OR a top-level payment-history array (transformed Stripe invoice data). UNIQUE — every prior per-source-file GET smoke pins an object-shaped response; this is the FIRST that pins a bare-array shape. Siblingsubscription-query.spec.tscovers theGETexport ofapps/web/app/api/user/subscription/route.ts-- SAMEauth()gate + Stripe customer lookup + two-tier catch dispatcher pattern, but returns an OBJECT-wrapped response. Distinct from EVERY prior session-gated GET smoke: top-level-ARRAY success response (UNIQUE -- the 200-branch returnspaymentHistoryas a JS array directly viaNextResponse.json(paymentHistory)); no-customer-found 200 EMPTY ARRAY[](ifcustomerIdis null, the handler returns[]with status 200, NOT 401 / 404 / 4xx -- distinct from thesubscriptionsibling which returns{ hasActiveSubscription: false, message: 'No Stripe customer found' }); bare ONE-key{ error: 'Unauthorized' }401 envelope (NOsuccesskey, NOmessagekey -- same shape assubscriptionsibling); two-tier 500 catch dispatcher -- inner Stripe-error → 500'Failed to fetch payment data from Stripe'; outer → 500'Failed to fetch payment data'(UNIQUE -- TWO different 500 messages with the SAME ONE-key{ error }shape); zero-arg GET signature (export async function GET()with NOrequest/contextarguments); Stripe Invoices + Subscriptions DUAL-list load-bearing chain -- the handler calls BOTHstripe.invoices.listANDstripe.subscriptions.listto enrich each invoice with subscription metadata (FIRST per-source-file GET smoke pinning a dual-Stripe-list invariant); filtered status whitelist (only invoices withstatus === 'paid' || status === 'open'appear in the response). The GET handler combinesauth()session lookup (!session?.user?.id→ 401 ONE-key{ error: 'Unauthorized' }),initializeStripeProvider()+getStripeInstance()AFTER the auth gate, the load-bearing customer-id lookupstripe.getCustomerId(session.user as any)(null → 200 EMPTY ARRAY[]),stripe.invoices.list({ customer, limit: 100 })load-bearing invoice list,stripe.subscriptions.list({ customer, limit: 100 })load-bearing subscription list (DUAL-list invariant), filter + transform + sort (paid/open only, sort by date desc), inner catch 500'Failed to fetch payment data from Stripe', outer catch 500'Failed to fetch payment data', and method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk asserting< 500; a canonical ONE-key 401-envelope assertion; a strict ONE-key envelope-shape assertion (nosuccess/message/dataleak); a no-array-leak CRITICAL invariant pinning that the unauth response is NEVER an array; a gate-before-post-auth invariant pinning that NEITHER 500 message must appear in any unauth response; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a Stripe-SDK-calls-not-entered invariance walk -- CRITICAL:initializeStripeProvider/invoices.list/subscriptions.list/getCustomerIdmust NEVER run on unauth (nohosted_invoice_url/invoice_pdf/amount_paid/paymentProvider/subscriptionId/billingIntervalleak); a two-tier-catch-dispatcher-not-entered invariance walk; a no-stripe-error-message-leak invariant; a cross-permutation status invariance walk). Cross-references the companion subscription GET siblingsubscription-query.spec.ts(SAME pattern but OBJECT-wrapped response), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 92-of-N and thetests/api/per-spec-file sub-rollout extends to 90-of-many, and the first per-source-file GET smoke pinning a top-level-ARRAY success response lands -- pinning a no-customer-found 200 empty-array fallback contract, a two-tier 500 catch dispatcher with two distinct messages on the same envelope shape, and a Stripe Invoices + Subscriptions DUAL-list load-bearing invariant that no prior GET smoke covers. - E2E Cron Sync Query Spec (
apps/web-e2e/tests/api/cron-sync-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Vercel-cron sync GET header smoke spec paired withapps/web-e2e/tests/api/cron-sync-query.spec.ts, the ninety-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-ninth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/cron/sync/route.ts— the first per-source-file GET smoke the docs tree publishes for a Bearer-token-secret-gated cron endpoint. The existing multi-cron siblingcron-jobs.spec.tscovers the OTHER cron routes (subscription-expiration, subscription-reminders); this spec drills into the cron/sync handler specifically. Distinct from EVERY prior GET smoke: Bearer-token-secret auth -- the handler accepts ONLYAuthorization: Bearer ${CRON_SECRET}(NOT session-based auth) -- the FIRST per-source-file GET smoke pinning a Bearer-token-only auth contract; dev-mode short-circuit -- ifCRON_SECRETis NOT configured AND env isdevelopment, the handler allows access without auth (same pattern as lemonsqueezy/update's dev-mode short-circuit); FOUR-key 401 envelope{ success: false, timestamp: <ISO>, duration: <ms>, message: 'Unauthorized' }-- UNIQUE: NOerrorfield; usesmessage(noterror) for the auth failure -- the FIRST per-source-file smoke pinning a 401 envelope WITHOUT anerrorfield; performance tracking viastartTime = Date.now()andduration: Date.now() - startTimein BOTH the unauth response AND the success/catch responses (matches lemonsqueezy/update's richest-envelope spec but with amessage-only envelope); customCache-Control: no-cache, no-store, must-revalidateheader on success; conditional success status --{ status: result.success ? 200 : 500 }based on the sync result. The GET handler combines a Bearer-token-secret check (request.headers.get('authorization') === \Bearer ${cronSecret}`-- failure → 401 4-key envelope unless dev-mode short-circuit), the dev-mode short-circuit (if (!cronSecret && isDevelopment)→ bypass), the load-bearingtriggerManualSync()call, success payload{ success, timestamp, duration, message, details? }with status 200 (ifresult.success) or 500, outer catch withsafeErrorMessage(error, 'Unknown error')extracted into the catch envelope at status 500, and a method-resolution surface where the route exports ONLYGET(POST / PUT / PATCH / DELETE must round-trip to a< 500status). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk including various Authorization probes -- wrong Bearer, empty Bearer, non-Bearer scheme, Basic auth -- plus side-channels asserting< 500; a 4-key 401 envelope assertion when no Authorization header is present pinningsuccess: false,message: 'Unauthorized', ISOtimestamp, numericduration, and NOerrorkey; a strict envelope-shape assertion viaObject.keys(body).sort()when 401 is reached; an ISO timestamp regex assertion on whichever envelope is reached; a numeric duration field assertion -- the FIRST per-source-file GET smoke pinning request-duration measurement on the unauth branch; a no-Bearer-secret-echo invariant pinning that the caller-supplied secret marker is NEVER echoed in the response body; a cross-method probe (POST / PUT / PATCH / DELETE); a side-channel walk pinning that fabricated session cookies / X-User-Id / X-Forwarded-For do NOT bypass the Bearer-token auth; a triggerManualSync-not-entered invariance walk pinning that the load-bearing sync call NEVER runs on unauth and nodetailsfrom a sync result is leaked). Cross-references the multi-cron sibling [cron-jobs.spec.ts](https://github.com/ever-works/directory-web-template/tree/develop/apps/web-e2e/tests/api/cron-jobs.spec.ts), the LemonSqueezy update POST sibling [lemonsqueezy-update-body-spec.md](./plugins/lemonsqueezy-update-body-spec.md) (also pins a dev-mode short-circuit + performance-tracking envelope but with a different shape), and to [Spec 010 -- E2E Test Coverage](https://github.com/ever-works/directory-web-template/tree/develop/docs/spec/010-e2e-test-coverage) for the governing spec. With this entry the **per-spec-file docs rollout extends to 91-of-N** and the **tests/api/per-spec-file sub-rollout extends to 89-of-many**, and the **first per-source-file GET smoke for a Bearer-token-secret-gated cron endpoint** lands -- pinning a 4-key 401 envelope WITHOUT anerror` field, performance-tracking measurement on the unauth branch, and a Bearer-token-only auth contract that no prior GET smoke covers. - E2E Stripe Subscription Portal Body Spec (
apps/web-e2e/tests/api/stripe-subscription-portal-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe billing-portal session-creation POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-subscription-portal-body.spec.ts, the ninetieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/subscription/portal/route.ts— the first per-source-file POST smoke the docs tree publishes for a Stripe billing-portal session-creation endpoint. Distinct from EVERY prior Stripe per-source-file POST smoke AND the polar-subscription-portal-body sibling: zero-arg POST signature (UNIQUE — the FIRST per-source-file POST smoke pinning a zero-arg POST contract with NOrequest/contextarguments — every other Stripe POST handler reads the request to parse a body or extract a header);!session?.usergate with a ONE-key{ error: 'Unauthorized' }envelope (NOsuccesskey, NOmessagekey — distinct from stripe-checkout's TWO-key shape and stripe-setup-intent-id's{ success: false, error }shape);getCustomerId(...)returns null → 404 ONE-key{ error: 'Stripe customer ID not found' };buildUrl('/settings/billing')+new URL(...)URL-validation contract — invalid URL throws → 500 TWO-key{ error: 'Invalid return URL configuration', message: 'The application URL is not properly configured' }(UNIQUE — no prior per-source-file smoke pins anew URL()validation contract on a constructed return URL); FOUR-key Stripe-error catch envelope onbillingPortal.sessions.create(...)failures → 400{ error: 'Invalid request to Stripe', message, code, type }(UNIQUE — the FIRST per-source-file POST smoke pinning a FOUR-key envelope with bothcodeANDtypefields surfaced from the Stripe error object, vs payment-methods-create's THREE-key shape and other handlers' TWO-key shapes); structuredLogger.create('StripePortal')call in the inner catch (FIRST per-source-file POST smoke pinning a structured-logger contract on the inner Stripe-error branch);safeErrorMessagehelper in BOTH the inner-stripe-error catch AND the outer catch; TWO-key outer-catch 500 envelope{ error: 'Failed to create billing portal session', message }. The POST handler combinesauth()session lookup (!session?.user→ 401 ONE-key),initializeStripeProvider()+getStripeInstance()AFTER auth gate,stripe.getCustomerId(session.user as any)load-bearing customer-id lookup (null → 404),buildUrl('/settings/billing')+new URL(...)URL-validation (invalid → 500 TWO-key),stripeInstance.billingPortal.sessions.create(...)load-bearing Stripe SDK call wrapped in INNER try/catch, inner-stripe-error catch (400 FOUR-key), success payload 200{ success: true, data: response, message: 'Billing portal session created' }, outer catch 500 TWO-key, and method-resolution surface (the route exports ONLYPOST;GET/PUT/PATCH/DELETEmust round-trip to a< 500status). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~9-body bulk-loop walk all asserting< 500; a canonical 401 ONE-key Unauthorized envelope assertion; a strict ONE-key envelope-shape assertion pinning thatsuccess/message/data/code/typekeys are all undefined; a no-portal-url-leak CRITICAL security invariant pinning that the success-branch portalurl/id/customermust NEVER leak; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear on the unauth branch; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); an initializeStripeProvider-and-getCustomerId-and-billingPortal-sessions-create-not-entered invariance walk — CRITICAL — pinning that the load-bearing Stripe SDK calls NEVER run on unauth; a URL-validation-catch-not-entered invariance walk pinning the gate-before-URL-validation order; an inner-stripe-error-catch-FOUR-key-envelope-not-entered invariance walk — CRITICAL — pinning that the FOUR-key{ error, message, code, type }envelope NEVER surfaces on unauth; an outer-catch-not-entered invariance walk pinning the gate-before-outer-catch order; a no-stripe-error-message-leak invariant pinning that'No such customer'/'resource_missing'/'billing_portal'substrings NEVER leak; a body-shape invariance walk pinning that the unauth 401 envelope is IDENTICAL across body shapes (the handler is zero-arg POST and never reads the body); a cross-permutation status-invariance assertion; a no-XSS / open-redirect leak invariant pinning that hostile body content never appears in the response). Cross-references the Polar customer-portal siblingpolar-subscription-portal-body-spec.md(different provider's portal pattern), the Stripe checkout root POST siblingstripe-checkout-body-spec.md(TWO-key Unauthorized envelope vs ONE-key here), the Stripe setup-intent [id] GET siblingstripe-setup-intent-id-query-spec.md(different{ success: false, error }TWO-key shape), the Stripe payment-methods create POST siblingstripe-payment-methods-create-body-spec.md(different envelope shape on Stripe-error catch), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 90-of-N and thetests/api/per-spec-file sub-rollout extends to 88-of-many, and the first per-source-file POST smoke for a Stripe billing-portal session-creation endpoint lands -- pinning the zero-arg POST contract no prior smoke covers, thenew URL()validation contract no prior smoke covers, and the FOUR-key Stripe-error catch envelope (with bothcodeANDtypesurfaced) no prior smoke covers. - E2E Item Comments Rating [commentId] Update Method Spec (
apps/web-e2e/tests/api/item-comments-rating-id-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-comment rating-update PATCH dynamic-segment / body / header smoke spec paired withapps/web-e2e/tests/api/item-comments-rating-id-update-method.spec.ts, the eighty-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-seventh underapps/web-e2e/tests/api/. Pairs with thePATCHexport ofapps/web/app/api/items/[slug]/comments/rating/[commentId]/route.ts— the first per-source-file PATCH smoke the docs tree publishes that documents a Q-010-style NO-AUTH-GATE finding for a non-admin mutating route (the handler has NOauth()call, NO ownership check, NO rating validation; ANY caller can update ANY comment's rating to ANY value so long asDATABASE_URLis configured). The spec pins this finding as the CURRENT contract -- a future PR that adds auth would explicitly break this spec, prompting an update. Distinct from EVERY prior mutating-method smoke: NOauth()gate; NO ownership check (handler trusts path-paramcommentIddirectly); NO rating validation (any value passed straight toupdateCommentRating(...)); production-leftoverconsole.logwith debug arrow'============rating=============>'(NOT dev-gated); returns raw comment row verbatim (no wrapper envelope);checkDatabaseAvailability()as the SOLE gate. The PATCH handler combinescheckDatabaseAvailability()(the ONLY gate; 503 if missing),{ commentId } = await paramsdynamic-segment resolution,{ rating } = await request.json()body parse with NO validation, the production-leftoverconsole.logdebug statement, the load-bearing UNGUARDEDupdateCommentRating(commentId, rating)DB write, success payload as raw comment row verbatim, and outer catch 500'Failed to update comment rating'. Documents the at-a-glance scenario tree (a ~6-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a NO-401 contract assertion; an auth-signal-ignored contract pinning that fabricated auth headers produce SAME status as bare; a no-validation contract pinning that invalid rating values produce SAME status as valid values; a cross-method probe (POST / PUT / DELETE); a no-catch-on-valid-body assertion; a no-wrapper-envelope assertion pinning the UNUSUAL raw-comment-row response shape). Cross-references the companion minimal smokeitem-comment-rating-by-id.spec.ts, the parent route's PUT/DELETE handlers atitem-comments-id-method-spec.md(DO enforce auth + ownership), the companion comment-create POST siblingitem-comments-create-body-spec.md(samecheckDatabaseAvailability()but with explicitauth()gate), the public per-item rating-aggregate GET siblingitem-comments-rating-query-spec.md,docs/questions.mdfor the Q-### entry tracking the no-auth-gate finding, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 89-of-N and thetests/api/per-spec-file sub-rollout extends to 87-of-many, and the first Q-010-style no-auth-gate finding for a non-admin mutating route lands -- pinning the no-auth-gate contract until a future PR adds authentication. - E2E Stripe Payment-Methods [id] Method Spec (
apps/web-e2e/tests/api/stripe-payment-methods-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe per-id payment-method GET + DELETE dual-method dynamic-segment / header smoke spec paired withapps/web-e2e/tests/api/stripe-payment-methods-id-method.spec.ts, the eighty-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-sixth underapps/web-e2e/tests/api/. Pairs with theGETANDDELETEexports ofapps/web/app/api/stripe/payment-methods/[id]/route.ts— the first per-source-file GET + DELETE dual-method smoke the docs tree publishes for any Stripe per-id primitive route. Sibling specs:stripe-payment-methods-delete-body-spec.md(DELETE-by-body on the static/deletepath with id-in-body, NO dynamic segment);stripe-setup-intent-id-query-spec.md(GET-only on a dynamic-segment per-id path with NO DELETE);stripe-payment-methods-update-method-spec.md(PUT + PATCH dual-method smoke on a STATIC/updatepath with NO dynamic segment). Distinct from EVERY prior per-id Stripe smoke: TWO methods exported on the same dynamic-segment path (GET retrieves filtered fields AND DELETE detaches with default-reassignment cascade — the FIRST per-source-file GET + DELETE dual-method smoke pinning both methods on the same[id]route); customer-metadata-driven IDOR check on BOTH methods viacustomer.metadata?.userId !== session.user.id→ 403'Unauthorized - payment method does not belong to user';!paymentMethod.customercheck distinct for each method (GET → 400'Payment method not associated with any customer'withany; DELETE → 400'Payment method not associated with a customer'witha— UNIQUE: the only known per-source-file smoke pinning a one-word article-shiftanyvsabetween two methods on the same handler); DELETE default-reassignment cascade — if the deleted method was the customer's default and there are other methods, re-assign default to the first remaining method; if there are no remaining methods, set default toundefined(the FIRST per-source-file DELETE smoke pinning a default-reassignment cascade); THREE-branch StripeError catch on BOTH methods —error.code === 'resource_missing'→ 404'Payment method not found'; otherStripeError→ 400 with rawerror.message; default → 500 ('Failed to retrieve payment method'for GET,'Failed to delete payment method'for DELETE — distinct 500 messages per method). The handler chain on each method combinesauth()session lookup (!session?.user?.id→ 401{ success: false, error: 'Unauthorized' }SAME envelope on BOTH methods);{ id } = await paramsdynamic-segment resolution;!idcheck (→ 400'Payment method ID is required'SAME on both methods); the load-bearingstripe.paymentMethods.retrieve(id)call on BOTH methods;!paymentMethod.customercheck on DELETE /paymentMethod.customer ? … : elsebranch on GET (diverges on this critical structural difference between the two methods);stripe.customers.retrieve(...)second load-bearing call on BOTH methods; string-or-deleted customer check → 404'Customer not found'on both methods; customer-metadata IDOR check → 403 on both methods; DELETE-only default-reassignment cascade viastripe.paymentMethods.list+stripe.customers.update; DELETE-onlystripe.paymentMethods.detach(id)mutation; GET success payload with filtered fields ({ id, type, card, billing_details, created, metadata, is_default, customer_id }); DELETE success payload{ success: true, message: 'Payment method deleted successfully', data: { was_default } }; THREE-branch outer catch on each method; method-resolution surface (the route exportsGETANDDELETE;POST/PUT/PATCHmust round-trip to a< 500status). Documents the at-a-glance scenario tree (TWO header bulk-loop walks — ~7 headers each, GET + DELETE — all asserting< 500; canonical 401-envelope assertions on GET AND DELETE; a cross-method envelope-equality assertion pinning byte-identical 401 envelopes on GET and DELETE; strict envelope-shape assertions on both methods (no leak of the success-branchdata/messagefields); a gate-before-post-auth invariant pinning that NONE of nine candidate messages must appear on either method; a gate-before-success-build invariant on GET — CRITICAL — pinning no leak ofcard/billing_details/is_default/customer_id; a gate-before-success-build invariant on DELETE — CRITICAL — pinning no leak ofwas_default; a side-channel walk; a cross-method probe (POST / PUT / PATCH); a paymentMethods-retrieve-and-customers-retrieve-and-IDOR-and-detach-and-default-reassignment-not-entered invariance walk — CRITICAL — pinning that load-bearing Stripe SDK calls NEVER run on unauth; a catch-branch-dispatcher-not-entered invariance walk pinning the gate-before-catch-dispatcher order across distinct 500 messages per method; a no-stripe-error-message-leak invariant pinning that'No such payment_method'/'resource_missing'substrings NEVER leak; a cross-id-invariance walk pinning that three different payment-method IDs produce IDENTICAL unauth envelopes on BOTH methods; a no-XSS-id-substring leak invariant on either method; a default-reassignment-cascade-not-entered invariance walk on DELETE — CRITICAL — pinning that the customer-default mutation cascade NEVER runs on unauth). Cross-references the Stripe payment-methods delete DELETE-by-body siblingstripe-payment-methods-delete-body-spec.md(uses static/deletepath with id-in-body vs this dynamic-segment route), the Stripe setup-intent GET-by-id siblingstripe-setup-intent-id-query-spec.md(uses SAME customer-metadata-driven IDOR check on a GET-only dynamic-segment route), the Stripe payment-methods PUT + PATCH dual-method siblingstripe-payment-methods-update-method-spec.md(uses different dual-method pair on a static path), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 88-of-N and thetests/api/per-spec-file sub-rollout extends to 86-of-many, and the first per-source-file GET + DELETE dual-method smoke for a Stripe per-id primitive route lands -- pinning the default-reassignment cascade contract no prior DELETE smoke covers, the article-shift contract (anyvsa) no prior smoke covers, and the customer-default mutation cascade gate-before invariant on a per-id DELETE surface. - E2E Stripe Setup-Intent [id] Query Spec (
apps/web-e2e/tests/api/stripe-setup-intent-id-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe per-id setup-intent retrieval GET dynamic-segment / header smoke spec paired withapps/web-e2e/tests/api/stripe-setup-intent-id-query.spec.ts, the eighty-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-fifth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/stripe/setup-intent/[id]/route.ts— the first per-source-file GET smoke the docs tree publishes for a Stripe per-id primitive route (the setup-intent root POST is documented atstripe-setup-intent-body-spec.md) AND the first per-source-file GET smoke that pins aerror.code === 'resource_missing'substring detection in the catch (UNIQUE: dispatches on Stripe's enum-typedcodeproperty to surface a 404). Distinct from the stripe-setup-intent (POST) root sibling: GET method (not POST);!session?.user?.idgate with{ success: false, error: 'Unauthorized' }envelope (vs root POST's{ error: 'Unauthorized' }ONE-key envelope withoutsuccesskey); customer-metadata-driven IDOR check (customer.metadata?.userId !== session.user.id→ 403); filtered SetupIntent fields in success payload (vs root POST's raw provider object); Stripe-error.code === 'resource_missing'substring detection in catch → 404. The GET handler combinesauth()session lookup (!session?.user?.id→ 401),{ id } = await paramsdynamic-segment resolution,!idcheck (→ 400'Setup intent ID is required'), the load-bearingstripe.setupIntents.retrieve(id)call, customer-metadata IDOR check viastripe.customers.retrieve+customer.metadata?.userId !== session.user.id→ 403, success payload with filtered SetupIntent fields, and THREE-branch outer catch (error.code === 'resource_missing'→ 404, other StripeError → 400 with rawerror.message, default → 500). Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk asserting< 500; a canonical 401-envelope assertion; a strict envelope-shape assertion; a no-client_secret-leak CRITICAL security invariant; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a setupIntents-retrieve-and-customers-retrieve-and-IDOR-check-not-entered invariance walk -- CRITICAL: load-bearing Stripe SDK calls must NEVER run on unauth; a catch-branch-dispatcher-not-entered invariance walk; a no-stripe-error-message-leak invariant pinning that'No such setupintent'/'resource_missing'substrings NEVER leak; a cross-id-invariance walk pinning that three different setup-intent IDs produce IDENTICAL unauth envelopes). Cross-references the Stripe setup-intent POST root siblingstripe-setup-intent-body-spec.md, the Stripe payment-methods delete DELETE siblingstripe-payment-methods-delete-body-spec.md(uses SAME customer-metadata-driven IDOR check), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 87-of-N and thetests/api/per-spec-file sub-rollout extends to 85-of-many, and the first per-source-file GET smoke for a Stripe per-id primitive route lands -- pinning theerror.codesubstring-detection contract no prior smoke covers and the no-client_secret-leak CRITICAL security invariant on a per-id GET surface. - E2E Stripe Payment-Methods Update Method Spec (
apps/web-e2e/tests/api/stripe-payment-methods-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-method-update PUT + PATCH method / body / header smoke spec paired withapps/web-e2e/tests/api/stripe-payment-methods-update-method.spec.ts, the eighty-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-fourth underapps/web-e2e/tests/api/. Pairs with thePUTandPATCHexports ofapps/web/app/api/stripe/payment-methods/update/route.ts— the first per-source-file PUT + PATCH smoke the docs tree publishes for a non-admin payment-method route. Sibling to thestripe-payment-methods-delete-body-spec.mdroute which exportsDELETE. Distinct: TWO mutation methods exported on the same path -- PUT (full update with{ payment_method_id, metadata, billing_details, set_as_default }) AND PATCH (set-default-only with{ payment_method_id }); the FIRST per-source-file mutating smoke pinning a PUT + PATCH dual-method export. Shared helper-function-extraction design with the delete sibling -- both PUT and PATCH usevalidateSession,validatePaymentMethodOwnership, andhandleApiError. PUT preserves existing metadata via spread (metadata: { ...paymentMethod.metadata, ...metadata, userId }; the FIRST per-source-file PUT smoke pinning a metadata-merge contract);userIdalways present in metadata via explicit set AFTER the spread (caller cannot override). Both handlers share the same auth + ownership chain:validateSession()→ 401{ success: false, error: 'Authentication required' }; JSON body parse; PUT usesupdatePaymentMethodSchema.parse(body)while PATCH usessetDefaultPaymentMethodSchema.parse(body);validatePaymentMethodOwnershiphelper; PUT callsstripe.paymentMethods.update(...)while PATCH callsstripe.customers.update(...). Documents the at-a-glance scenario tree (a doubled header walk -- ~6 headers × 2 methods; per-method body walks -- ~9 PUT bodies + ~5 PATCH bodies; canonical 401-envelope assertions on PUT AND PATCH; a cross-method envelope-equality assertion pinning byte-identical 401 envelopes on PUT and PATCH; a gate-before-post-auth invariant pinning that NONE of eight candidate messages must appear on either method; a no-metadata.userId-leak invariant on PUT; a cross-method probe (GET / POST / DELETE); an ownership-check-helper-and-paymentMethods-update-and-customers-update-not-entered invariance walk -- CRITICAL: the load-bearing Stripe SDK calls must NEVER run on unauth; a no-payment_method_id-leak invariant for XSS-shaped values). Cross-references the Stripe payment-methods delete DELETE siblingstripe-payment-methods-delete-body-spec.md(SAME helper-function-extraction design), the Stripe payment-methods create POST siblingstripe-payment-methods-create-body-spec.md, the per-comment edit/delete siblingitem-comments-id-method-spec.md(another dual-method spec PUT + DELETE; this Stripe update spec is PUT + PATCH), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 86-of-N and thetests/api/per-spec-file sub-rollout extends to 84-of-many, and the first PUT + PATCH dual-method export smoke lands -- pinning the metadata-merge contract no prior PUT smoke covers and completing the Stripe payment-methods CRUD trio (create POST + update PUT/PATCH + delete DELETE). - E2E Stripe Payment-Methods Delete Body Spec (
apps/web-e2e/tests/api/stripe-payment-methods-delete-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-method-delete DELETE body / header smoke spec paired withapps/web-e2e/tests/api/stripe-payment-methods-delete-body.spec.ts, the eighty-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-third underapps/web-e2e/tests/api/. Pairs with theDELETEexport ofapps/web/app/api/stripe/payment-methods/delete/route.ts— the first per-source-file DELETE smoke the docs tree publishes for a non-admin payment-method route (note: the route's mutation method is DELETE, NOT POST as is typical for other payment-method mutations) AND the first per-source-file mutating smoke that pins a multi-helper-function-extraction handler design (the handler delegates to FIVE helper functions:validateSession,validatePaymentMethodOwnership,handleDefaultPaymentMethodReassignment,checkAffectedSubscriptions, andhandleApiError) AND the first per-source-file mutating smoke that pins a customer-metadata-driven IDOR check (the handler retrieves the Stripe customer associated with the payment method and verifiescustomer.metadata?.userId === userId→ 403 if mismatch). Distinct from the stripe-payment-methods-create sibling: DELETE method (not POST); ONE-key 401 envelope{ success: false, error: 'Authentication required' }; multi-helper-function-extraction design (5 helpers); customer-metadata IDOR check; Stripe-error-echo with'Stripe error: 'prefix; default-payment-method reassignment side-effect; affected-subscriptions read-only count. The DELETE handler combinesvalidateSession()helper, JSON body parse AFTER auth gate,deletePaymentMethodSchema.parse(body)Zod throwing parse,validatePaymentMethodOwnership(paymentMethodId, userId)helper with three-stage chain,handleDefaultPaymentMethodReassignmentside-effect,checkAffectedSubscriptionscount, the load-bearingstripe.paymentMethods.detach(paymentMethodId)call, success payload{ success: true, message, data: { was_default, affected_subscriptions, new_default_payment_method } }, andhandleApiErrorTHREE-helper catch dispatcher. Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~11-body bulk-loop walk all asserting< 500; a canonical 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-Stripe-error-prefix invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / POST / PUT / PATCH); a Zod-throw-catch-not-entered invariance walk; an ownership-check-helper-and-detach-and-reassignment-and-sub-count-not-entered invariance walk -- CRITICAL:paymentMethods.detachmust NEVER run on unauth (catastrophic); a no-paymentMethodId-leak invariant pinning that XSS-shaped paymentMethodId is NEVER echoed back). Cross-references the Stripe payment-methods create POST siblingstripe-payment-methods-create-body-spec.md, the Stripe webhook POST siblingstripe-webhook-body-spec.md, the Stripe checkout POST siblingstripe-checkout-body-spec.md, the per-comment edit/delete siblingitem-comments-id-method-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 85-of-N and thetests/api/per-spec-file sub-rollout extends to 83-of-many, and the first DELETE smoke pinning a multi-helper-function-extraction design + customer-metadata-driven IDOR check lands -- pinning three FIRST contracts no prior mutating smoke covers. - E2E Stripe Subscription [subscriptionId] Update Body Spec (
apps/web-e2e/tests/api/stripe-subscription-id-update-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe subscription-plan-update POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-subscription-id-update-body.spec.ts, the eighty-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-second underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/subscription/[subscriptionId]/update/route.ts— the third sibling in the Stripe subscription-management trio (cancel + reactivate + update), completing the Stripe POST trio that mirrors the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan) and the Polar pair (cancel + reactivate), bringing the cross-provider subscription-management surface to eight per-source-file POST smokes (three Stripe + three LemonSqueezy + two Polar). It is the first per-source-file POST smoke the docs tree publishes that pins a USER-SCOPED IDOR check on a Stripe subscription endpoint -- after the tenant-scoped DB lookup the handler also comparesuserSubscription.userId !== session.user.idand returns a merged 404'Subscription not found or access denied'; this sits at the user-scoped end of the stripe-subscription IDOR spectrum (cancel = no IDOR; reactivate = tenant-only; update = full user-scoped). It is also the first per-source-file POST smoke pinning a THREE-state allow-list pre-check 400 (subscription.status !== 'active' && subscription.status !== 'pending' && subscription.status !== 'paused'→ 400'Subscription is not active'-- distinct from the reactivate sibling's SINGLE-flag pre-check oncancelAtPeriodEnd), the first per-source-file POST smoke pinning a PaymentPlan-enum-from-@/lib/constantsincludes-validation (Object.values(PaymentPlan).includes(newPlanId)→ 400'Invalid plan ID'-- distinct from the LemonSqueezy update-plan sibling which uses ZodsafeParsefor its validation chain), and the first per-source-file POST smoke pinning a conditional tenant filter on a Drizzle UPDATE WHERE clause (...(tenantId ? [eq(subscriptions.tenantId, tenantId)] : [])-- spreads zero or one Drizzle filter into the AND clause based on whethergetTenantId()returns a truthy value at request time; every prior tenant-scoped POST smoke uses an unconditional tenant filter). Distinct from EVERY prior POST smoke: USER-scoped IDOR check; THREE-state pre-check 400; PaymentPlan-enum-includes validation; conditional tenant-filter on DB UPDATE WHERE; body parsing IS used (await request.json()IS called, matches stripe cancel + LS update-plan siblings, distinct from stripe reactivate sibling); TWO required fields ({ newPlanId, newPriceId }); multi-step write chain (provider call + DB update + email side-effect); plan-changed email contract with BOTHoldPlanName: subscription.planIdANDnewPlanName: newPlanId(FIRST per-source-file POST smoke pinning an email with both old + new plan names); dynamic success message (Plan updated to ${newPlanId} successfullytemplate literal withnewPlanIdinterpolation, distinct from reactivate sibling's static message); returns rawupdatedSubscriptionin thedatafield (Stripe SDK provider object verbatim); generic 500 catch (single static'Failed to update subscription', NO substring detection). The POST handler combinesauth()session lookup (!session?.user→ 401{ error: 'Unauthorized' }bare envelope),{ newPlanId, newPriceId } = await request.json()body parse AFTER auth gate,{ subscriptionId } = await paramsdynamic-segment param resolution, PaymentPlan-enum-includes validation (!Object.values(PaymentPlan).includes(newPlanId)→ 400'Invalid plan ID'),getOrCreateStripeProvider()singleton, the merged tenant-scoped DB IDOR check + user-id equality check (getSubscriptionByProviderSubscriptionId('stripe', subscriptionId)returns null ORuserSubscription.userId !== session.user.id→ 404'Subscription not found or access denied'), the THREE-state pre-check 400, the load-bearingstripeProvider.updateSubscription({ subscriptionId, priceId: newPriceId })provider call,getTenantId()resolution + DB UPDATE with conditional tenant filter, asyncpaymentEmailService.sendSubscriptionPlanChangedEmail(...)side-effect (failure does NOT fail the update), success payload{ success: true, data: <updatedSubscription>, message: 'Plan updated to ${newPlanId} successfully' }with status 200, and outer catch 500{ error: 'Failed to update subscription' }. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-dynamic-success-message-leak invariant via regex; a no-input-echo invariant pinning that caller-suppliednewPlanId/newPriceIdmarkers must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel walk includingX-User-IdANDX-Tenant-Idprobes; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a no-500-from-body-parse-error invariant; a PaymentPlan-enum-includes-validation-not-entered invariance walk (4 shapes); a user-scoped-IDOR-check-not-entered invariance walk; a THREE-state-pre-check-400-not-entered invariance walk (3 shapes); an updateSubscription-DB-update-email-send-chain-not-entered invariance walk; a no-'Failed to update subscription'-leak invariant; a body-completely-ignored invariance walk; a cross-subscription-ID invariance walk pinning that the tenant-scoped DB read AND the user-id equality check are NOT entered upstream of the auth gate). Cross-references the Stripe subscription-cancel POST siblingstripe-subscription-id-cancel-body-spec.md(NO IDOR), the Stripe subscription-reactivate POST siblingstripe-subscription-id-reactivate-body-spec.md(TENANT-only IDOR + SINGLE-flag pre-check), the LemonSqueezy update-plan POST siblinglemonsqueezy-update-plan-body-spec.md(Zod multi-field validation; this Stripe update spec uses raw enum-includes validation), the Polar subscription-cancel POST siblingpolar-subscription-id-cancel-body-spec.md(full user-scoped IDOR via customer-id-equality; this Stripe update spec uses user-id-equality), the Stripe webhook signature-verified POST siblingstripe-webhook-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec anddocs/questions.mdfor the Q-### entries tracking the Stripe subscription IDOR-spectrum findings (Q-010-family). With this entry the per-spec-file docs rollout extends to 84-of-N and thetests/api/per-spec-file sub-rollout extends to 82-of-many, and the first user-scoped IDOR + THREE-state pre-check 400 + PaymentPlan-enum-includes validation + conditional tenant-filter POST smoke lands -- completing the Stripe subscription-management POST trio and pinning FOUR FIRST contracts no prior POST smoke covers. - E2E LemonSqueezy Update Body Spec (
apps/web-e2e/tests/api/lemonsqueezy-update-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy generic-subscription-update POST body / header smoke spec paired withapps/web-e2e/tests/api/lemonsqueezy-update-body.spec.ts, the eighty-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighty-first underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/lemonsqueezy/update/route.ts— the richest per-source-file POST smoke the docs tree publishes, pinning SIX FIRST contracts: (1)success: false-AND-code-typed FIVE-key 401 envelope{ success: false, error: 'Unauthorized', code: 'UNAUTHORIZED', requestId: <uuid>, timestamp: <ISO> }-- distinct from cancel/reactivate/update-plan sibling THREE-key envelope; FIRST per-source-file POST smoke pinning a 5-key envelope with bothrequestIdandtimestamp; (2) Per-request UUID viacrypto.randomUUID?.() || Math.random().toString(36).substring(2)(UUID v4 with browser-fallback; FIRST per-source-file POST smoke pinning per-request UUID generation with optional-chain fallback); (3) Performance tracking viastartTime = Date.now()andduration: ${Date.now() - startTime}msin the catch envelope (FIRST per-source-file POST smoke pinning request-duration measurement); (4) Development-mode short-circuit viaif (process.env.NODE_ENV === 'development')returns 200 with input echoed BEFORE calling the provider (FIRST per-source-file POST smoke pinning a dev-mode short-circuit contract); (5) Custom response headers --Cache-Control: no-cache, no-store, must-revalidate,X-Request-ID,X-Response-Time(FIRST per-source-file POST smoke pinning custom response headers); (6) Five-tier catch dispatcher --errorCodeextracted fromerror.code, dispatched toVALIDATION_ERROR→ 400,UNAUTHORIZED→ 401,SUBSCRIPTION_NOT_FOUND→ 404,PROVIDER_UNAVAILABLE→ 503, default → 500 (FIRST per-source-file POST smoke pinning a five-tier catch dispatcher). Distinct from the cancel + reactivate + update-plan siblings:!session?.usergate (NOT!session?.user?.email);code: 'UNAUTHORIZED'(NOT'AUTH_REQUIRED'); 5-key 401 envelope withsuccess: false+code+requestId+timestamp; dev-mode short-circuit. Documents the at-a-glance scenario tree (a ~7-header bulk-loop walk -- including fabricatedX-Request-IDto verify request-id-forgery prevention -- + a ~13-body bulk-loop walk all asserting< 500; a FIVE-key 401-envelope assertion with all fields verified including ISO-formattimestamp; a strict envelope-shape assertion; a per-request-UUID-uniqueness assertion (three requests must produce three different requestIds); a request-id-forgery-prevention assertion pinning that caller-suppliedX-Request-ID: 'attacker-injected-uuid'is NEVER echoed in the body'srequestIdfield; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of three messages and four codes may appear on unauth; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a validation-chain-not-entered invariance walk; a dev-mode-short-circuit-and-provider-call-and-5-tier-catch-not-entered invariance walk; a no-custom-header invariant on the unauth branch -- pinning thatX-Request-IDandX-Response-Timeonly appear on success/catch). Cross-references the companion cancel siblinglemonsqueezy-cancel-body-spec.md, the companion reactivate siblinglemonsqueezy-reactivate-body-spec.md, the companion update-plan siblinglemonsqueezy-update-plan-body-spec.md, the LemonSqueezy webhook POST siblinglemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST siblinglemonsqueezy-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 83-of-N and thetests/api/per-spec-file sub-rollout extends to 81-of-many, and the richest per-source-file POST smoke lands -- pinning SIX FIRST contracts no prior smoke covers (5-key envelope with requestId + timestamp; per-request UUID with browser-fallback; performance tracking; dev-mode short-circuit; custom response headers; five-tier catch dispatcher). - E2E Stripe Subscription [subscriptionId] Reactivate Body Spec (
apps/web-e2e/tests/api/stripe-subscription-id-reactivate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe subscription-reactivate POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-subscription-id-reactivate-body.spec.ts, the eighty-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eightieth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/subscription/[subscriptionId]/reactivate/route.ts— the first per-source-file POST smoke pinning a TENANT-SCOPED-but-NOT-USER-SCOPED partial-IDOR finding (the handler authenticates the user viaauth()and looks up the subscription viagetSubscriptionByProviderSubscriptionId('stripe', subscriptionId), which scopes the query bytenantIdbut NOT byuserId; sits between the stripe/cancel sibling which has NO IDOR check at all and the polar/cancel sibling which enforces full user-scoped IDOR; FIRST per-source-file POST smoke pinning a tenant-scoped-but-NOT-user-scoped IDOR contract) AND the first per-source-file POST smoke pinning a STATE-MACHINE PRE-CHECK 400 contract (the handler readssubscription.cancelAtPeriodEndfrom the DB row and returns 400'Subscription is not scheduled for cancellation'BEFORE calling the provider; distinct from the polar/subscription/[id]/reactivate sibling which surfaces the same 400 via a catch-substring detection on the upstream Polar error message; this Stripe variant has the 400 baked into the handler's own state-machine pre-check from a DB-row column read). Cross-references the four sibling per-source-file POST smokesstripe-subscription-id-cancel-body-spec.md,polar-subscription-id-cancel-body-spec.md,polar-subscription-id-reactivate-body-spec.md, andlemonsqueezy-cancel-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 82-of-N and thetests/api/per-spec-file sub-rollout extends to 80-of-many, and the first tenant-scoped-but-NOT-user-scoped partial-IDOR contract + state-machine pre-check 400 contract POST smoke lands. - E2E LemonSqueezy Update-Plan Body Spec (
apps/web-e2e/tests/api/lemonsqueezy-update-plan-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy plan-update POST body / header smoke spec paired withapps/web-e2e/tests/api/lemonsqueezy-update-plan-body.spec.ts, the eighty-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-ninth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/lemonsqueezy/update-plan/route.ts— the third sibling in the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan), all sharing the same email-gated auth contract, THREE-key 401 envelope withcode: 'AUTH_REQUIRED', ZodsafeParsevalidation, andtimestampfield in success envelope. Distinct from the cancel + reactivate siblings: (a) Multi-field Zod schema with defaults -- theupdatePlanSchemahas TWO required fields (subscriptionId,variantId) AND FOUR OPTIONAL fields with defaults (proration,invoiceImmediately,disableProrations,billingAnchor); FIRST per-source-file POST smoke pinning a multi-field-with-defaults Zod schema; (b)z.coerce.number().positive()-- FIRST per-source-file POST smoke pinning a Zod coerce-number contract (string-to-number coercion); (c)z.enumwith default -- FIRST per-source-file POST smoke pinning a Zod enum-with-default contract; (d)z.number().min(1).max(31)forbillingAnchor(day-of-month range constraint); (e) Plan-update-specific metadata -- writes 7 metadata fields includingsession.user.emailasupdatedBy; same email-in-metadata pattern as reactivate sibling, but with FOUR additional flag fields. The POST handler combinesauth()session lookup (!session?.user?.email→ 401 THREE-key envelope), JSON body parse,updatePlanSchema.safeParse(body)with multi-field schema (failure → 400 withcode: 'VALIDATION_ERROR'),getOrCreateLemonsqueezyProvider()singleton, the load-bearinglemonsqueezy.updateSubscription({...metadata: { action, proration, invoiceImmediately, disableProrations, billingAnchor, updatedAt, updatedBy } })call, success payload withtimestamp, and outer catchsafeErrorResponse(error, 'Failed to update subscription plan'). Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a THREE-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-VALIDATION_ERROR-or-UPDATE_FAILED-codes invariant; a no-updatedBy-leak invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a multi-field-validation-chain-not-entered invariance walk pinning that NONE of five validation-failure shapes (negative variantId, invalid enum, out-of-range billingAnchor, etc.) may surface on unauth; an updateSubscription-call-with-metadata-write-not-entered invariance walk). Cross-references the companion cancel siblinglemonsqueezy-cancel-body-spec.md, the companion reactivate siblinglemonsqueezy-reactivate-body-spec.md(same email-in-metadata pattern), the LemonSqueezy webhook POST siblinglemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST siblinglemonsqueezy-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 81-of-N and thetests/api/per-spec-file sub-rollout extends to 79-of-many, and the first multi-field-with-defaults Zod schema POST smoke lands -- completing the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan) and pinning four FIRST Zod-schema-pattern contracts no prior smoke covers. - E2E Stripe Subscription [subscriptionId] Cancel Body Spec (
apps/web-e2e/tests/api/stripe-subscription-id-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe subscription-cancel POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-subscription-id-cancel-body.spec.ts, the eightieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/subscription/[subscriptionId]/cancel/route.ts— the first per-source-file POST smoke the docs tree publishes that documents a Q-010-style IDOR finding for a Stripe subscription endpoint (the handler authenticates the user viaauth()but does NOT verify that thesubscriptionIdfrom the path belongs to the authenticated user; compare to the polar/subscription/[id]/cancel sibling which DOES enforce ownership viagetCustomerId→getPolarSubscription→subscriptionCustomerId === userPolarCustomerId; the Stripe cancel handler trusts the path parameter directly) AND the first per-source-file POST smoke that pins a DB-sync-after-provider-call contract (afterstripeProvider.cancelSubscription(...)succeeds, the handler ALSO callsupdateSubscriptionBySubscriptionId({...})to sync the cancellation state back to the local DB). Distinct from the polar/subscription/[id]/cancel sibling: NO IDOR-protection (FIRST per-source-file POST smoke pinning a Q-010-style finding for a Stripe subscription endpoint); NO Content-Length 413 pre-check; DB sync side-effect (FIRST per-source-file POST smoke pinning a DB-sync-after-provider-call contract); email-send with fault-tolerance (paymentEmailService.sendSubscriptionCancellingEmail(...)wrapped in try/catch -- failure does NOT fail the cancellation); NO try/catch aroundrequest.json(). The POST handler combinesauth()session lookup (!session?.user→ 401{ error: 'Unauthorized' }bare envelope), JSON body parse with destructured default{ cancelAtPeriodEnd = true },{ subscriptionId }param resolution,getOrCreateStripeProvider()singleton, the load-bearingstripeProvider.cancelSubscription(subscriptionId, cancelAtPeriodEnd)call WITHOUT IDOR protection,updateSubscriptionBySubscriptionId({...})DB sync side-effect, email-send with fault-tolerance, success payload{ success: true, data: <cancelledSubscription>, message: <conditional> }, and outer catch 500{ error: 'Failed to cancel subscription' }. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~10-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a cancelSubscription-DB-sync-email-send-not-entered invariance walk; a catch-branch-generic-500-not-echoed invariance walk; a NO-IDOR-protection contract walk pinning that the unauth 401 envelope is IDENTICAL across different subscription IDs -- pinning the CURRENT contract). Cross-references the polar/subscription/[id]/cancel POST siblingpolar-subscription-id-cancel-body-spec.md(enforces IDOR protection; this Stripe spec documents the lack of it), the polar/subscription/[id]/reactivate POST siblingpolar-subscription-id-reactivate-body-spec.md, the Stripe webhook signature-verified POST siblingstripe-webhook-body-spec.md, the Stripe checkout POST siblingstripe-checkout-body-spec.md, the LemonSqueezy subscription-cancel POST siblinglemonsqueezy-cancel-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec anddocs/questions.mdfor the Q-### entry tracking the Stripe IDOR finding. With this entry the per-spec-file docs rollout extends to 80-of-N and thetests/api/per-spec-file sub-rollout extends to 78-of-many, and the first Q-010-style IDOR finding for a Stripe subscription endpoint lands -- pinning the no-IDOR-protection contract until a future PR adds ownership verification (which would explicitly break this spec, prompting an update). - E2E LemonSqueezy Reactivate Body Spec (
apps/web-e2e/tests/api/lemonsqueezy-reactivate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy subscription-reactivate POST body / header smoke spec paired withapps/web-e2e/tests/api/lemonsqueezy-reactivate-body.spec.ts, the seventy-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-seventh underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/lemonsqueezy/reactivate/route.ts— the complement to thelemonsqueezy-cancel-body-spec.mdsibling: both routes share the same email-gated auth contract, THREE-key 401 envelope withcode: 'AUTH_REQUIRED', ZodsafeParsevalidation, andtimestampfield in the success envelope. The reactivate route differs in: (a) Reactivation-specific metadata -- the handler callsupdateSubscription({..., metadata: { action: 'reactivate', reactivateAction: true, reactivatedAt: <ISO>, reactivatedBy: session.user.email }}); the FIRST per-source-file POST smoke pinning a session.user.email-in-metadata contract (the user's email is written to provider-side metadata asreactivatedBy); (b)safeErrorResponse(...)direct in catch -- single line, distinct from the sibling cancel route's manual FOUR-key catch envelope; (c) Static success message'Subscription reactivated successfully'(no conditional branch). The POST handler combinesauth()session lookup (!session?.user?.email→ 401 THREE-key envelope), JSON body parse viaawait request.json()AFTER auth gate,reactivateSubscriptionSchema.safeParse(body)(failure → 400 withcode: 'VALIDATION_ERROR'),getOrCreateLemonsqueezyProvider()singleton, the load-bearinglemonsqueezy.updateSubscription({ subscriptionId, cancelAtPeriodEnd: false, metadata: { action, reactivateAction, reactivatedAt, reactivatedBy } })call (writes session.user.email to provider-side metadata), success payload withtimestamp, and outer catchsafeErrorResponse(error, 'Failed to reactivate subscription'). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~10-body bulk-loop walk all asserting< 500; a THREE-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant; a no-VALIDATION_ERROR-code invariant; a no-reactivatedBy-leak invariant pinning that caller-supplied attacker email is NEVER echoed; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a validation-chain-not-entered invariance walk; an updateSubscription-call-with-metadata-write-not-entered invariance walk). Cross-references the companion cancel siblinglemonsqueezy-cancel-body-spec.md, the LemonSqueezy webhook POST siblinglemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST siblinglemonsqueezy-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 79-of-N and thetests/api/per-spec-file sub-rollout extends to 77-of-many, and the first session.user.email-in-metadata POST smoke lands -- pinning the user's email being written to provider-side metadata as a contract. - E2E Polar Subscription [subscriptionId] Reactivate Body Spec (
apps/web-e2e/tests/api/polar-subscription-id-reactivate-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Polar subscription-reactivate POST body / header smoke spec paired withapps/web-e2e/tests/api/polar-subscription-id-reactivate-body.spec.ts, the seventy-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-sixth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/polar/subscription/[subscriptionId]/reactivate/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a NO-BODY POST handler (the handler does NOT callrequest.json()at all; the body is COMPLETELY ignored upstream of the auth gate; EVERY prior POST smoke either parses the body (lemonsqueezy, polar/cancel, stripe/checkout) OR explicitly extracts a header (sponsor-ads); FIRST per-source-file POST smoke pinning a body-less POST contract) AND the first per-source-file POST smoke that pins a THREE-string error-message-detection catch with a 400 minted from the catch dispatcher (NOT from a schema validation step; EVERY prior POST smoke that pins a 400 does so via ZodsafeParse; this is the FIRST 400 minted from the catch's substring-detection on a business-rule violation:error.message.includes('not scheduled for cancellation')→ 400'Subscription is not scheduled for cancellation'). Distinct from EVERY prior POST smoke: NO body parsing (request.json()is NEVER called); NO Content-Length 413 pre-check (distinct from polar/cancel sibling which DOES pin a 413 pre-check); THREE-string catch dispatcher ('not found'/'404'→ 404,'Unauthorized'/'401'→ 401,'not scheduled for cancellation'→ 400); 400-from-catch contract; static success message'Subscription reactivated successfully'(NOT conditional based on a body flag, distinct from polar/cancel and lemonsqueezy/cancel siblings); same IDOR-protection chain as polar/cancel sibling (getCustomerId→ 403, private(polarProvider as any).polarextraction → 500,getPolarSubscriptionownership check → merged 404). The POST handler combinesauth()session lookup (!session?.user→ 401{ error: 'Unauthorized' }bare envelope),{ subscriptionId }param resolution, the IDOR-protection chain (getCustomerId→ 403, polar client extraction → 500,getPolarSubscription→ 404), the load-bearingpolarProvider.reactivateSubscription(subscriptionId)call with NO body-driven flags, success payload{ success: true, data: { id, status, cancelAtPeriodEnd, currentPeriodEnd, priceId, customerId }, message: 'Subscription reactivated successfully' }, and THREE-string error-message-detection catch dispatching to 404 / 401 / 400 / 500 viasafeErrorResponse(...). Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~9-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of seven candidate messages must appear; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a no-body-parse contract walk on malformed JSON; an IDOR-protection-chain-not-entered invariance walk pinning that NONE of the three IDOR-chain error messages may appear; a reactivateSubscription-call-not-entered invariance walk; a catch-branch-THREE-string-dispatcher-not-entered invariance walk; a 400-catch-dispatcher-contract-not-entered invariance walk; a body-completely-ignored invariance walk -- the strongest no-body-parse contract in the rollout). Cross-references the Polar subscription-cancel POST siblingpolar-subscription-id-cancel-body-spec.md(uses SAME IDOR-protection chain and private(polarProvider as any).polarextraction, BUT pins a TWO-string catch dispatcher (no 400 branch), Content-Length 413 pre-check, body parsing with fault-tolerance, and conditional success message), the Polar webhook signature-verified POST siblingpolar-webhook-body-spec.md, the Polar checkout POST siblingpolar-checkout-body-spec.md(uses SAME(polarProvider as any).polarprivate-property-access pattern), the Polar subscription portal POST siblingpolar-subscription-portal-body.spec.ts, the LemonSqueezy subscription-cancel POST siblinglemonsqueezy-cancel-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 78-of-N and thetests/api/per-spec-file sub-rollout extends to 76-of-many, and the first NO-body + 400-from-catch + THREE-string-catch-dispatcher POST smoke lands -- pinning two FIRST contracts no prior smoke covers and completing the polar/subscription/[id]/* POST pair (cancel + reactivate). - E2E Polar Subscription [subscriptionId] Cancel Body Spec (
apps/web-e2e/tests/api/polar-subscription-id-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Polar subscription-cancel POST body / header smoke spec paired withapps/web-e2e/tests/api/polar-subscription-id-cancel-body.spec.ts, the seventy-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-fifth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/polar/subscription/[subscriptionId]/cancel/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a Content-Length 413 pre-check (the handler readsrequest.headers.get('content-length')BEFORE the body parse and returns 413'Request body too large. Maximum size is 1024 bytes.'if declared length exceeds 1KB; EVERY prior POST smoke uses 4xx/5xx in standard ranges; this is the FIRST 413 (Payload Too Large) contract in the rollout) AND the first per-source-file POST smoke that pins an IDOR-protection chain (aftergetCustomerIdlookup → 403, the handler retrieves the subscription viagetPolarSubscription(...)and explicitly checkssubscriptionCustomerId === userPolarCustomerId, returning a merged 404 message'Subscription not found or access denied'for both not-found AND ownership-mismatch cases; FIRST per-source-file POST smoke pinning a merged 404+403 IDOR-protection message). Distinct from EVERY prior POST smoke: Content-Length 413 pre-check; IDOR-protection chain with merged 404+403 message; private property access via(polarProvider as any).polarfor direct Polar client access (matches polar-checkout one_time branch); helper-function injection --getPolarSubscription(...)takesformatErrorMessageANDloggeras dependency-injected helpers; TWO-string error-message-detection catch ('not found' || '404'→ 404,'Unauthorized' || '401'→ 401, default → 500); conditional success message based oncancelAtPeriodEnd; body-parse fault tolerance with size-error detection (catches'exceeded','too large','75000'→ 413). The POST handler combinesauth()session lookup (!session?.user→ 401{ error: 'Unauthorized' }bare envelope), Content-Length 413 pre-check, body parse with fault-tolerance (silently defaultscancelAtPeriodEnd = true),{ subscriptionId }param resolution, the IDOR-protection chain (getCustomerId→ 403, polar client extraction → 500,getPolarSubscription→ 404), the load-bearingpolarProvider.cancelSubscription(subscriptionId, cancelAtPeriodEnd)call, success payload{ success: true, data: { id, status, cancelAtPeriodEnd, currentPeriodEnd, priceId, customerId }, message: <conditional> }, and TWO-string error-message-detection catch dispatching to 404 / 401 / 500 viasafeErrorResponse(...). Documents the at-a-glance scenario tree (a ~8-header bulk-loop walk + a ~10-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of eight candidate messages must appear; a 413-pre-check-not-triggered-on-unauth invariance walk; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; an IDOR-protection-chain-not-entered invariance walk pinning that NONE of the three IDOR-chain error messages may appear; a cancelSubscription-call-not-entered invariance walk; a catch-branch-error-message-detection-not-entered invariance walk). Cross-references the Polar webhook signature-verified POST siblingpolar-webhook-body-spec.md, the Polar checkout POST siblingpolar-checkout-body-spec.md(uses SAME(polarProvider as any).polarprivate-property-access pattern), the Polar subscription portal POST siblingpolar-subscription-portal-body.spec.ts, the LemonSqueezy subscription-cancel POST siblinglemonsqueezy-cancel-body-spec.md(email-gated auth, THREE-key 401 envelope,codefield; this Polar cancel spec uses bare 401 envelope and IDOR-protection), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 77-of-N and thetests/api/per-spec-file sub-rollout extends to 75-of-many, and the first 413-pre-check + IDOR-protection-chain POST smoke lands -- pinning two FIRST contracts no prior smoke covers. - E2E LemonSqueezy Cancel Body Spec (
apps/web-e2e/tests/api/lemonsqueezy-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy subscription-cancel POST body / header smoke spec paired withapps/web-e2e/tests/api/lemonsqueezy-cancel-body.spec.ts, the seventy-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-fourth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/lemonsqueezy/cancel/route.ts— the first per-source-file POST smoke the docs tree publishes that pins an email-gated auth contract --!session?.user?.email(NOT!session?.user,!session?.user?.id, or!session?.user?.id?.); the FIRST per-source-file POST smoke gating on session email -- AND the first per-source-file POST smoke that pins acodefield in the 401 envelope ({ error: 'Unauthorized', message: 'Authentication required', code: 'AUTH_REQUIRED' }; EVERY prior 401 envelope is at most TWO keys; this is the FIRST THREE-key 401 envelope with an enum-typed code field) AND the first per-source-file POST smoke that pins atimestampfield in the success AND catch envelopes (both branches add an ISO timestamp vianew Date().toISOString()). Distinct from EVERY prior POST smoke: email-gated auth; THREE-key 401 envelope withcode: 'AUTH_REQUIRED';codefield in 400 validation envelope (code: 'VALIDATION_ERROR'); FOUR-key catch envelope withcode: 'CANCEL_FAILED'ANDtimestamp(the FIRST per-source-file POST smoke pinning a 4-key catch envelope); conditional success message based oncancelAtPeriodEndflag ('Subscription will be cancelled at the end of the current period'vs'Subscription cancelled immediately');timestampfield in success AND catch envelopes;safeErrorMessageextracted into the catch envelope'smessagefield (NOT into theerrorfield as in stripe-checkout-body). The POST handler combinesauth()session lookup (!session?.user?.email→ 401 THREE-key envelope), JSON body parse viaawait request.json()AFTER auth gate (NO try/catch),cancelSubscriptionSchema.safeParse(body)(failure → 400 withcode: 'VALIDATION_ERROR'),getOrCreateLemonsqueezyProvider()singleton, the load-bearinglemonsqueezy.cancelSubscription(subscriptionId, cancelAtPeriodEnd)call, conditional success message, FOUR-key success payload{ success: true, data: <result>, message: <conditional>, timestamp: <ISO> }, and FOUR-key outer catch withcode: 'CANCEL_FAILED'ANDtimestamp. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a THREE-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of four candidate messages must appear; a no-VALIDATION_ERROR-or-CANCEL_FAILED-codes invariant; a no-timestamp-leak invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a validation-chain-not-entered invariance walk; a cancelSubscription-call-not-entered invariance walk; a catch-branch-four-key-envelope-not-echoed invariance walk). Cross-references the LemonSqueezy webhook signature-verified POST siblinglemonsqueezy-webhook-body-spec.md, the LemonSqueezy checkout POST siblinglemonsqueezy-checkout-body-spec.md(uses ENUM-typed error codes in the catch -- not in the 401 -- so this cancel route extends the same enum-typed-code pattern into the 401 envelope), and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 76-of-N and thetests/api/per-spec-file sub-rollout extends to 74-of-many, and the first email-gated + 3-key-401 + timestamp-fielded POST smoke lands -- pinning three FIRST contracts no prior smoke covers. - E2E Stripe Payment-Methods Create Body Spec (
apps/web-e2e/tests/api/stripe-payment-methods-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-method-create-from-setup-intent POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-payment-methods-create-body.spec.ts, the seventy-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-third underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/payment-methods/create/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a Zodparse(NOTsafeParse) contract --createPaymentMethodSchema.parse(body)THROWS on validation failure and the outer catch detectserror instanceof z.ZodErrorto dispatch a 400 envelope; EVERY prior per-source-file POST smoke usessafeParseto handle validation gracefully -- this is the FIRST throw-on-invalid Zod contract -- AND the first per-source-file POST smoke that pins a Stripe-error-echo contract in the outer catch (error instanceof Stripe.errors.StripeError→ 400{ success: false, error: error.message }reflects the raw Stripe error message in the response; EVERY prior catch uses static-string messages). Distinct from EVERY prior POST smoke: Zod.parse(body)throwing; stripe-error-echo catch; multi-step Stripe SDK orchestration (setupIntents.retrieve→ conditionalcustomers.create→ conditionalpaymentMethods.attach→ conditionalpaymentMethods.update→ conditionalcustomers.update(default payment method) → re-retrieve -- the most complex stripe SDK orchestration in any per-source-file POST smoke); formatted response payload (success branch extracts{ id, type, card: { brand, last4, exp_month, exp_year, funding } | null, created, metadata }-- NOT raw provider object, distinct from setup-intent and payment-intent which return raw). The POST handler combinesauth()session lookup (!session?.user?.id→ 401{ success: false, error: 'Unauthorized' }), JSON body parse viaawait request.json()AFTER auth gate,createPaymentMethodSchema.parse(body)Zod throwing parse,stripe.setupIntents.retrieve(setup_intent_id)lookup,setupIntent.status !== 'succeeded'check (→ 400),!setupIntent.payment_methodcheck (→ 400), get-or-create customer viagetUserStripeCustomerId/saveUserStripeCustomerId, conditional attach viapaymentMethods.attach, conditional metadata update viapaymentMethods.update, conditional default update viacustomers.update, re-retrieve viapaymentMethods.retrieve, formatted success payload, and three-branch outer catch (z.ZodError→ 400 withdetails,StripeError→ 400 witherror.messageechoed, default → 500). Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a canonical 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of five candidate messages must appear; a Zod-throw-catch-not-entered invariance walk pinning that the'Invalid request data'400 withdetailsmust NEVER fire on unauth; a stripe-error-echo-catch-not-entered invariance walk pinning that no stripe error message can leak; a no-card-details-leak CRITICAL security invariant pinning thatcard.last4/card.brand/ etc. are NEVER exposed; a no-metadata-userId-spread-leak invariant; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a multi-step-Stripe-orchestration-not-entered invariance walk; a catch-branch-generic-500-not-echoed invariance walk). Cross-references the Stripe setup-intent POST siblingstripe-setup-intent-body-spec.md(the source of thesetup_intent_idconsumed by this payment-methods/create POST), the Stripe payment-intent POST siblingstripe-payment-intent-body-spec.md, the Stripe checkout POST siblingstripe-checkout-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 75-of-N and thetests/api/per-spec-file sub-rollout extends to 73-of-many, and the first throw-on-invalid Zod + stripe-error-echo POST smoke lands -- pinning two FIRST contracts no prior smoke covers and the most complex Stripe SDK orchestration in the rollout. - E2E Stripe Payment-Intent Body Spec (
apps/web-e2e/tests/api/stripe-payment-intent-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-intent creation POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-payment-intent-body.spec.ts, the seventy-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-second underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/payment-intent/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a NO-body-validation contract (the handler destructures{ amount, currency = 'usd', metadata, planId }and passes them straight tostripeProvider.createPaymentIntent(...)with NOif (!amount)check, NO Zod, NO type checking; EVERY prior POST smoke has at least one body-validation gate -- this is the FIRST trust-the-body POST contract), and the second per-source-file POST smoke that pins a raw payment-provider object as the success payload (afterstripe-setup-intent-body-spec.md) -- the PaymentIntent'sclient_secretfield is the same critical-leak vector. Distinct from the stripe-setup-intent sibling: body destructure withcurrency = 'usd'default; caller-controlledmetadata: { userId, planId, ...metadata }spread (the caller'smetadata.userIdOVERRIDES the session userId because...metadataspreads AFTER -- the spec pins this risk-surface metadata is NEVER reached on unauth); trust-the-body contract; GET sibling with?payment_intent_id=query-param-required check. Distinct from EVERY prior POST smoke: NO-body-validation contract; bare 401 envelope{ error: 'Unauthorized' }matches setup-intent (distinct from canonical{ success: false, error }envelope); raw PaymentIntent object as success payload; caller-controlled metadata spread allows session userId override. The POST handler combinesauth()session lookup (!session?.user→ 401{ error: 'Unauthorized' }), JSON body parse via destructuredawait request.json()AFTER auth gate (NO try/catch, NO validation),getOrCreateStripeProvider()singleton,stripeProvider.getCustomerId(session.user)lookup (null → 400'Failed to create customer'), the load-bearingstripeProvider.createPaymentIntent({ amount, currency, customerId, metadata: { userId, planId, ...metadata } })call, raw PaymentIntent object as success payload, and outer catch 500'Failed to create payment intent'. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~15-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofid/client_secret/amount/customermay appear; a gate-before-post-auth invariant; a no-PaymentIntent-client_secret-leak CRITICAL security invariant; a no-PaymentIntent-fields-leak invariant pinning the full set; a no-metadata-userId-spread-leak invariant pinning that caller-suppliedmetadata.userId(e.g.'attacker_user_id') is NEVER echoed; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a createPaymentIntent-and-getCustomerId-not-entered invariance walk; a catch-branch-not-entered invariance walk). Cross-references the Stripe setup-intent POST siblingstripe-setup-intent-body-spec.md(zero-arg signature; this payment-intent uses body-destructure), the Stripe checkout POST siblingstripe-checkout-body-spec.md(TWO-key 401 envelope, NOT bare like this payment-intent spec), the Stripe webhook signature-verified POST siblingstripe-webhook-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 74-of-N and thetests/api/per-spec-file sub-rollout extends to 72-of-many, and the first NO-body-validation POST smoke lands -- pinning the trust-the-body contract no prior smoke covers and the same CRITICALclient_secret-leak security invariant as setup-intent applied to PaymentIntent. - E2E Sponsor-Ads User [id] Renew Body Spec (
apps/web-e2e/tests/api/sponsor-ads-user-id-renew-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-owned sponsor-ad renew POST body / header smoke spec paired withapps/web-e2e/tests/api/sponsor-ads-user-id-renew-body.spec.ts, the seventy-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventy-first underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/sponsor-ads/user/[id]/renew/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a swallow-and-continue body-parse contract (try { const body = await request.json(); successUrl = body.successUrl; cancelUrl = body.cancelUrl; } catch { /* Body is optional */ }— malformed JSON OR missing body silently leavessuccessUrl/cancelUrlasundefined; FIRST swallow-and-continue contract in the rollout, distinct from the sibling cancel route's silent-coalesce-to-{} pattern). Also the first per-source-file POST smoke pinning TWO open-redirect-validated URLs in the SAME body (successUrlANDcancelUrl, each throughvalidateRedirectUrlcomparingprotocol,hostname, ANDportagainstappUrl) AND a multi-providerswitchdispatch with default-case 400 (Stripe / LemonSqueezy / Polar / default → 400'Payment configuration is incomplete. Please contact support.') AND a state-machine 400 branch with status interpolation (Cannot renew sponsor ad with status: ${sponsorAd.status}. Only active or expired ads can be renewed.; whitelist gaterenewableStatuses = [ACTIVE, EXPIRED]). The smoke spec pins a canonical 401{ success: false, error: 'Unauthorized' }two-key envelope, a strict envelope-shape assertion, gate-before-post-auth / -ownership / -state-machine / -provider-switch / -outer-catch invariants, swallow-and-continue body-parse fault-tolerance, a CRITICAL no-attacker-URL-leak invariant pinning that caller-suppliedsuccessUrl/cancelUrlopen-redirect attacker URLs are NEVER echoed in the unauth response or in any redirect-style header, a no-dangerous-URL-pseudo-protocol-leak invariant pinning thatjavascript:/data:/file:/ protocol-relative//hostURLs are NEVER echoed, a status-interpolation walk pinning that XSS-shaped caller-suppliedstatusvalues are NEVER reflected, a side-channel walk, and a cross-method probe (GET / PUT / PATCH / DELETE) — pinning the swallow-and-continue + TWO-URL open-redirect-prevention sponsor-ad renew contract and CRITICAL no-attacker-URL-leak security invariant. Completes the user-owned sponsor-ad action POST pair with the sibling cancel route covered bysponsor-ads-user-id-cancel-body-spec.md. Cross-references the sibling sponsor-ads checkout POST smokesponsor-ads-checkout-body-spec.md(similar multi-provider dispatch but for ad CREATION rather than RENEWAL), the public sponsor-ads list smokesponsor-ads-public.spec.ts, and to Spec 010 -- E2E Test Coverage and Spec 004 -- Payment Providers for the governing specs. With this entry the per-spec-file docs rollout extends to 73-of-N and thetests/api/per-spec-file sub-rollout extends to 71-of-many, and the first swallow-and-continue body-parse + TWO-URL open-redirect-prevention POST smoke lands -- pinning four FIRST contracts no prior smoke covers and completing the user-owned sponsor-ad action POST pair. - E2E Stripe Setup-Intent Body Spec (
apps/web-e2e/tests/api/stripe-setup-intent-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe setup-intent creation POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-setup-intent-body.spec.ts, the seventy-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventieth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/setup-intent/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a zero-argumentPOST()handler signature (norequestparameter at all; EVERY prior POST smoke takes either aNextRequestorRequestparameter; this is the FIRST zero-arg POST contract in the rollout), and the first per-source-file POST smoke that pins a raw payment-provider object as the success payload (return NextResponse.json(setupIntent)returns the Stripe SetupIntent object verbatim, NO{ success, data, message }wrapper envelope; this makes the spec's no-client_secret-leak assertion a CRITICAL security invariant -- a regression that rancreateSetupIntent(...)before the auth gate would expose the SetupIntent'sclient_secretfield, giving any caller the ability to attach a payment method to the fabricated customer). Distinct from EVERY prior POST smoke: zero-argumentPOST()signature; bare 401 envelope{ error: 'Unauthorized' }(UNIQUE -- distinct from stripe-checkout's TWO-key envelope and the canonical{ success: false, error }envelope);!session?.usergate (matches stripe-checkout); raw provider-object success payload (no wrapper envelope); single-line catch ({ error: 'Failed to create setup intent' }500); only one load-bearing call (stripeProvider.createSetupIntent). The POST handler combinesauth()session lookup (!session?.user→ 401{ error: 'Unauthorized' }),getOrCreateStripeProvider()singleton initialization AFTER the auth gate, the load-bearingstripeProvider.createSetupIntent(session.user)call (NO body parse, NO body validation), the raw SetupIntent object as success payload, and outer catch 500{ error: 'Failed to create setup intent' }. Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~10-body bulk-loop walk -- all bodies IGNORED by zero-arg handler -- all asserting< 500; a bare 401-envelope assertion{ error: 'Unauthorized' }; a strict envelope-shape assertion -- exactlyerrorkey, nosuccess/message/dataleak; a gate-before-post-auth invariant; a no-SetupIntent-client_secret-leak CRITICAL security invariant; a no-SetupIntent-fields-leak invariant pinning the full set of fields --id/client_secret/status/usage/customer/created-- as forbidden on unauth; a body-IGNORED invariance walk pinning that the zero-arg handler ignores body content; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a createSetupIntent-and-provider-not-entered invariance walk; a catch-branch-not-entered invariance walk). Cross-references the Stripe checkout POST siblingstripe-checkout-body-spec.md(TWO-key 401 envelope, NOT bare like this setup-intent spec), the Stripe webhook signature-verified POST siblingstripe-webhook-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 72-of-N and thetests/api/per-spec-file sub-rollout extends to 70-of-many, and the first zero-arg POST + raw-provider-object payload smoke lands -- pinning the simplest auth-gated POST contract in the rollout and a CRITICAL no-client_secret-leak security invariant. - E2E Sponsor-Ads User [id] Cancel Body Spec (
apps/web-e2e/tests/api/sponsor-ads-user-id-cancel-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's user-owned sponsor-ad cancel POST body / header smoke spec paired withapps/web-e2e/tests/api/sponsor-ads-user-id-cancel-body.spec.ts, the seventy-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-ninth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/sponsor-ads/user/[id]/cancel/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a body-parse-fault-tolerant contract viaawait request.json().catch(() => ({})) ?? {}-- malformed JSON ORnullbody OR empty body silently coalesces to{}(NO 400 for malformed JSON); EVERY prior POST smoke either has a per-call try/catch returning 400 OR no try/catch at all -- this is the FIRST silent-coalesce contract -- and the first per-source-file POST smoke that pins a conditional Zod validation contract (cancelSponsorAdSchema.omit({ id: true }).safeParse(body)runs unconditionally but the 400-rejection only fires if validation FAILS ANDbody.cancelReason !== undefined). Distinct from EVERY prior POST smoke: (a) Body-parse-fault-tolerant contractawait request.json().catch(() => ({})) ?? {}; (b) Conditional Zod validation with.omit({ id: true })-- the FIRST per-source-file POST smoke pinning a schema-omit + conditional-validation contract; (c) Default-fallback string forcancelReason(parsed.data.cancelReason?.trim() || 'Cancelled by user'); (d) THREE-branch outer catch with mixed exact-string + substring detection --error.message === 'Sponsor ad not found'→ 404 (exact);error.message.includes('Cannot cancel')→ 400 (substring); default → 500 -- the FIRST per-source-file POST smoke pinning a mixed-detection catch dispatcher. The POST handler combinesauth()session lookup (!session?.user?.id→ 401{ success: false, error: 'Unauthorized' }),{ id }param resolution via dynamic-segment route, body parse with silent coalesce (await request.json().catch(() => ({})) ?? {}),cancelSponsorAdSchema.omit({ id: true }).safeParse(body)with conditional 400 rejection,cancelReasondefault fallback to'Cancelled by user',sponsorAdService.getSponsorAdById(id)lookup (null → 404'Sponsor ad not found'), ownership verification (sponsorAd.userId !== session.user.id→ 403), the load-bearingsponsorAdService.cancelSponsorAd(id, cancelReason)call (!cancelledAd→ 500'Failed to cancel sponsor ad'), success payload{ success: true, data: <cancelledAd>, message: 'Sponsor ad cancelled successfully' }, and three-branch outer catch. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~12-body bulk-loop walk all asserting< 500; a canonical one-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of six candidate messages must appear in any unauth response; a silent-coalesce-body-parse-without-400 invariance walk pinning that malformed JSON does NOT produce an 'Invalid JSON' 400; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a conditional-Zod-validation-not-entered invariance walk pinning that over-length / wrong-typecancelReasonmust NEVER produce validation messages on unauth; an ownership-and-cancelSponsorAd-not-entered invariance walk; a three-branch-outer-catch-not-entered invariance walk; a no-cancelReason-leak assertion pinning that XSS-shaped values must NEVER be echoed). Cross-references the sibling sponsor-ads checkout POST smokesponsor-ads-checkout-body-spec.md(uses the SAMEsponsorAdService.getSponsorAdById(...)service), the public sponsor-ads list smokesponsor-ads-public.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 71-of-N and thetests/api/per-spec-file sub-rollout extends to 69-of-many, and the first silent-coalesce body-parse + conditional-Zod + three-branch-catch POST smoke lands -- pinning four FIRST contracts no prior smoke covers. - E2E Auth Change-Password Body Spec (
apps/web-e2e/tests/api/auth-change-password-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's rate-limit-FIRST password-change POST body / header smoke spec paired withapps/web-e2e/tests/api/auth-change-password-body.spec.ts, the seventieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/auth/change-password/route.ts— the first per-source-file POST smoke the docs tree publishes that pins a rate-limit-FIRST gate posture -- the rate-limit check fires BEFORE the auth gate (the handler callsratelimit('change-password:<clientIP>', 5, 15 minutes)as the FIRST gate, then runsauth(), then Zod validation, then a multi-stage post-auth chain -- tenant / user / OAuth / bcrypt-current / bcrypt-duplicate); EVERY prior per-source-file POST smoke pins auth as the first gate, this is the FIRST rate-limit-before-auth contract in the rollout. The companion minimal specauth-change-password.spec.tspins only the< 500no-server-error contract; this spec drills into the body / header surface with detailed invariants. Distinct from EVERY prior per-source-file POST smoke: (a) Rate-limit-FIRST gate posture -- returns 429{ success: false, error: 'Too many password change attempts. Please try again later.', retryAfter: <seconds> }; the FIRST per-source-file POST smoke that pins aretryAfterfield in the response body; (b)'Unauthorized. Please sign in.'401 message -- UNIQUE imperative-phrased 401 envelope distinct from all prior 401 messages; (c) OAuth-account check --!user.passwordHash→ 400'Password change not available for OAuth accounts. Please contact support.'; the FIRST per-source-file POST smoke that pins an OAuth-account-restriction contract; (d) Dual bcrypt.compare gates -- current-password verification AND duplicate-password prevention; the FIRST per-source-file POST smoke that pins a dual bcrypt.compare contract; (e) Cross-field Zod validation via.refine-- thechangePasswordSchemauses.refineto checknewPassword === confirmPassword; the FIRST per-source-file POST smoke that pins a cross-field validation contract; (f) Email-send fault tolerance -- thesendPasswordChangeConfirmationEmail(...)call is wrapped in a try/catch that does NOT fail the password change. The POST handler combines a rate-limit gate FIRST (ratelimit('change-password:<ip>', 5, 15 minutes)→ 429),auth()session lookup (!session?.user?.id→ 401{ success: false, error: 'Unauthorized. Please sign in.' }), ZodsafeParse(body)with cross-field.refine(failure → 400{ success: false, error: 'Invalid input data', details: <zod issues> }),getTenantId()resolution (null → 403'Tenant not found'), user lookup byidANDtenantId(not found → 404'User not found'), OAuth-account check (!user.passwordHash→ 400),bcrypt.compare(currentPassword, hash)(false → 400'Current password is incorrect'),bcrypt.compare(newPassword, hash)for duplicate detection (true → 400'New password must be different from current password'), the load-bearingbcrypt.hash(newPassword, 12)+db.update(users)write, the fault-tolerantsendPasswordChangeConfirmationEmail(...)side-effect, success payload{ success: true, message: 'Password changed successfully' }, and outer catch 500{ success: false, error: 'Internal server error. Please try again later.' }. Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk -- includingX-Forwarded-ForandX-Real-Ipfor rate-limit -- + a ~13-body bulk-loop walk all asserting< 500; an imperative-phrased 401-envelope assertion{ success: false, error: 'Unauthorized. Please sign in.' }; a strict envelope-shape assertion -- exactlysuccess+errorkeys, nodetailsleak on 401; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the eight candidate static messages must appear in any unauth response; a 429-envelope-includes-retryAfter assertion when rate-limit fires; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a Zod-validation-chain-not-entered invariance walk; a bcrypt-compare-gates-not-entered invariance walk; an OAuth-account-check-and-db-update-and-email-send-not-entered invariance walk pinning that the unauth branch must NEVER reach the password-write logic). Cross-references the companion minimal smokeauth-change-password.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 70-of-N and thetests/api/per-spec-file sub-rollout extends to 68-of-many, and the first rate-limit-FIRST POST smoke lands -- pinning a gate posture distinct from every prior auth-FIRST POST smoke and adding aretryAfter-field 429 envelope contract no prior smoke covers. - E2E Item Comments [commentId] Method Spec (
apps/web-e2e/tests/api/item-comments-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's per-comment edit / delete PUT + DELETE method / body / header smoke spec paired withapps/web-e2e/tests/api/item-comments-id-method.spec.ts, the sixty-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-seventh underapps/web-e2e/tests/api/. Pairs with thePUTandDELETEexports ofapps/web/app/api/items/[slug]/comments/[commentId]/route.ts— the first per-source-file PUT + DELETE smoke the docs tree publishes for a public per-comment edit / delete route, and the first per-source-file PUT or DELETE smoke that pins a plain-text 401 envelope instead of a JSON one (the unauth branches returnnew NextResponse('Unauthorized', { status: 401 })-- a plain-text body, NOT JSON; EVERY prior per-source-file mutating-method smoke pins a JSON 401 envelope; this is the FIRST plain-text 401 contract in the rollout). Distinct from the comment-create POST siblingitem-comments-create-body-spec.md: plain-text 401 envelope (NOT JSON like comments POST); plain-text 404 / 403 envelopes for client-profile / tenant errors (NOT JSON); MIXED-envelope contract -- auth / profile / tenant errors return PLAIN-TEXT, body-validation errors (PUT only) return JSON with{ error }(the FIRST per-source-file smoke pinning a mixed plain-text + JSON envelope contract on the same handler); three-step ownership chain -- PUT and DELETE both callcheckDatabaseAvailability()first, thenauth(), thengetClientProfileByUserId(...), thengetTenantId(), then a Drizzle query that filters byuserId === clientProfile.idANDtenantId === <user's tenant>ANDdeletedAt IS NULL(single query); DELETE returns 204 No Content (NOT 200 with a body); PUT body validationcontent === undefined && rating === undefined→ 400 JSON{ error: 'At least one of content or rating must be provided' }(the FIRST per-source-file PUT smoke pinning a partial-update validation contract). Documents the at-a-glance scenario tree (a doubled header walk -- ~10 headers × 2 methods = ~20 tests; a PUT body walk -- ~12 bodies; a canonical plain-text 401 envelope assertion on PUT; a canonical plain-text 401 envelope assertion on DELETE; a no-JSON-prefix invariant for unauth bodies on both methods; a gate-before-post-auth invariant on PUT pinning that NONE of five candidate messages -- mixed plain + JSON -- must appear in any unauth response; a gate-before-post-auth invariant on DELETE across four candidate messages; a parameterised-vs-baseline status-stability comparison; a cross-method probe (POST / PATCH); a malformed-JSON-body invariance walk for PUT; a body-validation-chain-not-entered invariance walk for PUT pinning that the JSON'At least one of content or rating'400 must NEVER fire on unauth; a Drizzle-ownership-query-not-entered invariance walk for both methods; an updateComment-and-deleteComment-not-entered invariance walk pinning that DELETE must NEVER return 204 and PUT must NEVER return a comment payload). Cross-references the companion comment-create POST siblingitem-comments-create-body-spec.md, the public per-item comment-list GET smokeitem-comments-query.spec.ts, the per-comment rating siblingitem-comment-rating-by-id.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 69-of-N and thetests/api/per-spec-file sub-rollout extends to 67-of-many, and the first per-source-file PUT + DELETE smoke pinning a plain-text 401 envelope lands -- expanding the rollout's mutating-method coverage beyond the JSON-envelope family for the first time and pinning the mixed plain-text + JSON envelope contract no prior smoke covers. - E2E Sponsor-Ads Checkout Body Spec (
apps/web-e2e/tests/api/sponsor-ads-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated multi-provider sponsor-ad checkout-session creation POST body / header smoke spec paired withapps/web-e2e/tests/api/sponsor-ads-checkout-body.spec.ts, the sixty-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-sixth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/sponsor-ads/checkout/route.ts— the first per-source-file POST smoke for an auth-gated MULTI-PROVIDER dispatching checkout endpoint the docs tree publishes, extending the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe) into a quintet by adding a checkout endpoint that is NOT tied to a single provider but insteadswitch-dispatches to all three providers based onprocess.env.NEXT_PUBLIC_PAYMENT_PROVIDER. Distinct from ALL FOUR siblings in the checkout quartet: (a) Multi-provider switch dispatch -- the handlerswitch (ACTIVE_PAYMENT_PROVIDER)betweenPaymentProvider.STRIPE,LEMONSQUEEZY, andPOLAR, falling through to a 400 default on unknown providers; the FIRST per-source-file POST smoke that pins a three-way provider dispatch via env var; (b)success: falseenvelope on every error branch -- distinct from the quartet's two-key{ error, message }envelopes; sponsor-ads/checkout returns{ success: false, error }on every error branch (matching the admin endpoints' shape); (c) Open-redirect validation -- the handler callsvalidateRedirectUrl(successUrl)andvalidateRedirectUrl(cancelUrl)to reject cross-origin URLs at the application boundary; the FIRST per-source-file POST smoke that pins an open-redirect-prevention contract: any XSS-shaped or cross-origin URL is silently replaced with a default${appUrl}/sponsor/...route, with aconsole.warn(no echo on the wire); (d) Three-stage post-auth gate stack (404 → 403 → 400) -- after!session?.user?.id→ 401, the handler runssponsorAdService.getSponsorAdById→ 404, thensponsorAd.userId !== session.user.id→ 403 (UNIQUE -- no other checkout has a forbidden branch), thensponsorAd.status !== SponsorAdStatus.PENDING_PAYMENT→ 400; (e)getPriceId(interval, provider)map -- the handler maps(WEEKLY | MONTHLY) × (STRIPE | LEMONSQUEEZY | POLAR)to env-driven price IDs and short-circuits to a generic 400 if the price is not configured; the FIRST per-source-file POST smoke that pins a 2×3 price-matrix lookup; (f)!session?.user?.idgate (matches lemonsqueezy; distinct from polar + solidgate + stripe's!session?.user); (g) Generic 500 on outer catch -- distinct from stripe's three-key envelope and solidgate'ssafeErrorMessageextraction; sponsor-ads/checkout returns the generic{ success: false, error: 'Failed to create checkout session' }on every catch (no detail leak); (h) POST-only export -- distinct from the quartet (which all exportGET+POST); sponsor-ads/checkout only exportsPOST, soGETjoinsPUT/PATCH/DELETEin the cross-method walk. The POST handler combinesauth()session lookup (load-bearing first gate;!session?.user?.id→ 401{ success: false, error: 'Unauthorized' }), JSON body parse via destructuredawait request.json()AFTER the auth gate (NO per-call try/catch),!sponsorAdId→ 400,sponsorAdService.getSponsorAdById(sponsorAdId)lookup (null → 404),sponsorAd.userId !== session.user.id→ 403,sponsorAd.status !== SponsorAdStatus.PENDING_PAYMENT→ 400 with current-status echo,getPriceId(interval, provider)lookup (null → 400'Payment configuration is incomplete. Please contact support.'),validateRedirectUrl(successUrl / cancelUrl)open-redirect prevention (invalid URLs replaced with default${appUrl}/sponsor/{success|cancel}routes), the switch-on-ACTIVE_PAYMENT_PROVIDERdispatch (STRIPE→createStripeCheckout,LEMONSQUEEZY→createLemonSqueezyCheckout,POLAR→createPolarCheckout,default→ 400),!checkoutResult.url→ 500'Failed to create checkout URL', success payload{ success: true, data: { checkoutId, checkoutUrl, provider }, message: 'Checkout session created successfully' }, and outer catch{ success: false, error: 'Failed to create checkout session' }with status 500 (generic message, no detail leak). Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a canonical success-false 401-envelope assertion{ success: false, error: 'Unauthorized' }; a strict envelope-shape assertion -- exactlysuccess+errorkeys,success: falsediscriminant, nomessage/data/detailsleak; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the seven candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (GET / PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a sponsorAdId-required-validation-not-entered invariance walk; an ownership/status/not-found-checks-not-entered invariance walk; a provider-switch-dispatch-not-entered invariance walk pinning that all three provider branches must NEVER produce adata.checkoutUrlon the unauth branch; a catch-branch-not-entered invariance walk; a no-redirect-leak assertion pinning that XSS-shapedsuccessUrl/cancelUrlvalues must NEVER be echoed; a provider-name non-disclosure assertion pinning thatdata.providerand the literal strings'stripe'/'lemonsqueezy'/'polar'must NEVER appear). Cross-references the four sibling auth-gated checkout POST smokessolidgate-checkout-body-spec.md,polar-checkout-body-spec.md,lemonsqueezy-checkout-body-spec.md, andstripe-checkout-body-spec.md, the public read-side counterpartsponsor-ads-public.spec.ts, the five admin write-side counterparts underadmin/sponsor-ads/, the multi-provider siblingpayment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 68-of-N and thetests/api/per-spec-file sub-rollout extends to 66-of-many, and the first per-source-file POST smoke for an auth-gated MULTI-PROVIDER dispatching checkout endpoint lands -- extending the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe) into a quintet and pinning the multi-provider switch dispatch + open-redirect validation + three-stage post-auth gate stack contract that distinguishes sponsor-ads/checkout from its four siblings. - E2E Stripe Checkout Body Spec (
apps/web-e2e/tests/api/stripe-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated Stripe checkout-session creation POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-checkout-body.spec.ts, the sixty-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-fifth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/checkout/route.ts— the fourth and final per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes, completing the auth-gated checkout quartet (aftersolidgate-checkout-body-spec.md,polar-checkout-body-spec.md, andlemonsqueezy-checkout-body-spec.md). Distinct from ALL three siblings: (a) Three-way mode ternary mapping --mode === 'one_time' ? 'payment' : mode === 'subscription' ? 'subscription' : 'setup'-- UNIQUE: unknown mode values fall through to the'setup'Stripe mode (Setup Intent flow); distinct from polar's two-way subscription/one_time dispatch and lemonsqueezy/solidgate which take whatever mode the body provides; (b) Trial-amount validation --hasTrial = trialPeriodDays > 0 && isAuthorizedTrialAmount; ifhasTrial && !trialAmountId→ 400{ error: 'Invalid trial configuration', message: 'trialAmountId is required when trial is enabled' }; the FIRST per-source-file POST smoke that pins a trial-config validation contract; (c) Helper-function pipeline -- the handler chainsbuildCheckoutLineItems(...),createBaseCheckoutParams(...), andapplySubscriptionConfig(...)from the co-located./helpersmodule; the FIRST per-source-file POST smoke that pins a multi-helper assembly pipeline; (d)safeErrorMessage(NOTsafeErrorResponse) in catch -- the outer catch usessafeErrorMessage(error, 'Failed to create checkout session')to extract the message and wraps it in a manual 500 envelope with{ error, message, details: <dev-only-stack> }; distinct from polar'ssafeErrorResponse(...)and solidgate'ssafeErrorMessage(...)-- stripe-checkout returns THREE keys on catch (matching solidgate's success-branch shape); (e) Stripe SDK direct call -- the handler callsstripe.checkout.sessions.create(checkoutParams)viastripeProvider.getStripeInstance(); the FIRST per-source-file POST smoke that pins a direct-SDK-instance access contract via a public method (NOT private propertyas anylike polar's one_time branch); (f)!session?.usergate (matches polar + solidgate; distinct from lemonsqueezy's!session?.user?.id). The POST handler combinesauth()session lookup (load-bearing first gate; missing → 401{ error: 'Unauthorized', message: 'Authentication required' }),getOrCreateStripeProvider()+getStripeInstance(), JSON body parse via destructuredawait request.json()AFTER the auth gate (NO per-call try/catch), the three-way mode ternary mapping,stripeProvider.getCustomerId(session.user)lookup (failure → 400'Failed to create customer'), the trial-config validation, the helper-function pipeline, the load-bearingstripe.checkout.sessions.create(checkoutParams)Stripe SDK call, success payload{ data: { id, url }, status: 200, message: 'Checkout session created successfully' }, and outer catch withsafeErrorMessage(error, 'Failed to create checkout session')and dev-only stack-trace details (500 with three-key envelope). Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~16-body bulk-loop walk all asserting< 500; a canonical two-key 401-envelope assertion; a strict envelope-shape assertion -- exactlyerror+messagekeys, nosuccessordetailsleak; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the four candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a trial-config-validation-not-entered invariance walk; a mode-ternary-not-entered invariance walk pinning that all three mode branches must NEVER produce adata.urlon the unauth branch; a helper-pipeline-and-stripe-SDK-not-entered invariance walk; a catch-branch-not-entered invariance walk pinning thatdetails(dev-only stack) must NEVER appear on the unauth branch; a no-redirect-leak assertion pinning that XSS-shapedsuccessUrl/cancelUrlvalues must NEVER be echoed). Cross-references the first auth-gated checkout POST smokesolidgate-checkout-body-spec.md, the secondpolar-checkout-body-spec.md, the thirdlemonsqueezy-checkout-body-spec.md, the Stripe webhook companionstripe-webhook-body-spec.md(signature-verified webhook on same provider, completely different gate posture), the multi-provider siblingpayment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 67-of-N and thetests/api/per-spec-file sub-rollout extends to 65-of-many, and the fourth and final per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- completing the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe) and pinning the three-way mode ternary + trial-config validation + helper-function pipeline contract that distinguishes Stripe from its three siblings. - E2E LemonSqueezy Checkout Body Spec (
apps/web-e2e/tests/api/lemonsqueezy-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated LemonSqueezy checkout-session creation POST body / header smoke spec paired withapps/web-e2e/tests/api/lemonsqueezy-checkout-body.spec.ts, the sixty-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-fourth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/lemonsqueezy/checkout/route.ts— the third per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes (aftersolidgate-checkout-body-spec.mdandpolar-checkout-body-spec.md). Distinct from BOTH siblings: (a)!session?.user?.idgate -- NOT!session?.userlike polar and solidgate; pins the user-id-required 401 contract; (b) Custom validator returning{ isValid, errors[] }-- the handler callsvalidateCheckoutRequestBody(body)from@/lib/payment/config/validation(NOT ZodsafeParselike solidgate; NOT simpleif (!field)like polar); errors joined with', '; the FIRST per-source-file POST smoke that pins a custom-validator contract; (c) Per-call try/catch aroundawait request.json()with explicit'Invalid JSON in request body'400 envelope (matches solidgate; distinct from polar which has NO try/catch); (d) Dev-only PII-sanitizedconsole.log-- inNODE_ENV === 'development'the handler logs the truncated email, redacted custom price, and dark-mode flag; the FIRST per-source-file POST smoke that pins a PII-sanitized dev-only logging contract; (e) FOUR-string-scan catch with THREE different status codes --'Missing required environment variables'→ 500 CONFIGURATION_ERROR;'Invalid email format'→ 400 VALIDATION_ERROR;'Custom price must be'→ 400 VALIDATION_ERROR;'Lemonsqueezy'→ 503 PAYMENT_SERVICE_ERROR; the FIRST per-source-file POST smoke that pins a four-string error-message-detection chain spanning 400 / 500 / 503; (f)ERROR_TYPESenum-typederrorfield -- uses constants from@/lib/payment/config/types(VALIDATION_ERROR,CONFIGURATION_ERROR,PAYMENT_SERVICE_ERROR,INTERNAL_ERROR); UNIQUE: the FIRST per-source-file POST smoke that pins enum-typed error codes; (g) GET export with NO auth gate -- the GET handler reads query params and creates checkouts WITHOUT auth -- a Q-010-style finding; the cross-method probe pins this divergence from POST; (h)success: truediscriminant in success payload -- distinct from polar + solidgate which use literalstatus: 200. The POST handler combinesauth()session lookup (load-bearing first gate;!session?.user?.id→ 401{ error: 'Unauthorized', message: 'Authentication required' }), JSON body parse viaawait request.json()INSIDE per-call try/catch (failure → 400{ error: 'VALIDATION_ERROR', message: 'Invalid JSON in request body' }),getOrCreateLemonsqueezyProvider()singleton initialization,validateCheckoutRequestBody(body)(failure → 400 with joined errors), dev-only PII-sanitizedconsole.log, the load-bearinglemonsqueezyProvider.createCustomCheckout({ email, customPrice, variantId, metadata, dark })call, success payload{ success: true, data: { checkoutUrl, email, customPrice, variantId, metadata }, message: 'Checkout session created successfully' }, and outer catch with FOUR error-message-scan branches. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~17-body bulk-loop walk all asserting< 500; a canonical two-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion; a gate-before-post-auth invariant pinning that NONE of the five candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion acrossUnauthorizedand fourERROR_TYPEScodes; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a JSON-parse-failure-AFTER-auth-gate invariance walk pinning that the unauth branch must produce 401 even with malformed JSON; a validation-chain-not-entered invariance walk; a createCustomCheckout-not-entered invariance walk; a four-string-scan-catch-not-entered invariance walk pinning that NONE ofCONFIGURATION_ERROR/PAYMENT_SERVICE_ERROR/INTERNAL_ERRORmay appear on the unauth branch). Cross-references the first auth-gated checkout POST smokesolidgate-checkout-body-spec.md, the secondpolar-checkout-body-spec.md, the LemonSqueezy webhook companionlemonsqueezy-webhook-body-spec.md(signature-verified webhook on same provider, completely different gate posture), the LemonSqueezy list siblinglemonsqueezy-list-query.spec.ts, the multi-provider siblingpayment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 66-of-N and thetests/api/per-spec-file sub-rollout extends to 64-of-many, and the third per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- expanding the rollout's payment-provider checkout coverage from two providers (Solidgate + Polar) to three (Solidgate + Polar + LemonSqueezy) and pinning the custom-validator + four-string-scan catch + enum-typed-error-codes contract no prior auth-gated payment-provider POST smoke covers. - E2E Polar Checkout Body Spec (
apps/web-e2e/tests/api/polar-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated Polar checkout-session creation POST body / header smoke spec paired withapps/web-e2e/tests/api/polar-checkout-body.spec.ts, the sixty-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-third underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/polar/checkout/route.ts— the second per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes (aftersolidgate-checkout-body-spec.md). Distinct from solidgate-checkout: (a) Branching mode dispatch -- the handler branches onmode === 'subscription'(default, callspolarProvider.createSubscription(...)) vsmode === 'one_time'(calls privatepolar.checkouts.create(...)via(polarProvider as any).polar); the FIRST per-source-file POST smoke that pins a mode-dispatched two-branch POST contract; (b) NO Zod validation -- distinct from solidgate which usescheckoutSchema.safeParse(json); polar uses simpleif (!productId)check; (c) NO try/catch aroundrequest.json()-- malformed JSON cascades to the OUTER catch (distinct from solidgate's per-call try/catch); (d) 503 error-message detection -- outer catch scanserror.messagefor three strings ('Payments are currently unavailable','needs to complete their payment setup','payment setup incomplete') and downgrades 500 → 503 with a custom payment-setup-incomplete message; the FIRST per-source-file POST smoke that pins a 503-via-error-message-scan contract; (e) Private property access viaas any-- the'one_time'branch reaches into(polarProvider as any).polarand.organizationId; the FIRST per-source-file POST smoke that pins a private-property-bypass contract; (f) GET export companion -- the route exportsGETfor retrieve-checkout-by-id (unauth GET → 401 ONE-key envelope, distinct from POST's TWO-key envelope). The POST handler combinesauth()session lookup (load-bearing first gate; missing → 401{ error: 'Unauthorized', message: 'Authentication required' }),getOrCreatePolarProvider()singleton initialization, JSON body parse via destructuredawait request.json()AFTER the auth gate (NO per-call try/catch),productIdrequired check (if (!productId)→ 400{ error: 'Invalid request', message: 'Product ID is required' }),polarProvider.getCustomerId(session.user)lookup (failure → 400{ error: 'Failed to create customer', message: 'Unable to create Polar customer' }), the mode dispatch (subscription default OR one_time branch with private-property access), success payload{ data: { id, url }, status: 200, message: 'Checkout session created successfully' }, and outer catch with payment-setup-incomplete-string scan (downgrades 500 → 503 with custom message) orsafeErrorResponse(error, 'Failed to create checkout session'). Documents the at-a-glance scenario tree (a ~10-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a canonical two-key 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofdataand literalstatus: 200may appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the six candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a productId-required-check-not-entered invariance walk; a mode-dispatch-not-entered invariance walk pinning that the unauth response must NEVER echo adata.urlordata.id; a 503-payment-setup-incomplete-not-triggered-on-unauth invariance walk; a no-redirect-leak assertion pinning that XSS-shapedsuccessUrl/cancelUrlvalues must NEVER be echoed). Cross-references the first auth-gated checkout POST smokesolidgate-checkout-body-spec.md, the Polar webhook companionpolar-webhook-body-spec.md(signature-verified webhook on the same provider, completely different gate posture), the Polar subscription portal POST siblingpolar-subscription-portal-body.spec.ts(single-key 401 envelope), the multi-provider siblingpayment-checkouts.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 65-of-N and thetests/api/per-spec-file sub-rollout extends to 63-of-many, and the second per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- expanding the rollout's payment-provider checkout coverage from a single drilled-into provider (Solidgate) to two (Solidgate + Polar) and pinning the mode-dispatched + 503-via-error-message-scan contract no prior auth-gated payment-provider POST smoke covers. - E2E Stripe Webhook Body Spec (
apps/web-e2e/tests/api/stripe-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Stripe payment-provider webhook POST body / header smoke spec paired withapps/web-e2e/tests/api/stripe-webhook-body.spec.ts, the sixty-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-second underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/stripe/webhook/route.ts— the fourth and final per-source-file webhook POST smoke the docs tree publishes, completing the four-provider webhook quartet (afterpolar-webhook-body-spec.md,lemonsqueezy-webhook-body-spec.md, andsolidgate-webhook-body-spec.md). This is the simplest of the four handlers: (a) Single-header signature check viastripe-signature-- unique header name distinct from polar (webhook-signature), lemonsqueezy (x-signature), and solidgate (x-signature || solidgate-signature); (b) NO JSON parse -- the handler reads the raw body viaawait request.text()and passes it as a STRING directly tostripeProvider.handleWebhook(body, signature)(matches lemonsqueezy; distinct from polar and solidgate which parse viaJSON.parse(body)); (c) NOvalidateWebhookPayloadcheck -- distinct from polar's 4-tier chain; (d) NO idempotency check -- distinct from solidgate's in-memorySet<string>tracker; (e) NO event-type-string-fallback in the switch dispatcher -- matches ONLY theWebhookEventTypeenum values (8 mapped + the UNIQUEBILLING_PORTAL_SESSION_UPDATEDStripe-specific event = 9 cases; distinct from solidgate which accepts both enum AND lowercase strings); (f)BILLING_PORTAL_SESSION_UPDATEDin switch -- UNIQUE Stripe-specific event handler that NO other webhook smoke covers; (g) POST-only export -- matches polar and lemonsqueezy; distinct from solidgate which also exports a documentation GET handler; (h) Same 400-default catch as all three siblings -- outer catch returns 400 (NOT 500) via rawNextResponse.json({ error: 'Webhook processing failed' }, { status: 400 }). The POST handler combines a raw-body read viaawait request.text(), astripe-signatureheader presence check (missing → 400{ error: 'No signature provided' }),stripeProvider.handleWebhook(body, signature)for the load-bearing signature-verification call (receives the RAW body STRING, not a parsed object), a!webhookResult.receivedcheck (400{ error: 'Webhook not processed' }), the switch-statement event dispatcher (9 event types matched onWebhookEventTypeenum values ONLY -- no string fallback), success payload{ received: true }with status 200, and outer catchconsole.error+ 400{ error: 'Webhook processing failed' }. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~11-body bulk-loop walk all asserting< 500; a first-gate signature-header-presence-rejection assertion{ error: 'No signature provided' }; a strict envelope-shape assertionObject.keys(body) === ['error']across all rejection branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant pinning that NO unhandled error escapes as 5xx; an allowed-pre-delivery-error static-string allow-list assertion -- 3-message set; a sibling-provider-headers-ignored assertion pinning that polar / lemonsqueezy / solidgate signature headers do NOT satisfy the Stripe gate; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a signature-verification-call-gated-by-header-check invariant; a switch-statement-dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 9 event handlers including the Stripe-uniqueBILLING_PORTAL_SESSION_UPDATEDcase). Cross-references the first webhook POST smokepolar-webhook-body-spec.md, the secondlemonsqueezy-webhook-body-spec.md, the thirdsolidgate-webhook-body-spec.md, the multi-provider siblingwebhooks.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 64-of-N and thetests/api/per-spec-file sub-rollout extends to 62-of-many, and the fourth and final per-source-file webhook POST smoke lands -- completing the four-provider webhook quartet (Polar + LemonSqueezy + Solidgate + Stripe) and pinning the simplest webhook handler contract that distinguishes Stripe from its three siblings. - E2E Solidgate Checkout Body Spec (
apps/web-e2e/tests/api/solidgate-checkout-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated Solidgate checkout-session creation POST body / header smoke spec paired withapps/web-e2e/tests/api/solidgate-checkout-body.spec.ts, the sixty-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixty-first underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/solidgate/checkout/route.ts— the first per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes (the existing multi-providerpayment-checkouts.spec.tscovers all four providers' checkout endpoints with a single< 500assertion each; this spec drills into the Solidgate handler specifically and pins its load-bearing 401-before-everything gate posture). Distinct from the closest analoguepolar-subscription-portal-body-spec.md(auth-gated POST, 401-before-everything posture) in five ways: (a) TWO-key 401 envelope -- Solidgate returns{ error: 'Unauthorized', message: 'Authentication required' }(TWO keys); polar-portal returns{ error: 'Unauthorized' }(ONE key); UNIQUE: the FIRST per-source-file POST body smoke that pins a two-key 401 envelope on a payment-provider route; (b) ZodsafeParseAFTER the auth gate -- thecheckoutSchema.safeParse(json)and the surroundingtry/catcharoundrequest.json()fire only AFTERauth(), so the unauth branch never reaches them; polar-portal does NOT callrequest.json()at all on the unauth branch; (c) FIVE-key success envelope -- solidgate's success branch returns{ data: { id, url }, status, message }(THREE top-level keys, including a literalstatus: 200field embedded in the body, separate from the HTTP status); UNIQUE: the FIRST per-source-file POST smoke that pins a literal-status-key success envelope; (d) 500 catch (NOT 400) -- solidgate's outer catch returns 500 with{ error, message, details }(dev-only stack); polar-webhook usessafeErrorResponse(..., 400); UNIQUE: the FIRST per-source-file POST smoke that pins a 500-default catch on a payment-provider route -- but ONLY reachable AFTER the auth gate; (e) POST-only export -- GET / PUT / PATCH / DELETE are NOT exported; method-resolution returns 405. The POST handler combinesauth()session lookup (load-bearing first gate; missing → 401{ error: 'Unauthorized', message: 'Authentication required' }),getOrCreateSolidgateProvider()singleton initialization, the ZodcheckoutSchema(amount.positive(),currency.default('USD'),mode.enum(['one_time', 'subscription']).default('one_time'),successUrl.url(),cancelUrl.url(),metadata.optional()), a per-callrequest.json()try/catch (failure → 400{ error: 'Invalid JSON', message: 'Request body must be valid JSON' }; schema mismatch → 400{ error: 'Invalid request body', message: <zod-issues-joined> }),solidgateProvider.getCustomerId(session.user)(not found → 400{ error: 'Failed to create customer', message: 'Unable to create Solidgate customer' }),solidgateProvider.createPaymentIntent(...)for the load-bearing payment-provider call, success payload{ data: { id, url }, status: 200, message: 'Checkout session created successfully' }, and outer catchsafeErrorMessage(error, 'Failed to create checkout session')+ dev-onlydetails: error.stack→ 500. Documents the at-a-glance scenario tree (a ~25-body bulk-loop walk + a ~12-header bulk-loop walk all asserting< 500; a canonical two-key 401-envelope assertion pinning the load-bearing envelope shape exactly; a strict envelope-shape invariance walk pinningObject.keys(body).sort() === ['error', 'message']across every body permutation; a no-Zod-issue-leak invariance walk pinning thatamount/successUrl/cancelUrl/mode/'Invalid request body'/'Invalid JSON'strings must NEVER appear in the unauth response -- a regression that ransafeParse(...)before the auth gate would surface here; a no-success-key-leak invariance walk pinning thatdata/id/url/ literalstatus: 200must NEVER appear -- a regression that rancreatePaymentIntent(...)before the auth gate would surface here; a no-redirect-leak assertion pinning that caller-suppliedsuccessUrl/cancelUrlvalues must NEVER be echoed; a malformed-JSON-pre-gate-non-downgrade assertion pinning thatrequest.json()parse-fail does NOT downgrade the unauth 401 to a 400'Invalid JSON'; a catch-branch-non-entry walk pinning that the unauth branch must NEVER reach the 500 outer catch; a static-string allow-list assertion pinning a 1-message set (only'Unauthorized'is reachable on the unauth surface); a side-channel walk pinning that fabricated cookies / Bearer tokens / admin-shaped headers do NOT satisfyauth(); a cross-method probe pinning that GET / PUT / PATCH / DELETE return< 500(Next.js 405); a 401-status-invariance walk pinning that every documented bypass-key shape rounds-trips to the same 401 as the empty-body baseline). Cross-references the sibling Solidgate webhook smokesolidgate-webhook-body-spec.md(same provider singleton, completely different gate posture -- signature verification vs session auth), the closest analoguepolar-subscription-portal-body-spec.md, the multi-provider siblingpayment-checkouts.spec.ts(covers all four providers' checkout endpoints with a single< 500assertion each), and to Spec 010 -- E2E Test Coverage and Spec 002 -- Plugin Architecture for the governing specs. With this entry the per-spec-file docs rollout extends to 63-of-N and thetests/api/per-spec-file sub-rollout extends to 61-of-many, and the first per-source-file POST smoke for an auth-gated payment-provider checkout endpoint lands -- expanding the rollout's payment-provider checkout coverage from a single< 500smoke to a deep body-surface walk on the Solidgate handler and pinning the two-key 401 envelope contract no prior auth-gated payment-provider POST smoke covers. - E2E Solidgate Webhook Body Spec (
apps/web-e2e/tests/api/solidgate-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Solidgate payment-provider webhook POST body / header smoke spec paired withapps/web-e2e/tests/api/solidgate-webhook-body.spec.ts, the sixty-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixtieth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/solidgate/webhook/route.ts— the third per-source-file webhook POST smoke the docs tree publishes (afterpolar-webhook-body-spec.mdandlemonsqueezy-webhook-body-spec.md). Distinct from BOTH polar and lemonsqueezy: (a) Two-header signature fallback -- Solidgate readsx-signature || solidgate-signature-- UNIQUE: NEITHER polar (webhook-signature) NOR lemonsqueezy (x-signatureonly) uses this two-header fallback pattern; (b) Manual JSON parse like polar but NOvalidateWebhookPayloadcheck -- the handler callsJSON.parse(body)(matching polar) but DOES NOT validate the parsed shape (distinct from polar's 4-tier chain); (c) In-memory idempotency Set -- the FIRST webhook smoke that pins an idempotency contract;processedWebhooks: Set<string>trackswebhookIdfor 24 hours viasetTimeout(...); duplicate webhook IDs return 200{ received: true }(NOT 400) -- the FIRST webhook smoke with TWO 200-success branches; (d) Switch dispatcher accepting BOTH enum AND string values -- 9 event types are matched on BOTH theWebhookEventType.PAYMENT_SUCCEEDEDenum AND the lowercase'payment_succeeded'string; (e) GET export with informative message -- the route exports a GET handler that returns 200{ message: 'Solidgate webhook endpoint', instructions: '...', method: 'POST' }-- UNIQUE: polar and lemonsqueezy export only POST; (f) Same 400-default catch as polar + lemonsqueezy. The POST handler combines a raw-body read viaawait request.text(), the two-header signature fallback (x-signature || solidgate-signature; missing BOTH → 400'No signature provided'),JSON.parse(body)INSIDE the outer try block (failure cascades to catch), an idempotency check (webhookId = parsedBody.id || x-request-id; ifprocessedWebhooks.has(webhookId)→ 200{ received: true }),solidgateProvider.handleWebhook(parsedBody, signature, body)for the load-bearing signature-verification call (receives parsed body, raw signature, AND raw body string), a!webhookResult.receivedcheck (400'Webhook not processed'), the switch-statement event dispatcher (9 event types matched on both enum and string), success payload{ received: true }with status 200, and outer catchconsole.error+ 400'Webhook processing failed'. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~11-body bulk-loop walk all asserting< 500; a first-gate two-header-fallback rejection assertion pinning that missing BOTHx-signatureANDsolidgate-signatureproduces 400; a fallback-header-acceptance assertion pinning thatsolidgate-signaturealone satisfies the gate; a strict envelope-shape assertion across all rejection branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant; an allowed-pre-delivery-error static-string allow-list assertion; a polar-shape-headers-ignored assertion pinning that polar'swebhook-signaturedoes NOT satisfy the Solidgate gate; a side-channel walk; a GET-200-with-informative-message assertion -- UNIQUE to Solidgate; a cross-method probe (PUT / PATCH / DELETE); a signature-verification-call-gated-by-header-check invariant; a switch-statement-dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 9 event handlers). Cross-references the first webhook POST smokepolar-webhook-body-spec.md, the second webhook POST smokelemonsqueezy-webhook-body-spec.md, the multi-provider siblingwebhooks.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 62-of-N and thetests/api/per-spec-file sub-rollout extends to 60-of-many, and the third per-source-file webhook POST smoke lands -- expanding the rollout's payment-provider webhook coverage from two providers (Polar + LemonSqueezy) to three (Polar + LemonSqueezy + Solidgate) and pinning the in-memory idempotency contract no prior webhook smoke covers. - E2E Item Comments Rating Query Spec (
apps/web-e2e/tests/api/item-comments-rating-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item rating-aggregate GET query-param / header smoke spec paired withapps/web-e2e/tests/api/item-comments-rating-query.spec.ts, the sixty-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-ninth underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/items/[slug]/comments/rating/route.ts— the first per-source-file query smoke for a public item-detail endpoint that usescheckDatabaseAvailability()as a graceful-fallback gate (NOT as a 503-returning gate like the siblingitem-comments-create-body-spec.mdPOST). Whenprocess.env.DATABASE_URLis missing OR the tenant resolution returns null OR the Drizzle aggregate query throws, the handler returns the SAME success-shaped envelope{ averageRating: 0, totalRatings: 0 }with status 200 — NEVER a 4xx or 5xx. This is a deliberately permissive contract: the item-detail page reads ratings on every render, and a 503 / 500 response would cause a render-time failure instead of a quiet zero-ratings display. It is also the first per-source-file query smoke that pins a two-key envelope shape ({ averageRating, totalRatings }) with NOsuccessdiscriminant key — distinct from every prior per-source-file query spec which uses either the canonical{ success: true, data: ... }envelope OR the bare{ error }envelope. The GET handler is a zero-request-read signature: it only awaitsparams, callscheckDatabaseAvailability(),getItemIdFromSlug(slug),getTenantId(), and a single Drizzleselect({ avg, count })aggregate filtered byeq(itemId)+isNull(deletedAt)+eq(tenantId). There is NOrequest.url,request.headers, orsearchParams.get(...)access anywhere — the route is invariant to any query parameter the caller appends. The handler combines the load-bearingcheckDatabaseAvailability()graceful-fallback gate (NON-null → 200 zero-rating envelope),paramsresolve,getItemIdFromSlug(slug)synchronous slug→id mapping,getTenantId()graceful-fallback (null → 200 zero-rating envelope), the Drizzleselect({ avg(rating), count() })aggregate, success payload{ averageRating: Number(avg) || 0, totalRatings: Number(count) || 0 }with status 200, and outer catchconsole.warn(dev-only) + 200{ averageRating: 0, totalRatings: 0 }(NOT a 500 -- the route NEVER surfaces a 5xx). Documents the at-a-glance scenario tree (a ~57-path bulk-loop walk asserting< 500; a canonical-envelope 200 zero-rating assertion{ averageRating: 0, totalRatings: 0 }; a strict envelope-shape assertionObject.keys(body) === ['averageRating', 'totalRatings']with NOsuccess/data/errorkeys; a Number-cast invariant pinning thataverageRatingandtotalRatingsare bothnumber-- a regression that bypasses theNumber(...)cast (returning the raw Drizzleavg(...)string) would surface here; a graceful-degrade non-disclosure walk pinning that the response must NEVER echoerrororsuccesskeys; a parameterised-vs-baseline status-stability comparison across five paths; an envelope-shape invariance walk pinning that all five parameterised paths return the same{ averageRating: 0, totalRatings: 0 }envelope; anAcceptheader isolation walk; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a graceful-degrade catch-branch invariance walk pinning that no error path surfaces a 5xx). Cross-references the siblingitem-comments-create-body-spec.md(usescheckDatabaseAvailability()as a load-bearing 503-returning gate -- the OPPOSITE of this route's graceful-fallback gate), the auth-gateditem-votes-status-query-spec.md, the public count-only siblingitem-vote-count-query.spec.ts, the per-comment rating siblingitem-comment-rating-by-id.spec.ts, the public per-item-endpoint gauntletitem-public.spec.ts(which already pins the< 500no-server-error contract for this route as one of many; this spec drills into the query-param surface specifically), and to Spec 010 -- E2E Test Coverage and Spec 002 -- Plugin Architecture for the governing specs. With this entry the per-spec-file docs rollout extends to 61-of-N and thetests/api/per-spec-file sub-rollout extends to 59-of-many, and the first graceful-fallbackcheckDatabaseAvailability()query smoke the docs tree publishes lands as a complementary surface to the sibling 503-returning POST -- pinning a two-key zero-rating envelope contract no prior smoke covers. - E2E LemonSqueezy Webhook Body Spec (
apps/web-e2e/tests/api/lemonsqueezy-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's LemonSqueezy payment-provider webhook POST body / header smoke spec paired withapps/web-e2e/tests/api/lemonsqueezy-webhook-body.spec.ts, the sixtieth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-eighth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/lemonsqueezy/webhook/route.ts— the second per-source-file webhook POST smoke the docs tree publishes (afterpolar-webhook-body-spec.md). Distinct from the polar sibling: (a) Different signature header -- LemonSqueezy usesx-signature(lowercase, single field); Polar useswebhook-signature+webhook-timestamp+webhook-id; (b) NO manual JSON parse -- the handler reads the raw body viaawait request.text()and passes it as a STRING tolemonSqueezyProvider.handleWebhook(body, signature); the provider parses the body itself, the route does NOT callJSON.parse(bodyText); (c) Simpler 2-tier rejection chain -- only'No signature provided'(400) and'Webhook not processed'(400). Polar has 4 tiers (Invalid JSON payload / Invalid webhook payload / No signature provided / Webhook not processed); (d) Switch-statement event dispatcher --webhookResult.typeis mapped viamapLemonSqueezyEventType(...)and dispatched into one of 8 distinct handlers (subscription created / updated / cancelled, payment succeeded / failed, subscription payment succeeded / failed, trial ending); (e) Same 400-default catch as polar but via rawNextResponse.jsoncall -- outer catch isNextResponse.json({ error: 'Webhook processing failed' }, { status: 400 }), NOTsafeErrorResponse(...)like polar uses. The POST handler combines a raw-body read viaawait request.text(), ax-signatureheader presence check (missing → 400{ error: 'No signature provided' }),lemonSqueezyProvider.handleWebhook(body, signature)for the load-bearing signature-verification call (receives the RAW body STRING, not a parsed object), a!webhookResult.receivedcheck (400{ error: 'Webhook not processed' }),mapLemonSqueezyEventType(webhookResult.type)mapping, the switch-statement event dispatcher (8 mapped handlers + defaultconsole.log), success payload{ received: true }with status 200, and outer catchconsole.error+ 400{ error: 'Webhook processing failed' }. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~12-body bulk-loop walk all asserting< 500; a first-gate signature-header-presence-rejection assertion{ error: 'No signature provided' }; a strict envelope-shape assertionObject.keys(body) === ['error']across all rejection branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant pinning that NO unhandled error escapes as 5xx; an allowed-pre-delivery-error static-string allow-list assertion -- 3-message set vs polar's 5-message set; a polar-shape-headers-ignored assertion pinning that polar'swebhook-signaturedoes NOT satisfy LemonSqueezy'sx-signaturegate; a side-channel walk; a cross-method probe -- POST is the only exported method; a signature-verification-call-gated-by-header-check invariant; a switch-statement-dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 8 event handlers). Cross-references the first webhook POST smokepolar-webhook-body-spec.md, the multi-provider siblingwebhooks.spec.ts, the lemonsqueezy/list siblinglemonsqueezy-list-query.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 60-of-N and thetests/api/per-spec-file sub-rollout extends to 58-of-many, and the second per-source-file webhook POST smoke lands -- expanding the rollout's payment-provider webhook coverage from one provider (Polar) to two (LemonSqueezy + Polar) and pinning the simpler 2-tier rejection chain that distinguishes LemonSqueezy from Polar's 4-tier chain. - E2E Item Votes Status Query Spec (
apps/web-e2e/tests/api/item-votes-status-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's auth-gated per-item vote-status GET query-param / header smoke spec paired withapps/web-e2e/tests/api/item-votes-status-query.spec.ts, the fifty-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-seventh underapps/web-e2e/tests/api/, and the tenth per-source-file query-spec the docs tree publishes (the first nine are all admin-tree). Pairs with theGETexport ofapps/web/app/api/items/[slug]/votes/status/route.ts— the first non-admin per-source-file query smoke for an auth-gated GET that returns the current user's vote record (ornull) for a specific item (distinct from the public/api/items/[slug]/votesGET thatitem-votes-query.spec.tscovers and from the public count-only siblingitem-vote-count-query.spec.ts). It is also the first per-source-file query smoke that pairs the'Authentication required'401 message (matching the siblingitem-comments-create-body-spec.mdPOST) with the bare{ error }envelope (nosuccess: falsewrapper) — distinct from the canonical{ success: false, error: 'Unauthorized' }envelope used by the siblingitem-votes-cast-body-spec.mdPOST. The GET handler is a zero-request-read signature: it awaitsauth()first, thencontext.params, then callsgetClientProfileByUserId(...)andgetVoteByUserIdAndItemId(...). There is NOrequest.url,request.headers, orsearchParams.get(...)access anywhere in the body — the route is invariant to any query parameter the caller appends. The handler combinesauth()session lookup, a!session?.user?.idgate (→ 401{ error: 'Authentication required' }with the bare envelope and NOsuccesskey),context.paramsresolve,getClientProfileByUserId(session.user.id)lookup (not found → 404{ error: 'Client profile not found' }),getVoteByUserIdAndItemId(clientProfile.id, slug)for the load-bearing data read, success payloadvotes[0] || nullwith status 200 (the only docs-tree per-source-file smoke that pins anull-or-record payload contract), and outer catchconsole.error+ 500{ error: 'Failed to fetch vote status' }. Documents the at-a-glance scenario tree (a ~57-path bulk-loop walk asserting< 500; a canonical-envelope authentication-required 401 assertion{ error: 'Authentication required' }; a strict envelope-shape assertionObject.keys(body) === ['error']with NOsuccesskey; a gate-before-post-auth invariant pinning that NONE of the three candidate post-auth static messages must appear in any unauth response; a vote-record non-disclosure walk pinning that NONE of the six record keys --id,userId,itemId,voteType,createdAt,updatedAt-- may appear on the unauth branch; a null-payload non-disclosure pinning that the unauth response is NOT the literalnullpayload; a parameterised-vs-baseline status-stability comparison across five paths; anAcceptheader isolation walk; a side-channel walk; a cross-method probe (POST / PUT / PATCH / DELETE); a client-profile-lookup-not-entered invariance walk; a vote-record-read-not-entered invariance walk). Cross-references the sibling POST smokesitem-votes-cast-body-spec.md(same auth-gate-then-data-read pattern, canonical{ success: false, error: 'Unauthorized' }envelope) anditem-comments-create-body-spec.md(same'Authentication required'401 message, but with{ success: false, error }envelope), the public per-item votes GET smokesitem-votes-public.spec.tsanditem-votes-query.spec.ts, the auth-gated-endpoint gauntletpayment-protected.spec.ts(which already pins the< 500no-server-error contract for this route as one of many; this spec drills into the query-param surface specifically), and to Spec 010 -- E2E Test Coverage for the governing spec and Spec 002 -- Plugin Architecture for the architectural target. With this entry the per-spec-file docs rollout extends to 59-of-N and thetests/api/per-spec-file sub-rollout extends to 57-of-many, and the first auth-gated non-admin per-source-file query smoke the docs tree publishes lands as a complementary surface to the sibling vote-casting POST -- pinning anull-or-record success payload contract no prior smoke covers. - E2E Item Comments Create Body Spec (
apps/web-e2e/tests/api/item-comments-create-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item comment-create POST body / header smoke spec paired withapps/web-e2e/tests/api/item-comments-create-body.spec.ts, the fifty-eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-sixth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/items/[slug]/comments/route.ts— the first non-admin per-source-file POST smoke the docs tree publishes that usescheckDatabaseAvailability()fromapps/web/lib/utils/database-check.tsas the load-bearing FIRST gate (BEFOREauth()) -- whenprocess.env.DATABASE_URLis missing, the helper returns a 503{ error: 'Database not configured', code: 'DATABASE_UNAVAILABLE', message: 'This feature requires database configuration' }envelope (the FIRST POST smoke that pins this helper-emitted envelope shape with a 503 status), the first non-admin POST smoke that uses the'Authentication required'401 message (distinct from the'Unauthorized'message used by the siblingitem-votes-cast-body-spec.md), and the second non-admin POST smoke that pins theisUserBlocked(clientProfile.status)moderation-status gate (the first beingitem-votes-cast-body-spec.md). In the e2e test environmentDATABASE_URLIS configured, so the db-availability gate passes through and the auth gate fires for unauthenticated requests, producing the 401'Authentication required'envelope. The POST handler combines acheckDatabaseAvailability()gate (the load-bearing FIRST gate; returns 503 with the DATABASE_UNAVAILABLE envelope whenDATABASE_URLis missing, otherwise returns null),auth()session lookup, a!session?.usergate (→ 401{ success: false, error: 'Authentication required' }-- NOTE: 'Authentication required', NOT 'Unauthorized'), JSON body parse, content validation (!content?.trim()→ 400'Content is required'), rating range validation (typeof rating !== 'number' || rating < 1 || rating > 5→ 400'Rating must be between 1 and 5'),getClientProfileByUserId(session.user.id!)lookup (not found → 404'Client profile not found'), theisUserBlocked(clientProfile.status)moderation-status gate (if true → 403 with the dynamicgetBlockReasonMessagemessage), the load-bearingcreateComment({ content, rating, userId, itemId })write,getCommentWithUserById(comment.id)post-write lookup (if null → 500'Failed to retrieve comment'-- the first POST smoke that pins a post-write null-check 500 envelope), success payload{ success: true, comment: commentWithUser }with status 200, and outer catchconsole.error+ 500'Failed to create comment'. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~17-body bulk-loop walk all asserting< 500; a canonical-envelope authentication-required 401 assertion{ success: false, error: 'Authentication required' }; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofcommentkeys must appear in any unauth response andsuccessmust befalse; a gate-before-post-auth invariant pinning that NONE of the five candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion that includes'Database not configured'for the 503 branch; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; a content-and-rating-validation-chain-not-entered invariance walk; a client-profile-lookup-and-moderation-gate-not-entered invariance walk pinning that the unauth response is 401, NOT 403; a createComment-and-post-write-lookup-not-entered invariance walk pinning that the unauth response must NEVER echo acommentkey or the'Failed to retrieve comment'500 message). Cross-references the companion public GET smokecomments.spec.ts, the first moderation-gated POST siblingitem-votes-cast-body-spec.md(uses the SAME moderation gate but with the'Unauthorized'401 message), the dynamic-segment per-comment routes atitems/[slug]/comments/[commentId]/route.ts, the rating sub-route atitems/[slug]/comments/rating/route.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 58-of-N and thetests/api/per-spec-file sub-rollout extends to 56-of-many, and the first checkDatabaseAvailability-helper-gated POST smoke the docs tree publishes lands as a complementary surface to the sibling vote-casting POST -- pinning a post-write null-check 500 envelope no prior smoke covers. - E2E Item Votes Cast Body Spec (
apps/web-e2e/tests/api/item-votes-cast-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item vote-casting POST body / header smoke spec paired withapps/web-e2e/tests/api/item-votes-cast-body.spec.ts, the fifty-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-fifth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/items/[slug]/votes/route.ts— the first non-admin per-source-file POST smoke the docs tree publishes that pins a moderation-status gate: after the auth + body-validation + client-profile gates, the handler runsisUserBlocked(clientProfile.status)fromapps/web/lib/db/queries/moderation.queries.tsand returns 403 with a dynamic message fromgetBlockReasonMessage(clientProfile.status)if the client is suspended or banned -- no prior POST smoke covers a moderation-status gate of this shape. The companion public GET smoke isitem-votes-public.spec.tswhich covers the GET surface (zero-vote fallback for unknown slugs); the mutating POST and DELETE surfaces have only generic< 500coverage initems-engagement-and-favorites.spec.ts, so this spec drills into the POST surface specifically. The POST handler combinesauth()session lookup + slug param resolution viaPromise.all([auth(), Promise.resolve(params.params)]), a!session?.user?.idgate (→ 401{ success: false, error: 'Unauthorized' }with the canonical envelope and short message), JSON body parse viaawait request.json()AFTER the auth gate, vote-type enum validation (if (!type || (type !== 'up' && type !== 'down'))→ 400{ success: false, error: "Invalid vote type. Must be 'up' or 'down'" }),getClientProfileByUserId(session.user.id)lookup (not found → 404{ success: false, error: 'Client profile not found' }), theisUserBlocked(clientProfile.status)moderation-status gate (load-bearing moderation invariant; if true → 403 with the dynamicgetBlockReasonMessagemessage), existing-votes lookup + replace logic viagetVoteByUserIdAndItemId(...)anddeleteVote(...), the load-bearingcreateVote({ userId, itemId, voteType })write (voteTypederived fromtype === 'up' ? VoteType.UPVOTE : VoteType.DOWNVOTE),getVoteCountForItem(slug)for the post-write count, success payload{ success: true, count, userVote: type }with status 200, and outer catchconsole.error+ 500{ success: false, error: 'Internal server error' }. Documents the at-a-glance scenario tree (a ~9-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a canonical-envelope bare-message 401 assertion{ success: false, error: 'Unauthorized' }; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a success-branch-key non-disclosure assertion that NONE ofcount,userVotekeys must appear in any unauth response andsuccessmust befalse; a gate-before-post-auth invariant pinning that NONE of the three candidate static messages must appear in any unauth response; an allowed-pre-delivery-error static-string allow-list assertion; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe walking only PUT and PATCH because GET + POST + DELETE are exported; a malformed-JSON-body invariance walk; a vote-type-validation-not-entered invariance walk; a client-profile-lookup-and-moderation-gate-not-entered invariance walk pinning that the unauth response is 401, NOT 403, regardless of body -- a regression that re-orderedisUserBlocked(...)before the auth gate would surface here; a createVote-and-getVoteCountForItem-not-entered invariance walk pinning that the unauth response must NEVER echo acountoruserVote). Cross-references the companion public GET smokeitem-votes-public.spec.ts, the public count-only siblingitem-vote-count-query.spec.ts, the auth-gatedvotes/statusGET siblingitem-votes-query.spec.ts, and the generic mutating-method< 500smokeitems-engagement-and-favorites.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 57-of-N and thetests/api/per-spec-file sub-rollout extends to 55-of-many, and the first moderation-status-gated POST smoke the docs tree publishes lands as the load-bearing invariant on a public, auth-gated vote-casting endpoint -- the third non-admin-tree per-source-file POST reference afterextract-body-spec.md,polar-webhook-body-spec.md, anditem-views-record-body-spec.md. - E2E Polar Webhook Body Spec (
apps/web-e2e/tests/api/polar-webhook-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's Polar payment-provider webhook POST body / header smoke spec paired withapps/web-e2e/tests/api/polar-webhook-body.spec.ts, the fifty-sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-fourth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/polar/webhook/route.ts— the first per-source-file webhook POST smoke the docs tree publishes (the existing multi-providerwebhooks.spec.tscovers Stripe / LemonSqueezy / Polar / Solidgate with two assertions each -- GET-not-5xx and POST-unauthenticated-rejected; this spec drills into the Polar webhook handler specifically), the first POST smoke that usesawait request.text()(raw body) instead ofawait request.json()(because Polar calculates signatures on the raw body, not the parsed JSON; the handler manually parses the raw text viaJSON.parse(bodyText)inside a try/catch), and the first POST smoke that usessafeErrorResponse(..., 400)in the outer catch (defaulting to 400 NOT 500 for unhandled webhook errors -- preventing a 5xx crash on signature/parsing errors that would otherwise trip Polar's webhook-retry logic). The POST handler combines a raw-body read viaawait request.text(), a manual JSON parse viaJSON.parse(bodyText)inside a per-call try/catch (failure → 400{ error: 'Invalid JSON payload' }), avalidateWebhookPayload(body)structure check (payload must have stringid, stringtype, and objectdatakeys → 400{ error: 'Invalid webhook payload' }if any missing), awebhook-signatureheader presence check (missing → 400{ error: 'No signature provided' }),polarProvider.handleWebhook(body, signatureHeader, bodyText, timestampHeader, webhookIdHeader)for the load-bearing signature-verification call, a!webhookResult.receivedcheck (400{ error: 'Webhook not processed' }),routeWebhookEvent(webhookResult.type, webhookResult.data)for the load-bearing event-routing call (subscription lifecycle, payment events, checkout updates) on the success branch, success payload{ received: true }with status 200, and outer catchsafeErrorResponse(error, 'Webhook processing failed', 400)(NOTE: defaults to 400 NOT 500). Documents the at-a-glance scenario tree (a ~13-header bulk-loop walk + a ~14-body bulk-loop walk all asserting< 500; a first-gate JSON-parse-rejection assertion{ error: 'Invalid JSON payload' }; a second-gate validate-payload-rejection assertion{ error: 'Invalid webhook payload' }; a third-gate signature-header-presence-rejection assertion{ error: 'No signature provided' }; a strict envelope-shape assertionObject.keys(body) === ['error']across all three pre-delivery branches; a success-branch-received-key non-disclosure assertion; a catch-branch-defaults-to-400 invariant pinning that NO unhandled error escapes as 5xx; an allowed-pre-delivery-error static-string allow-list assertion that every rejection message comes from the five-message set; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a signature-verification-call-gated-by-header-check invariant pinning that a valid payload without thewebhook-signatureheader must produce'No signature provided', not a 200{ received: true }). Cross-references the multi-provider siblingwebhooks.spec.ts, the polar/subscription/portal siblingpolar-subscription-portal-body.spec.ts, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 56-of-N and thetests/api/per-spec-file sub-rollout extends to 54-of-many, and the first per-source-file webhook POST smoke lands -- expanding the rollout into the payment-provider webhook layer for the first time and pinning a raw-body-signature-verification contract no prior smoke covers. - E2E Item Views Record Body Spec (
apps/web-e2e/tests/api/item-views-record-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's public per-item view-tracking POST body / header smoke spec paired withapps/web-e2e/tests/api/item-views-record-body.spec.ts, the fifty-fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-third underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/items/[slug]/views/route.ts-- the first non-admin POST smoke the docs tree publishes that pins a bot-detection graceful-degradation branch AS the load-bearing test invariant: the route importsisBot()fromapps/web/lib/utils/bot-detection.tswhoseBOT_PATTERNSregex array contains/bot/i,/crawl/i,/spider/i,/playwright/i,/puppeteer/i,/headless/i,/curl/i,/python-requests/i,/axios/i,/node-fetch/iAND treats an empty UA as a bot. The smoke spec EXPLICITLY sets a known-bot User-Agent (Googlebot/2.1) on the deterministic-assertion tests so the bot gate fires regardless of the Playwright runtime's default UA, BEFORE the route ever callsitemRepository.findBySlug(...), theauth()owner check, thecookies()viewer-id read, OR therecordItemView(...)write -- making the canonical envelope{ success: true, counted: false, reason: 'bot' }(status 200) the load-bearing invariant for the spec. It is also the first POST smoke the docs tree publishes that pins a synthetic-User-Agent override branch -- the same endpoint, called with a non-bot Chrome UA against an intentionally non-existent slug, progresses past the bot gate, reachesitemRepository.findBySlug(slug), and lands on theif (!item) return 404 { success: false, error: 'Item not found' }branch. The two branches together pin the gate-before-find order as a load-bearing invariant: a regression that re-orders thefindBySlug(...)call before the bot gate would surface here as adata-key disclosure on the bot branch OR as a status-code change. The POST handler combines a database-availability gate (if (!process.env.DATABASE_URL)→ 503{ error: 'Database not configured', code: 'DATABASE_UNAVAILABLE', message: '...' }; in the e2e environmentDATABASE_URLIS set so this branch must NOT fire), a bot-detection gate (if (isBot(userAgent))→ 200{ success: true, counted: false, reason: 'bot' }), an item existence check (const item = await itemRepository.findBySlug(slug); if (!item)→ 404{ success: false, error: 'Item not found' }), an owner-exclusion gate (const session = await auth(); if (session?.user?.id && item.submitted_by === session.user.id)→ 200{ success: true, counted: false, reason: 'owner' }), a viewer-cookie read / write step (readsever_viewer_idcookie viacookies(), generatescrypto.randomUUID()if absent, sets cookie withhttpOnly: true,sameSite: 'lax',path: '/', andmaxAge: VIEWER_COOKIE_MAX_AGE-- 365 days -- on the first-write branch), a view recording step (recordItemView({ itemId: slug, viewerId, viewedDateUtc })returnscounted: boolean; response:{ success: true, counted }), and outer catchconsole.error(dev-only) + 500{ success: false, error: 'Failed to record view' }. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a 200-with-bot-envelope assertion{ success: true, counted: false, reason: 'bot' }; a strict envelope-shape assertionObject.keys(body).sort() === ['counted', 'reason', 'success']; a post-bot-gate-key non-disclosure assertion that NONE oferror,data,codekeys must appear in any bot response andsuccessmust betrue; a gate-before-post-bot invariant pinning that NONE of the three candidate static messages ('Item not found','Failed to record view','Database not configured') must appear in any bot response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a malformed-JSON-body invariance walk pinning the gate-before-body-read order -- the route never callsrequest.json(), so malformed JSON bodies still land on the same 200 envelope; an item-not-found-not-entered invariance walk pinning the gate-before-find order; a database-unavailable-not-entered invariance walk pinning the post-DATABASE_URL-configuration invariant; a non-bot-UA-override-progresses-to-404 assertion pinning the gate-before-find order from the non-bot side -- non-bot UA + non-existent slug → 404{ success: false, error: 'Item not found' }; a bot-branch-non-disclosure-on-the-non-bot-branch assertion that NOreason: 'bot'echo, NOcounted: falseecho, MUST havesuccess: false; an owner-exclusion-not-entered invariance walk pinning that anonymous requests can NEVER receivereason: 'owner'regardless of UA ORsubmitted_bybody-field bypass attempts). Cross-references the siblingextract-body-spec.md(the first non-admin-tree per-source-file body-surface reference, with feature-disabled graceful-degradation branch -- this views-record spec is the SECOND non-admin-tree body-surface reference and the FIRST that pins a bot-detection graceful-degradation branch), and to Spec 010 -- E2E Test Coverage for the governing e2e spec and Spec 008 -- Analytics Providers for the analytics-providers spec the views route sits inside. With this entry the per-spec-file docs rollout extends to 55-of-N and thetests/api/per-spec-file sub-rollout extends to 53-of-many, and the first bot-detection-graceful-degradation POST smoke the docs tree publishes lands as the load-bearing invariant on a public, non-auth-gated endpoint -- the second non-admin-tree per-source-file reference afterextract-body-spec.md. - E2E Extract Body Spec (
apps/web-e2e/tests/api/extract-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's URL-extraction proxy POST body / header smoke spec paired withapps/web-e2e/tests/api/extract-body.spec.ts, the fifty-fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-second underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/extract/route.ts— the first non-admin-tree per-source-file reference the docs tree publishes (every prior per-source-file e2e reference covers anapps/web/app/api/admin/**route; this spec covers the extraction-proxy atapps/web/app/api/extract/route.ts-- a non-admin proxy that forwards to the Ever Works Platform API), the first non-admin-gated POST smoke the docs tree publishes that pins a "feature disabled" graceful-degradation branch (whenprocess.env.PLATFORM_API_URLis missing, the handler returns a 200 -- NOT 401, NOT 503 -- with the envelope{ success: false, featureDisabled: true, message: 'URL extraction feature is not available. This feature requires PLATFORM_API_URL to be configured.' }-- no prior smoke spec covers afeatureDisabled: trueenvelope shape), and the first POST smoke that uses ZodsafeParse+result.error.issues[0].message(NOTflatten()like the admin-tree POST smokes such asadmin/items/import) to surface the FIRST validation issue as the 400 envelope'serrorfield. The POST handler combines a feature-disabled gate (if (!platformApiUrl)→ 200 with thefeatureDisabled: trueenvelope), a JSON body parse AFTER the feature-disabled gate, a ZodsafeParsewith single-issue surfacing (extractSchema.safeParse(body)→ 400{ success: false, error: '<first issue>' }; schema requiresurl: z.string().url()and optionalexistingCategories: z.array(z.string())), an external fetch proxy that builds the extraction endpoint URL by trimming trailing slashes and POSTs to the Platform API with optionalAuthorization: Bearer <PLATFORM_API_SECRET_TOKEN>header, an upstream-error pass-through (tries to parse upstream error JSON forerrorData.message, falls back toresponse.statusText), a success pass-through (returns the upstream payload verbatim), and outer catchconsole.error+ 500{ success: false, error: 'Internal server error during extraction' }. In the e2e test environmentPLATFORM_API_URLis NOT configured, so EVERY POST request lands on the feature-disabled branch and gets a 200 response regardless of body shape -- making the spec a pinning of the feature-disabled envelope as the load-bearing invariant. Documents the at-a-glance scenario tree (a ~12-header bulk-loop walk + a ~15-body bulk-loop walk all asserting< 500; a 200-with-feature-disabled-envelope assertion{ success: false, featureDisabled: true, message: '...' }; a strict envelope-shape assertionObject.keys(body).sort() === ['featureDisabled', 'message', 'success']; a no-error-key assertion on the feature-disabled branch; a feature-disabled-before-post-feature-disabled invariant pinning that NONE of the three candidate post-gate messages must appear in any feature-disabled response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe -- POST is the only exported method, so all four other HTTP verbs are probed; a malformed-JSON-body invariance walk; a Zod-validation-chain-not-entered invariance walk pinning that the response must NEVER echo'Invalid URL format'; an external-fetch-proxy-not-entered invariance walk pinning that the response must always includefeatureDisabled: truein the test environment). Cross-references the Zod-flatten()siblingsadmin-items-import-validate-body-spec.mdandadmin-twenty-crm-config-save-body-spec.md, and to Spec 010 -- E2E Test Coverage for the governing spec. With this entry the per-spec-file docs rollout extends to 54-of-N and thetests/api/per-spec-file sub-rollout extends to 52-of-many, and the first non-admin-tree per-source-file reference lands -- expanding the rollout beyond theadmin/**route family for the first time and pinning a feature-disabled graceful-degradation envelope shape no prior smoke covers. - E2E Admin Location Index Manage Body Spec (
apps/web-e2e/tests/api/admin-location-index-manage-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin location-index manage POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-location-index-manage-body.spec.ts, the fifty-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fifty-first underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/location-index/route.ts— the first POST smoke the docs tree publishes that uses thecheckAdminAuth()helper from@/lib/auth/admin-guard.ts(the GET-siblingadmin-location-index-query-spec.mdalready covers the helper for the query endpoint; this is the first POST smoke that does the same), and the first action-enum-dispatched POST smoke that branches on abody.action === 'rebuild' | 'clear'enum into TWO distinct destructive operations on the same path:'rebuild'callsitemRepository.findAll()+service.rebuildIndex(items)-- the heaviest service call across the entire admin tree (re-indexes EVERY item with location data);'clear'callsclearLocationIndex()-- a destructive table-wipe that drops every row from the location_index table. The POST handler combines acheckAdminAuth()helper call that folds three branches into one helper (!session?.user→ 401'Unauthorized',!session.user.id→ 401'User ID not found',!userIsAdmin→ 403'Insufficient permissions') -- for an unauthenticated request the FIRST branch fires returning 401{ success: false, error: 'Unauthorized' }(canonical envelope withsuccess: falseAND short'Unauthorized'message), a JSON body parse AFTER the gate (inside the try block), the action enum dispatch with three branches ('rebuild'→ 200{ success: true, data: <rebuildResult> };'clear'→ 200{ success: true, data: { cleared: <count> } }; else → 400{ success: false, error: 'Invalid action. Use "rebuild" or "clear".' }), and outer catchconsole.error+ 500{ success: false, error: 'Internal server error' }. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~12-body bulk-loop walk all asserting< 500; a canonical-envelope bare-message 401 assertion{ success: false, error: 'Unauthorized' }; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a success-branch-key non-disclosure assertion that NONE ofdata,clearedkeys must appear in any unauth response andsuccessmust befalse; a gate-before-post-auth invariant pinning that NONE of the four candidate static messages ('Invalid action. Use "rebuild" or "clear".','Internal server error','User ID not found','Insufficient permissions') must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (PUT / PATCH / DELETE); a malformed-JSON-body invariance walk; an action-enum-dispatch-not-entered invariance walk; a rebuild-and-clear-destructive-paths-not-entered invariance walk pinning that the unauth response must NEVER echo adatakey from the rebuild result or theclearedcount from the destructive table-wipe). Cross-references the GET-siblingadmin-location-index-query-spec.md, the dashboard-stats siblingadmin-clients-dashboard-query-spec.md(the secondcheckAdminAuth()helper consumer for query endpoints), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 53-of-N and thetests/api/per-spec-file sub-rollout extends to 51-of-many, and the first destructive-action-enum-dispatch POST admin-tree smoke lands -- complementing the existing query-surface coverage of the samecheckAdminAuth()-gated location-index route. - E2E Admin Navigation Update Method Spec (
apps/web-e2e/tests/api/admin-navigation-update-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin navigation-update PATCH body / header smoke spec paired withapps/web-e2e/tests/api/admin-navigation-update-method.spec.ts, the fifty-second per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fiftieth underapps/web-e2e/tests/api/. Pairs with thePATCHexport ofapps/web/app/api/admin/navigation/route.ts— the second admin-tree smoke the docs tree publishes that usesgetCachedApiSession(req)instead ofauth()(afteradmin/settingsPATCH), and the first PATCH-only admin-tree smoke that pins a per-item path-format XSS-prevention validation loop viaisValidNavigationPath(item.path). The PATCH handler combines a single-step!session?.user?.isAdmingate that returns 401{ error: 'Unauthorized' }(BARE envelope, NOsuccesskey, SHORT message), a JSON body parse, atypeenum check ('header' | 'footer'→ 400'Type must be "header" or "footer"'), anitemsarray check (!Array.isArray(items)→ 400'Items must be an array'), a per-item structure validation loop (!label || !path || typeof !== 'string'→ 400'Each item must have non-empty "label" and "path" string fields'), a per-item path-format XSS-prevention validation (isValidNavigationPath(item.path)→ 400'Invalid path format. Paths must start with "/" for internal routes or "http://"/"https://" for external URLs.') -- the first PATCH smoke with a per-item XSS-prevention validation in a loop, thenconfigManager.updateNestedKey('custom_header'|'custom_footer', items)for the load-bearing works.yml write, an update-failed branch (500'Failed to update navigation'if falsy), success payload{ success: true, type, items }with status 200 (UNIQUE: echoes bothtypeanditemsfrom the input), and outer catchconsole.error+ 500'Failed to update navigation'. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk + a ~13-body bulk-loop walk all asserting< 500; a bare 401-envelope assertion; a strict envelope-shape assertionObject.keys(body) === ['error']; a success-branch-key non-disclosure assertion that NONE ofsuccess,type,itemskeys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the five candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe (POST / PUT / DELETE); a malformed-JSON-body invariance walk; a type-enum-and-items-array-validation-not-entered invariance walk; a per-item-XSS-prevention-loop-not-entered invariance walk pinning that the unauth response must NEVER echo'Invalid path format.'; a configManager-update-not-entered invariance walk pinning that the unauth response must NEVER echo atypeoritemskey from the input). Cross-references the settings-update PATCH companionadmin-settings-update-method-spec.md(the FIRST cached-session-lookup PATCH config-write admin-tree smoke; this navigation-update PATCH smoke is the SECOND), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 52-of-N and thetests/api/per-spec-file sub-rollout extends to 50-of-many, and the first per-item-XSS-prevention navigation-update PATCH admin-tree smoke lands -- complementing the existing settings-update PATCH coverage of the cached-session-lookup config-write surface. - E2E Admin Twenty CRM Config Save Body Spec (
apps/web-e2e/tests/api/admin-twenty-crm-config-save-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Twenty CRM config-save POST body / header smoke spec paired withapps/web-e2e/tests/api/admin-twenty-crm-config-save-body.spec.ts, the fifty-first per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-ninth underapps/web-e2e/tests/api/. Pairs with thePOSTexport ofapps/web/app/api/admin/twenty-crm/config/route.ts— the first admin-tree POST smoke the docs tree publishes that combines the compound single-ifgate!session?.user?.isAdmin || !session.user.id(matchingadmin/sponsor-ads/[id]/approveand/rejectPOST handlers but for a CRM-config-save endpoint), a Zod-safeParse-like validation viavalidateTwentyCrmConfig(body)that returns a custom{ success, data | error }shape and is translated to adetails: [{field, message}]400 envelope, AND alogActivity(...)side-effect that capturesrequest.headers.get('x-forwarded-for')for the audit log -- the first POST smoke that reads a request header for an audit side-effect. The POST handler returns{ success: true, message: 'Configuration saved successfully', data: <savedConfig> }with status 200 on success, andconsole.error+ 500{ success: false, error: 'Failed to save configuration' }in the outer catch. The companionadmin-twenty-crm-config-query.spec.tscovers the GET surface of the same route. Documents the at-a-glance scenario tree (a ~14-header bulk-loop walk includingX-Forwarded-For+ a ~11-body bulk-loop walk all asserting< 500; a canonical longer 401-envelope assertion; a strict envelope-shape assertion; a success-branch-key non-disclosure assertion that NONE ofdata,details,messagekeys must appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the three candidate static messages must appear in any unauth response; a parameterised-vs-baseline status-stability comparison; a side-channel walk; a cross-method probe; a malformed-JSON-body invariance walk; a validation-chain-not-entered invariance walk; a configRepository-saveConfig-and-logActivity-not-entered invariance walk pinning that the unauth response must NEVER echo'Configuration saved successfully'or adatakey from the saved config). Cross-references the companionadmin-twenty-crm-config-query-spec.md, the sibling test-connection POST smoke atadmin-twenty-crm-test-connection-body.spec.ts, and the other compound-single-ifPOST smokesadmin-sponsor-ads-id-approve-method-spec.mdandadmin-sponsor-ads-id-reject-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 51-of-N and thetests/api/per-spec-file sub-rollout extends to 49-of-many, and the first audit-logged CRM-config-save POST admin-tree smoke lands -- complementing the existing query-surface coverage of the same Twenty-CRM-config route. - E2E Admin Tags All Query Spec (
apps/web-e2e/tests/api/admin-tags-all-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin Git-CMS tags-listing query-param smoke spec paired withapps/web-e2e/tests/api/admin-tags-all-query.spec.ts, the forty-ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the forty-seventh underapps/web-e2e/tests/api/. Pairs with theGETexport ofapps/web/app/api/admin/tags/all/route.ts-- the second Git-CMS-backed admin-tree query smoke the docs tree references (the first was the siblingadmin-categories-all-query.spec.tscovered indirectly via theclient-trash-page-object.mdco-tenant cross-link). The route reads from the per-locale tag list stored in the Git-based content repository (cloned fromDATA_REPOSITORYinto.content/) viagetCachedItems({ lang })-- distinct from every other admin-tree route's drizzle / Postgres posture EXCEPT the sibling categories-all route. The handler combinesauth()session lookup, a single-step!session?.user?.isAdmingate returning 401{ success: false, error: 'Unauthorized' }(canonical envelope, bare'Unauthorized'message -- NOT'Unauthorized. Admin access required.'/'Forbidden'), a?locale=query param read AFTER the gate withsearchParams.get('locale') || 'en'default coercion, a dead-branchtypeof locale !== 'string'defensive narrowing that emits 400{ success: false, error: 'Invalid locale parameter' }but can never fire today (becausesearchParams.get(name)always returnsstring | nulland the|| 'en'default coerces null to a string before the typeof check), thengetCachedItems({ lang: locale })for the load-bearing per-locale tag list read, success payload{ success: true, data: tags }with status 200, and outer catchconsole.error+ 500'Failed to fetch tags'. Distinct from the siblingadmin-categories-all-query.spec.tsroute which omits the dead-branch defensive narrowing entirely. Documents the at-a-glance scenario tree (a ~50-path bulk-loop walk asserting< 500; a bare 401-envelope assertion; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for?locale=/?userId=/?token=/?bypass=/?repo=&branch=&commit=key families; a gate-before-locale-narrowing invariant pinning that the dead-branchtypeof locale !== 'string'narrow must never fire on the unauth branch; a gate-before-Git-CMS-read invariant pinning that thegetCachedItems({ lang: locale })Git-CMS reader must NEVER be entered on the unauth branch). Cross-references the DB-backed siblingadmin-tags-query.spec.tswhich covers the database-backed/api/admin/tagslisting route (with pagination), the Git-CMS sibling for categories atadmin-categories-all-query.spec.ts(same posture WITHOUT the dead-branch defensive narrowing), the page-object driveradmin-tags-page-object.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 49-of-N and thetests/api/per-spec-file sub-rollout extends to 47-of-many, and the first dead-branch type-narrowing Git-CMS query smoke lands -- complementing the existing per-source-file references for the categories-all sibling and the page-object driver covering the same admin-tree route's UI shell. - E2E Admin Featured Items [id] Method Spec (
apps/web-e2e/tests/api/admin-featured-items-id-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin single-featured-item CRUD GET / PUT / DELETE method / id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-featured-items-id-method.spec.ts, the thirty-third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirty-first underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/featured-items/[id]/route.ts— the first triple-method admin-tree smoke the docs tree publishes that combines a non-admin gate (the route gates on!session?.user?.idrather than!session?.user?.isAdmin, so any authenticated user with a tenant can hit it — a Q-010b-style auth-gate-divergence finding, the fourth admin route the smoke layer documents as effectively non-admin-restricted today) with a soft-delete DELETE (setsisActive: falserather than removing the row), a validation-less PUT (seven body fields shoved straight intodb.update(...)), and a two-step!session?.user?.id→!tenantIdgate envelope. All three handlers share a hybrid bare-message +success: false401 envelope ({ success: false, error: 'Unauthorized' }-- matchingadmin/users/[id]andadmin/roles/[id]/permissions), inline Drizzle queries with tenant scoping, and aconsole.error+ 500 catch with handler-specific messages ('Failed to fetch\|update\|remove featured item'). Each handler diverges on its post-gate surface: GET runs an inline Drizzleselect()with tenant scoping returning 404'Featured item not found'ifresult.length === 0or{ success: true, data: <featured-item> }; PUT parses JSON body AFTER tenant resolution and runs NO body validation (seven fields destructured:itemName,itemIconUrl,itemCategory,itemDescription,featuredOrder,featuredUntil,isActive-- shoved straight intodb.update(...).set({...}).returning()), returns 404 ifupdatedItem.length === 0or{ success: true, data: <updatedItem>, message: 'Featured item updated successfully' }; DELETE runs soft delete viadb.update(...).set({ isActive: false, updatedAt: new Date() }).returning()-- distinct from every prior admin DELETE smoke that actually removes the row -- returns 404 ifupdatedItem.length === 0or{ success: true, message: 'Featured item removed successfully' }(NOdatakey). Documents the at-a-glance scenario tree (a ~6-id × 3-method bulk-loop walk + a ~17-header × 3-method bulk-loop walk + a ~16-PUT-body bulk-loop walk all asserting< 500; per-method hybrid 401-envelope assertions; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion across all three methods; a gate-before-post-auth invariant pinning that NONE of the seven post-auth messages must appear in any unauth response; an unauth-lands-on-401-not-403 invariant pinning that the FIRST gate step fires for unauth clients (the response is 401 not 403, and must NEVER echo'Tenant not found'); a per-id-shape status-stability comparison; a PUT body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting POST / PATCH round-trip to< 500; a malformed-JSON-body invariance walk for PUT; a Drizzle-query-not-entered invariance walk across all three handlers; a soft-delete-update-not-entered invariance walk pinning that the unauth response must NEVER echo'Featured item removed successfully'). Cross-references the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.md, the categories-CRUD triple-methodadmin-categories-id-method-spec.md, the 403-on-unauth triple-methodadmin-comments-id-method-spec.md, the Zod-parse()-with-details-envelope triple-methodadmin-companies-id-method-spec.md, and to Spec 010 -- E2E Test Coverage, Spec 009 -- Admin Dashboard, anddocs/questions.md(the Q-010b auth-gate-divergence finding) for the governing specs. With this entry the per-spec-file docs rollout extends to 33-of-N and thetests/api/per-spec-file sub-rollout extends to 31-of-many, and the first non-admin-gated triple-method admin-tree smoke lands as a complementary surface to the canonical admin-gated triple-method smokes the sub-rollout previously published, and the fourth admin route flagged by Q-010b picks up its own per-source-file reference (joiningadmin-roles-query-spec.md,admin-roles-active-query-spec.md, and the broaderadmin-by-id.spec.tscoverage of similar tenant-only-gated routes). - E2E Admin Collections [id] Items Method Spec (
apps/web-e2e/tests/api/admin-collections-id-items-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin collection-items GET / POST nested-dynamic-id / body / header smoke spec paired withapps/web-e2e/tests/api/admin-collections-id-items-method.spec.ts, the twenty-seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twenty-fifth underapps/web-e2e/tests/api/. Pairs withapps/web/app/api/admin/collections/[id]/items/route.ts— the first nested-[id]/<sub-resource>dual-method admin smoke the docs tree publishes (every prior dynamic-segment admin smoke covers a[id]/<sub-resource>route as a SINGLE-method export —[id]/reviewPOST,[id]/historyGET,[id]/readPATCH,[id]/approve|reject|cancelPOST,[id]/permissionsGET+PUT — but this is the first that combinesGET+POSTon a nested path). All three handlers share the SAME single-step inline!session?.user?.isAdmingate and the SAME canonical longer 401 envelope, but each has its own divergent post-gate surface: GET has no body parse, callscollectionRepository.getAssignedItems(id), returns{ success: true, items: [...] }(success key isitems, NOTdata— distinct from every prior admin GET smoke), catches withsafeErrorResponse(error, 'Failed to fetch collection items'); POST parses JSON viaawait request.json(), validatesArray.isArray(body.itemIds)→ 400'itemIds array is required', callscollectionRepository.assignItems(id, body.itemIds), then runsinvalidateContentCaches()+ tworevalidatePath(...)calls (/collectionsand/collections/<slug>), returns{ success: true, collection, updatedItems, message: 'Collection items updated successfully' }(FOUR success-branch keys --success,collection,updatedItems,message-- distinct from every prior admin POST smoke which uses at most three keys), catches withsafeErrorResponse(error, 'Failed to assign collection items'). Documents the at-a-glance scenario tree (a ~6-id × 2-method bulk-loop walk + a ~17-header × 2-method bulk-loop walk + a ~16-POST-body bulk-loop walk all asserting< 500; per-method canonical-longer 401-envelope assertions; a strict envelope-shape assertion; a cross-method envelope-equality assertion; a success-branch-key non-disclosure assertion that NONE of the route-specificitems,collection,updatedItemskeys plusmessageandsuccess: truemust appear in any unauth response; a gate-before-post-auth invariant pinning that NONE of the four post-auth messages must appear in any unauth response; a per-nested-id-shape status-stability comparison; a POST body-permutation status-stability comparison; a cross-method side-channel cookie /X-*header walk; a cross-method probe asserting PUT / PATCH / DELETE round-trip to< 500; a malformed-JSON-body invariance walk for POST; a service-not-entered invariance walk; a side-effects-not-entered invariance walk pinning that theinvalidateContentCaches()+revalidatePath(...)chain is unreachable on the unauth branch). Cross-references the full set of sibling per-spec-file references undertests/api/, including the canonical-longer-envelope triple-methodadmin-items-id-method-spec.md, the bare-envelope triple-methodadmin-clients-clientid-method-spec.md, the hybrid-envelope triple-methodadmin-users-id-method-spec.md, and the dual-methodadmin-roles-id-permissions-method-spec.md, and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 27-of-N and thetests/api/per-spec-file sub-rollout extends to 25-of-many, and the first nested-[id]/<sub-resource>dual-method admin smoke lands as a complementary surface to the leaf-[id]triple- and dual-method smokes the sub-rollout previously published. - E2E Admin Items [id] History Query Spec (
apps/web-e2e/tests/api/admin-items-id-history-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin item-audit-history query-param / dynamic-segment smoke spec paired withapps/web-e2e/tests/api/admin-items-id-history-query.spec.ts, the sixteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the fourteenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/items/[id]/history/route.ts-- the first admin-tree route the smoke layer covers that combines a dynamic-segment[id]GEThandler with all four of (a) anauth()gate, (b) a 404 item-existence branch, (c) a query-param surface, and (d) a per-key enum-validation 400 branch. Documents the unique combination of: (1) dynamic-segmentGEThandler distinct from the dynamic-segmentPOSTroute covered byadmin-items-id-review-body-spec.md; (2) single-stepauth()chain with the canonical longer envelope{ success: false, error: 'Unauthorized. Admin access required.' }; (3) canonical longer 401 message matching the canonical-envelope family; (4)success: falseenvelope key on the 401 branch with a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; (5) item-existence check viaitemRepository.findById(itemId, true)AFTER the gate AND AFTERawait params-- the first admin-tree route the smoke layer covers that has a 404 item-existence branch between the gate and the query-param parse, with the 404 envelope{ success: false, error: 'Item not found' }and the boolean second argumenttruetofindByIdopting the lookup into including soft-deleted items (distinct from every other admin-tree route the smoke layer covers that does not lift the soft-delete filter); (6) query params parsed AFTER the existence check --searchParams.get('page')/searchParams.get('limit')/searchParams.get('action')are all read AFTER the 404 branch; (7)pageclamping viaNumber.isNaN(rawPage) ? 1 : Math.max(1, rawPage)(NaN-safe, defaults to 1, clamps to >= 1); (8)limitclamping viaMath.min(100, Math.max(1, Number.isNaN(rawLimit) ? 20 : rawLimit))(NaN-safe, defaults to 20, clamps to 1..100) -- the first admin-tree route the smoke layer covers that documents theMath.min(100, Math.max(1, ...))two-sided bound posture distinct from the wider unilateralvalidatePaginationParams(searchParams)posture of the sibling admin-roles / admin-sponsor-ads routes; (9)actionenum-validation 400 branch with a dynamically-interpolated messageInvalid action filter(s): <bad>. Valid actions are: <list>-- the first admin-tree route the smoke layer covers that emits a dynamic 400 message constructed from user-supplied bad-action strings, with the unauth branch asserting that the response body must NEVER match the/^Invalid action filter\(s\):/regex prefix nor contain any of the six valid action names (created,updated,status_changed,reviewed,deleted,restored) via word-boundary regexes; (10)itemAuditService.getHistory(...)call AFTER all four gates with success-branch payload{ success: true, data: { logs, total, page, limit, totalPages } }; (11)safeErrorResponse(error, 'Failed to fetch item history')catch matching theadmin/items/[id]/reviewcatch family; (12) method-resolution surface withGET-only export, so every other method (POST/PUT/PATCH/DELETE) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~30-query bulk-loop walk + a ~10-id bulk-loop walk all asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a gate-before-existence-check invariant pinning that the'Item not found'404 message must NEVER appear in the unauth response body; a gate-before-query-parse invariant pinning that the dynamic'Invalid action filter(s): ...'400 message must NEVER appear in the unauth response body; an action-enum non-disclosure assertion that the six valid action names must NEVER appear in the unauth response body via word-boundary regexes; a negative-property assertion that the unauth response does NOT echodata.logs/data.total/data.totalPageskeys orsuccess: true; a gate-before-catch invariant pinning that the'Failed to fetch item history'message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting POST / PUT / PATCH / DELETE round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a dynamic-[id]-segment invariance walk pinning that numeric / zero / 200-char-padded / Unicode / URL-encoded / soft-deleted-shape ids all round-trip to the same status as the baseline UUID; a repeated-key invariance walk pinning that?limit=1&limit=999/?page=1&page=2/?action=created&action=invalidall round-trip to the same status; an action-enum boundary-fuzzing invariance walk pinning that empty / comma-only / mixed-valid+invalid / SQL-injection-shape / XSS-shape / null-byte-encoded action probes all round-trip to the same status). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md,admin-items-bulk-body-spec.md, andadmin-clients-bulk-method-spec.md(the sibling per-spec-file references),admin-items-page-object.md(the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 16-of-N and thetests/api/per-spec-file sub-rollout extends to 14-of-many, and the first dynamic-segment-GET-with-404 admin smoke lands as a complementary surface to the dynamic-segment-POSTadmin/items/[id]/reviewsmoke and to the static-path query smokes the sub-rollout previously published. - E2E Admin Clients Bulk Method Spec (
apps/web-e2e/tests/api/admin-clients-bulk-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin clients-bulk-action method / body / header smoke spec paired withapps/web-e2e/tests/api/admin-clients-bulk-method.spec.ts, the fifteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the thirteenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/clients/bulk/route.ts-- the first admin-tree route the smoke layer covers that exports two HTTP methods on the same path (PUTfor bulk update andDELETEfor bulk deletion), distinct from every prior admin-tree smoke spec which covers a single-method route. Documents the unique combination of: (1) dual-method export (PUT+DELETE) -- the first admin-tree route the smoke layer covers where TWO methods are valid handlers and the cross-method probe walks exactly THREE remaining methods (GET/POST/PATCH) rather than the usual four; (2) bare'Unauthorized'401 message with bare{ error: 'Unauthorized' }envelope (nosuccess: falsekey) -- distinct from the canonical longer family ofadmin-items-bulk-body-spec.md,admin-categories-reorder-method-spec.md, andadmin-items-id-review-body-spec.md, and the same bare-message family asadmin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md, andadmin-notifications-mark-all-read-method-spec.md; (3) single-stepauth()chain with bare-message envelope -- the gate isif (!session?.user?.isAdmin)returning 401 with the bare envelope, filling the previously-empty "single-step gate × bare envelope" quadrant in the admin-tree smoke matrix (every prior single-step gate had the canonical longer envelope; every prior bare envelope had a two-step gate); (4) body parse viaawait request.json()AFTER the gate AND inside the per-methodtryblock -- the gate-then-parse-then-validate-then-loop order is the load-bearing invariant of both methods; (5) single-step body validation AFTER the gate AND AFTER the body parse with one 400 message'Invalid request: clients array is required'that fires on!Array.isArray(body.clients) || body.clients.length === 0; (6) per-client try/catch loop -- both methods iteratefor (const [index, clientData] of body.clients.entries())collecting successes into aresults: { index, success: true, data | clientId }[]array and failures into aerrors: { index, error, clientData }[]array, distinct from the single-arrayresults: BulkActionResult[]shape ofadmin/items/bulk; (7) direct DB-helper call without a repository abstraction -- both methods callupdateClientProfile/deleteClientProfiledirectly from@/lib/db/queries, distinct from theitemRepository.review(...)/itemRepository.delete(...)calls ofadmin/items/bulk; (8) per-method success-branch payload divergence -- the success branch returns a 200 with a payload shape that differs only in themessagetemplate ('Bulk update completed: ...'vs'Bulk deletion completed: ...') and the per-result inner key (data: <clientProfile>forPUTvsclientId: <id>forDELETE); (9) per-method catch-branch envelope divergence -- each method'stry/catchwraps the entire handler body and returns its ownsafeErrorResponse(error, '<msg>')envelope ('Failed to process bulk update'forPUT,'Failed to process bulk deletion'forDELETE), the first admin-tree route the smoke layer covers with two distinct catch envelopes on the same path; (10)safeErrorMessage+safeErrorResponsetwin-import surface -- the second admin-tree route the smoke layer covers that imports BOTH helpers (afteradmin/items/bulk); (11) method-resolution surface withPUTANDDELETEexports, so every other method (GET/POST/PATCH) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~18-header × 2-method bulk-loop walk + a ~17-body × 2-method bulk-loop walk both asserting< 500; per-method bare 401-envelope assertions pinning{ error: 'Unauthorized' }exactly; a cross-method response-parity assertion that thePUTandDELETE401 envelopes are byte-identical -- the load-bearing invariant of the dual-method smoke layer; per-method negative-property assertions that the unauth response does NOT echoresults/errors/summarykeys, the per-resultdata(PUT) /clientId(DELETE) keys, orsuccess: true; a gate-before-body-validation invariant pinning that the'Invalid request: clients array is required'and'Client ID is required'messages must NEVER appear in the unauth response body for either method; a gate-before-catch invariant pinning that neither the'Failed to process bulk update'nor the'Failed to process bulk deletion'message appears in the unauth response body; per-method parameterised-vs-baseline status-stability comparisons; per-method side-channel cookie /X-*header walks; a cross-method probe assertingGET/POST/PATCHround-trip to< 500-- the first admin-tree probe that walks exactly THREE remaining methods; a strict envelope-shape assertionObject.keys(body).sort() === ['error'](nosuccess/codekeys) for both methods; a malformed-JSON-body invariance walk for both methods; a per-client-loop-not-entered invariance walk pinning thatbody.results/body.errors/body.summaryare undefined andbody.messagedoes NOT start with either success-branch template). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md,admin-items-id-review-body-spec.md, andadmin-items-bulk-body-spec.md(the sibling per-spec-file references),admin-clients-page-object.md(the admin clients page-object driver paired with the same admin-clients area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 15-of-N and thetests/api/per-spec-file sub-rollout extends to 13-of-many, and the first dual-method admin-tree smoke (PUT+DELETEon the same path with byte-identical 401 envelopes and per-method catch envelopes) lands as a complementary surface to the single-method specs the sub-rollout previously published. - E2E Admin Items Bulk Body Spec (
apps/web-e2e/tests/api/admin-items-bulk-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-bulk-action body / header / method smoke spec paired withapps/web-e2e/tests/api/admin-items-bulk-body.spec.ts, the fourteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the twelfth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/items/bulk/route.ts-- the first admin-tree route the smoke layer covers that pairs the static-path single-step-gate posture of theadmin/categories/reorderandadmin/twenty-crm/test-connectionfamily with the most-validation-step body validation chain in the admin tree (six distinct 400 messages), distinct from the immediately-preceding dynamic-segmentadmin-items-id-review-body-spec.md. Documents the unique combination of: (1)POSThandler with a static path distinct from the dynamic[id]ofadmin/items/[id]/review; (2) single-stepauth()chain matching the canonical longer message family (if (!session?.user?.isAdmin)), distinct from the two-step gates ofadmin/users/check-email/admin/users/check-username/admin/notifications/mark-all-read; (3) canonical longer'Unauthorized. Admin access required.'401 message matching theadmin/categories/reorder,admin/items/[id]/review, andadmin/twenty-crm/*family; (4)success: falseenvelope key matching the same family, distinct from the bare{ error: 'Unauthorized' }envelope of the two-step-gated routes; (5) body parse viaawait request.json()AFTER the gate -- the gate-then-parse-then-validate-then-loop order is the load-bearing invariant of this route; (6) six-step body validation chain AFTER the gate AND AFTER the body parse with six distinct 400 messages ("Action must be 'approve', 'reject', or 'delete'",'At least one item ID is required','Maximum 100 items per bulk action','All item IDs must be non-empty strings','Duplicate item IDs are not allowed','Rejection reason is required (minimum 10 characters)') -- the most validation messages of any admin-tree route the smoke layer covers, distinct from the single-step body validation ofadmin/items/[id]/reviewand the three-step body validation ofadmin/categories/reorder; (7) per-id try/catch loop -- the first admin-tree route the smoke layer covers where individual id failures are collected into aresults: BulkActionResult[]array rather than failing the whole request, with the per-id catch usingsafeErrorMessage(error, 'Unknown error')to extract the per-id error string; (8) conditional repository routing onactionrouting each id to one ofitemRepository.review(id, { status: 'approved' }, auditUser)(with fire-and-forgetsendReviewNotification(item, 'approved')),itemRepository.review(id, { status: 'rejected', review_notes: trimmedReason }, auditUser)(with fire-and-forgetsendReviewNotification(item, 'rejected', trimmedReason)), oritemRepository.delete(id, auditUser)(no email side-effect), with success-branch payload{ success: true, message: 'Bulk <action> completed: <successful> <past-tense>, <failed> failed', results, summary }; (9)safeErrorResponse(error, 'Failed to process bulk action')catch matching theadmin/categories/reorderandadmin/items/[id]/reviewcatch family; (10)safeErrorMessage+safeErrorResponsetwin-import surface -- the only admin route the smoke layer covers that imports BOTH helpers (every other admin-tree route imports onlysafeErrorResponse); (11) method-resolution surface withPOST-only export, so every other method (GET/PUT/PATCH/DELETE) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~32-body bulk-loop walk both asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a negative-property assertion that the unauth response does NOT echoresults/summarykeys orsuccess: true; a gate-before-body-validation invariant pinning that ALL six 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the'Failed to process bulk action'message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk; an ids-array-length-invariance walk pinning that at-the-bound (length 100), over-the-bound (length 101), and far-over-the-bound (length 200) probes all round-trip to the same status as the no-body baseline; a per-id-loop-not-entered invariance walk pinning thatbody.results/body.summaryare undefined andbody.messagedoes NOT match the success-branch'Bulk <action> completed: ...'template). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-categories-reorder-method-spec.md, andadmin-items-id-review-body-spec.md(the sibling per-spec-file references),admin-items-page-object.md(the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 14-of-N and thetests/api/per-spec-file sub-rollout extends to 12-of-many, and the most-validation-step admin-tree smoke (six distinct 400 body-validation messages) lands as a complementary surface to the dynamic-segment single-step-validationadmin/items/[id]/reviewsmoke that immediately precedes it. - E2E Admin Items [id] Review Body Spec (
apps/web-e2e/tests/api/admin-items-id-review-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin item-review body / header / method smoke spec paired withapps/web-e2e/tests/api/admin-items-id-review-body.spec.ts, the thirteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eleventh underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/items/[id]/review/route.ts-- the first dynamic-segment admin-tree route the smoke layer covers, distinct from the prior static-path specs (admin-categories-reorder-method-spec.md,admin-notifications-mark-all-read-method-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md). Documents the unique combination of: (1)POSThandler with a dynamic[id]path parameter -- the first admin-tree dynamic-segment route the smoke layer pins, with a{ params: Promise<{ id: string }> }second argument resolved AFTER the gate AND AFTER the body validation; (2) single-stepauth()chain matching the siblingadmin/categories/reordergate shape (if (!session?.user?.isAdmin)), distinct from the two-step gates ofadmin/users/check-email/admin/users/check-username/admin/notifications/mark-all-read; (3) canonical longer'Unauthorized. Admin access required.'401 message matching theadmin/categories/reorderandadmin/twenty-crm/*family; (4)success: falseenvelope key matching theadmin/categories/reorderenvelope, distinct from the bare{ error: 'Unauthorized' }envelope of the two-step-gated routes; (5) body parse viaawait request.json()AFTER the gate -- the gate-then-parse-then-validate-then-resolve-params-then-call order is the load-bearing invariant of this route; (6) single-step body validation with the 400 message"Review status must be either 'approved' or 'rejected'"-- distinct from the three-step body validation ofadmin/categories/reorderand the one-key'Email is required'requirement ofadmin/users/check-email; (7)itemRepository.review(id, { status, review_notes }, auditUser)call followed by a fire-and-forgetEmailNotificationService.sendSubmissionDecisionEmailside-effect, with success-branch payload{ success: true, data: <item>, message: 'Item <status> successfully' }; (8)safeErrorResponse(error, 'Failed to review item')catch matching theadmin/categories/reordercatch family; (9) method-resolution surface withPOST-only export, so every other method (GET/PUT/PATCH/DELETE) must round-trip to a< 500status. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~19-body bulk-loop walk both asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a negative-property assertion that the unauth response does NOT echo'Item approved successfully'/'Item rejected successfully'orsuccess: true; a gate-before-body-validation invariant pinning that the 400 message"Review status must be either 'approved' or 'rejected'"must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the'Failed to review item'message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / PUT / PATCH / DELETE round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk; a dynamic-[id]-segment invariance walk pinning that three different[id]shapes (123,0, a 200-char padded id) all round-trip to the same status as the baseline UUID, pinning the gate-before-params-resolve order). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md,admin-notifications-mark-all-read-method-spec.md, andadmin-categories-reorder-method-spec.md(the sibling per-spec-file references),admin-items-page-object.md(the admin items page-object driver paired with the same admin-items area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 13-of-N and thetests/api/per-spec-file sub-rollout extends to 11-of-many, and the first dynamic-segment admin-tree smoke lands -- distinct from the static-path admin-tree smokes the sub-rollout previously published. - E2E Admin Categories Reorder Method Spec (
apps/web-e2e/tests/api/admin-categories-reorder-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin categories-reorder method / body / header smoke spec paired withapps/web-e2e/tests/api/admin-categories-reorder-method.spec.ts, the twelfth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the tenth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/categories/reorder/route.ts-- the firstPUT-method admin-tree route the smoke layer covers, distinct from the priorPATCH-method spec foradmin/notifications/mark-all-read(admin-notifications-mark-all-read-method-spec.md). Documents the unique combination of: (1)PUThandler withrequest: NextRequestbody-reading signature distinct from the barePATCH()ofadmin/notifications/mark-all-read(which narrows the request surface to zero); (2) single-stepauth()chain that collapses unauthenticated and authenticated-non-admin into the SAME 401 envelope, distinct from the two-step gates ofadmin/notifications/mark-all-read,admin/users/check-email, andadmin/users/check-username-- this route's gate isif (!session?.user?.isAdmin)returning 401 with the canonical longer envelope; (3) canonical longer'Unauthorized. Admin access required.'message matching theadmin/twenty-crm/*family andadmin/sponsor-ads, distinct from the bare'Unauthorized'message of the two-step-gated routes; (4)success: falseenvelope key matching theadmin/twenty-crm/test-connectionenvelope, distinct from the bare{ error: 'Unauthorized' }envelope (nosuccesskey) ofadmin/notifications/mark-all-read; (5) body parse viaawait request.json()AFTER the gate distinct from the barePATCH()/POST()of the bare-handler routes which never read the body -- the gate-then-parse-then-validate-then-call order is the load-bearing invariant of this route; (6) three-step body validation AFTER the gate AND AFTER the body parse with three distinct 400 messages ('categoryIds must be an array','categoryIds array cannot be empty','All category IDs must be strings') -- the unauth branch must NEVER reach any of the three validation steps; (7)categoryRepository.reorder(categoryIds)call followed byinvalidateContentCaches(), with success-branch payload{ success: true, message: 'Categories reordered successfully' }-- the unauth branch must NEVER reach the repository call; (8)safeErrorResponse(error, 'Failed to reorder categories')catch distinct from theconsole.error+'Internal server error'catch of the sibling check-email / check-username routes and the bare'Failed to test connection'catch ofadmin/twenty-crm/test-connection; (9) method-resolution surface withPUT-only export, so every other method (GET/POST/PATCH/DELETE) must round-trip to a 405 deterministically. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~15-body bulk-loop walk both asserting< 500; a canonical-longer 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }exactly; a negative-property assertion that the unauth response does NOT echo the success-branch'Categories reordered successfully'message orsuccess: truekey; a gate-before-body-validation invariant pinning that the three 400 messages must NEVER appear in the unauth response body; a gate-before-catch invariant pinning that the'Failed to reorder categories'message must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / POST / PATCH / DELETE round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; a malformed-JSON-body invariance walk pinning the gate-before-body-parse order). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md, andadmin-notifications-mark-all-read-method-spec.md(the sibling per-spec-file references), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 12-of-N and thetests/api/per-spec-file sub-rollout extends to 10-of-many, and the first PUT-method admin-tree smoke lands as the fourth HTTP-method-distinct posture the docs tree publishes (after the GET-tree query smokes, the POST-tree body smokes, and the PATCH-tree method smoke). - E2E Admin Notifications Mark-All-Read Method Spec (
apps/web-e2e/tests/api/admin-notifications-mark-all-read-method.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin mark-all-notifications-read method / body / header smoke spec paired withapps/web-e2e/tests/api/admin-notifications-mark-all-read-method.spec.ts, the eleventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the ninth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/notifications/mark-all-read/route.ts-- the first admin-tree route the smoke layer covers that documents the unique combination of: (1)PATCHhandler -- the firstPATCH-only route the e2e suite exercises, distinct from every other admin-tree smoke spec which targetsGET/POST; (2) barePATCH()handler signature (norequestparameter) narrowing the request surface to zero -- a regression that addsrequestparameter and starts reading the body would surface as a status divergence from the no-body baseline; (3) two-stepauth()chain that splits 401 vs 403 on thetenantIdboundary distinct from the siblingadmin/users/check-emailandadmin/users/check-usernameroutes' two-step gates that split onisAdmin-- this route's first step checks!session?.user?.id(returning 401'Unauthorized') and the second step checks!tenantId(returning 403'Tenant not found'); (4)'Tenant not found'403 envelope distinct from the sibling routes' bare'Forbidden'message -- the route-specific message is the production-source convention for endpoints that require multi-tenancy context; (5) direct Drizzle DB call without a repository abstraction -- the handler importsdb/notificationsfrom@/lib/db/drizzleand@/lib/db/schemadirectly and runs an inlinedb.update(notifications).set({…}).where(and(…)).returning()Drizzle pipeline distinct from the sibling routes' repository abstractions; (6) per-tenant scope on the success branch scoping the update to (a) the authenticated user's notifications only, (b) the unread subset, and (c) the user's tenant only; (7) method-resolution surface -- the route exports ONLYPATCH, so every other method (GET/POST/PUT/DELETE) must round-trip to a 405 deterministically. Documents the at-a-glance scenario tree (a ~18-header bulk-loop walk + a ~8-body bulk-loop walk both asserting< 500; a bare 401-envelope assertion pinning{ error: 'Unauthorized' }exactly; a negative-property assertion that the unauth response does NOT echo the success-branchsuccess: true/updatedCountkeys; a gate-step-ordering invariant pinning that the 403'Tenant not found'envelope must NEVER appear in the unauth response body; a parameterised-vs-baseline status-stability comparison; a side-channel cookie /X-*header walk including fabricatedX-Tenant-Id/X-User-Id/Authorization: Bearer/X-Api-Key/X-Admin-Tokenheaders; a cross-method probe asserting GET / POST / PUT / DELETE round-trip to< 500; a strict envelope-shape assertion). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md,admin-users-check-username-body-spec.md(the sibling per-spec-file references),admin-notifications-page-object.md(the admin notifications page-object driver paired with the same admin-notifications area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 11-of-N and thetests/api/per-spec-file sub-rollout extends to 9-of-many, and the first PATCH-method admin-tree smoke lands as the third HTTP-method-distinct posture the docs tree publishes (after the GET-tree query smokes and the POST-tree body smokes). - E2E Admin Users Check-Username Body Spec (
apps/web-e2e/tests/api/admin-users-check-username-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin check-username request-body / header smoke spec paired withapps/web-e2e/tests/api/admin-users-check-username-body.spec.ts, the tenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the eighth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/users/check-username/route.ts-- the sibling ofapps/web/app/api/admin/users/check-email/route.ts(already covered byadmin-users-check-email-body-spec.md). The two routes share an identical authorization shell (same two-stepauth()chain 401 + 403, same bare'Unauthorized'/'Forbidden'envelopes, sameawait request.json()-after-gate body parse, sameif (!field)-after-body-parse 400 validation, sameconsole.error+'Internal server error'catch, same{ available, exists }success-branch payload shape), differing in exactly four respects: (1) documented field isusernamevsemail; (2) body-validation message is'Username is required'vs'Email is required'; (3) repository call isuserRepository.usernameExists(username, excludeId)vsuserRepository.emailExists(email, excludeId); (4) catch-log prefix is'Error in POST /api/admin/users/check-username:'vs'Error in POST /api/admin/users/check-email:'. The unauth branch is INVARIANT to all four divergences -- both routes return the same bare 401 envelope on the first gate step. The per-spec separation surfaces three regression classes a shared spec would mask: (1) cross-route field-validation regression that swaps theif (!username)validation toif (!email)(or vice versa), (2) one-route-only auth-gate-removal regression, (3) username-shape boundary fuzzing on the unauth branch (the username field accepts shorter / narrower inputs than the email field, so the boundary-fuzzing payloads diverge: the username spec walks Unicode / RTL-override / null-byte / SQL injection / XSS / Cyrillic-homoglyph / zero-width-character / collation-sensitivity / leading/trailing-space shapes that target theusernameExists(...)repository call's collation-sensitivity surface, distinct from the email spec's MX-record / CRLF email-header / RFC-5322 boundary surface). Documents the at-a-glance scenario tree (a ~50-body bulk-loop walk asserting< 500; a bare 401-envelope assertion; a negative-property assertion; a parameterised-vs-baseline status-stability comparison; a sibling-route response-parity assertion that the bare-401 envelope of THIS route is byte-identical to the siblingadmin/users/check-emailroute's bare-401 envelope -- the load-bearing invariant of the cross-route smoke layer; a malformed-JSON-body invariance walk; a side-channel cookie /X-*header walk; a cross-method probe; a strict envelope-shape assertion; a Username-required-validation-does-NOT-fire assertion that pins the gate-before-body-validation order). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md,admin-users-check-email-body-spec.md(the sibling per-spec-file references),admin-clients-page-object.md(the admin clients page-object driver paired with the same admin-users area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 10-of-N and thetests/api/per-spec-file sub-rollout extends to 8-of-many, and the first cross-route response-parity assertion the docs tree publishes (betweenadmin/users/check-emailandadmin/users/check-username) lands as the load-bearing invariant of the cross-route smoke layer. - E2E Admin Users Check-Email Body Spec (
apps/web-e2e/tests/api/admin-users-check-email-body.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin check-email request-body / header smoke spec paired withapps/web-e2e/tests/api/admin-users-check-email-body.spec.ts, the ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the seventh underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/users/check-email/route.ts-- the first admin-tree route the docs tree publishes a per-source-file body-surface reference for that documents the full two-stepauth()chain posture splitting 401 (no session) from 403 (session withoutisAdmin). Where the siblingadmin-twenty-crm-test-connection-body.spec.tswalks the body surface of aPOSTroute with a single-step gate that collapses both branches into 401 with the canonical longer message ('Unauthorized. Admin access required.'), this spec is its complement -- documenting the unique combination of (a) two-step gate that splits 401 vs 403 with the bare shorter messages ('Unauthorized'/'Forbidden') and lacks thesuccess: falseenvelope key; (b) body parse viaawait request.json()AFTER the gate (distinct from the barePOST()of the test-connection route which never reads the body); (c) body-validation stepif (!email)AFTER the gate AND AFTER the body parse (the gate-then-parse-then-validate-then-call order is the load-bearing invariant); (d) internal-error catch withconsole.error(a side-channel observable via the server log) before returning the bare'Internal server error'envelope (out of scope for the unauth branch); (e) per-user PII non-disclosure on the unauth branch (the success-branch{ available, exists }keys must NEVER appear in the unauth response, which would indicate the gate was bypassed anduserRepository.emailExists(email, excludeId)was reached). Documents the at-a-glance scenario tree (a ~45-body bulk-loop walk asserting< 500; a bare 401-envelope assertion pinning{ error: 'Unauthorized' }exactly; a negative-property assertion that the unauth response does NOT echo the success-branchavailable/existskeys; a parameterised-vs-baseline status-stability comparison; a malformed-JSON-body invariance walk pinning the body-parse-after-gate order; a side-channel cookie /X-*header walk; a cross-method probe asserting GET / PUT / DELETE / PATCH round-trip to< 500; a strict envelope-shape assertionObject.keys(body).sort() === ['error']with nosuccess/available/existskeys). Includes email-shape boundary fuzzing on the unauth branch (null-byte injectionadmin@example.com\x00malicious@evil.com, CRLF email-header injection, XSS-shape email, SQL-shape email) -- a regression that runs the email validation before the gate would surface here. Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-items-export-query-spec.md(the sibling per-spec-file references),admin-clients-page-object.md(the admin clients page-object driver paired with the same admin-users area's UI shell), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 9-of-N and thetests/api/per-spec-file sub-rollout extends to 7-of-many, and the first two-stepauth()chainPOSTroute in the admin tree picks up its per-source-file body-surface reference (the second body-surface reference overall, after the single-stepadmin-twenty-crm-test-connection-body-spec.md). - E2E Admin Items Export Query Spec (
apps/web-e2e/tests/api/admin-items-export-query.spec.ts) -- Per-source-file reference for the Playwright e2e suite's admin items-export query-param smoke spec paired withapps/web-e2e/tests/api/admin-items-export-query.spec.ts, the eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/and the sixth underapps/web-e2e/tests/api/. Pairs with a new spec coveringapps/web/app/api/admin/items/export/route.ts-- the per-tenant items dump counterpart to the sample-template route already covered by the existingapps/web-e2e/tests/api/admin-items-export-sample-query.spec.tssmoke. The two routes share an identical authorization shell (same admin gatesession?.user?.isAdmin, same canonical 401 message'Unauthorized. Admin access required.', sameexportQuerySchemaZod parse with the'csv' | 'xlsx'enum and'csv'default, samesafeErrorResponse(...)catch envelope, sameContent-Disposition: attachment; filename="…"binary-stream return shape on the happy path), differing only in the post-gate service call: the sample route callsexportService.generateSampleCSV / XLSX()(a static schema-documentation template), whereas the route under test here callsexportService.exportToCSV / XLSX()(the per-tenant items dump, i.e. every item in the directory's CMS / DB) — and in the catch message ('Failed to export items'vs'Failed to generate sample template'). The unauth branch is INVARIANT to that distinction (both routes return the same canonical 401 envelope), so the smoke walk pins the same load-bearing "401-before-any-service-call" contract; the per-spec separation surfaces three regression classes a shared spec would mask: (1) sample-route-only catch-message regression that copies the items-export route's'Failed to export items'catch message into the sample route, (2) items-export-route-only service-call regression that swapsexportToCSV()forgenerateSampleCSV()(or vice versa), and (3) one-route-only auth-gate-removal regression that removes the admin gate from one route but not the other. Documents the at-a-glance scenario tree (a ~85-path bulk-loop walk asserting< 500; a canonical 401-envelope assertion pinning{ success: false, error: 'Unauthorized. Admin access required.' }; a parameterised-vs-baseline status-stability comparison; per-key isolation walks for?format=covering the case-sensitiveCSV/XLSXrejections,?userId=/?token=/?bypass=covering impersonation / magic-token / admin-override keys,?filename=covering the path-traversal../../etc/passwd/ null-byte%00maliciousattack-vector pins,?metadata=covering the#include-metadatacheckbox in theAdminDataExportPagedriver; anAcceptheader walk including theapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheetXLSX MIME type; a side-channel cookie /X-*header walk; a repeated-key walk). Cross-references tosmoke-health-spec.md,smoke-navigation-spec.md,admin-settings-map-status-query-spec.md,admin-twenty-crm-config-query-spec.md,admin-sponsor-ads-query-spec.md,admin-roles-query-spec.md,admin-roles-active-query-spec.md(the sibling per-spec-file references),admin-data-export-page-object.md(the admin data-export page-object driver paired with the same admin route's UI shell),admin-item-form-page-object.mdandadmin-items-page-object.md(the sibling admin-items shells), and to Spec 010 -- E2E Test Coverage and Spec 009 -- Admin Dashboard for the governing specs. With this entry the per-spec-file docs rollout extends to 8-of-N and thetests/api/per-spec-file sub-rollout extends to 6-of-many, and the sample-template / items-dump pair of admin-export routes both pick up per-source-file references documenting their identical authorization shell and divergent post-gate service calls. - E2E Item-Detail Page Object (
apps/web-e2e/page-objects/public/item-detail.page.ts) -- Per-source-file reference for the Playwright e2e suite's item-detail page driver paired withapps/web-e2e/page-objects/public/item-detail.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andlanguage-switcher-page-object.mddocuments the suite's locale-switching driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's per-item detail-page driver boundary — the smallest possible page object that lets a spec drive the public item-detail page (/items/[slug]) end-to-end (navigate either to a known slug or to the first item linked from/via a 30 s seed-data-tolerant slug-agnostic discovery primitive, click the canonical OR-of-two-aria-label"Upvote"/"Remove upvote"toggle button to upvote or un-upvote the item, read the live vote count via the[aria-live="polite"]accessibility region with a?? '0'nullish-coalesce that pins the public return type toPromise<string>and a.first()strict-mode-correctness append, toggle the favorite via thearia-label*="favorites"substring-matched.first()button whose plural-by-design substring survives a future"Save favorite"rename without masking a real label-rename regression, locate the comments section by its OR-of-tagssection, divfilter on the^comments-anchored case-insensitive regex that survives a future wrapping-tag refactor, fill the#commenttextarea via the production-source HTML-formid-based accessibility wiring and click thename=/post comment/ibutton via the case-insensitive accessible-name match to post a comment as an authenticated user, hover any existing comment to surface the per-commentaria-label="Edit comment"/aria-label="Delete comment"buttons that the production source hides behind a:hoverCSS state, and resolve the delete-comment confirmation dialog via the[role="dialog"]filter on the/delete comment/ibody text re-evaluated on every read via aget-accessor instead of a stalereadonly Locatorfield). Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import; theexport class ItemDetailPage extends BasePagesingle named export with theextends BasePageclause — the page-route driver posture; the eightreadonly Locatorfields coveringheading/voteButton/voteCount/favoriteButton/commentsSection/commentTextarea/postCommentButton/signInToCommentButton; the synchronous constructor that callssuper(page)first then pre-binds every per-page Locator in a single pass; thenavigateToItem(slug)slug-driven primitive; thenavigateToFirstItem()slug-agnostic discovery primitive with the 30 s seed-data tolerance; theclickVote()bare upvote primitive; thegetVoteCount(): Promise<string>polite-aria-live region read with the?? '0'fallback; theisVoted(): Promise<boolean>strict-equality state pin on'Remove upvote'; theclickFavorite()bare favorite-toggle primitive; thepostComment(text)composite fill-then-click primitive; thegetComment(text): Locatorper-comment factory; theeditComment(text)/deleteComment(text)hover-then-click primitives; theget deleteCommentDialog()re-evaluating Locator getter); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming specs atapps/web-e2e/tests/public/item-detail.spec.ts(heading visible, body content visible) andapps/web-e2e/tests/public/votes-and-comments.spec.ts(vote toggle, favorite toggle, comment post / edit / delete); the "Why the class extendsBasePage" walkthrough that pins the three load-bearing reasons (page-route navigation via inheritedgoto(), globalheader/footer/navLinkschrome surfaced through inherited composite getters,waitForPageReady()post-navigation stabiliser); the "Why the vote button uses an OR-of-two-aria-labels selector" walkthrough that pins the three reasons (state-agnostic resolution across the pre-vote / post-vote DOM flip, exact-match collision-signal preservation against future per-comment"Vote on comment"buttons, symmetry withisVoted()'s strict-equality check); the "Why the favorite button uses anaria-label*="favorites"substring selector" walkthrough that pins the three reasons (state-agnostic resolution, plural-by-design"favorites"survives a future"Save favorite"rename,.first()strict-mode-correctness append); the "WhygetVoteCount()returnsPromise<string>" walkthrough that pins the three reasons (screen-reader-contract string emission,?? '0'fallback, locale-thousands-separator preservation); the "WhyisVoted()checks the exact'Remove upvote'label" walkthrough that pins the three reasons (OR-of-two-aria-labels collapse, substring drift avoidance, symmetry with thevoteButtonselector); the failure matrix covering every item-detail-page-level mistake (type-only import drop,extends BasePageremoval,readonlydrop on any Locator, single-aria-labelre-bind on the vote button, substringaria-label*="vote"re-bind,.first()drop onvoteCount, single-aria-labelre-bind on the favorite button, singular*="favorite"rebind,.first()drop on the favorite, single-tag re-bind oncommentsSection, non-anchored regex oncommentsSection, non-idre-bind oncommentTextarea, exact-match re-bind onpostCommentButton,signInToCommentButtonfield drop,super(page)drop, pre-parse tonumberongetVoteCount, substringincludes('Remove')re-bind onisVoted, hover-step drop oneditComment/deleteComment,readonly Locatorfield conversion ofdeleteCommentDialog, non-role="dialog"re-bind,data-testidre-bind, file move,.tsxrename, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming specs, the production-source DOM contract underapps/web/components/item-detail/*,base-page-object.mdfor the inherited surface,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,fixtures-index.mdfor a future authenticated variant) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto vote-button-aria-label-rename, polite-region-selector-change, favorite-button-substring-drop, comments-section-heading-rename,#comment-id-change,[role="dialog"]-removal, middleware-prefix-change, andbaseURL-change failures; and theitem-detail.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/item-detail.spec.tsandapps/web-e2e/tests/public/votes-and-comments.spec.ts), abase-page-object.mdcross-check, a production-source cross-check (the H1 role, the OR-of-two-aria-labelon the vote button, the[aria-live="polite"]polite region, thearia-label*="favorites"substring on the favorite, the#commenttextareaid, the "Post comment" / "Sign in to comment" button text, and the[role="dialog"]confirm-modal), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future authenticated variant would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the item-detail spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Item Detail"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Star-Rating Page Object (
apps/web-e2e/page-objects/public/star-rating.page.ts) -- Per-source-file reference for the Playwright e2e suite's five-star rating-picker driver paired withapps/web-e2e/page-objects/public/star-rating.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andsort-menu-page-object.mddocuments the suite's listing-sort driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's per-item rating-picker driver boundary — the smallest possible page object that lets a spec drive the item-detail page's five-star rating picker end-to-end (locate the picker by its WAI-ARIA[role="radiogroup"]/aria-label="Rating"pair pinned to the first match for strict-mode safety, click any star 1–5 by its visiblearia-label*="N star"substring scoped through the container Locator, and read the currently selected rating value via a reversearia-checked="true"scan that returns the highest-numbered checked star or0when nothing is checked). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class StarRatingsingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage; thereadonly container: Locatorpage.locator('[role="radiogroup"][aria-label="Rating"]').first()exact-match dual selector that pins to both the WAI-ARIAradiogrouprole AND the exactaria-label="Rating"value to disambiguate against future sibling radio groups while deliberately avoiding a substring match because a future translation layer would localize the label and break the selector silently — see thei18nrow in the failure matrix; theconstructor(page: Page)that stores thepageand pre-binds the single container Locator without asuper(page)call; thestar(n: number): Locatorlocator-factory that constructs a per-star Locator at call-time by interpolating the numericninto thearia-label*="${n} star"substring match, scoped throughthis.container.locator(…)rather thanthis.page.locator(…)to survive a future "1 star = bad, 5 stars = great" legend rendered in the page footer or sidebar, with the substring match accommodating both the singular"1 star"and plural"2 stars"/"3 stars"/"4 stars"/"5 stars"shapes, returning the Locator without clicking so consuming specs can compose visibility / role / aria-checked assertions on the same Locator; therate(n: number)composite "click the nth star" primitive that reuses thestar(n)factory; thegetValue(): Promise<number>accessor that reverse-iteratesi = 5..1, readsawait this.star(i).getAttribute('aria-checked'), returnsion the first'true'and falls through toreturn 0when nothing is checked, with the reverse-iteration load-bearing for the highest-checked-wins semantics that handle the host app's HeroUI fill-up-to-N pattern correctly and thereturn 0floor turning the no-rating state into a definitive numeric sentinel that pins the public return type toPromise<number>); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/star-rating.spec.ts(picker container visibility on the item-detail page reachable from the firsta[href*="/items/"]link on/discover/1, fourth-star click viarate(4)with a 500 ms settle delay before readinggetValue()returns 4, all five star buttons present via per-star visibility loop — all three tests soft-skip withtest.skip(true, …)when the picker container is not visible because ratings may be disabled, the comment form may not surface, or the user may not be authenticated); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the item-detail surface, reusability on non-item-detail surfaces like a future "Rate this content" inline widget on the dashboard / per-tag rating mosaic / admin-shell rating-distribution widget, constructor parity with non-page widgets liketheme-toggle.page.ts/language-switcher.page.ts/share-button.page.ts/view-toggle.page.ts/scroll-to-top.page.ts/search-bar.page.ts/sort-menu.page.ts); the "Why[role="radiogroup"][aria-label="Rating"]exact match" walkthrough that pins the three reasons for the dual selector (theradiogrouprole is the WAI-ARIA-canonical primitive for a single-select radio surface vs the related-but-different[role="group"]pattern, thearia-label="Rating"anchor disambiguates against future sibling radio groups likeDifficulty/Recommend/ per-survey radio sets, no production-source change required because the role and label are already there for accessibility); the "Why.first()on the container Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second rating picker like a sticky-header rating-picker mirror / portal-rendered modal duplicate / Rate-average mosaic badge, strict-mode collision against an already-rendered duplicate during item-to-item navigation transitions with exit-animation overlap, strict-mode collision against a portal-rendered duplicate); the "Whystar(n)returns aLocatorinstead of clicking" walkthrough that pins the three reasons for the locator-factory shape (composability withexpect(starRating.star(3)).toBeVisible()/toHaveAttribute('aria-checked', 'true')/.hover()chains, symmetric posture withrate(n), type-narrowed Locator return for IntelliSense); the "Whyaria-label*="N star"substring match (and not exact)" walkthrough that pins the three reasons for the substring posture (plural-form variance between singular"1 star"and plural"2 stars"/.../"5 stars", future locale variance with labels like"1 star (out of 5)", future a11y-label expansion with labels like"Rate as 1 star"); the "Why reverse iteration ingetValue()" walkthrough that pins the three reasons for thei = 5..1walk (highest-checked-wins semantics correctly handle the fill-up-to-N pattern that would always return1on a forward iteration, short-circuit on the most-likely-rating common case 4–5, symmetric to the visual left-to-right rendering); the "Whyreturn 0as the no-rating sentinel" walkthrough (type narrowing toPromise<number>, defensive symmetry with siblinggetCurrentTheme(): Promise<string>/getValue(): Promise<string>posture, 0 as the natural null-rating value matching the host app's data-model); the failure matrix covering every star-rating-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop onpageorcontainer,aria-label-only orrole-only single anchor swap,[aria-label*="Rating"]substring swap that breaks under translation,data-testidswap,.first()drop oncontainer,aria-label="N star"exact match swap that breaks plural forms,this.page.locator(…)swap that loses container scoping,await this.star(n).click()shape swap that loses Locator composability, forward iteration ingetValue(),Promise.allswap that loses short-circuit,return 0fallback drop, file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/star-rating.spec.ts, future smoke / a11y specs that driverate(N)for any N in 1..5 or audit per-stararia-label, the item-detail-page production-source component for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,discover-page-object.mdfor the/discover/[N]listing-route contract,fixtures-index.mdfor theclientPagefixture barrel that grants authenticated-user state) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto[role="radiogroup"]semantic-loss soft-skip,aria-label="Rating"translation soft-skip,<button>→<input type="radio">element-swap, picker-label rename, item-route-change, middleware-prefix, baseURL-change, feature-flag-disable (intended soft-skip), library-swap, andclientPagefixture-removal failures; and thestar-rating.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/star-rating.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (the container's[role="radiogroup"]ARIA role /aria-label="Rating"anchor / per-stararia-label*="N star"accessible-name shape / per-stararia-checkedattribute), adiscover-page-object.mdcross-check (the/discover/[N]listing-route contract the consuming spec follows before navigating to the item-detail surface), afixtures-index.mdcross-check (theclientPagefixture for authenticated-user state), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the star-rating spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Star Rating"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Sort-Menu Page Object (
apps/web-e2e/page-objects/public/sort-menu.page.ts) -- Per-source-file reference for the Playwright e2e suite's listing-sort dropdown driver paired withapps/web-e2e/page-objects/public/sort-menu.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andshare-button-page-object.mddocuments the suite's social-share driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's listing-sort driver boundary — the smallest possible page object that lets a spec drive the public listing's sort dropdown end-to-end (open the dropdown by clicking thearia-haspopup="menu"trigger button pinned to the first match for strict-mode safety, select any sort option by visible-text regex via the dual[role="menuitemradio"], [role="menuitem"]ARIA-role selector that accommodates both single-select and free-action menu shapes, read the trigger button's current text content viatextContent()?.trim() ?? ''to assert that the post-select label changed). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class SortMenusingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePageAND is also used insideselectOptionto construct the option Locator at call-time rather than constructor-time because the option set materialises only afteropen()has been called; thereadonly trigger: Locatorpage.locator('button[aria-haspopup="menu"]').first()exact-match selector pinned to the canonical ARIA-spec valuemenu(nottrueand notlistbox) for strict-mode-correctness against[role="menu"]popups; thereadonly menuContent: Locatorpage.locator('[role="menu"]').first()deliberately-exposed dropdown Locator that lets a spec assert on the opened dropdown's properties without reaching through a method; theconstructor(page: Page)that stores thepageand pre-binds the two Locators in a single pass without asuper(page)call; theopen()minimal "open the dropdown" primitive every other method composes against; theselectOption(text: RegExp)composite "open the dropdown then click the first option matching the regex" primitive with the load-bearingRegExpparameter type that forces consuming specs to think about the case-insensitive substring contract rather than hard-coding an English-only string and the dual-role[role="menuitemradio"], [role="menuitem"]selector that accommodates both single-select and free-action option shapes; thegetCurrentLabel(): Promise<string>accessor that reads the trigger button's text content viatextContent()?.trim() ?? ''with the?? ''nullish-coalesce that pins the public return type toPromise<string>and mirrors the siblingtheme-toggle-page-object.md'sgetCurrentTheme()andsearch-bar-page-object.md'sgetValue()posture); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/sort-menu.spec.ts(trigger visibility on/discover/1after a 2-second settle delay, dropdown opens with at least one menu item, second-option selection updates the trigger label — all three tests soft-skip withtest.skip(true, …)when the trigger is not visible); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the listing surface, reusability on non-listing surfaces like a future admin-shell sort menu / client-shell sort menu on the user submissions page / category-page sort menu, constructor parity with non-page widgets liketheme-toggle.page.ts/language-switcher.page.ts/share-button.page.ts/view-toggle.page.ts/scroll-to-top.page.ts/search-bar.page.ts/star-rating.page.ts); the "Whyaria-haspopup="menu"exact match and not a substring" walkthrough that pins the three reasons for the strict-equality posture (themenuvalue is the WAI-ARIA-spec-canonical match for a trigger that opens a[role="menu"]popup vs the related-but-differentlistbox/dialogpatterns, strict equality survives a futurearia-haspopup="dialog"Filter button regression that adds another popup trigger to the same listing, no production-source change required because thearia-haspopupattribute is already there for accessibility); the "Why[role="menu"]exact match formenuContent" walkthrough that pins the three reasons for the role-anchor posture (screen-reader-driven primitive, library invariance against HeroUI / Radix / Headless UI / shadcn-ui swaps, strict-mode resilience against future submenus); the "Why.first()on every Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second sort trigger / dropdown / portal mirror, strict-mode collision against an already-open[role="menu"]from a profile-dropdown / navigation submenu, strict-mode collision against a portal-rendered duplicate); the "Why the dual-role selector inselectOption" walkthrough that pins the three reasons for the comma-separated[role="menuitemradio"], [role="menuitem"]selector (single-select vs free-action option-shape mismatch where HeroUI writesmenuitemradiofor single-select but a future multi-select / reset-action sort would writemenuitem, library variance because different menu libraries default to different role choices, compatibility with the consuming spec's option-count check that uses the same dual selector); the "Whytext: RegExpand nottext: string" walkthrough that pins the three reasons for the regex parameter type (locale invariance becauseselectOption(/votes/i)matches translations like"Stimmen", strict-mode resilience because theiflag tolerates casing variants, type-narrowed Locator construction that documents the case-insensitivity expectation); the "Why?.trim() ?? ''ongetCurrentLabel" rationale (type narrowing toPromise<string>, whitespace tolerance against flex / gap rendering, defensive symmetry with siblinggetCurrentTheme()/getValue()posture); the failure matrix covering every sort-menu-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop onpageor any of the two Locator fields,aria-haspopup*="menu"substring swap,aria-haspopup="true"legacy-ARIA swap,data-testidswap,.first()drop ontriggerormenuContent,[role="menu"]ARIA-role anchor drop onmenuContent, dual-role selector drop inselectOption,text: RegExp→text: stringparameter swap,.first()drop on the option Locator,?.trim()drop ongetCurrentLabel,?? ''drop ongetCurrentLabel, pre-construction of the option Locator in the constructor beforeopen()materialises the option set, file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/sort-menu.spec.ts, future smoke / a11y specs that callselectOption(/votes/i)to drive specific sort flows or readmenuContentforaria-orientationaudits / role-count audits / screenshot diffs, the listing-page production-source component for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,discover-page-object.mdfor the/discover/[N]listing-route contract the consuming spec follows before reaching the sort menu) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto trigger-visibility-soft-skip,Locator not found, listbox-pattern-swap, sort-option-rename, route-change, middleware-prefix, baseURL-change, and library-removal failures; and thesort-menu.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/sort-menu.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (the trigger'saria-haspopupattribute, the[role="menu"]ARIA contract on the dropdown, the[role="menuitemradio"]ormenuitemrole on every option, the trigger button's text-content shape thatgetCurrentLabel()reads), adiscover-page-object.mdcross-check (the/discover/[N]listing-route contract the consuming spec follows before reaching the sort menu), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound sort menu would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the sort-menu spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Sort Menu"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Share-Button Page Object (
apps/web-e2e/page-objects/public/share-button.page.ts) -- Per-source-file reference for the Playwright e2e suite's share-button dropdown driver paired withapps/web-e2e/page-objects/public/share-button.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andscroll-to-top-page-object.mddocuments the suite's scroll-position driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's social-share driver boundary — the smallest possible page object that lets a spec drive the item-detail page's social-share dropdown end-to-end (open the dropdown by clicking the trigger button whose visible text matches the case-insensitive/share/iregex pinned to the first match for strict-mode safety, click any of the four[role="menuitem"]menu items — Copy Link / Twitter (X) / Facebook / LinkedIn — to fire the per-platform share intent, and read each menu-item Locator for visibility / a11y assertions). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class ShareButtonsingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage; thereadonly trigger: Locatorpage.locator('button').filter({ hasText: /share/i }).first()regex-filter selector that pins to the visible text of the share button (the host app's button has noaria-labeltoday); thereadonly copyLinkItem: Locator[role="menuitem"]ARIA-role anchor with the case-insensitive/copy link/iregex filter for the always-present Copy Link entry; thereadonly twitterItem: Locatorwith the dual-substring/twitter|x \(/iregex that survives the X rebrand by matching either the legacy"Twitter"text or the post-rebrand"X (formerly Twitter)"shape via thex \(disambiguator; thereadonly facebookItem: Locatorandreadonly linkedinItem: Locatorsiblings with the same[role="menuitem"]andi-flag posture; theconstructor(page: Page)that stores thepageand pre-binds the five Locators in a single pass without asuper(page)call; theopen()minimal "open the dropdown" primitive every other action method composes against; thecopyLink()composite "open the dropdown then click Copy Link" primitive — the only end-to-end action method in the class today because it is the only deterministic per-platform action whose effect can be verified without a popup-verification harness). Documents the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/share-button.spec.ts(trigger visibility on the item-detail page reachable from/discover/1, dropdown opens with at least two[role="menuitem"]entries, both tests soft-skip withtest.skip(true, …)when the trigger is not visible); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the item-detail surface where the share button is one of several item-detail widgets, reusability on non-item-detail surfaces like a future profile / collection / per-tag share button, constructor parity with non-page widgets liketheme-toggle.page.ts/language-switcher.page.ts/view-toggle.page.ts/scroll-to-top.page.ts/search-bar.page.ts/sort-menu.page.ts/star-rating.page.ts); the "Whyfilter({ hasText: /share/i })and not anaria-label" walkthrough that pins the three reasons for the visible-text regex filter posture (production source carries noaria-labeltoday so adding one would be a production-source concession to the e2e suite, visible-text invariance because the substringsharesurvives every label evolution and theiflag tolerates case drift, strict-mode resilience against a future second share button with.first()); the "Why[role="menuitem"]and not adata-testid" walkthrough that pins the three reasons for the ARIA-role anchor posture (no production-source change required because the role is already there for accessibility and every menu library — HeroUI / Radix / Headless UI / shadcn-ui — writes it onto the dropdown rows, screen-reader-driven discoverability so the e2e suite drives the UI the same way an assistive-technology user does, strict-mode resilience against a future second[role="menuitem"]group on the same page like a profile-dropdown / navigation submenu / context menu); the "Why the Twitter regex uses/twitter|x \(/i" walkthrough that pins the three reasons for the dual-substring posture (survives the X rebrand by matching legacy"Twitter", post-rebrand"X", and transitional"X (formerly Twitter)"copy variants, thex \(fragment with the literal opening parenthesis disambiguates from incidental matches like"Export"/"Excel"/"Mr. X", the trailing space +(survives copy variants while staying strict-mode-safe even after a future copy-team revision); the "Why.first()on every Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second share trigger like a sticky share widget / mobile-drawer share mirror / admin-only "share with team" trigger, strict-mode collision against a future nested dropdown like a profile-dropdown rendered alongside the share dropdown with its own[role="menuitem"]entries, strict-mode collision against a portal-rendered duplicate like a mobile drawer that mirrors the desktop share dropdown); the "Why theiflag on every regex" walkthrough that pins the locale-style casing drift survival, production-source casing drift survival, and per-tenant override survival; the "Why onlyopen()andcopyLink()action methods" walkthrough that pins the three reasons for the no-per-platform-method posture (copyLink()is the only deterministic action whose effect can be verified via clipboard inspection while the per-platform entries open externalwindow.open(...)URLs that require a popup-verification harness, symmetric posture preserves a future addition the day a popup-verification harness lands, direct-Locator access discipline lets specs interact with the four item Locators for visibility /getAttribute('href')/ a11y audits without reaching through a method); the failure matrix covering every share-button-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop onpageor any of the five Locator fields,aria-label-based selector swap ontrigger,i-flag drop on any regex filter,.first()drop on any Locator, bare/x/iswap on the Twitter regex causing strict-mode chaos, bare/twitter/iswap that stops matching post-rebrand"X"entries,[role="menuitem"]ARIA-role anchor drop on item Locators,[role="menuitem"]→data-testidswap, unconditionalselectTwitter()/selectFacebook()/selectLinkedIn()add without a popup-verification harness,copyLink()composite shape drop, any of the four item fields drop,open()method drop while keepingcopyLink(), file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/share-button.spec.ts, future smoke / a11y specs that audit per-platformgetAttribute('href')and role counts plus future per-platform action methods once popup verification lands, the item-detail-page production-source component for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,discover-page-object.mdfor the/discover/[N]listing-route contract the consuming spec follows before reaching the share button) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto trigger-visibility-soft-skip, menu-item-Locator not found, item-detail-route-change, and dropdown-library-swap failures; and theshare-button.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/share-button.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (the trigger's visible text, the four menu-item entries' visible text, the[role="menuitem"]ARIA contract on every dropdown row, the host app's clipboard-write /window.openposture for the Copy Link / per-platform actions), adiscover-page-object.mdcross-check (the/discover/[N]listing-route contract the consuming spec follows before reaching the item-detail share button), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound share button would surface there), a per-platform popup-verification harness cross-check if a futureselectTwitter()/selectFacebook()/selectLinkedIn()method is added, dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the share-button spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "Share Button"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Scroll-To-Top Page Object (
apps/web-e2e/page-objects/public/scroll-to-top.page.ts) -- Per-source-file reference for the Playwright e2e suite's scroll-to-top floating-button driver paired withapps/web-e2e/page-objects/public/scroll-to-top.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andview-toggle-page-object.mddocuments the suite's listing-view-mode driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's scroll-position driver boundary — the smallest possible page object that lets a spec drive the global "back to top" floating button end-to-end (scroll the page down by an arbitrary pixel offset to trigger the button to appear, click the button to animate the page back to the top, read the currentwindow.scrollYvalue at any point during the flow to make scroll-position assertions). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class ScrollToTopsingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage; thereadonly button: Locatorpage.locator('button[aria-label="Scroll to top"]')exact-match selector with no.first()because the floating button is a single-instance fixed-position widget on every page; theconstructor(page: Page)that stores thepageand pre-binds the single button Locator without asuper(page)call; thescrollDown(pixels = 500)primitive that runswindow.scrollBy(0, pixels)inside the page context to scroll the document bypixelspixels vertically with a 500-pixel default that comfortably clears the production source's ~300-pixel threshold; theclick()primitive that clicks the floating button; thegetScrollY(): Promise<number>accessor that returnswindow.scrollYfor both precondition and postcondition assertions); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/scroll-to-top.spec.ts(button hidden at the top of/, button appears afterscrollDown(500), page returns to the top afterclick()); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against any host surface where the scroll-to-top button is a global floating widget rendered on every page in every role tree, reusability across all role trees including future admin-shell or client-shell scroll-to-top consumers, constructor parity with non-page widgets liketheme-toggle.page.ts/view-toggle.page.ts/language-switcher.page.ts/share-button.page.ts/search-bar.page.ts); the "Why exactaria-label=\"Scroll to top\"and not a substring" walkthrough that pins the three reasons for the exact-match posture (the label is the canonical accessibility primitive a screen reader announces, no collision risk requiring substring-tolerance because there is no plausible label variant, strict-mode safety from the single-button shape with future regressions surfacing as strict-mode violations); the "Why no.first()on the button Locator" walkthrough that pins the three reasons for the bare Locator (single-instance invariant, strict-mode signal preservation against future duplicate-button regressions, symmetric posture with future a11y assertions that count exactly one scroll-to-top button); the "Whypage.evaluate(() => window.scrollBy(0, px), pixels)forscrollDown" walkthrough that pins the deterministic scroll distance, the no-reliance on viewport-shape forPageDown, and the threshold-test ergonomics; the "Whypixels = 500default onscrollDown" rationale (comfortable threshold clearance, symmetric with the consuming spec's per-test override, documentation-by-default); the "WhygetScrollYreadswindow.scrollYand not React state" walkthrough that pins the production-source-first signal, the no reach-in to React internals, and the symmetric click-and-return assertion shape; the failure matrix covering every scroll-to-top-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop onpageorbutton, substringaria-label*=swap,data-testidswap, accidental.first()add,page.mouse.wheelswap onscrollDownwith wheel-acceleration flake,page.keyboard.press('PageDown')swap with viewport-dependence,pixels = 500default drop, React-state read ongetScrollY,Promise<number>annotation drop, file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/scroll-to-top.spec.ts, future smoke / a11y specs that countbutton.count() === 1and auditaria-label, the listing-page / item-detail-page production-source components for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift ontoLocator not found, threshold-clearance failure, and JavaScript-disabled-route failures; and thescroll-to-top.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/scroll-to-top.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (the exactaria-label="Scroll to top"attribute, the fixed-position floating shape, the ~300-pixel scroll threshold, the React-state-driven visibility flip), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound scroll-to-top would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the scroll-to-top spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "scroll-to-top"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E View-Toggle Page Object (
apps/web-e2e/page-objects/public/view-toggle.page.ts) -- Per-source-file reference for the Playwright e2e suite's public-listing view-toggle driver paired withapps/web-e2e/page-objects/public/view-toggle.page.ts, sitting inside thepublic/page-object subtree alongside the thirteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andtheme-toggle-page-object.mddocuments the suite's theme-switch driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's listing-view-mode driver boundary — the smallest possible page object that lets a spec drive the public listing's list / grid / masonry / map view-toggle end-to-end (read the four toggle buttons, switch to one of the three named view modes, observe which button is the "active" one via thescale-105Tailwind utility class written into the button's class list). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class ViewTogglesingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage; thereadonly listButton: Locatorpage.locator('button[aria-label*="list" i]').first()case-insensitive substring selector pinning to the first match; thereadonly gridButton: Locatorpage.locator('button[aria-label*="grid" i]').first()mirror for the grid mode; thereadonly masonryButton: Locatorpage.locator('button[aria-label*="masonry" i]').first()mirror for the masonry mode; thereadonly mapButton: Locatorpage.locator('button[aria-label*="map" i]').first()mirror for the map mode (with noselectMap()method today because the map-view feature is gated behindfeatures/map-view.md); theconstructor(page: Page)that stores thepageand pre-binds the four button Locators in a single pass without asuper(page)call; theselectList()/selectGrid()/selectMasonry()symmetric click primitives; theisActive(button: Locator)predicate that reads the supplied button'sclassattribute and returns whether thescale-105Tailwind utility-class substring is present, with the?? falsenullish-coalesce that pins the public return type toPromise<boolean>and mirrors the siblingtheme-toggle-page-object.md'sisDarkMode()posture); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/view-toggle.spec.ts(visibility on/discover/1, grid-active flip afterselectGrid(), list-active flip afterselectList()); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the listing surface, reusability on non-listing surfaces like a future admin-shell list/grid switch, constructor parity with non-page widgets liketheme-toggle.page.ts/language-switcher.page.ts/share-button.page.ts/search-bar.page.ts); the "Whyaria-label*=\"…\" iand not adata-testid" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required for tests, view-label invariance because the substring is invariant to phrasing variants like"View as list"/"List view"/"Show as a list", strict-mode resilience against a future second view toggle with.first()); the "Why.first()on every button Locator" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future admin-shell view toggle, strict-mode collision against per-card "Add to list" buttons, strict-mode collision against a portal-rendered mobile drawer mirror); the "Why theiflag on every substring selector" walkthrough that pins the locale-style casing drift survival, production-source casing drift survival, and per-tenant override survival; the "Why there is noselectMap()method today" walkthrough that pins the three reasons (map-view is feature-gated behindfeatures/map-view.md, symmetric posture preserves a future addition the day the map mode becomes always-on, the exposedmapButtonfield permits direct-Locator interaction); the "WhyisActive()reads thescale-105substring" walkthrough that pins the production-source-first signal, strict-mode safety against future class-list expansion, and future-proofing againstaria-pressedadoption as an additive a11y signal; the "Why?? falseon the class-list scan" rationale (type narrowing toPromise<boolean>, defensive symmetry withtheme-toggle-page-object.md'sisDarkMode()); the failure matrix covering every view-toggle-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop on any of the five fields, exactaria-labelmatch instead of substring,iflag drop,.first()drop on any button,aria-label*=→data-testidswap, unconditionalselectMap()add,mapButtonfield drop, React state /aria-pressedreach-in forisActive(),scale-105→bg-primarysubstring swap with hover false-positive,?? falsedrop onisActive(), file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/view-toggle.spec.ts, future smoke / a11y specs that need themapButtonfield for a feature-gated map-view spec, the listing-page production-source component for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL,features/map-view.mdfor the map-view feature gate) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift ontoLocator not found,isActive()-returns-false-on-active-button, andmapButtonhalf-rendered failures; and theview-toggle.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/view-toggle.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (thearia-labelshape on every button, thescale-105Tailwind utility-class hook on the active button, the four button positions in the toggle row), adiscover-page-object.mdcross-check (the/discover/[N]listing-route contract the consuming spec relies on), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound view-toggle would surface there), afeatures/map-view.mdcross-check (if a futureselectMap()method is added), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the view-toggle spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "view-toggle"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Search-Bar Page Object (
apps/web-e2e/page-objects/public/search-bar.page.ts) -- Per-source-file reference for the Playwright e2e suite's public-listing search-input driver paired withapps/web-e2e/page-objects/public/search-bar.page.ts, sitting inside thepublic/page-object subtree alongside the fourteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andtheme-toggle-page-object.mddocuments the suite's theme-switch driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's directory-search driver boundary — the smallest possible page object that lets a spec drive the public listing's search<input>end-to-end (type a search term, clear the input, read back the current value). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class SearchBarsingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage; thereadonly input: Locatorpage.locator('input[placeholder*="Search" i]').first()case-insensitive substring selector on the<input>'splaceholderattribute pinned to the first match; thereadonly clearButton: Locatorpage.locator('button', { hasText: '×' }).first()Locator pinned to the multiplication-sign glyph U+00D7 (not the lower-case Latin x U+0078); theconstructor(page: Page)that stores thepageand pre-bindsinputandclearButtonwithout asuper(page)call; thesearch(term: string)method that calls Playwright'sfill()for debounce-deterministic single-round-trip semantics; theclear()method that calls Playwright'sclear()to handle empty-input safety regardless of the clear button's visibility; thegetValue(): Promise<string>accessor with the?? ''nullish coalesce that future-proofs against a Playwright API change tostring | null); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/search.spec.ts(three flows: visibility on/, fill+read after debounce, clear+read after debounce); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against any host surface where the search input is page-mounted not page-shaped, reusability on non-listing surfaces like a future admin-shell or client-shell search widget, constructor parity with non-page widgets liketheme-toggle.page.ts/language-switcher.page.ts/share-button.page.ts/view-toggle.page.ts); the "Whyplaceholder*=\"Search\" iand not adata-testid" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required for tests, placeholder-text invariance because the prefix\"Search\"survives every label evolution and theiflag tolerates case drift, strict-mode resilience against a future second search input with.first()); the "WhyhasText: '×'for the clear button" walkthrough that pins the three reasons for the multiplication-sign-glyph selector (glyph invariance becausenext-intldoes not translate Unicode symbols, visual symmetry with the production source's Tailwind utility-class glyph rendering, strict-mode resilience against a stacked clear-filters button using the same glyph); the "Why.first()oninputandclearButton" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future second search input from an admin shell or footer newsletter, strict-mode collision against the autocomplete dropdown's input, strict-mode collision against a portal-rendered Cmd-K palette duplicate); the "Whyfill()instead ofpressSequentially()forsearch()" walkthrough that pins the debounce semantics (single React state update vs N collapsed events), the speed advantage (single round-trip vs N round-trips), and the deterministic value-readback against the React reconciler; the "Whyclear()instead of clickingclearButton" walkthrough that pins the empty-input safety (the clear button is hidden when the input is empty so clicking it would flake), the clear-button-drift survival, and the production-source-first input semantics (Playwrightclear()dispatches the same keyboard sequence a real user would); the "Why?? ''ongetValue()" rationale (API future-proofing against a futurestring | nullreturn, type-narrowed assertion shape for consuming specs, defensive symmetry withgetCurrentTheme()); the failure matrix covering every search-bar-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop, exactplaceholder=\"Search\"match instead of substring,iflag drop,.first()drop oninput,placeholder*=→data-testidswap,hasText: 'x'Latin-x swap onclearButton, CSS class selector swap onclearButton,.first()drop onclearButton,fill()→pressSequentially()swap onsearch(),clear()→clearButton.click()swap,?? ''drop ongetValue(), file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/search.spec.ts, future smoke / a11y specs that readgetValue(), the production source for the listing's search input React component for the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift ontoLocator not found,inputValue()-returns-empty, and clear-button-glyph-misses failures; and thesearch-bar.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/search.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (placeholder substring,×glyph, React-controlled value), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound search bar would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the search-bar spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep search), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Discover Page Object (
apps/web-e2e/page-objects/public/discover.page.ts) -- Per-source-file reference for the Playwright e2e suite's public directory-listing driver paired withapps/web-e2e/page-objects/public/discover.page.ts, sitting inside thepublic/page-object subtree alongside the fourteen other public-surface page objects (item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,theme-toggle.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andtheme-toggle-page-object.mddocuments the suite's theme-switch driver boundary underapps/web-e2e/page-objects/public/, this page documents the suite's directory-listing driver boundary — the smallest possible page object that lets a spec drive/discover/[N]end-to-end (navigate to a page in the directory, count the items rendered, click into the first item, observe the pagination control). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theimport { BasePage } from '../base.page'runtime import — the only runtime import in the file; theexport class DiscoverPage extends BasePagesingle named export with the inherited(page: Page)constructor signature; thereadonly itemLinks: Locatorpage.locator('a[href*="/items/"]')substring-selector matching every directory-card link; thereadonly pagination: Locatorpage.locator('nav[aria-label*="pagination"], nav[aria-label*="Pagination"]')dual-substring selector tolerating production-source case drift; thereadonly heading: Locatorpage.getByRole('heading', { level: 1 })role+level resolution that survives translation churn; theconstructor(page: Page)that callssuper(page)and pre-binds the three Locators above; thenavigate(pageNum = 1)method that wraps the inheritedgotowith the canonical/discover/[N]route and defaultspageNumto 1; thegetItemCount()method that returns Playwright'scount()over theitemLinksLocator without throwing on an empty fixture; theclickFirstItem()method that clicks the first directory-card link with.first()for strict-mode safety); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming specs atapps/web-e2e/tests/public/discover.spec.ts(three flows) andapps/web-e2e/tests/public/map.spec.ts(precondition seeding); the "Why the class extendsBasePage" walkthrough that pins the three load-bearing reasons (the route is a navigable page in the URL sense sogoto/waitForPageReadyare useful for free, shared page-shell Locators are useful for "directory page renders the global header / footer" assertions, constructor parity with sibling page-shaped page objects across the four role trees); the "Whya[href*="/items/"]foritemLinks" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required, locale invariance against/en/items///fr/items///de/items///es/items///zh/items///ar/items/, slug invariance against every data-source change); the "Why dual-substringaria-label*forpagination" walkthrough that pins the production-source case drift tolerance, the strict-mode safety from the unique landmark, and the zero-false-positive substring narrowness; the "WhygetByRole('heading', { level: 1 })forheading" walkthrough that pins the locale invariance, the single accessible-name source of truth, and the production-source-first discipline; the "WhypageNum = 1default onnavigate" rationale that pins the most-common call site shortest, the explicit-page-number documentation for pagination tests, and the type-narrowedPromise<void>posture; the "Why.first()onclickFirstItem" walkthrough that pins the three failure modes of dropping it (strict-mode collision against multiple matching links, render-order independence with URL-substring assertions, future highlighted-item-at-position-0 support); the failure matrix covering every discover-page-level mistake (type-only import drop,extends BasePagedrop,readonlydrop,a[href^="/items/"]prefix-only swap,data-testidswap, dual-substring drop on pagination,data-testidswap on pagination,h1tag-selector swap on heading,pageNum = 1default drop, template-literal-to-concat swap,.first()drop onclickFirstItem, hard-coded slug, file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/discover.spec.ts, the precondition consumerapps/web-e2e/tests/public/map.spec.ts, the production source atapps/web/app/[locale]/discover/[page]/page.tsxfor the DOM contract,base-page-object.mdfor the inherited primitives,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift ontogetItemCount() returns 0,Locator not found, andnavigate timeoutfailures; and thediscover.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/discover.spec.tsandapps/web-e2e/tests/public/map.spec.ts), abase-page-object.mdcross-check, a production-source cross-check (the directory-card link shape, the pagination landmark shape, the H1 contract), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound directory driver would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the discover and map spec subsets (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep "discover|map"), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Theme-Toggle Page Object (
apps/web-e2e/page-objects/public/theme-toggle.page.ts) -- Per-source-file reference for the Playwright e2e suite's header theme-switch driver paired withapps/web-e2e/page-objects/public/theme-toggle.page.ts, sitting inside thepublic/page-object subtree alongside the fourteen other public-surface page objects (discover.page.ts,item-detail.page.ts,language-switcher.page.ts,map.page.ts,newsletter.page.ts,profile-dropdown.page.ts,public-pages.page.ts,scroll-to-top.page.ts,search-bar.page.ts,share-button.page.ts,sort-menu.page.ts,star-rating.page.ts,view-toggle.page.ts). Wherebase-page-object.mddocuments the page-object inheritance root andsignin-page-object.mddocuments the suite's sign-in surface boundary underapps/web-e2e/page-objects/auth/, this page documents the suite's theme-switch driver boundary — the smallest possible page object that lets a spec drive the header theme-switch dropdown end-to-end (open the dropdown, select the light or dark option, observe the canonicalaria-labelshape, observe thedarkclass flip on the<html>element). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theexport class ThemeTogglesingle named export with noextendsclause — the public-tree widget-driver posture; thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage; thereadonly toggleButton: Locatorpage.locator('button[aria-label*="Current theme"]').first()substring-selector pinning to the first match; theconstructor(page: Page)that stores thepageand pre-bindstoggleButtonwithout asuper(page)call; thegetCurrentTheme()method that reads the toggle button'saria-labeland returns'light'/'dark'/'unknown'; theopen()minimal "open the dropdown" primitive every other method composes against; theselectLight()andselectDark()methods that composeopen()+ role+regex-name resolution + click; theisDarkMode()method that reads the<html>class list and returns whether thedarksubstring is present); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the consuming spec atapps/web-e2e/tests/public/theme-toggle.spec.ts; the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance against the header surface, reusability on non-page surfaces like a future admin-shell theme switch, constructor parity with non-page widgets likelanguage-switcher.page.ts/share-button.page.ts/view-toggle.page.ts); the "Whyaria-label*=\"Current theme\"and not adata-testid" walkthrough that pins the three reasons for the production-source-first selector posture (no production-source change required for tests, theme-label invariance because the prefix\"Current theme\"survives every theme rename, strict-mode resilience against a future second theme switch with.first()); the "Why.first()on the toggle button" walkthrough that pins the three failure modes of dropping it (strict-mode collision against a future admin-shell theme switch, strict-mode collision against the dropdown's per-option labels, strict-mode collision against a portal-rendered duplicate like a mobile drawer); the "Why parse thearia-labelsubstring instead of querying state" walkthrough that pins the three reasons for the black-box discipline (no React-internals reach-in, storage drift survival fromlocalStorage→ cookie → Drizzle row migration, theme-set extensibility because a future fifth theme returns'unknown'instead of breaking); the "Why role+regex name for the option buttons" walkthrough that pins the locale-invariance posture and the strict-mode resilience against an off-dropdown "light theme" CTA banner; the "WhyisDarkMode()reads<html>'s class" walkthrough that pins the TailwinddarkMode: 'class'posture, the server-render parity, and the no-flicker guarantee documented inrendering-hydration-no-flicker.md; the failure matrix covering every theme-toggle-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop, exactaria-labelmatch instead of substring,.first()drop on the toggle button,aria-label*=→data-testidswap, CSS attribute selector swap on the option-button locator, text-only locator swap on the option button,.first()drop on the option button, React state /localStoragereach-in forgetCurrentTheme(),<body>/<main>swap onisDarkMode(),'unknown'branch drop ongetCurrentTheme(), file move, rename,.tsxextension, CRLF line endings); the per-line walkthrough table that pins each line of the file to its purpose; the read / write surface summary that maps every caller (the consuming spec atapps/web-e2e/tests/public/theme-toggle.spec.ts, future smoke / a11y specs that readgetCurrentTheme(), the production source atapps/web/components/header/theme-switch.tsxfor the DOM contract,e2e-tsconfig.mdfor theincludeglob,playwright-config.mdfor thebaseURL) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift ontoLocator not foundandisDarkMode()-returns-false-in-dark-mode failures; and thetheme-toggle.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/public/theme-toggle.spec.ts), abase-page-object.mdcross-check (if the new shape inherits, document why), a production-source cross-check (aria-labelshape, dropdown option-button shape,<html>-class hook), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob), aplaywright-config.mdcross-check (thebaseURLposture), afixtures-index.mdcross-check (a future fixture-bound theme-toggle would surface there), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the theme-toggle spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep theme), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Sign-in Page Object (
apps/web-e2e/page-objects/auth/signin.page.ts) -- Per-source-file reference for the Playwright e2e suite's soleauth/-tree page object paired withapps/web-e2e/page-objects/auth/signin.page.ts, sitting at the root of theauth/page-object subtree the same waybase-page-object.mdsits at the root of the page-objects tree as a whole,fixtures-index.mdsits at the root of the fixtures tree, ande2e-test-data.mdsits at the root of the helpers tree. Wherebase-page-object.mddocuments the page-object inheritance root andauth-fixture.mddocuments the suite's authenticated-fixture boundary that turns the persisted storage states minted at pre-flight into per-test isolated contexts, this page documents the suite's sign-in surface boundary — the smallest possible page object that lets a spec drive/auth/signinend-to-end (fill the email, fill the password, submit, optionally wait for the post-sign-in redirect, observe the success / error alerts). Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only Playwright import that mirrors the base-class discipline; theimport { BasePage } from '../base.page'runtime import — the only runtime import in the file; theexport class SignInPage extends BasePagesingle named export with the inherited(page: Page)constructor signature; thereadonly emailInput: Locatorform-scoped#emailinput; thereadonly passwordInput: Locatorform-scoped#passwordinput; thereadonly submitButton: Locatorpage.getByRole('button', { name: /sign in/i })role-based regex-name lookup that survives translation churn and refactors that move the button outside the form; thereadonly forgotPasswordLink: Locatorform-scopeda[href*="forgot-password"]substring selector that is invariant to thelocalePrefix: 'as-needed'middleware posture; thereadonly errorAlert: Locatorpage.locator('.bg-red-50').first()first-banner pinning that mirrors the base class'sheader.first()/footer.first()posture; thereadonly successAlert: Locatorsymmetric.bg-green-50.first()pinning; theconstructor(page: Page)that callssuper(page)and pre-binds the seven Locators above using a localauthFormLocator that filterspage.locator('form')to the form containing#email; thenavigate()method that wraps the inheritedgotowith the canonical/auth/signinroute; thesignIn(email, password)form-fill kernel that finishes withpasswordInput.press('Enter')to submit via the default form-submission path instead of clicking the button; thesignInAndWaitForRedirect(email, password, expectedUrl)happy-path wrapper that delegates tosignIn()and awaitspage.waitForURL(expectedUrl, { timeout: 60_000 })); the full file annotated chunk-by-chunk; the spec context cross-link to Spec 010 — E2E Test Coverage and the auth-flow spec set underapps/web-e2e/tests/auth/; the "Why scope every form Locator toauthForm" walkthrough that pins the three failure modes of unscoped selection (newsletter / search drawer collision injecting a second#email, auth-shell drift from header newsletter widgets, sign-up vs sign-in form parity since/auth/signupreuses the same IDs) against the form-scoping posture; the "Why role+regex name forsubmitButton" walkthrough that pins the three rejected alternatives (form-scoped role lookup that breaks on a button-floats-outside-form refactor, CSS attribute selectorbutton[type="submit"]that collides with social-login button rows, text selector:has-text("Sign In")that fails on every non-English locale) against the role+name regex with theiflag; the "Why Enter-key submission" walkthrough that pins the three reasons for the keyboard-driven flow (mirrors real-user form submission, avoids button-state flakes when the button is hidden during submit, keepssignIn()andsignInAndWaitForRedirect()semantics consistent with no click-driven intermediate); the "Why.bg-red-50.first()and.bg-green-50.first()" walkthrough that pins the three failure modes of dropping.first()(strict-mode violations on stacked banners, future "all errors" assertions still allowed via.locator('.bg-red-50')directly, dark-mode / theme drift survives because the locator matches the Tailwind class present in both light and dark variants); the "WhysignInAndWaitForRedirectexists alongsidesignIn" rationale that pins the kernel-vs-wrapper split (the kernel does not wait so failure-path specs can assert onerrorAlertimmediately, the wrapper waits for the URL change so happy-path specs land on the post-sign-in route, the 60s timeout is generous because cold-start CI runs can take 30+ seconds for the NextAuth handshake + cookie set + redirect + server render chain); the failure matrix that maps eachsignin.page.tsmistake (dropimport typeforPage/Locator→ bundle cost and circular-import risk, drop theextends BasePageclause → loses inheritedgoto/gotoLocalized/waitForPageReady/getTitleand inheritedheader/footer/navLinksLocators, dropreadonlyfrom any field → cross-test state leak risk, drop the form-scoping onemailInput/passwordInput→ strict-mode collision with footer newsletter#email/ sign-up modal#email, switchsubmitButtonto a CSS attribute selector → multiple submit buttons collide under strict-mode, switchsubmitButtonto a text-only locator → localised pages fail to resolve the button, switchforgotPasswordLinkfromhref*=tohref=→ localised forgot-password URLs fail to match, drop.first()fromerrorAlert/successAlert→ stacked alerts collide under strict-mode, switchsignIn()from Enter-key to button click → loses real-user submission semantics and flakes when the button is hidden during submit, tightensignInAndWaitForRedirect's timeout below 30s → cold-start CI flakes on the post-sign-in redirect chain, raise the timeout above 60s → genuinely-broken redirects hang the suite, add a new field that captures global state → cross-test leakage on serial runs, move the file out ofapps/web-e2e/page-objects/auth/→Cannot find moduleon every importing spec, renameSignInPage→ every importer needs a matching rename, switch the file extension to.tsx→ falls out of theinclude: ["./**/*.ts"]glob and every importing spec breaks, drop the trailing newline → Prettier diff, ship the file with CRLF → same as above) onto the layer that surfaces each one; the per-line walkthrough table that pins each line of the 37-line file to its purpose; the read / write surface summary that maps every caller (apps/web-e2e/tests/auth/**specs, theBasePageparent class,auth.fixture.tswhich today bypasses the form by pre-loading storage states,apps/web/components/auth/**production source for the DOM contract,e2e-tsconfig.json,playwright.config.ts) to the fields they touch; the read / write surface failure modes table that maps production-source / middleware / config drift onto strict-mode andCannot find modulefailures; and thesignin.page.ts-change checklist that ties any change to a spec audit (every spec underapps/web-e2e/tests/auth/), abase-page-object.mdcross-check (inherited methods and Locators), a production-source cross-check (the seven Locator strings must match production sign-in form components and the route underapps/web/app/[locale]/auth/signin/), anauth-fixture.mdcross-check (the authenticated fixtures bypass the form today; a future fresh-sign-in fixture would cross-reference this file), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob picks up this file), ane2e-package-manifest.mdcross-check (Playwright major bumps may change thegetByRoleoverload set), aplaywright-config.mdcross-check (thebaseURLresolves the relative/auth/signinpath), aglobal-setup.mdcross-check (the pre-flight global setup also drives/auth/signinto mint storage states; the form's identifiers must stay in sync between this file and the global setup's selectors), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run targeting the auth-spec subset (pnpm --filter @ever-works/web-e2e test:e2e:chromium --grep auth), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Base Page Object (
apps/web-e2e/page-objects/base.page.ts) -- Per-source-file reference for the Playwright e2e suite's foundational page-object class paired withapps/web-e2e/page-objects/base.page.ts, the page-object inheritance root sitting atapps/web-e2e/page-objects/base.page.tsthe same wayfixtures-index.mdsits at the root of the fixtures tree ande2e-test-data.mdsits at the root of the helpers tree. Wherefixtures-index.mddocuments the directory-level fixture-export boundary ande2e-test-data.mddocuments the suite's shared-data boundary, this page documents the page-object inheritance root — the smallest possible class every concrete page object underapps/web-e2e/page-objects/admin/,apps/web-e2e/page-objects/auth/,apps/web-e2e/page-objects/client/, andapps/web-e2e/page-objects/public/extends. Documents the at-a-glance summary table of every load-bearing element (theimport type { Page, Locator } from '@playwright/test'type-only import that stays out of the runtime bundle and prevents a runtime require of Playwright's runner internals at module-load for every consumer; theexport class BasePagesingle named export inherited by 30+ subclasses today across the four role trees; thereadonly page: Pagefield that gives every subclass the PlaywrightPagehandle for ad-hoc locator construction; thereadonly header: Locatorpre-boundpage.locator('header').first()Locator pinned to the first<header>because Next 16 layouts can stack a global header above a section header; thereadonly footer: Locatorsymmetricpage.locator('footer').first()pinning; thereadonly navLinks: Locatorheader.getByRole('link')header-scoped link enumeration so footer links and in-page links do not pollute navigation assertions; theconstructor(page: Page)that stores the page handle and pre-binds the three structural Locators above; thegoto(path: string)suite-wide navigation primitive with thewaitUntil: 'domcontentloaded'override of Playwright's default'load'posture; thegotoLocalized(path: string, locale: string)locale-aware navigation that special-cases'en'to bare paths and prefixes every other locale with/${locale}, encoding the host app'slocalePrefix: 'as-needed'middleware posture; thewaitForPageReady()re-await primitive for post-navigation interactions that uses the same'domcontentloaded'load state asgoto(); thegetTitle(): Promise<string>shortcut forpage.title()so subclasses do not importpage.titlefrom inside their own assertions); the full file annotated chunk-by-chunk; the "Whyimport typeand not a runtime import" walkthrough that pins the three failure modes of a runtime import (circular-import risk between this file and the runner'stestfunction, bundle-size cost on type-only consumers, fixture-vs-runner ambient drift between the type-only page-object tree and the runtime-bearing fixtures barrel) against the type-only shape; the "Whypage.locator('header').first()and not a plainheader" walkthrough that pins the three failure modes of unscoped selection (strict-mode violations on item-detail pages, cross-page assertion drift, header / footer asymmetry) against thefirst()posture; the "Whyheader.getByRole('link')fornavLinks" walkthrough that pins the three differences between header and footer link inventories (primary vs secondary navigation, auth-state divergence, modal / drawer pollution risk) against the header-scoped enumeration; the "Whygoto()useswaitUntil: 'domcontentloaded'" walkthrough that pins the three reasons for the override (Next 16 streaming<Suspense>boundaries pending theloadevent, image-heavy pages adding ~3-5s per spec onload,networkidlebeing a footgun on pages with pollinguseEffectlike analytics heartbeats) against the deliberate single-load-state choice; the "WhygotoLocalized()special-cases'en'" walkthrough that pins the three rejected alternatives (always-prefix triggers a 308 redirect onen, never-prefix lands on the wrong page for non-English locales, branch-on-'en'-at-every-call-site duplicates the prefix logic across page objects) against the encodedlocalePrefix: 'as-needed'posture; the "WhywaitForPageReady()is a thin re-state ofgoto's wait" rationale (post-gotointeraction patterns: pagination button clicks,next/linkclient-side navigations, modal dismissals); the "WhygetTitle()exists" rationale (subclass discoverability, future title-sanitisation override site, type-narrowedPromise<string>return type); the failure matrix that maps eachbase.page.tsmistake (drop theimport typeand switch to a runtime import → bundle-size cost and circular-import risk, dropreadonlyfrom any field → cross-test state leak risk, drop.first()fromheaderorfooter→ strict-mode locator violation on stacked-header pages, switchnavLinksfromheader.getByRole('link')topage.getByRole('link')→ footer / in-page links pollute the inventory, switchgoto()fromdomcontentloadedtoload→ ~3-5s slower per spec on image-heavy pages, switchgoto()fromdomcontentloadedtonetworkidle→ spec timeouts on pages with analytics heartbeats, drop the'en'special-case fromgotoLocalized()→ 308 redirect on every English call, hard-code/${locale}${path}for every locale → same as above, switchwaitForPageReady()to a different load state → asymmetry withgoto(), dropgetTitle()→ subclass drift, move the file fromapps/web-e2e/page-objects/base.page.ts→ massCannot find modulefailures at TS gate, renameBasePage→ everyextends BasePageclause breaks, add a public field that holds shared state across tests → cross-test leakage, add aprotected staticcache → same as above, makegoto()return aPromise<Response | null>→ subclass drift, make the constructor accept anything other thanpage: Page→ everysuper(page)breaks, drop the trailing newline → Prettier diff, ship the file with a CRLF line ending → same as above, switch the file extension to.tsx→ falls out of theinclude: ["./**/*.ts"]glob) onto the layer that surfaces each one; the per-line walkthrough table that pins each line of the 32-line file to its purpose; and thebase.page.ts-change checklist that ties any change to a subclass audit (every page object under the four role trees), afixtures-index.mdcross-check (today nothing in the page-object tree is re-exported through the fixtures barrel; a future "default page object" addition cross-references this file), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob picks up this file), ane2e-package-manifest.mdcross-check (the package'sdevDependencies.@playwright/testunderwrites thePageandLocatortypes this file uses; a Playwright major bump may change the type signatures), aplaywright-config.mdcross-check (thebaseURLis whatpage.goto(path)resolves relative to), anauth-fixture.mdcross-check (every authenticated page object instantiated from the auth fixture's storage-state-bearing context still goes through this base class), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run against a representative spec from each subclass tree (admin, auth, client, public), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept that affects test authoring, and a reviewer pass. - E2E Test Data Helpers (
apps/web-e2e/helpers/test-data.ts) -- Per-source-file reference for the Playwright e2e suite's central test-data and constants module paired withapps/web-e2e/helpers/test-data.ts, the shared-data companion toglobal-setup.md(which destructuresTEST_DATA,ADMIN_STATE_FILE,CLIENT_STATE_FILE,AUTH_STATE_DIR, andREQUIRED_ENV_VARSfrom this module) and toglobal-teardown.md(which will consume the same constants when the no-op placeholder grows into a real cleanup sequence). Whereglobal-setup.mddocuments the suite's pre-flight boundary andglobal-teardown.mddocuments the suite's post-flight boundary, this page documents the suite's shared-data boundary — every constant, generator, env-var name, route path, auth-state file path, and seeded credential the rest of the suite reads through. Documents the at-a-glance summary table of every export (requireEnv(name)private fail-fast helper that throws on missing OR empty env-vars with a contributor-actionable message naming the missing key and the file.env.localit must be set in;TEST_DATA.ADMIN_EMAIL/TEST_DATA.ADMIN_PASSWORDlazy getters callingrequireEnv('SEED_ADMIN_EMAIL')/requireEnv('SEED_ADMIN_PASSWORD')so the env-var read happens at access time, not module load — specs that never touch admin credentials never trigger the throw;TEST_DATA.CLIENT_PASSWORDstatic'TestClient123!'because per-run uniqueness comes from the email so the password is irrelevant to identity and a static value makes failed sign-ups immediately reproducible from the trace;TEST_DATA.generateClientEmail()returninge2e-client-${Date.now()}-${randomSuffix}@test.localwith a 6-char base-36 random suffix giving ~36⁶ ≈ 2.2B values per millisecond and the@test.localTLD reserved by RFC 6761 to prevent accidental real-world delivery;TEST_DATA.generateItemName()returningE2E Test Item ${Date.now()}-${randomSuffix}with a 4-char suffix;TEST_DATA.generateItemUrl()returninghttps://e2e-test-${Date.now()}.example.comwith no random suffix because URL hostnames have syntactic constraints andexample.comis reserved by IANA RFC 2606 to prevent accidental real-world traffic;REQUIRED_ENV_VARSas constwhitelist['SEED_ADMIN_EMAIL', 'SEED_ADMIN_PASSWORD']consumed byglobal-setup.md'spromptForMissingEnv()step;PUBLIC_ROUTES13-rowas consttable of every public route the navigation shell links to (/,/discover/1,/categories,/tags,/collections,/pricing,/about,/help,/privacy-policy,/terms-of-service,/cookies,/auth/signin,/auth/register) consumed by every public-shell smoke spec;AUTH_STATE_DIRliteral'auth-states',ADMIN_STATE_FILEtemplate-composed${AUTH_STATE_DIR}/admin.json,CLIENT_STATE_FILEtemplate-composed${AUTH_STATE_DIR}/client.jsonso a future move fromauth-states/to.auth/is a one-line edit); the full file annotated chunk-by-chunk; the "Why the lazy-getter pattern for the admin credentials" walkthrough that pins the three failure modes of property assignment (process.envread at module load punishes specs that do not need credentials,?? ''fallback turns the error into silent''propagation 30 seconds in,??removes the type-check safety net) against the lazy-getter shape (access-time read, fail-fast throw with contributor-actionable message, narrowstringreturn type); the "Why the generators useDate.now()+ base-36 random" walkthrough with the three rejected alternatives (crypto.randomUUID()introduces a Node-specific import in an otherwise import-free module,@faker-js/faker'sfaker.internet.email()generates real-world TLDs that risk accidental delivery, per-worker static emails collide with prior runs and require a per-run cleanup step that does not exist) against the current shape (no imports, RFC-reserved TLD, ~2.2B values per millisecond); the "WhyPUBLIC_ROUTESis areadonlyarray of objects" rationale (thenamefield is the test description so the report showsHome/Categories/Sign Innot slugs, thenamesurvives a route rename so the report does not become unreadable after a/auth/signin→/loginmove, theas constposture lets specs type-check against literal values for compile-time typo safety); the failure matrix that maps eachtest-data.tsmistake (drop therequireEnv()empty-string check →?email=''30 seconds in instead of fail-fast, switch the getters to property assignments → punishes specs that never touch admin credentials, add a?? ''fallback → silent''propagation, switch the client TLD from@test.localto@example.com→ MX records exist onexample.comso a leaked test account could receive a real email, switch the URL apex fromexample.comto a real domain → accidental traffic to that site, drop theDate.now()prefix from a generator → ~2.2B values across the suite's lifetime instead of per millisecond, add a required env-var without updatingREQUIRED_ENV_VARS→ pre-flight prompt does not ask for it, dropas constfrom a literal array → typos become silent test failures instead of TypeScript errors, add a public route to the navigation without adding it toPUBLIC_ROUTES→ ships without smoke coverage, remove a public route without removing it fromPUBLIC_ROUTES→ smoke matrix asserts on a 404 the route legitimately produces, hard-code'auth-states'inglobal-setup.tsinstead of importing → multi-file rename becomes lossy, switch theCLIENT_PASSWORDfrom a static string to a generator → failed sign-ups become harder to reproduce from trace, move the file fromapps/web-e2e/helpers/test-data.ts→Cannot find moduleon every import, exportrequireEnvand call from another module → multiple opinions of "missing env-var" semantics drift) onto the layer that surfaces each one; the per-line walkthrough table; and thetest-data.ts-change checklist that ties any export change to aglobal-setup.mdcross-check, aglobal-teardown.mdcross-check, aplaywright-config.mdcross-check (thewebServer.cwdresolves the relative paths inADMIN_STATE_FILE/CLIENT_STATE_FILE), ane2e-tsconfig.mdcross-check, the.gitignorecross-check (AUTH_STATE_DIRmust match the gitignore entry), the public-routes smoke spec cross-check underapps/web-e2e/tests/public/, theapps/web/.env.exampleand workspace README propagation for new required env-vars, dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run that confirms the pre-flight prompt walks the new env-var andauth-states/still contains bothadmin.jsonandclient.jsonafter the run, adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept, and a reviewer pass. - Playwright Global Teardown (
apps/web-e2e/global-teardown.ts) -- Per-source-file reference for the Playwright e2e suite's per-run global teardown paired withapps/web-e2e/global-teardown.ts, the post-flight companion toglobal-setup.md. Where the setup mints the two persisted authentication storage states by driving a real Chromium browser against the host web app's/auth/signinand/auth/registerscreens, this page documents the suite's post-flight boundary — what the runner does once after the last test, in what order, with what failure modes — even when, today, the answer is nothing: the file is a deliberate no-op placeholder (async function globalTeardown() { /* Placeholder for future cleanup (e.g., test database reset) */ } export default globalTeardown;) wired intoplaywright-config.mdvia the always-resolvedglobalTeardown:field. Documents the at-a-glance summary table of every load-bearing element (async function globalTeardown()with the empty parameter list because the no-op does not use Playwright'sFullConfig, the single marker comment that prevents the file from being deleted as dead code, the default export shape Playwright's runner imports as(await import(...)).default, and the absence of imports because there is nothing to clean up today); the full file annotated chunk-by-chunk; the "Why a no-op placeholder instead of dropping the file" walkthrough that pins the lowest-coupling rationale against the three rejected alternatives (drop the file and the config field together, point the field at a realnoop.tsthat does not communicate intent, or keep both with a self-descriptive empty stub); the five concrete cleanup buckets the placeholder reserves the slot for (per-runauth-states/directory cleanup that would force every CI run to re-mint admin / client state, per-run client account deletion viaTEST_DATA.generateClientEmail()against a hypotheticalDELETE /api/admin/users?email=...to keep the seeded-DB row count stable, per-run Stripe / Polar / LemonSqueezy sandbox fixture cleanup via per-providercustomers.del(...)/subscriptions.cancel(...),apps/web-e2e/test-results/directory cleanup on success that walks files older than the run's start time to prevent crashed-workertrace.zip/video.webm/screenshot.pngcross-pollution, and test-database snapshot reset for any future seeded fixture); the "Why the parameter list is empty today" rationale that pins the(config: FullConfig) => Promise<void> | voidPlaywright contract against the future-friendly addition of(config: FullConfig)for a teardown that needs the resolvedbaseURL/ project list /outputDir; the "WhyglobalTeardownis not allowed to throw" rationale that pins the recommended per-buckettry / catch+console.error('[global-teardown] bucket #N cleanup failed:', err)pattern (so one failing bucket does not skip the others, the run's real test outcomes are not overwritten in the reporter, and CI reruns do not re-execute the whole suite for a non-fatal cleanup error); the "WhyglobalTeardownruns once, not per-worker" rationale that pins the global-shared cleanup buckets (auth states / client account / payment-provider sandbox /test-results/) against the race-condition cost of pushing cleanup down to project / file / test level; the failure matrix that maps eachglobal-teardown.tsmistake (drop the file →ENOENTon every run beforeglobalSetup, drop theglobalTeardownfield → silent skip with no error, switch to a named export →TypeError: undefined is not a functionat run end, make the function synchronous and throw → "tests passed but the run failed" log noise, leave the body empty without the marker comment → contributor deletes the file as dead code, move the file toapps/web-e2e/setup/global-teardown.ts→ENOENTfrom the hard-codedpath.resolve(__dirname, './global-teardown.ts'), add aprocess.exit(0)→ emptyplaywright-report/directory, hard-code anawaiton a database client → failure on minimal local-dev configurations, add asetTimeout/ long-running async wait → 60-s end-of-run blocker that produces false "run timed out" results) onto the layer that surfaces each one; the per-line walkthrough table; and theglobal-teardown.ts-change checklist that ties any flip back to aglobal-setup.mdcross-check (anything the setup mints is the natural target for the teardown), aplaywright-config.mdcross-check (theglobalTeardown:field's path resolution is the only thing pointing the runner at this file), aapps/web-e2e/helpers/test-data.tscross-check (AUTH_STATE_DIR,ADMIN_STATE_FILE,CLIENT_STATE_FILE, andTEST_DATA.generateClientEmail()are the constants the teardown will use), ane2e-tsconfig.mdcross-check (theinclude: ["./**/*.ts"]glob picks up this file), the dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset run that confirms the runner starts (noENOENT), exits cleanly (the teardown returns within the per-test timeout), and writes the HTML report (playwright-report/index.htmlexists), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the teardown gains a real cleanup bucket, and a reviewer pass. - Playwright Global Setup (
apps/web-e2e/global-setup.ts) -- Per-source-file reference for the Playwright e2e suite's per-run pre-flight hook paired withapps/web-e2e/global-setup.ts, the pre-flight companion toplaywright-config.md. Where the config locks the suite's runtime boundary (which directory the runner walks, how many workers, which browsers, theuse-defaults, thewebServerblock), this page documents the suite's pre-flight boundary — what the runner does once before the first test, in what order, with what failure modes. Documents the at-a-glance summary table of every load-bearing step (promptForMissingEnv()first walkingREQUIRED_ENV_VARS = ['SEED_ADMIN_EMAIL', 'SEED_ADMIN_PASSWORD']with theprocess.env.CIshort-circuit that prevents CI hangs and the TTY-onlyreadline/promisesprompt with the empty-answer guard,baseURLresolution fromconfig.projects[0]?.use?.baseURL ?? 'http://localhost:3000'with the defensive fallback, recursivemkdirSync(auth-states/)because Playwright'sstorageState({ path })does not auto-create the parent directory, the__dirname-anchored absolute path resolution that surviveswebServer.cwd: '../..', single sharedchromium.launch()reused by both auth flows for the boot-cost / memory-footprint win, the admin sign-in flow against/auth/signinwith stable#email/#passwordIDs and the role-tolerantwaitForURL(/\/(admin|client\/dashboard)/)redirect regex, the per-run client sign-up flow withTEST_DATA.generateClientEmail()returning a uniquee2e-client-${Date.now()}-${randomSuffix}@test.localaddress that prevents parallel-worker collisions, thepress('Enter')keyboard submit that avoids button-text dependencies, thewaitForURL(/\/client\/dashboard/, { timeout: 120_000, waitUntil: 'domcontentloaded' })slow-path-tolerant client-redirect wait, the per-flowtry / catchthat closes the browser on failure, the singleawait browser.close()that runs only on the happy path, and theexport default globalSetupPlaywright contract); the full file annotated chunk-by-chunk; the "WhypromptForMissingEnvis the first call" walkthrough that pins the fail-fast posture against thelocator('#email').fill(undefined)failure mode 30 seconds in; the "Why one Chromium, two contexts" cost / benefit rationale (~500 ms × 1 vs ~500 ms × 2 boot, ~150 MB × 1 vs ~150 MB × 2 memory); the "WhystorageState({ path })instead of cookies-only" rationale (captures both NextAuth cookies and anylocalStorageset by the auth callback); the "Why the admin flow accepts both/adminand/client/dashboard" role-tolerance rationale; the "Why the client flow usesdomcontentloadedinstead ofload" rationale (analytics pixels and fonts would push wall-clock past the budget); the "Why theauth-states/directory is per-suite, not per-worker" rationale (workers share the global setup's output by design, so per-worker auth states would multiply the Chromium boot cost without buying isolation the suite does not need); the failure matrix that maps eachglobal-setup.tsmistake (droppromptForMissingEnv()→ crypticfill(undefined)30 s in, drop theprocess.env.CIbranch → CI hangs forever, drop the empty-answer guard → silent failure later, hard-codebaseURL→BASE_URL=override stops working, drop the?? '...'fallback →goto(undefined)TypeError, dropmkdir auth-states/→ENOENT, useprocess.cwd()→ broken paths underwebServer.cwd: '../..', twochromium.launch()calls → doubled wall-clock and memory, droptry / catch→ leaked Chromium processes, hard-code admin email → suite breaks on seed rotation, hard-code client email → parallel-worker collisions, use real-world TLD on client email → accidental delivery risk, use.click()instead ofpress('Enter')on register → button-text dependency, usewaitUntil: 'load'on client redirect → analytics pixel wall-clock blow-up, use 30-s client timeout → cold-render flakes, dropstorageState({ path })→ every authenticated test re-runs sign-in, tighten admin redirect regex to/adminonly → breaks on demoted seeded admin, loosen admin redirect regex to/→ succeeds when sign-in fails, remove per-successconsole.log→ silent CI on success, dropAUTH_STATE_DIR/ADMIN_STATE_FILE/CLIENT_STATE_FILEconstants → path drift across files) onto the layer that surfaces each one; the per-line walkthrough table; and theglobal-setup.ts-change checklist that ties any flip back to aplaywright-config.mdcross-check, aapps/web-e2e/helpers/test-data.tscross-check, dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset run that proves both auth states land inapps/web-e2e/auth-states/, the per-CI-vs-local both-modes verification (setCI=1to exercise the no-prompt branch), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link, and a reviewer pass. - Playwright Runner Configuration (
apps/web-e2e/playwright.config.ts) -- Per-source-file reference for the Playwright e2e suite's runner configuration paired withapps/web-e2e/playwright.config.ts, the runtime companion toe2e-tsconfig.md. Where the tsconfig locks the suite's type-checking posture (which files the type-checker walks, which preset they extend), this page documents the suite's runtime boundary — which directory the runner discovers tests from, how many workers run them, which browsers they execute against, what theuse-defaults are, and how the host app is booted before the first test. Documents the at-a-glance summary table of every load-bearing field (thedotenv.config({ path: '../web/.env.local' })cross-app env loading that keeps a single source of truth between the suite and the booted host app, theBASE_URLoverride hatch with the'http://localhost:3000'default that lets CI / staging point the suite at deployed previews, theisCI = !!process.env.CIboolean gate, thetestDir: './tests'andoutputDir: './test-results'artefact boundaries,fullyParallel: truefor spec interleaving,workers: isCI ? 2 : 1for CI throughput vs local determinism,retries: isCI ? 2 : 0for CI flake auto-recovery, the per-environment reporter set with['html', { open: 'never' }] + ['github'] + ['list']on CI vs['html', { open: 'on-failure' }] + ['list']locally, the60_000per-test and30_000expect()timeouts, theglobalSetup/globalTeardownpaths, theuse-block defaults (baseURL,trace: isCI ? 'on-first-retry' : 'retain-on-failure',screenshot: 'only-on-failure',video: isCI ? 'on-first-retry' : 'off',navigationTimeout: 60_000,actionTimeout: 30_000,locale: 'en-US',timezoneId: 'America/New_York'), the three browser projects (Chromium, Firefox, WebKit), and thewebServerblock with the per-environmentcommand(pnpm --filter @ever-works/web build && starton CI,pnpm --filter @ever-works/web devlocally),cwd: '../..'monorepo-root anchor,reuseExistingServer: !isCI, the per-environment timeouts (300_000CI,120_000local), and thestdout: 'pipe'/stderr: 'pipe'self-diagnosing posture); the full file annotated line-by-line; the "Whydotenv.config({ path: '../web/.env.local' })" walkthrough that pins the single-source-of-truth posture against drift, the impossibility of two-file divergence, the contributor cost of the convention, and the trade-off (env overrides only-for-the-suite must be set in the shell); the "WhyBASE_URLis the only env-var override surface" rationale (locked posture for everything else, future contributors must add explicit fields rather than read fresh env vars inline); the per-CI-vs-local branch matrix that maps eachisCI ? X : Ybranch (workers,retries,reporter,use.trace,use.video,webServer.command,webServer.reuseExistingServer,webServer.timeout) to its trade-off (CI optimises for throughput / reliability, local optimises for fast iteration / clear failure signals); the "Why the three browser projects" cost / benefit matrix (Chromium catches the largest cross-section, Firefox catches Gecko-specific bugs, WebKit catches Safari-only divergence especially around Stripe Apple Pay / IDN URLs / cookie SameSite); the "WhywebServer.cwdis the monorepo root" rationale (pnpm --filterresolves the workspace alias from thepnpm-workspace.yamlanchor); the "Whystdout: 'pipe'andstderr: 'pipe'" self-diagnosing rationale (the default'ignore'swallows host-app compile errors and produces cryptic timeout failures); the failure matrix that maps eachplaywright.config.tsmistake (droppeddotenv.config(...)→ cryptic 500s in DB-touching specs, separateapps/web-e2e/.env.local→ drift between suite and booted server,BASE_URLfallback dropped → cannot target deployed previews,fullyParallel: false→ ~3× wall-clock on file-size-heavy specs,workers > 2on CI → resource contention flakes against the booted server,retries: 0on CI → un-mergeable flake amplification across a 50-spec suite,githubreporter dropped → no inline annotations on the GitHub Actions run page,htmlreporter dropped → unreproducible flakes because traces / screenshots / videos live in the HTML report,open: 'always'on CI → CI hangs trying to launch a browser on no-display,timeoutreduction → cold-render flakes on never-warmed routes,globalSetupdropped → unseeded specs failing erratically,use.trace: 'off'on CI → un-diagnosable CI-only flakes,use.locale: 'en-GB'→ date-format breakage onen-US-asserting specs,use.timezoneId: 'UTC'→ timestamp-render breakage, project drop → engine-specific regressions slip past CI until a real user files an issue, project add without matrix bump → wall-clock blow-up,next startwithoutbuildstep on CI → cold-checkout failure,webServer.cwd: __dirname→ERR_PNPM_NO_WORKSPACE_FOUND,reuseExistingServer: falselocally →EADDRINUSEon every invocation when anext devis already running,stdout: 'ignore'→ silent host-app errors and cryptic timeouts) onto the layer that surfaces each one; the per-line walkthrough table; and theplaywright.config.ts-change checklist that ties any field change to ae2e-tsconfig.mdcross-check, apnpm tsc --noEmitrun, a smoke-subset run (pnpm test:e2e:chromium -- --grep '@smoke'), the per-CI-vs-local both-modes verification (run withCIunset andCI=trueset), adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link, and a reviewer pass. - Workspace Root Manifest (
package.json) -- Per-source-file reference for the monorepo's workspace-coordination manifest paired withpackage.jsonat the repo root, the third root-level config reference afterpnpm-workspace.mdandturbo-config.md. Wherepnpm-workspace.mddocuments which folders become workspace members andturbo-config.mddocuments what tasks those members can run, in what order, with what inputs, this page documents the workspace-coordination posture itself — the top-level scripts every contributor and CI runner invokes (pnpm build,pnpm dev,pnpm dev:web,pnpm dev:docs,pnpm lint,pnpm test:e2e,pnpm clean,pnpm format,pnpm build:web,pnpm build:docs,pnpm build:docs:en), the runtime / package-manager floor every environment must meet (engines.node>=20.19.0,packageManagerexact pinpnpm@10.31.0enforced by Corepack), the version-pinning posture for transitive dependencies viapnpm.overrides(@types/react,@types/react-dom,esbuild,esbuild-register,@opentelemetry/api), the public-hoist rule for@opentelemetry/*that protects OTel's global registration model, the 11-entrypnpm.onlyBuiltDependenciesallow-list (@vercel/speed-insights,@heroui/shared-utils,@parcel/watcher,@scarf/scarf,@sentry/cli,@swc/core,core-js,core-js-pure,esbuild,protobufjs,sharp) that gates which packages can runpostinstallhooks duringpnpm install, and the workspace-wide Prettier formatting baseline (printWidth: 120,singleQuote,semi,useTabswithtabWidth: 4,arrowParens: 'always',trailingComma: 'none', plus the two language-specific overrides for*.scssand*.ymlthat switch to 2-space indents because YAML cannot use tab characters at the syntax level). Documents the at-a-glance summary table of every load-bearing field with its value and why-it-matters note; the file-contents walk-through (the full JSON file); the per-field walkthrough —nameas the workspace-root label (not a package identifier),version: '0.1.0'as symbolic-only becauseprivate: trueblocks publishing,private: trueas the hard-block on accidentalpnpm publish,license: AGPL-3.0as the inheritance root for every workspace member,packageManager: pnpm@10.31.0as the exact-pin Corepack reads to download the right pnpm,engines.node: '>=20.19.0'as the floor required by Next.js 16 /apps/web/scripts/check-env.js's ESM APIs /node:testparity, the elevenscripts.*entries with theirturbo run <task>delegations and the--filter=@ever-works/<name>shortcut rationale fordev:web/dev:docs/build:web/build:docs/build:docs:en, the twodevDependencies.turbo/prettierranges with their exact-version rationales (Turborepo 2.x cache-key semantics +$schemaenforcement +persistent: truehonouringdependsOn; Prettier 3.x'soverridesmatcher syntax), thepnpm.publicHoistPattern: ['@opentelemetry/*']rule and why it must coexist with the@opentelemetry/apioverride (two resolved copies break OTel's global-registration model), thepnpm.overridesfield with the per-entry rationale for each pin (React typings to lock the React 19 narrowedReactNode,esbuildto align Next.js / Drizzle Kit / Trigger.dev bundler output,esbuild-registerto keep TS syntax features parsing identically across the workspace,@opentelemetry/apifor OTel singleton enforcement), thepnpm.onlyBuiltDependenciesallow-list as pnpm 10's deny-by-defaultpostinstallhardening with a per-entry table of why each package needs to run a build step, theprettierblock as the single-source-of-truth for formatting rules (no.prettierrcat the repo root by intent), and the two language-specific overrides for SCSS conventions and YAML's tab-disallow syntax constraint; the deliberately-absent-fields matrix covering top-leveldependencies,workspaces(because pnpm readspnpm-workspace.yaml),main/exports/types/module,bin,type,repository/homepage/bugs,peerDependencies,engineStrict/os/cpuwith the reason each is omitted; the "Why this file lives at the repo root" rationale (pnpm, Corepack, Turborepo, Vercel, GitHub Actions, Renovate, and editors all walk upward and stop at the firstpackage.json); the consumer table mapping each reader (Corepack, pnpm, Turborepo CLI, Prettier CLI, Vercel build runner, GitHub Actions, editors, Renovate / Dependabot, contributors) to the fields it consumes; the failure matrix that maps each manifest-level mistake (ERR_PNPM_UNSUPPORTED_ENGINEfrom a Node-floor regression,Wrong package managerfrom a Corepack drift, OTel span loss from a hoist or override drop,pnpm installignored build scriptwarnings from a missing allow-list entry, React 19 typings clash from a missing override, YAML re-formatted with tabs from a missing override,--filtershortcut breakage from a renamed packagename,Couldn't find a turbo binaryfrom a droppeddevDependency, Vercel'spnpm: command not foundfrom disabled Corepack) onto the layer that surfaces each one; and the public-surface change checklist that ties any field change to a Spec 001 plan cross-check, a CI workflow Corepack-enable check, a.github/workflows/*.ymlpropagation check, anapps/*/vercel.jsonpropagation check, a workspace-widepnpm formatround-trip, adocs/log.mdentry, and the Constitution-Check note in the PR description for Article III (Public-Surface Stability) and Article IX (Test Coverage Bar). @ever-works/plugin-sdk-- Capability interfaces, slot ids, manifest types, and thedefineDirectoryPluginfactory.@ever-works/plugin-runtime--PluginRegistry, config loader, and the<SlotHost />React component.@ever-works/plugin-demo-- Reference plugin used by the test suite and as a teaching example.
Use Cases
This template project is perfect for:
- Tool directories (like ProductHunt for tools)
- Service marketplaces
- Resource catalogs
- Professional directories
- Product showcases
- Community platforms
Ever Works Platform
The Template can be used standalone or paired with the Ever Works Platform for AI-powered content generation. For Platform documentation, visit docs.ever.works. See Platform vs Template for a detailed comparison.
Need Help?
- Check our documentation for general information
- Join our Discord community for support
- Visit the demo site to see it in action
- Contact support for technical assistance