Documentation & Specs Change Log
A running log of meaningful changes to documentation, specs, and the project's living-document set (constitution, agent rules, plans). One line per change, newest at the top. Every line follows the form:
YYYY-MM-DD area: short summary
Where area is one of:
docs/<section>— a docs page.spec-NNN— a feature spec underdocs/spec/NNN-…/.constitution— an amendment to.specify/memory/constitution.md.agents—AGENTS.mdchange.claude—CLAUDE.mdchange.index—docs/index.mdchange.questions—docs/questions.mdchange.
This file lives in the docs site and acts as a hand-maintained companion to git history. Use this when reading what changed and why at a higher level than per-commit diffs.
2026-05-10 (cont — Spec 019 Pattern C wired up)
apps/web/proxy.tsImplements Pattern C in middleware: whenLOCALE_DETECTION_MODE=server-redirectenv var is set, the middleware parsesAccept-Language, picks the closest supported locale, and 307s on first visit (noNEXT_LOCALEcookie). The redirect sets the cookie so subsequent requests flow through the inline<head>cookie-redirect script inapp/layout.tsx.- This finishes Spec 019 — the previous commit shipped the YAML knob
- Pattern A banner + Pattern B URL-style env, but left Pattern C
as docs-only. Now all three patterns described in
docs/performance/locale-detection.mdare runnable.
- Pattern A banner + Pattern B URL-style env, but left Pattern C
as docs-only. Now all three patterns described in
2026-05-10 — Spec 019: CDN-cacheable public surface + pluggable locale detection
spec-019Drafted spec/plan/tasks for019-cdn-cacheable-i18n— fixesdemo.ever.worksshippingCache-Control: no-storeon every request despite ISR being configured. Root cause:next-intllocaleDetection: truemutates the response so Vercel's edge cache bypasses it.i18n/routing.tsDefault flips tolocaleDetection: false. New build-time env varLOCALE_URL_STYLE(as-needed|always) controls URL prefix style.apps/web/lib/utils/settings.tsNewgetLocaleDetection()readssettings.i18n.locale_detectionfrom the site YAML;client-banner(default) |none.apps/web/components/i18n/locale-suggestion-banner.tsx,locale-cookie-redirect.tsxNew userland Pattern A implementation: a dismissible banner suggests the visitor's browser locale after hydration, and a tiny inline<head>script redirects returning visitors before paint based onNEXT_LOCALEcookie. Keeps/fully edge-cacheable.apps/web/lib/content.tsfetchItemsnow throws when an item read fails with an IO error instead of silently dropping it from the listing. Closes the "few items rendered" silent-symptom bug.apps/web/proxy.tsRemoved[Client Guard Debug]console.logthat ran on every/client/*request.apps/web/next.config.tsoutput: 'standalone'is now gated onSTANDALONE_BUILD=trueenv var (Dockerfile sets it; Vercel doesn't). Lets Vercel use its native serverless packaging.docs/performance/cdn-cacheability.md,docs/performance/locale-detection.md,docs/performance/content-loading.mdNew operator-facing pages.apps/web-e2e/tests/public/home-perf.spec.ts,apps/web-e2e/tests/i18n/locale-detection-banner.spec.tsNew Playwright coverage.questionsAdded Q-019a (server-redirect in YAML?) and Q-019b (localized banner copy?), both with chosen defaults.
2026-05-09 (cont 2 — swap to feed library)
lib/seo/feeds.tsapps/web/package.jsonReplaced ~250 LOC of hand-rolled RSS/Atom/JSON Feed XML/JSON string templating with delegation to the npmfeedlibrary (~5M weekly DLs, emits all three formats from one populatedFeedinstance). Public signatures (generateRss,generateAtom,generateJsonFeed,buildFeedEntries,resolveFeedConfig,FeedEntry,FeedConfig) unchanged so the route handlers inapp/rss.xml/,app/atom.xml/,app/feed.json/keep working without edits.generateJsonFeedpost-processes the library's JSON Feed 1.0 output to bump theversionURL tohttps://jsonfeed.org/version/1.1and add the 1.1-onlylanguagefield. Same swap applied symmetrically to the Astro minimal template's@ever-works/plugin-rsspackage.
2026-05-09 (cont)
app/rss.xmlapp/atom.xmlapp/feed.jsonlib/seo/feeds.tsapp/[locale]/layout.tsxlib/seo/ai-crawlers.tsFeeds + AI-crawler list update (follow-up to the discoverability pass earlier the same day):- Added
lib/seo/feeds.ts— pure helpersbuildFeedEntries,generateRss,generateAtom,generateJsonFeedsharing a singleFeedConfig. Items sorted byupdated_atdesc, capped at 50. - New routes at
app/rss.xml/route.ts,app/atom.xml/route.ts,app/feed.json/route.ts(RSS 2.0, Atom 1.0, JSON Feed 1.1 respectively). Replaces the previously-advertised-but-unimplemented/atom.xmlreference inllms.txt. - Locale layout
generateMetadatanow emits<link rel="alternate" type="application/rss+xml" …>plus the Atom and JSON-Feed equivalents for autodiscovery on every page. lib/seo/ai-crawlers.tstrimmed and randomized: list is now exactly 18 bots (GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, Claude-User, Claude-SearchBot, anthropic-ai, PerplexityBot, Perplexity-User, Google-Extended, Applebot, Applebot-Extended, Bingbot, CCBot, Meta-ExternalAgent, Amazonbot, Bytespider, cohere-ai), rendered in randomized order so no single operator appears clustered or "first" in robots.txt. Removed speculative extras (Diffbot, MistralAI-User, YouBot, Timpibot, Meta-ExternalFetcher, DuckAssistBot, Claude-Web, cohere-training-data-crawler).app/robots.tsallow-list extended with/rss.xml,/atom.xml,/feed.json.app/llms.txt/route.tsadvertises/feed.jsonand/rss.xmlalongside the previously listed/atom.xmland/sitemap.xml.
- Added
2026-05-09
-
docs/logNoted category sidebar and overflow tag dropdown virtualization for large taxonomy catalogs. -
docs/logNoted production log-noise cleanup for missing optional CMS pages, stale item directories, oversized listing cache payloads, and anonymous engagement metric tenant resolution. -
docs/features/seoapp/robots.tsapp/llms.txtapp/llms-full.txtAI agent / LLM discoverability pass:- Added
lib/seo/ai-crawlers.ts— explicit allow-list for major AI bots (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, etc.) wired intoapp/robots.ts. Default policy isallow; override viaAI_CRAWLERSenv var (allow|disallow|none| comma-list). - Added
app/llms-full.txt/route.ts— long-form companion to/llms.txtthat concatenates every category, tag, item body, and comparison into a single Markdown document. - Added per-page Markdown mirrors at
<path>.mdfor items, categories, tags, collections, comparisons,/pages/<slug>, and the static info pages (about,help,pricing,privacy-policy,terms-of-service,cookies). HTML pages advertise the mirror via<link rel="alternate" type="text/markdown">. Renderers live inlib/seo/markdown-mirror.ts; URLs are wired vianext.config.tsrewrites onto internal_md/route.tsand_static-md/[slug]/route.tshandlers. - Added
<BreadcrumbJsonLd>server component atcomponents/seo/breadcrumb-json-ld.tsxand emitted Schema.orgBreadcrumbListJSON-LD on every public listing/detail/static page that previously lacked it. lib/seo/listing-metadata.tsnow takes ahasMarkdownMirrorflag that, when true, addsalternates.types['text/markdown']to the page's<head>.app/llms.txt/route.tsupdated to advertise/llms-full.txtand the per-page.mdmirror URL convention.
- Added
2026-05-05
-
docs/pluginsdocs/indexAdded the dedicated per-source-file landing pagedocs/plugins/featured-items-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/featured-items-query.spec.tspaired with theGETexport ofapps/web/app/api/featured-items/route.ts-- the first per-source-file GET smoke pinning a public (no-auth-gate) tenant-resolving listing endpoint combining aNumber.parseInt(searchParams.get('limit') ?? '6', 10)default-6parse path with explicit radix-10(load-bearing for any caller submitting a leading-0value that someparseIntimplementations would treat as octal), aMath.min(Math.max(rawLimit, 1), 50)two-sided silent clamp (the FIRST per-source-file GET smoke pinning a clamp that covers BOTH endpoints of a[1, 50]range -- 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 that usesMath.floorinstead ofNumber.parseInt-based truncation), aNumber.isFinite(rawLimit)non-finite fallback (NaN / empty /abccollapse to the default6), a strict-stringsearchParams.get('includeExpired') === 'true'boolean-from-string parse (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), anawait getTenantId()tenant- resolution null-short-circuit (the FIRST per- source-file GET smoke pinning a route whose null- tenant branch returns the SAME{ success: true, data: [], count: 0 }envelope as the success branch and thecheckDatabaseAvailability()short-circuit -- the route does NOT 401 or 403 on a null tenant), anisActive: true+tenantIdtwo-condition WHERE clause (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; theincludeExpiredparameter only affects whether the optionalor(isNull(featuredItems.featuredUntil), gte(featuredItems.featuredUntil, currentDate))expiration filter is appended), a multi-key composite ORDER BY (the FIRST per-source-file GET smoke pinning a Drizzle two-key composite orderingdesc(featuredItems.featuredOrder), desc(featuredItems.featuredAt)), a{ success, data, count }three-key envelope (the FIRST per-source-file GET smoke pinning a public- route success envelope that adds acount: numbercardinality key alongsidesuccess/data), and a try / catch empty-list fallback (NOT 500) (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 -- three distinct branches all collapse onto the same observable success envelope). 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. The new page documents theNumber.parseInt(value ?? '6', 10)default-6parse path with explicit radix-10, theMath.min(Math.max(rawLimit, 1), 50)two-sided silent clamp, theNumber.isFinite(rawLimit)non-finite fallback, the strict-string=== 'true'boolean-from-string parse, thegetTenantId()tenant-resolution null-short-circuit, theisActive: true+tenantIdtwo-condition WHERE clause, thedesc(featuredItems.featuredOrder), desc(featuredItems.featuredAt)multi-key composite ORDER BY, the{ success, data, count }three-key envelope, the try / catch empty-list fallback, 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), the cross-references to 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), the neighbouring auth-gated admin siblingadmin-featured-items-id-method-spec.md, the neighbouring admin listing siblingadmin-featured-items-create-body-spec.md, the neighbouring popularity-scores siblingitems-popularity-scores-query-spec.md, the neighbouring sponsor-ads siblingsponsor-ads-checkout-body-spec.md, and the change protocol (update this page in the same PR that touches the source spec, updatedocs/log.md, runpnpm tsc --noEmitinapps/web-e2e). 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 a tenant-resolution null-short-circuit envelope shape, a two-sided silent clamp, an explicit-radixNumber.parseIntparse path, a strict-string boolean parser, a multi-key composite ORDER BY, a three-key{ success, data, count }envelope, and a try / catch empty-list fallback that no prior per- source-file public-route GET smoke covers.docs/plugins/collections-exists-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/collections-exists-query.spec.tspaired 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 -- both still in flight on open PRs). With this entry the three-member existence-probe trio is now fully documented per-source-file. UNIQUE within the trio: this is the catch-and-500 member -- 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). Also UNIQUE within the trio in three other dimensions: (1) the route reads zero query parameters today (the_requestparameter is underscored to mark it deliberately unused -- vs?locale=on categories and?type=on surveys); (2) the route runs above the DB-repository backing store viacollectionRepository.findAlldirectly (NOT a Git-CMS reader like categories and NOT a service- layer wrapper like surveys); (3) the catch branch firesconsole.errorunconditionally on every environment (NOT only in development mode like categories and NOT silently like surveys). The hard-coded{ includeInactive: false }repository flag is also load-bearing: a future contributor who wires?includeInactive=trueinto the call would also need to flip the response envelope shape or add a separateinactiveCountfield; the new spec pins this with a per-flag invariance walk that no other existence-probe spec has. The new page documents the cross-route exists-probe matrix (this route vs/api/categories/existsvs/api/surveys/exists-- now with backing store + query-param + catch-status + catch-envelope + catch-logging columns), the at-a-glance scenario tree (~50-path bulk-loop walk + five hand-written invariants including the UNIQUE zero-query-input contract walk and the UNIQUE?includeInactive=invariance walk), the cross-references to the catch-and-200 Git-CMS-backed sibling, the catch-and-200 DB-service-backed sibling, the cross-cuttingfeature-existence.spec.tsno-arg- baseline sibling, the DB-backed admin sibling at/api/admin/collections, the collection-detail GET / PUT / DELETE sibling, the collection-create POST sibling, and the Spec 010 governance anchor. Matchingdocs/index.mdentry added at the surveys cluster (just above thesurveys-id-responses-method-specentry) of the per-source-file rollout list. The corresponding e2e spec file is unchanged -- this run lands the docs landing page that was missing.docs/plugins/health-database-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/health-database-query.spec.tspaired with theGETexport ofapps/web/app/api/health/database/route.ts-- the first per-source-file GET smoke pinning 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). The new page documents the hard- codedSELECT 1round-trip, the two-branch shared{ status, database, timestamp }envelope shape, the bare zero-argumentGET()handler signature, the[200, 500]two-valid- status contract, the status-invariance under URL changes contract (parameterised URL's status MUST equal baseline's AND parameterised body'sstatusfield MUST equal baseline's), the SQL- injection invariance contract (SQL-injection- shaped?schema=/?table=values do NOT reach the SQL layer becausesql`SELECT 1`is hard- coded with no parameter binding), the canonical health-envelope shape contract (statusis a string from['healthy', 'unhealthy'],databaseis a string from['connected', 'disconnected'],timestampis aDate.parse-able ISO-8601 string), the non-JSONformatinvariance (the route always respondsapplication/jsonregardless of anyAcceptheader or?format=text/?format=prometheusparameter), the at-a-glance scenario tree (one query-string bulk-loop walk covering ~50 permutations PLUS three hand-written tests -- canonical-envelope shape assertion, status- invariance test, SQL-injection invariance test -- all asserting the[200, 500]two-valid-status contract), the cross-references to 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](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](cron-sync-query-spec.md) (another zero-argument GET handler that mirrors this spec's bareGET()signature posture), and the change protocol (update this page in the same PR that touches the source spec, updatedocs/log.md, runpnpm tsc --noEmitinapps/web-e2e). 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, 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.docs/plugins/admin-clients-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/admin-clients-query.spec.tspaired with theGETexport ofapps/web/app/api/admin/clients/route.ts-- the first per-source-file admin-tree GET smoke pinning the bare-message single-step-collapse{ error: 'Unauthorized' }401 envelope posture (matches the siblingadmin/comments/admin/companies/admin/usersroutes; distinct from the canonical-longer-message family ofadmin/categories/admin/items/admin/items/import/admin/items/import/validateAND from the two-step-split-401-vs-403 family ofadmin/notifications/[id]/read/admin/notifications/mark-all-read/admin/users/check-email/admin/users/check-username/admin/clients/bulkAND from the auth-gate- divergence-finding posture of the un-gatedadmin/roles/admin/roles/activefamily). UNIQUE: every prior admin-tree query smoke pins one of three different gate postures; this is the FIRST per-source-file admin-tree GET smoke pinning the bare-message single-step-collapse envelope. The new page documents the single-stepsession?.user?.isAdmingate ahead of the sharedvalidatePaginationParams(searchParams)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 -- the unauth branch hits 401 BEFORE the helper runs), the six optional query-param reads, all AFTER the gate (?search=,?status=,?plan=,?accountType=,?provider=-- parsed via rawsearchParams.get('…') || undefinedcalls, NO inline enum coercion or Zod schema validation, distinct from theadmin/rolesroute's narrow inline ternary enum coercion), the legacygetClientProfiles({…})query helper (distinct from theadmin/categoriesroute'scategoryRepository.findAllPaginated(...)repository-pattern posture; the spec stays green if a future contributor refactors the route to aclientRepositoryabstraction), the three-key{ success, data: { clients }, meta }success envelope (thedatakey carries a singleclients: []sub-key, distinct from theadmin/usersroute's bare{ success, data: [...], pagination: {…} }shape), thePOSTbranch with environment-flag-gated CRM sync (out of scope for this GET-only spec but documented so future contributors who add aPOSTsmoke must defend against the synchronouscreateTwentyCrmSyncServiceFromEnv()upsert viaTWENTY_CRM_ENABLED=falseenvironment override), 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'), the cross- references to 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(the two per-source-file specs together pin both thePOSTbody surface and theGETquery surface on the SAME route file), 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 the change protocol (update this page in the same PR that touches the source spec, updatedocs/log.md, runpnpm tsc --noEmitinapps/web-e2e). 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. -
apps/docsapps/webAdded Vercel build-cost controls to both Vercel-deployed apps in this monorepo (directory-web-template-docsrooted atapps/docs/,directory-web-template-demorooted atapps/web/). Each app'svercel.jsonnow carries:- An
ignoreCommandallowlist that tells Vercel to skip the build for any branch other thanmain,master,develop, orstage. Vercel's Ignored Build Step semantics are inverted from intuition: exit1continues the build, exit0skips it. The script matches$VERCEL_GIT_COMMIT_REFagainst the four allowed branches and exits1only on a hit, otherwise0. Result: feature-branch / dependabot / EW-* / chore/* pushes no longer burn build minutes on either Vercel project. - An explicit
github.autoJobCancelation: truethat locks in Vercel's default cancel-older-build-on-newer- push behavior so it can't drift from a dashboard toggle. Pairs with a separate one-time API flip (out of repo scope, applied viaPATCH /v9/projects/{projectId}on Vercel's REST API) that sets each project'sresourceConfig.buildQueue.configurationtoWAIT_FOR_NAMESPACE_QUEUE-- one active build per branch -- so a flurry ofdeveloppushes collapses to "build only the latest commit". The combined effect on a rapid-firedeveloppush: at most one build is running at a time AND any in-progress build is canceled the instant a newer commit lands. The Web app's existingcrons[]schedule (/api/cron/syncdaily 03:00 UTC,/api/cron/subscription-remindersdaily 09:00 UTC,/api/cron/subscription-expirationdaily 00:00 UTC) is preserved verbatim. NOTE: the four allowlisted branchesmain/master/develop/stageare exhaustive for the docs and demo Vercel projects today; introducing a new long-lived branch (e.g.release/*) means extending theif [ … ] || [ … ]chain in BOTHapps/docs/vercel.jsonandapps/web/vercel.json. TheWAIT_FOR_NAMESPACE_QUEUEflip is a project-level Vercel setting that lives in Vercel's config store (NOT invercel.json), so it is documented here for traceability and re-applicability after a project re-create.
- An
-
docs/pluginsdocs/indexAdded the dedicated per-source-file landing pagedocs/plugins/client-items-coordinates-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/client-items-coordinates-query.spec.tspaired with theGETexport ofapps/web/app/api/client/items/coordinates/route.ts-- 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, 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 three additional contracts: 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. The new page documents the discriminated-union auth-gate contract, the nested-coordinates-keyed success envelope, thegetClientItemRepository(). getCoordinatesByUser(userId)singleton-factory repository-delegation, theserverErrorResponse ('Failed to fetch item coordinates')outer- catch, the nine-bypass-prevention assertion battery (extending the six-test battery ofclient-geo-stats-querywith single-item- lookup, content-negotiation, and Accept-header invariance contracts that no prior per-source- file GET smoke covers; ALSO covering the defensive?lat=NaN/?lat=Infinityspatial- filter values that no prior per-source-file GET smoke pins, AND the?zoom=…/?center=lat,lngmap-control bypass-prevention keys that no prior per-source-file GET smoke pins), 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, magic-auth keys, geographic-filter keys, spatial / map-control filter keys including defensiveNaN/Infinityvalues, item-status filter keys, pagination keys, projection keys, cache- busting keys, content-negotiation keys including geographic formats, i18n keys, sort- override keys, multi-tenancy keys, admin- override keys, empty values, repeated keys, special-character values, 500-character long values, bogus / typo'd query keys, all asserting< 500, plus NINE hand-written tests), the cross-references to the neighbouringrequireClientAuth()-gated GET siblings (client-geo-stats-query-spec.mdshares thegetClientItemRepository()singleton-factory with this route but diverges on which repository method it invokesgetGeoStatsByUservsgetCoordinatesByUser;client-items-stats-query-spec.mdALSO shares thegetClientItemRepository()singleton- factory but invokesgetStatsByUser;client-dashboard-stats-query-spec.mduses a different repository singleton entirely --getClientDashboardRepository()), the cross-references to the broaderrequireClientAuth()-gated client family, the cross-cuttingclient-protected.spec.ts, anddocs/plugins/client-geo-stats-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/client-geo-stats-query.spec.tspaired 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), 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. The new page documents the discriminated-union auth-gate contract, the spread-geo-stats success envelope, thegetClientItemRepository(). getGeoStatsByUser(userId)singleton-factory repository-delegation, theserverErrorResponse ('Failed to fetch geographic statistics')outer-catch, the six-bypass-prevention assertion battery (extending the five-test battery ofclient-dashboard-stats-querywith a geographic-filter bypass-prevention contract that no prior per-source-file GET smoke covers --?country=/?city=/?lat=/?lng=/?bbox=/?radius=invariance), the at-a- glance scenario tree (a single query-string bulk-loop walk covering ~95 permutations -- no- arg baseline, admin-impersonation keys, client- terminology variants, magic-auth keys, geographic-filter keys (?country=/?city=/?region=/?area=/?countryCode=), service-area filter keys (?serviceArea=/?service_area=/?coverage=), spatial- filter keys (?lat=/?lng=/?bbox=/?radius=), time-window keys, pagination keys (including?topN=per-bucket pagination), projection keys (including?fields=top_cities/?fields=top_countries,service_area_breakdownper-bucket projection), cache-busting keys, content-negotiation (including?format=geojson/?format=kmlgeographic format keys), i18n keys, filter keys, sort-override keys, multi- tenancy keys, 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), the cross-references to the neighbouringrequireClientAuth()-gated GET siblingclient-dashboard-stats-query-spec.md(pairs withclient-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 to and on which bypass-prevention assertions they pin) and the neighbouringrequireClientAuth()-gated GET siblingclient-items-stats-query-spec.md(shares thegetClientItemRepository()singleton-factory with this route, but diverges on which repository method it invokesgetStatsByUservsgetGeoStatsByUser), and the change protocol (update this page in the same PR that touches the source spec, updatedocs/log.md, runpnpm tsc --noEmitinapps/web-e2e). 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 nested-coordinates-keyed success envelope shape that no prior per-source-file GET smoke covers. NOTE: cross-references toclient-geo-stats-query-spec.mdandclient-dashboard-stats-query-spec.mdmay resolve as broken links until parallel PRs #723 and #724 land that add those per-source- file landing pages on develop.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 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 that no prior per-source-file GET smoke covers. NOTE: cross-references toclient-dashboard-stats-query-spec.mdmay resolve as broken links until the parallel PR #723 lands that adds the dashboard-stats per-source-file landing page on develop.docs/plugins/client-dashboard-stats-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/client-dashboard-stats-query.spec.tspaired 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. The new page documents the discriminated-union auth-gate contract, the spread-stats success envelope, thegetClient DashboardRepository()singleton-factory repository-delegation, theserverErrorResponse ('Failed to fetch dashboard statistics')outer- catch, the five-bypass-prevention assertion battery, the at-a-glance scenario tree (a single query-string bulk-loop walk covering ~60 permutations -- no-arg baseline, admin- impersonation keys, client-terminology variants, magic-auth keys, date-range filter keys, time- window keys, pagination keys, projection keys, cache-busting keys, content-negotiation, i18n keys, filter keys, sort-override keys, multi- tenancy keys, 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), the cross- references to the neighbouringrequireClientAuth()-gated GET siblingclient-items-stats-query-spec.md(pairs withclient-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), the neighbouringrequireClientAuth()-gated client family specs (client-items-method-spec.md,client-items-id- method-spec.md,client-items-import-method- spec.md,client-items-import-validate-method- spec.md,client-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 the Spec 010 (E2E Test Coverage) governance anchor. Matchingdocs/index.mdentry added at the agent-discovery cluster (just above theagent-discovery-specentry) of the per-source-file rollout list. The corresponding e2e spec file is unchanged -- this run lands the docs landing page that was missing.docs/plugins/auth-change-password-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/auth-change-password.spec.tspaired with thePOSTexport ofapps/web/app/api/auth/change-password/route.ts-- the bare-baseline companion to the already- documentedauth-change-password-body-spec.mdlanding page (paired with the rich-permutationauth-change-password-body.spec.ts). The body sibling 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; this sibling pins ONLY the bare two-test< 500no-server-error contract on the bare two-test smoke companion -- thePOST /api/auth/change-password without a session does not 5xxtest on a fully-shaped body and thePOST /api/auth/change-password with empty body does not 5xxtest on{}-- both assertingexpect(response.status()).toBeLessThan(500). 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 new page documents the body-sibling-vs-bare-baseline matrix (this spec vs the body sibling atauth-change-password-body -spec.md-- now with bulk-loop column + envelope-shape column + gate-ordering column + cross-method column + side-channel column), 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), the cross-references to the rich-permutation body sibling, the page-level forgot / reset password smokes (auth/forgot-password.spec.ts+auth/new-password.spec.ts), the Spec 003 (Auth Providers) governance anchor, and the Spec 010 (E2E Test Coverage) governance anchor. Matchingdocs/index.mdentry added at the agent- discovery cluster (just above theagent- discovery-specentry) of the per-source-file rollout list. The corresponding e2e spec file is unchanged -- this run lands the docs landing page that was missing.docs/plugins/surveys-exists-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/surveys-exists-query.spec.tspaired with theGETexport ofapps/web/app/api/surveys/exists/route.ts-- the third member of the public-existence-probe trio alongside the previously-documentedcategories-exists-query-spec.md(catch-and-200 Git-CMS sibling) and the still-undocumented DB-backedcollections-exists-query.spec.ts(catch-and-500 sibling). UNIQUE within the trio: this is the catch-and-no-count member -- same catch-and-200 posture as the categories-exists sibling but the response envelope is the leaner{ exists }shape with NOcountfield (since the route'slimit: 1short-circuit makes the count uninformative anyway). Distinct from every other public-existence probe the docs tree publishes: the route lives above a DB-backedsurveyService.getManycall that selects published surveys from the configured database (vs the categories-exists sibling's Git-CMSfetchItemsreader and the collections-exists sibling'scollectionRepository.findAllDB-repository reader), reads a?type=query param rather than?locale=(and uses a strict byte-for-bytetypeParam === SurveyTypeEnum.ITEMternary that maps every non-'item'value --nullfor the absent key,''for the empty value,'global'for the explicit value, every typo / unknown / case-variant -- to the same GLOBAL branch), and is silent in the catch branch on every environment (distinct from the categories-exists sibling which logs toconsole.errorin development mode and from the collections-exists sibling which logs unconditionally). The new page documents the cross-route exists-probe matrix (this route vs/api/categories/existsvs/api/collections/exists), the at-a-glance scenario tree (~50-path bulk-loop walk + five hand-written invariants including the 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, plus the branch-split shape-invariance walk pinning that the ITEM branch and the GLOBAL branch return the same envelope shape), the cross-references to the catch-and-200 Git-CMS-backed sibling, the catch-and-500 DB-backed sibling, the cross-cuttingfeature-existence.spec.tsno-arg-baseline sibling, the survey-detail GET / PUT / DELETE sibling, the per-survey-responses GET / POST sibling, the per-response-detail GET sibling, and the Spec 010 / Spec 005 governance anchors. Matchingdocs/index.mdentry added at the surveys cluster (just above thesurveys-id-responses-method-specentry) of the per-source-file rollout list. The corresponding e2e spec file is unchanged -- this run lands the docs landing page that was missing.docs/plugins/categories-exists-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/categories-exists-query.spec.tspaired 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). Distinct from every other public-route per-source-file GET smoke: the companioncollections-exists-query.spec.ts(sibling existence probe served fromapps/web/app/api/collections/exists/route.ts) has a catch-and-500 posture; theitems-popularity-scores-query-spec.mdsibling is also no-auth-gate but does NOT surface a navigation- shell-degradation contract. The categories-exists route is the catch-and-200 sibling of the collections-exists route — same{ exists, count }envelope, but the catch branch maps every thrown error to a200with{ exists: false, count: 0 }rather than a500. The distinction is load- bearing: the navigation shell hits both probes on every render and must degrade quietly when the content layer is unavailable rather than blocking the whole page. The new page documents the cross- route exists-probe matrix (this route vs/api/collections/existsvs/api/surveys/exists), the at-a-glance scenario tree (~50-path bulk-loop walk + four hand-written invariants including the 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), the cross-references to the catch-and- 500 DB-backed sibling, the surveys existence probe, the Git-CMS-backed admin sibling, the DB-backed admin sibling, the public-route per-source-file popularity-scores spec, and the Spec 010 / Spec 005 governance anchors. Matchingdocs/index.mdentry added at the top of the per-source-file rollout list (above theadmin-categories-all-query-specentry from the previous run). The corresponding e2e spec file is unchanged -- this run lands the docs landing page that was missing. -
docs/pluginsdocs/indexAdded the dedicated per-source-file landing pagedocs/plugins/admin-categories-all-query-spec.mdfor the existing pre-landed e2e specapps/web-e2e/tests/api/admin-categories-all-query.spec.tspaired with theGETexport ofapps/web/app/api/admin/categories/all/route.ts-- 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: sameauth()+!isAdminadmin 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). New page documents the cross-route Git-CMS-vs-DB matrix (this route vs/api/admin/tags/allvs/api/admin/categories/gitvs/api/admin/categoriesvs/api/admin/tags), the at-a-glance scenario tree (~50-path bulk-loop walk + 11 hand-written invariants including path-traversal-resistance and cache-bust-resistance invariants distinct from the tags-all sibling), the cross-references to the two Git-CMS siblings + two DB-backed siblings + GitHub-API-backed sibling + page-object driver + co-tenant page-object driver, and the Spec 010 / Spec 009 governance anchors. Matchingdocs/index.mdentry added at the top of the per-source-file rollout list (above theclient-items-id-restore-method-specentry from the previous run). The corresponding e2e spec file is unchanged -- this run lands the docs landing page that was missing. -
apps/web-e2edocs/pluginsdocs/indexAdded a per-source-file e2e specapps/web-e2e/tests/api/client-items-id-restore-method.spec.tsfor 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 delegating toclientItemRepository.restoreForUser(id, userId)with a THREE-branch nested catch dispatcher ('Item not found'exact -> 404,'permission'substring -> 403,'not deleted'substring -> 400) and a'Failed to restore item'outer-catch default. Companiondocs/plugins/client-items-id-restore-method-spec.mdreference plus the matchingdocs/index.mdentry added. The pre-existing minimal smokeclient-item-restore.spec.tsis preserved unchanged as the single-test canary. -
apps/webapps/web-e2edocs/pluginsFixed Web CI build failure where the new/items.jsonand/llms.txtagent-discovery routes treated thegetCachedItems()return value as a bare array instead of the actualFetchItemsResultenvelope ({ items, categories, tags, total, collections }). Both routes now read.itemsoff the result and fall back to an empty array on failure. Added the paired e2e smokeapps/web-e2e/tests/public/agent-discovery.spec.tsand per-source-file plugin referenceagent-discovery-spec.md— the first per-source-file public-route smoke the docs tree publishes that pins the agent-targeted discovery surface (/llms.txtper the llms.txt convention + paired canonical-data/items.jsonJSON dump). Distinct from the neighbouringseo-manifests.spec.ts(crawler-targeted SEO surface). -
docs/pluginsAddeditems-popularity-scores-query-spec.md— 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 the existingapps/web-e2e/tests/api/items-popularity-scores.spec.tssmoke covering 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 contracts: public (no-auth-gate) route (UNIQUE FIRST — every prioritems*GET smoke gates the handler);Math.min(parseInt(limit), 100)admit- clamp (UNIQUE FIRST — silent integer-clamp on a query parameter; the route NEVER 4xxs on a malformedlimit); logarithmic-scaling score formula (UNIQUE FIRST —Math.log10(value + 1) * weightengagement-scoring formula); featured boost (+10000) (UNIQUE FIRST — featured-item flat score boost as the load-bearing tie-breaker between featured and non-featured items); three- tier recency-decay schedule (UNIQUE FIRST — piecewise-linear recency-decay schedule); empty- items short-circuit envelope (UNIQUE FIRST — non- error early-return envelope on agetCachedItems( { lang })cache miss); stable rank-after-sort mutation (UNIQUE FIRST — sort-then-mutate-rankpattern); score-breakdown surface (UNIQUE FIRST —scoreBreakdownsub-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 smoke spec pins a single query- string bulk-loop walk (15 permutations: no-arg baseline, validlimit5/20, out-of-rangelimit999/10000 admit-clamped, empty /abc/ negative / zerolimitparseInt-default fallback, knownlocaleen/fr/zh, unknownlocaleempty-items short-circuit, combinedlimit + locale, combined out-of-range + locale clamp-then-locale order) asserting< 500. Cross-references the cross- cuttingdiscovery.spec.ts(also probes the no- arg baseline), the neighbouring engagement endpoint siblingitems-engagement-query-spec.md(when published), the neighbouring item-detail public specitem-public.spec.ts, and todocs/spec/010-e2e-test-coverage/for the governing spec. -
docs/pluginsAddedclient-items-import-sample-query-spec.md— 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 a newapps/web-e2e/tests/api/client-items-import-sample-query.spec.tsspec covering 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. Distinct contracts:requireClientAuth()+exportQuerySchemapair (UNIQUE FIRST — Zod-enumformatparse gated byrequireClientAuth(), distinct from priorclient-items*siblings which parse no query schema and from the admin siblingadmin-items-export-sample-querywhich uses bareauth()+isAdmin); binary-stream success contract (UNIQUE FIRST —Content-Type/Content-Disposition: attachment; filename="..."pair, distinct from JSON envelopes of every priorclient-items*GET smoke);safeErrorResponse(error, 'Failed to generate sample template')outer-catch helper that BYTE- IDENTICALLY matches the admin sibling 500-message (UNIQUE FIRSTrequireClientAuth()-gated GET smoke pinningsafeErrorResponsecross-utility helper, NOTserverErrorResponselike theclient-items-stats-querysibling);ItemExportServicedirect service-class instantiation (UNIQUE FIRST — distinct from repository-factory pattern ofclient-items-stats-querysibling and fromItemImportServiceofclient-items-import-method/client-items-import-validate-methodsiblings); longer-message TWO-key 401 envelope{ success: false, error: 'Unauthorized. Please sign in to continue.' };format=Zod- enum case-sensitivity (Zod enums match exactly, uppercase variants rejected on auth branch);format=enum default (.default('csv'), invariant on unauth branch). The smoke spec pins 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 (nodata/format/filenameleak), a gate-before-catch invariance walk, a gate-before-binary-stream-header invariance walk (CRITICAL —Content- Disposition: attachment; …NEVER appears on the unauth branch), a gate-before-binary-stream- content-type invariance walk (CRITICAL — unauth branch emitsapplication/json, NOTtext/csvor XLSX spreadsheetml), a gate-before-Zod-parse invariance walk pinning everyformat=value collapses to the no-arg baseline status, an impersonation / token / bypass / filename-traversal invariance walk, an Accept- header invariance walk, a side-channel walk (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), and a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every parameter combination. Cross-references the companionclient-items-import-method-spec(commit-mode JSON sibling), theclient-items-import-validate-method-spec(validate-mode multipart sibling), theclient-items-method-spec/client-items-id-method-spec/client-items-stats-query-specsiblings, theclient-protectedsibling, and the admin-tree counterpart atadmin/items/export/sample(covered separately byadmin-items-export-sample-query.spec.ts). -
docs/indexAdded an entry forclient-items-import-sample-query-spec.md. -
docs/logThis entry.
2026-05-04
-
docs/pluginsAddedsurveys-responses-id-query-spec.md— 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 a newapps/web-e2e/tests/api/surveys-responses-id-query.spec.tsspec covering 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. UNIQUE: pins a'Failed to fetch response'(singular) 500-catch helper that is distinct from the plural-collection sibling's'Failed to fetch responses'. Pairs with a new per-source-file docs reference atdocs/plugins/surveys-responses-id-query-spec.md. -
docs/pluginsAddedsurveys-id-responses-method-spec.md— 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 a newapps/web-e2e/tests/api/surveys-id-responses-method.spec.tsspec covering 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 (admin-gatedGET+ PUBLICPOSTwith a 404-survey-existence guard, distinct from the siblingsurveys/[surveyId]MIXED-auth gate). UNIQUE: pins abody.dataJSON-object guard (manualtypeof === 'object' && != null, NOT Zod), an IP / user-agent header capture (x-forwarded-for→x-real-ip→'unknown'), a survey-deriveditemIdcontract (handler IGNORES caller-supplieditemId), and a201 Createdsuccess status. Pairs with a new per-source-file docs reference atdocs/plugins/surveys-id-responses-method-spec.md. -
docs/pluginsAddedclient-items-import-validate-method-spec.md— 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 a newapps/web-e2e/tests/api/client-items-import-validate-method.spec.tsspec covering 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 parses JSON viaawait request.json(); this is the FIRST that pins arequireClientAuth()-gated handler that parsesmultipart/form-dataviaawait request.formData(). Distinct contracts:requireClientAuth()+ multipart/form-data pair (UNIQUE FIRST — JSON priors only); 5-step file / mapping validation chain after the gate (matches admin sibling chain);validateRows-not-executeImportservice call (UNIQUE FIRST — dry-run vs commit-mode); FOUR-key{ success, headers, suggestedMapping, validationResults, summary }success payload (UNIQUE FIRST vs priorresult-keyed two-key payload);safeErrorResponse(error, 'Failed to validate import file')outer-catch helper that BYTE-IDENTICALLY matches the admin sibling 500- message; hard-codedduplicateStrategy: 'skip'defaultStatus: 'pending'validation options (UNIQUE FIRST — client requests CANNOT override either via form data, distinct from admin sibling which DOES accept these as form fields); longer-message TWO-key 401 envelope{ success: false, error: 'Unauthorized. Please sign in to continue.' }. The smoke spec pins 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 (noheaders/suggestedMapping/validationResults/summaryleak), a gate- before-validation-chain invariant pinning the five 400-branch messages must NEVER appear, a gate-before-catch invariant, a validateRows- not-entered CRITICAL invariance walk (XSS markers in the multipart body are NEVER echoed AND the load-bearing service call NEVER executes), a success-branch-key non-disclosure walk, a malformed-multipart invariance walk, a file-extension invariance walk, a cross-method probe (GET / PUT / PATCH / DELETE), a side- channel walk on POST (Cookie / Authorization / X-User-Id), and a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every multipart permutation. Cross-references the companionclient-items-import-method-spec(commit-mode JSON sibling), theclient-items-method-spec/client-items-id-method-spec/client-items-stats-query-specsiblings, the admin-tree validate counterpart atadmin/items/import/validate, and the companion client-items-import-sample sibling.
-
docs/indexAdded an entry forclient-items-import-validate-method-spec.md. -
docs/logThis entry. -
docs/pluginsAddedclient-items-import-method-spec.md— 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 a newapps/web-e2e/tests/api/client-items-import-method.spec.tsspec covering 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. Distinct contracts:requireClientAuth()+ service- layer delegation pair (UNIQUE FIRST); nestedbody.rowsArray.isArrayguard (UNIQUE FIRST — manual guard, NOT ZodsafeParse);'Missing or invalid rows array.'Zod-free 400 message (UNIQUE FIRST);safeErrorResponse(error, 'Failed to execute import')outer-catch helper (UNIQUE FIRST — sourced from@/lib/utils/api-error, NOTclient-auth.serverErrorResponse);{ success, result }success payload with the service-derived{ total, created, updated, skipped, errors }result aggregate (UNIQUE FIRST —result-keyed vsitem/subscription/data/statspriors); longer-message TWO-key 401 envelope; hard- coded{ duplicateStrategy: 'skip', defaultStatus: 'pending', submittedBy: userId }import options that client requests CANNOT override. The smoke spec pins 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 (noresult/total/created/updated/skipped/errorsleak), a gate-before- post-auth invariance walk, an executeImport- not-entered invariance walk (CRITICAL — XSS markers in the rows array body are NEVER echoed AND the load-bearing service call NEVER executes), a gate-before-Array.isArray- guard invariance walk (every non-arrayrowsshape collapses to 401 NOT 400), a cross-method probe (GET / PUT / PATCH / DELETE), a side-channel walk on POST, and a cross-permutation status invariance walk pinning byte-identical 401 envelopes across every body permutation. Cross-references the companion client-items collection + per-id + stats siblings, the client-protected sibling, the admin-tree import counterpart, the companion client-items-import-validate sibling (validates rows pre-execute — covered separately), and the companion client-items-import-sample sibling (emits a sample CSV — covered separately). -
docs/pluginsAddedclient-items-id-method-spec.md— 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 a newapps/web-e2e/tests/api/client-items-id-method.spec.tsspec covering 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 contracts: 5-helper-import contract (UNIQUE);itemIdParamSchema.safeParseZod validation on a path param (UNIQUE — FIRST per-source-file triple-method smoke pinning Zod validation on a dynamic-segment parameter); GET success payload withengagement: { views, likes }nested sub-object (UNIQUE — FIRST per-source- file GET smoke pinning a nested engagement- metrics sub-object); PUT empty-update guard (Object.keys(updateData).length === 0→ 400'No fields to update'— UNIQUE FIRST); PUTstatusChangeddynamic success message (UNIQUE FIRST); PUT FOUR-branch nested catch dispatcher (UNIQUE FIRST —'Item not found'→ 404,'permission'→ 403,'deleted'→ 400, default → outer); DELETE THREE-branch nested catch dispatcher (UNIQUE FIRST); longer-message TWO- key 401 envelope; dedicatednotFoundResponse(message)404-helper +forbiddenResponse(message)403- helper (UNIQUE FIRST — vs rawNextResponse.json). The smoke spec pins four bulk-loop walks (~6 headers × 3 methods + ~7 PUT bodies all asserting< 500), longer-message TWO-key 401- envelope assertions on GET / PUT / DELETE, a cross-method 401-envelope-equality assertion across all three methods, a strict TWO-key envelope-shape assertion (noitem/engagementleak), a gate-before-post-auth invariant pinning EIGHT post-auth messages must not leak, an updateAsClient-not-entered invariance walk on PUT (CRITICAL — XSS markers never echoed), a softDeleteForUser-not-entered invariance walk on DELETE (CRITICAL — URL itemId marker never echoed), a gate-before- FOUR-branch-catch invariance walk on PUT, a gate-before-Zod-body-validation invariance walk on PUT, a cross-method probe (POST / PATCH), a side-channel walk on PUT, and a cross-id invariance walk pinning that the auth gate fires BEFORE any per-id branch (thenotFoundResponse/forbiddenResponsepaths are unreachable on unauth). With this addition the per-spec-file docs rollout extends to 109-of-N and thetests/api/per-spec-file sub-rollout extends to 107-of-many. -
docs/pluginsAddedclient-items-method-spec.md— 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 a newapps/web-e2e/tests/api/client-items-method.spec.tsspec covering 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 contracts:requireClientAuthhelper 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 — FIRST per-source-file smoke pinning a dedicated 400-builder helper); issues-joined Zod error message (UNIQUE); GET success payload with FLAT keys at top level (UNIQUE — FIRST per-source-file GET smoke pinning a flat-pagination success payload); POST returns 201 with review- workflow success message;?deleted=truequery branches to a different repo method (UNIQUE — FIRST per-source-file GET smoke pinning a query-driven repo-method dispatch contract); longer-message TWO-key 401 envelope. The smoke spec pins 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, a createAsClient-not-entered invariance walk on POST (CRITICAL), a gate-before-Zod-query- validation invariance walk on GET, a gate- before-Zod-body-validation invariance walk on POST, a cross-method probe (PUT / PATCH / DELETE), and a side-channel walk on POST. With this addition the per-spec-file docs rollout extends to 108-of-N and thetests/api/per-spec-file sub-rollout extends to 106-of-many. -
docs/pluginsAddedsponsor-ads-user-id-query-spec.md— 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 a newapps/web-e2e/tests/api/sponsor-ads-user-id-query.spec.tsspec covering 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 contracts: 404-mask user-scoped IDOR (UNIQUE); TWO-key 401 envelope; TWO-key 404 envelope used for BOTH not-found AND IDOR violations; TWO-key success payload; TWO-key 500 envelope. The smoke spec pins a ~6-header bulk-loop walk asserting< 500, a canonical TWO-key 401- envelope assertion, a strict TWO-key envelope- shape assertion, a gate-before-post-auth invariant, a side-channel walk, a cross-method probe (POST / PUT / PATCH / DELETE), a getSponsorAdById-not-entered invariance walk (CRITICAL — pinning that no sponsor-ad fields 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), and a catch-branch-not-entered invariance walk. With this addition the per- spec-file docs rollout extends to 107-of-N and thetests/api/per-spec-file sub-rollout extends to 105-of-many. -
docs/pluginsAddedsponsor-ads-user-stats-query-spec.md— 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 a newapps/web-e2e/tests/api/sponsor-ads-user-stats-query.spec.tsspec covering 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). 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 —overviewhas SEVEN status countstotal/pendingPayment/pending/active/rejected/expired/cancelled;byIntervalhas TWO interval countsweekly/monthly;revenuehas THREE rollupstotalRevenue/weeklyRevenue/monthlyRevenue); bareauth()session lookup (distinct fromrequireClientAuth()discriminated-union helper); TWO-key 401 envelope (same shape as parent/sponsor-ads/ userroute, distinct from bare ONE-key envelope); TWO-key success payload usingstatskey (NOTdata); service-call delegation (sponsorAdService.getSponsorAdStatsByUseris the ONLY post-auth load-bearing call); TWO-key 500 catch envelope'Failed to fetch sponsor ad stats'(distinct from parent route's'Failed to fetch sponsor ads'and'Failed to create sponsor ad'— NO 's' onstat); zero-arg GET signature. The smoke spec pins one 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, a sponsorAdService-not-entered CRITICAL invariance walk (NEITHER bucket names NOR inner keys leak), a side-channel walk, a cross-method probe (POST / PUT / PATCH / DELETE), a catch-branch isolation walk, and a cross-permutation status invariance walk. With this addition the per- spec-file docs rollout extends to 106-of-N and thetests/api/per-spec-file sub-rollout extends to 104-of-many. -
docs/pluginsAddedsponsor-ads-user-method-spec.md— 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 a newapps/web-e2e/tests/api/sponsor-ads-user-method.spec.tsspec covering 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 (UNIQUE — module-level constant); POST returns 201 status (UNIQUE among sponsor- ads POST smokes); POST 400 for invalid JSON with distinct message (FIRST per-source-file POST smoke pinning a try/catch aroundawait request.json()with distinct message); conditional already-exists 400 catch branch via'already have'message substring (UNIQUE — FIRST per-source-file POST smoke pinning a message-substring catch dispatcher with status override); 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 (UNIQUE); TWO-key 401 envelope on both methods. The smoke spec pins 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, a createSponsorAd-not- entered invariance walk on POST (CRITICAL), a gate-before-Zod-query-validation invariance walk on GET, a gate-before-body- parse-and-Zod-body-validation invariance walk on POST, a cross-method probe (PUT / PATCH / DELETE), and a side-channel walk on POST. With this addition the per-spec-file docs rollout extends to 105-of-N and thetests/api/per-spec-file sub-rollout extends to 103-of-many. -
docs/pluginsAddedstripe-subscriptions-method-spec.md— 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 a newapps/web-e2e/tests/api/stripe-subscriptions-method.spec.tsspec covering 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 (CONTRAST with the singular sibling which has NO IDOR — Q-010 finding — this plural sibling does it correctly). Distinct from EVERY prior method- method smoke: FOUR-method export (UNIQUE); GET conditional response shape based on?active=query (UNIQUE); POST returns 201 status (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 NOT body (UNIQUE — FIRST per- source-file DELETE smoke pinning a query- driven mutating DELETE); dynamic success message on DELETE; bare ONE-key 401 envelope consistent across ALL FOUR methods; three- field required-check on POST with comma- joined-field-list 400 message (UNIQUE — FIRST per-source-file POST smoke pinning a comma- joined-field-list 400 message). The smoke spec pins four header bulk-loop walks (~6 headers × 4 methods asserting< 500), canonical bare ONE-key 401-envelope assertions, a cross-method 401-envelope- equality assertion across all four methods, a strict ONE-key envelope-shape assertion, a gate-before-post-auth invariant, an updateSubscription-not-entered invariance walk on PUT (CRITICAL), a cancelSubscription-not-entered invariance walk on DELETE with query-string ID (CRITICAL), a cross-query invariance walk on GET, a cross-method probe (PATCH), a side- channel walk on POST, and a required-field- check-not-entered invariance walk on POST. With this addition the per-spec-file docs rollout extends to 104-of-N and thetests/api/per-spec-file sub-rollout extends to 102-of-many. -
docs/pluginsAddedstripe-subscription-method-spec.md— 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 a newapps/web-e2e/tests/api/stripe-subscription-method.spec.tsspec covering 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 (UNIQUE — FIRST per- source-file triple-method smoke pinning three DIFFERENT required-field shapes); POST 400'Failed to create customer'branch (UNIQUE — only POST has the!customerIdcheck); returns RAW Stripe subscription object verbatim on ALL THREE methods (UNIQUE — no wrapper envelope);metadata: { userId: session.user.id }OVERWRITE on PUT (UNIQUE — compounds the Q-010 finding by enabling ownership-record laundering); bare ONE-key 401 envelope consistent across all three methods. The smoke spec pins three header bulk-loop walks (~6 headers × 3 methods asserting< 500), canonical bare ONE-key 401-envelope assertions on POST/PUT/DELETE, a cross-method 401- envelope-equality assertion, a strict ONE-key envelope-shape assertion, a gate-before-post- auth invariant, 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), a cancelSubscription-not- entered invariance walk on DELETE (CRITICAL — same gate-before invariant), a cross-method probe (GET / PATCH), and a side-channel walk on POST. With this addition the per-spec-file docs rollout extends to 103-of-N and thetests/api/per-spec-file sub-rollout extends to 101-of-many; spawned a security task to add IDOR check to the PUT/DELETE handlers (the per-id sibling has the right pattern -- copy it). -
docs/pluginsAddedclient-items-stats-query-spec.md— 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/(a centennial milestone for thetests/api/sub-rollout). Pairs with a newapps/web-e2e/tests/api/client-items-stats-query.spec.tsspec covering 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 on failure/success, FIRST per-source-file GET smoke pinning a discriminated-union auth-helper return contract); longer-message 401 envelope'Unauthorized. Please sign in to continue.'(UNIQUE); TWO-key success payload{ success: true, stats: <statsObject> }(UNIQUE — usesstatskey NOTdata);serverErrorResponseouter-catch helper (UNIQUE distinct fromsafeErrorResponse); zero-arg GET signature. The smoke spec pins a ~6-header bulk-loop walk asserting< 500, a longer-message TWO-key 401-envelope assertion, a strict envelope-shape assertion, a gate-before-post- auth invariant, 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), and a cross-permutation status invariance walk. With this addition 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. -
docs/pluginsAddedpayment-account-id-query-spec.md— 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 a newapps/web-e2e/tests/api/payment-account-id-query.spec.tsspec covering 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); 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); 404 with bare envelope{ error: 'Payment account not found' }(UNIQUE); returns raw paymentAccount fields in success (matches POST/PUT siblings); DOES haveauth()gate (CONTRAST with the no- auth-gate POST/PUT siblings — security- asymmetry finding). The smoke spec pins 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, a side-channel walk, a cross-method probe (POST / PUT / PATCH / DELETE), a getUserPaymentAccountByProvider-not- entered invariance walk (CRITICAL — pinning that 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), and a cross-provider invariance walk. With this addition the per-spec-file docs rollout extends to 101-of-N and thetests/api/per- spec-file sub-rollout extends to 99-of-many; thepayment/accounttriplet (POST + PUT no- auth + GET-by-userId auth-gated) is now complete on per-source-file coverage. -
docs/pluginsAddedpayment-account-method-spec.md— 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 a newapps/web-e2e/tests/api/payment-account-method.spec.tsspec covering 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; NO ownership check;setupUserPaymentAccountruns UNCONDITIONALLY on both POST and PUT (PUT does NOT check thatidmatches an existing record -- it just callssetupUserPaymentAccountwith the body fields, effectively the same logic as POST plus anidgate); THREE-required-field cascade on POST and FOUR-required-field cascade on PUT (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; bare ONE-key 500 envelope; returns raw paymentAccount fields in success payload (UNIQUE: most success responses use{ success: true, data: {...} }). The smoke spec pins four bulk-loop walks (~6 headers × 2 methods + ~7 POST bodies + ~6 PUT bodies), a NO-401 contract assertion on BOTH POST and PUT, an auth-signal-ignored contract walk, a required-field cascade canonical-messages assertion (POST emits three distinct 400 messages, PUT emits a fourth'Account ID is required'), a strict ONE-key 400 envelope- shape assertion, a cross-method probe (GET / PATCH / DELETE), and a no-catch-on-valid-body contract. With this addition 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. -
docs/pluginsAddedpayment-id-method-spec.md— 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 a newapps/web-e2e/tests/api/payment-id-method.spec.tsspec covering 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)-- works with Stripe, LemonSqueezy, Polar, Solidgate). Distinct from EVERY prior dual- method smoke: provider-agnostic dual-method; provider-source split (GET readsproviderfrom QUERY STRING, PATCH readspaymentProviderfrom BODY — UNIQUE: FIRST per-source-file dual- method smoke pinning a SAME-NAMED-FIELD-from- DIFFERENT-SOURCES contract); dynamic enum- validation 400 message that lists the valid enum values; TWO distinct body-validation 400 messages on PATCH (FIRST per-source-file PATCH smoke pinning a two-tier body-validation chain); explicittypeof enabled !== 'boolean'type-check (UNIQUE pre-Zod boolean type- validation); user-scoped IDOR with explicit'Forbidden: You do not own this subscription'message; best-effort provider sync (UNIQUE — FIRST per-source-file PATCH smoke pinning a best-effort provider sync after a successful local DB write); dynamic success message based on theenabledtoggle. The smoke spec pins three bulk-loop walks (~6 headers × 2 methods- ~13 PATCH bodies), canonical bare ONE-key
401-envelope assertions on GET AND PATCH, a
cross-method 401-envelope-equality assertion,
a strict envelope-shape assertion, a gate-
before-post-auth invariant, a setAutoRenewal-
not-entered invariance walk (CRITICAL —
pinning that XSS markers in the body are
NEVER echoed back), a gate-before-enum-
validation invariance walk on GET, a cross-
method probe (POST / PUT / DELETE), a side-
channel walk, a gate-before-body-validation
invariance walk pinning that malformed JSON /
array body / non-bool enabled all produce 401
NOT 400, and a cross-subscription-ID
invariance walk. With this addition the per-
spec-file docs rollout extends to 99-of-N and
the
tests/api/per-spec-file sub-rollout extends to 97-of-many.
- ~13 PATCH bodies), canonical bare ONE-key
401-envelope assertions on GET AND PATCH, a
cross-method 401-envelope-equality assertion,
a strict envelope-shape assertion, a gate-
before-post-auth invariant, a setAutoRenewal-
not-entered invariance walk (CRITICAL —
pinning that XSS markers in the body are
NEVER echoed back), a gate-before-enum-
validation invariance walk on GET, a cross-
method probe (POST / PUT / DELETE), a side-
channel walk, a gate-before-body-validation
invariance walk pinning that malformed JSON /
array body / non-bool enabled all produce 401
NOT 400, and a cross-subscription-ID
invariance walk. With this addition the per-
spec-file docs rollout extends to 99-of-N and
the
-
docs/pluginsAddedverify-recaptcha-body-spec.md— 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 a newapps/web-e2e/tests/api/verify-recaptcha-body.spec.tsspec covering 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 smoke spec pins a ~12-header bulk-loop walk, a ~16-body bulk-loop walk, the load-bearing 400 token-required envelope assertion, a strict envelope-shape assertion (NOfeatureDisabledkey — DIFFERENT from extract-body sibling), a falsy-token uniformity assertion (empty-string / null / numeric-zero / boolean-false ALL hit the same envelope), 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 (dev-bypass / not-configured / Google-proxy), 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, and a gate-before- post-validation order assertion. With this addition the per-spec-file docs rollout extends to 98-of-N and thetests/api/per-spec-file sub-rollout extends to 96-of-many. -
docs/pluginsAddedcron-subscription-reminders-method-spec.md— 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 a newapps/web-e2e/tests/api/cron-subscription-reminders-method.spec.tsspec covering 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 (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 — 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 — UNIQUE, distinct from subscription-expiration which constructs an explicitdataenvelope); GET + POST dual-method-delegate exports (POST simply doesreturn GET(request)); outer catch viasafeErrorResponse(error, 'Cron job failed')(distinct message vs subscription-expiration's'Failed to process expired subscriptions'). The smoke spec pins two header bulk-loop walks (~9 headers × 2 methods asserting< 500), a BARE ONE-key 401 envelope assertion, a strict envelope-shape assertion, a no-Bearer-secret- echo invariant, a timing-safe length-mismatch handling assertion on the FULL header, a POST- delegates-to-GET assertion, 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 is leaked), a gate- before-post-auth invariant, and a no-207-on- unauth invariant. With this addition the per- spec-file docs rollout extends to 97-of-N and thetests/api/per-spec-file sub-rollout extends to 95-of-many; the cron triplet (sync + expiration + reminders) is now complete on per-source-file coverage. -
docs/pluginsAddedcron-subscription-expiration-method-spec.md— 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 a newapps/web-e2e/tests/api/cron-subscription-expiration-method.spec.tsspec covering 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(FIRST per-source-file smoke pinning a constant-time comparison contract); length-equality short-circuit (avoids thetimingSafeEquallength-mismatch throw);authHeader.replace('Bearer ', '')parsing (vs exact-match comparison like cron/ sync); 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 (does NOT fail the cron if email service unavailable); PII-strippedaffectedUsers(noemailfield — intentional PII protection). The smoke spec pins two header bulk-loop walks (~9 headers × 2 methods including various Authorization probes plus side-channels asserting< 500), a TWO-key 401 envelope assertion, a strict envelope-shape assertion, a no-Bearer-secret- echo invariant, a timing-safe length-mismatch handling assertion pinning that BOTH too-short AND too-long tokens 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 processExpiredSubscriptions-not-entered invariance walk (CRITICAL — the DB-write call NEVER runs on unauth and noaffectedUsers/processed/subscriptionIdis leaked), and a gate-before-post-auth invariant. With this addition the per-spec-file docs rollout extends to 96-of-N and thetests/api/per-spec-file sub-rollout extends to 94-of-many. -
docs/pluginsAddedsubscription-query-spec.md— 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 the existingapps/web-e2e/tests/api/subscription-query.spec.tsspec covering 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); no-customer-found 200 OBJECT{ hasActiveSubscription: false, message: 'No Stripe customer found' }(distinct from the user-payments sibling's[]empty-array fallback); bare ONE-key{ error: 'Unauthorized' }401 envelope; two-tier 500 catch dispatcher with TWO different 500 messages on the SAME ONE-key envelope shape ('Failed to fetch subscription data from Stripe'vs'Failed to fetch subscription data'); zero-arg GET signature; Stripe Subscriptions list withexpand: ['data.default_payment_method'](UNIQUE — the FIRST per-source-file GET smoke pinning a Stripe expansion-list invariant); active-subscription discriminator (sub.status === 'active' || sub.status === 'trialing'); cents-to-major- units transform; currency uppercase invariant; caller-supplied-Stripe-key bypass attempt walked via?stripeKey=/?stripe_key=query parameters. The smoke spec pins a query-string bulk-loop walk over many parameter permutations, a canonical ONE-key 401-envelope assertion, a cross-query envelope-byte-equality assertion, a?userId=/?customerId=walk (CRITICAL), a?stripeKey=walk (CRITICAL), a?token=walk (CRITICAL), and a cross-permutation status-and-shape invariance assertion. With this addition the per-spec-file docs rollout extends to 95-of-N and thetests/api/per-spec-file sub-rollout extends to 93-of-many. (Docs-only commit — the spec test fileapps/web-e2e/tests/api/subscription-query.spec.tswas already present in the repo; this commit publishes its per-source-file reference doc.) -
docs/pluginsAddedfavorites-id-method-spec.md— 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 a newapps/web-e2e/tests/api/favorites-id-method.spec.tsspec covering 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. Distinct from EVERY prior DELETE smoke:checkDatabaseAvailability()as the FIRST gate (auth check fires AFTER DB-availability); TWO- key 401 envelope + TWO-key 403 envelope (UNIQUE — 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 (UNIQUE — FIRST per-source-file DELETE smoke pinning a three-field IDOR viauserId === session.user.idANDitemSlug === path.itemSlugANDtenantId === currentTenantId); SELECT-then-DELETE pattern (the handler runs an inline SELECT pre-check BEFORE the DELETE to surface 404 if not found, distinct from single-step DELETE WHERE); TWO-key success payload{ success: true, message: 'Favorite removed successfully' }with NOdatafield (UNIQUE — most DELETE handlers returndata: { ... }with deletion details). The smoke spec pins a ~7-header bulk- loop walk includingX-Tenant-IdandX-User-Idside-channel probes, 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, 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 an XSS marker in the itemSlug URL is NEVER echoed back), a catch-branch- dispatcher-not-entered invariance walk, a cross- permutation status invariance walk, and a cross- itemSlug invariance walk pinning that the auth gate fires BEFORE any per-item-slug branch. With this addition the per-spec-file docs rollout extends to 94-of-N and thetests/api/per-spec-file sub-rollout extends to 92-of-many. -
docs/pluginsAddedsurveys-id-method-spec.md— 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 a newapps/web-e2e/tests/api/surveys-id-method.spec.tsspec covering 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 — FIRST per-source- file GET smoke pinning a 404-mask security pattern); ID-or-slug fallback lookup (UNIQUE — FIRST per-source-file dynamic-segment GET smoke pinning a dual-lookup-by-id-or-slug contract);error.message === 'Survey not found'catch- branch dispatch on PUT and DELETE (UNIQUE — FIRST per-source-file PUT/DELETE smoke pinning anError.messageequality-match catch- dispatcher); TWO-key{ success: false, error: 'Unauthorized' }401 envelope on PUT and DELETE;data: nullin DELETE success payload (UNUSUAL). The smoke spec pins 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, 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, and a catch-branch-dispatcher-not-entered invariance walk. With this addition the per- spec-file docs rollout extends to 93-of-N and thetests/api/per-spec-file sub-rollout extends to 91-of-many. -
docs/pluginsAddeduser-payments-query-spec.md— 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 a newapps/web-e2e/tests/api/user-payments-query.spec.tsspec covering 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.tscoversapps/web/app/api/user/subscription/route.tswith the 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); no-customer- found 200 EMPTY ARRAY[](distinct fromsubscriptionsibling); bare ONE-key{ error: 'Unauthorized' }401 envelope; two- tier 500 catch dispatcher with TWO different 500 messages on the SAME ONE-key envelope shape (UNIQUE —'Failed to fetch payment data from Stripe'vs'Failed to fetch payment data'); zero-arg GET signature; Stripe Invoices + Subscriptions DUAL-list load-bearing chain (FIRST per-source-file GET smoke pinning a dual-Stripe- list invariant); filtered status whitelist (paid || openonly). The smoke spec pins a ~7-header bulk-loop walk, a canonical ONE-key 401-envelope assertion, a strict ONE-key envelope- shape assertion, a no-array-leak CRITICAL invariant, a gate-before-post-auth invariant pinning that NEITHER 500 message appears on unauth, a side-channel walk, a cross-method probe (POST / PUT / PATCH / DELETE), a Stripe-SDK- calls-not-entered invariance walk (CRITICAL: 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, and a cross-permutation status invariance walk. With this addition the per-spec-file docs rollout extends to 92-of-N and thetests/api/per-spec- file sub-rollout extends to 90-of-many. -
docs/pluginsAddedcron-sync-query-spec.md— 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 a newapps/web-e2e/tests/api/cron-sync-query.spec.tsspec covering 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 FIRST per-source-file GET smoke pinning a Bearer-token- only auth contract — the handler accepts ONLYAuthorization: Bearer ${CRON_SECRET}and NOT session-based auth); 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; usesmessagenoterrorfor 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 smoke spec pins a ~9-header bulk-loop walk (including various Authorization probes — wrong Bearer, empty Bearer, non-Bearer scheme, Basic auth — plus side-channels), a 4-key 401 envelope shape assertion viaObject.keys(body).sort(), ISO timestamp + numeric duration assertions, a no-Bearer-secret-echo invariant pinning that the caller-supplied secret marker is NEVER echoed back, 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, and a triggerManualSync-not-entered invariance walk pinning that the load-bearing sync call NEVER runs on unauth and nodetailsfrom a sync result is leaked. With this addition the per-spec-file docs rollout extends to 91-of-N and thetests/api/per-spec-file sub-rollout extends to 89-of-many. -
docs/pluginsAddedstripe-subscription-portal-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-subscription-portal-body.spec.tsspec covering 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); 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 (UNIQUE — the FIRST per- source-file smoke pinning anew URL()validation contract on a constructed return URL, with TWO-key 500 envelope{ error: 'Invalid return URL configuration', message: 'The application URL is not properly configured' }); FOUR-key Stripe-error catch envelope (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, message: 'Billing portal session created' }, outer catch 500 TWO-key. 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-envelope assertion; a strict ONE-key envelope-shape assertion; a no-portal- url-leak CRITICAL security invariant; a gate- before-post-auth invariant pinning that NONE of six candidate messages must appear; 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; a URL-validation- catch-not-entered invariance walk; an inner- stripe-error-catch-FOUR-key-envelope-not- entered invariance walk — CRITICAL; an outer- catch-not-entered invariance walk; a no-stripe- error-message-leak invariant; a body-shape invariance walk; a cross-permutation status- invariance assertion; a no-XSS / open-redirect leak invariant). Cross-references the polar- subscription-portal-body sibling (different provider's portal pattern), the stripe-checkout POST root sibling (TWO-key Unauthorized envelope vs ONE-key here), the stripe-setup- intent-id GET sibling (different{ success: false, error }TWO-key shape), the stripe-payment-methods-create POST sibling (different envelope shape on Stripe-error catch), and to Spec 010. -
docs/pluginsAddeditem-comments-rating-id-update-method-spec.md— 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 a newapps/web-e2e/tests/api/item-comments-rating-id-update-method.spec.tsspec covering thePATCHexport ofapps/web/app/api/items/[slug]/comments/rating/[commentId]/route.ts— the first per-source-file PATCH smoke documenting a Q-010-style NO-AUTH-GATE finding for a non-admin mutating route (NOauth()call, NO ownership check, NO rating validation; ANY caller can update ANY comment's rating to ANY value). Pins this finding as the CURRENT contract -- a future PR that adds auth would explicitly break the spec. Distinct from EVERY prior mutating smoke: NO auth gate; NO ownership check; NO rating validation; production-leftover console.log debug arrow ('============rating=============>'); returns raw comment row verbatim (no wrapper envelope);checkDatabaseAvailability()as the SOLE gate. The smoke spec pins a NO-401 contract assertion, an auth-signal-ignored contract walk pinning that fabricated auth headers produce SAME status as bare requests, a no-validation contract walk pinning that invalid rating values produce SAME status as valid values, a cross-method probe, a no-catch-on-valid-body assertion, and a no- wrapper-envelope assertion pinning the UNUSUAL raw-comment-row response shape. Also spawned a separate task to audit the Q-010 finding and add auth + ownership + Zod validation, and remove the production-leftover console.log. -
docs/pluginsAddedstripe-payment-methods-id-method-spec.md— 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 a newapps/web-e2e/tests/api/stripe-payment-methods-id-method.spec.tsspec covering theGETANDDELETEexports ofapps/web/app/api/stripe/payment-methods/[id]/route.ts— the first per-source-file GET + DELETE dual-method smoke for any Stripe per-id primitive route. 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 — 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;!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: only known per-source-file smoke pinning a one-word article-shiftanyvsabetween two methods on the same handler); DELETE default- reassignment cascade — FIRST per-source-file DELETE smoke pinning a default-reassignment cascade (if deleted method was the customer's default and there are other methods, re-assign default to first remaining; if none, set default to undefined); THREE-branch StripeError catch on BOTH methods with distinct 500 messages per method ('Failed to retrieve payment method'for GET,'Failed to delete payment method'for DELETE). The smoke spec pins TWO header bulk-loops (~7 headers × 2 methods); canonical 401-envelope assertions on GET AND DELETE; cross-method envelope-equality assertion pinning byte-identical 401 envelopes; strict envelope-shape assertions on both methods; gate-before-post-auth invariant across nine candidate messages; gate-before-success-build invariant on GET (CRITICAL — no leak ofcard/billing_details/is_default/customer_id); gate-before-success-build invariant on DELETE (CRITICAL — no leak ofwas_default); side-channel walk; cross- method probe (POST / PUT / PATCH); a paymentMethods-retrieve-and-customers-retrieve- and-IDOR-and-detach-and-default-reassignment- not-entered invariance walk (CRITICAL); catch- branch-dispatcher-not-entered invariance walk; no-stripe-error-message-leak invariant; cross- id invariance walk on BOTH methods; no-XSS-id- substring leak invariant; and a default- reassignment-cascade-not-entered invariance walk on DELETE (CRITICAL — pins that the customer-default mutation cascade NEVER runs on unauth). -
docs/pluginsAddedstripe-setup-intent-id-query-spec.md— 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 a newapps/web-e2e/tests/api/stripe-setup-intent-id-query.spec.tsspec covering theGETexport ofapps/web/app/api/stripe/setup-intent/[id]/route.ts— the first per-source-file GET smoke for a Stripe per-id primitive route AND the first per-source-file GET smoke pinning aerror.code === 'resource_missing'substring detection in the catch (UNIQUE Stripe enum-typed code-based dispatcher). Distinct from setup- intent POST root: GET method;success: falseerror: 'Unauthorized'2-key envelope (vs root's bare 1-key); customer-metadata IDOR check; filtered SetupIntent fields in success (vs root's raw provider object); Stripe-error. codesubstring detection. The smoke spec pins a canonical 401-envelope assertion, a strict envelope-shape assertion, a no-client_secret- leak CRITICAL security invariant, a gate-before- post-auth invariant, a side-channel walk, a cross-method probe, a setupIntents-retrieve- and-customers-retrieve-and-IDOR-check-not- entered invariance walk (CRITICAL), a catch- branch-dispatcher-not-entered invariance walk, a no-stripe-error-message-leak invariant, and a cross-id-invariance walk pinning identical 401 envelopes across different setup-intent IDs.
-
docs/pluginsAddedstripe-payment-methods-update-method-spec.md— 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 a newapps/web-e2e/tests/api/stripe-payment-methods-update-method.spec.tsspec covering thePUT+PATCHexports ofapps/web/app/api/stripe/payment-methods/update/route.ts— the first per-source-file PUT + PATCH smoke for a non-admin payment-method route. Sibling to the delete DELETE route. Distinct: TWO mutation methods exported on same path (PUT full update + PATCH set-default-only; FIRST per- source-file mutating smoke pinning PUT + PATCH dual-method export); shared helper-function- extraction design with delete sibling; PUT preserves existing metadata via spread (FIRST PUT smoke pinning metadata-merge contract);userIdalways present in metadata (caller cannot override). The smoke spec pins canonical 401-envelope assertions on PUT AND PATCH, cross- method envelope-equality assertion pinning byte- identical 401 envelopes, gate-before-post-auth invariant, no-metadata.userId-leak invariant, cross-method probe, ownership-check-helper-and- paymentMethods-update-and-customers-update-not- entered invariance walk (CRITICAL), and no-payment_method_id-leak invariant — completing the Stripe payment-methods CRUD trio (create POST + update PUT/PATCH + delete DELETE). -
docs/pluginsAddedstripe-payment-methods-delete-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-payment-methods-delete-body.spec.tsspec covering theDELETEexport ofapps/web/app/api/stripe/payment-methods/delete/route.ts— the first per-source-file DELETE smoke for a non-admin payment-method route (mutation method is DELETE, NOT POST) AND the first per- source-file mutating smoke pinning a multi- helper-function-extraction handler design (5 helpers: validateSession, validatePaymentMethodOwnership, handleDefaultPaymentMethodReassignment, checkAffectedSubscriptions, handleApiError) AND the first per-source-file mutating smoke pinning a customer-metadata-driven IDOR check (customer.metadata?.userId === userId→ 403 if mismatch). Distinct from create sibling: DELETE method; ONE-key 401 envelope'Authentication required'; helper-function-extraction; customer- metadata IDOR; Stripe-error-echo with'Stripe error: 'prefix; default-payment-method reassignment side-effect; affected-subscriptions count. The smoke spec pins canonical 401-envelope, strict envelope-shape, success-branch non- disclosure, gate-before-post-auth, no-Stripe- error-prefix invariant, status stability, side- channel walk, cross-method probe, Zod-throw-catch- not-entered invariance walk, ownership-check- helper-and-detach-and-reassignment-and-sub-count- not-entered invariance walk (CRITICAL -- paymentMethods.detach must NEVER run on unauth), and no-paymentMethodId-leak invariant pinning XSS-shaped paymentMethodId NEVER echoed — pinning three FIRST contracts no prior mutating smoke covers. -
docs/pluginsAddedstripe-subscription-id-update-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-subscription-id-update-body.spec.tsspec covering 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). Distinct from EVERY prior POST smoke: USER-scoped IDOR check (userSubscription.userId !== session.user.id→ merged 404'Subscription not found or access denied'; FIRST per-source-file POST smoke pinning a USER-scoped IDOR on a Stripe subscription endpoint, sitting at the user- scoped end of the IDOR spectrum that began with stripe/cancel (no IDOR) and stripe/reactivate (tenant-only)); THREE-state allow-list pre-check 400 (subscription.status !== 'active' && subscription.status !== 'pending' && subscription.status !== 'paused'→ 400'Subscription is not active'; FIRST per- source-file POST smoke pinning a THREE-state allow-list pre-check 400, distinct from the reactivate sibling's SINGLE-flag pre-check oncancelAtPeriodEnd); PaymentPlan-enum-from-@/lib/constantsincludes-validation (Object.values(PaymentPlan).includes(newPlanId)→ 400'Invalid plan ID'; FIRST per-source-file POST smoke pinning an enum-from-constants membership-check validation, distinct from the LemonSqueezy update-plan sibling which uses ZodsafeParse); conditional tenant-filter on a Drizzle UPDATE WHERE clause (...(tenantId ? [eq(subscriptions.tenantId, tenantId)] : []); FIRST per-source-file POST smoke pinning a conditional tenant filter spread into a DB UPDATE); plan-changed email payload with BOTH old + new plan names (oldPlanName: subscription.planId, newPlanName: newPlanId; FIRST per-source-file POST smoke pinning an email with both old + new plan names); dynamic success message (Plan updated to ${newPlanId} successfully; template literal withnewPlanIdinterpolation, distinct from reactivate sibling's static message). The smoke spec pins 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 (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, 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, and 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 — completing the Stripe subscription- management POST trio and pinning FOUR FIRST contracts no prior POST smoke covers. -
docs/pluginsAddedlemonsqueezy-update-body-spec.md— 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 a newapps/web-e2e/tests/api/lemonsqueezy-update-body.spec.tsspec covering 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 withrequestId+timestamp; (2) per-request UUID viacrypto.randomUUID?.()with browser-fallback; (3) performance tracking viaDate.now() - startTime; (4) development-mode short-circuit; (5) custom response headers (Cache-Control/X-Request-ID/X-Response-Time); (6) five- tier catch dispatcher (VALIDATION_ERROR→ 400,UNAUTHORIZED→ 401,SUBSCRIPTION_NOT_FOUND→ 404,PROVIDER_UNAVAILABLE→ 503, default → 500). Distinct from cancel + reactivate + update-plan siblings:!session?.usergate (NOT email-gated);code: 'UNAUTHORIZED'(NOT'AUTH_REQUIRED'); 5-key 401 envelope; dev-mode short-circuit. The smoke spec pins a FIVE-key 401-envelope assertion, a strict envelope-shape assertion, a per-request-UUID-uniqueness assertion, a request-id-forgery-prevention assertion (caller-suppliedX-Request-IDis NEVER echoed), 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, a validation-chain-not- entered invariance walk, a dev-mode-short- circuit-and-provider-call-and-5-tier-catch-not- entered invariance walk, and a no-custom-header invariant on the unauth branch — pinning the richest envelope contract no prior smoke covers. -
docs/pluginsAddedlemonsqueezy-update-plan-body-spec.md— 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 a newapps/web-e2e/tests/api/lemonsqueezy-update-plan-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/lemonsqueezy/update-plan/route.ts— the third sibling in the LemonSqueezy subscription-management trio (cancel + reactivate + update-plan). Distinct from siblings: multi-field Zod schema with defaults (FIRST per-source-file POST smoke pinning a multi-field-with-defaults Zod schema);z.coerce.number().positive()(FIRST Zod coerce-number contract);z.enumwith default (FIRST Zod enum-with-default contract);z.number().min(1).max(31)for billingAnchor range constraint; plan-update-specific metadata (7 fields including session.user.email as updatedBy). The smoke spec pins THREE-key 401 envelope, strict envelope-shape, success-branch non-disclosure, gate-before-post-auth, no- validation-codes invariant, no-updatedBy-leak, status stability, side-channel walk, cross- method probe, malformed-JSON invariance, multi- field-validation-chain-not-entered invariance (5 shapes), and updateSubscription-call-not- entered invariance — completing the LemonSqueezy subscription-management trio and pinning four FIRST Zod-schema-pattern contracts. -
docs/pluginsAddedstripe-subscription-id-reactivate-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-subscription-id-reactivate-body.spec.tsspec covering 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 then 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). Distinct from EVERY prior POST smoke: tenant-scoped DB- IDOR check (partial-IDOR finding); state-machine pre-check 400 minted from a DB-row column read; no body parsing (matches polar/reactivate sibling); multi-step write (updateSubscriptionupdateSubscriptionBySubscriptionIdDB sync + async email side-effect with try/catch fault tolerance); generic 500 catch (single static string, NO substring detection); static success message; rawreactivatedSubscriptionprovider object indatafield. The smoke spec pins a bare 401-envelope assertion, a strict envelope- shape assertion, a success-branch-key non- disclosure assertion, a gate-before-post-auth invariant across four candidate messages, a parameterised-vs-baseline status-stability comparison (six permutations + tenant-scope probe), a side-channel walk includingX-Tenant-Id, a cross-method probe, a no-body- parse contract walk on malformed JSON, a tenant- scoped-DB-IDOR-check-not-entered invariance walk, a state-machine-400-pre-check-not-entered invariance walk, a multi-step-write-chain-not- entered invariance walk, a catch-branch-generic- 500-not-echoed invariance walk, a body-completely- ignored invariance walk, and a cross-subscription- ID invariance walk pinning that distinct sub IDs produce IDENTICAL unauth responses (proving the tenant-scoped DB read is NOT entered upstream of the auth gate) — pinning two FIRST contracts no prior smoke covers and completing the stripe/subscription/[id]/* POST pair (cancel + reactivate).
-
docs/pluginsAddedstripe-subscription-id-cancel-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-subscription-id-cancel-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/stripe/subscription/[subscriptionId]/cancel/route.ts— the first per-source-file POST smoke documenting a Q-010-style IDOR finding for a Stripe subscription endpoint (the handler authenticates viaauth()but does NOT verify ownership ofsubscriptionId; compare to the polar/subscription/[id]/cancel sibling which DOES enforce ownership) AND the first per-source- file POST smoke pinning 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 polar/subscription/ [id]/cancel sibling: NO IDOR-protection; NO Content-Length 413 pre-check; DB sync side- effect; email-send with fault-tolerance; NO try/catch aroundrequest.json(). The smoke spec pins 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, 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, and a NO-IDOR-protection contract walk pinning that the unauth 401 envelope is IDENTICAL across different subscription IDs -- pinning the CURRENT contract until a future PR adds ownership verification (which would explicitly break this spec, prompting an update). Also spawned a separate task to audit the Stripe IDOR finding and fix by mirroring the Polar pattern (getCustomerId→ ownership verify → cancel). -
docs/pluginsAddedlemonsqueezy-reactivate-body-spec.md— 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 a newapps/web-e2e/tests/api/lemonsqueezy-reactivate-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/lemonsqueezy/reactivate/route.ts— the complement to the lemonsqueezy-cancel- body-spec sibling: both routes share the same email-gated auth contract, THREE-key 401 envelope withcode: 'AUTH_REQUIRED', ZodsafeParsevalidation, andtimestampfield in success envelope. The reactivate route differs in: (a) Reactivation-specific metadata -- the handler writessession.user.emailto provider-side metadata asreactivatedBy; FIRST per-source-file POST smoke pinning a session.user.email-in-metadata contract; (b)safeErrorResponse(...)direct in catch (single line); (c) static success message (no conditional branch). The smoke spec pins 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, 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, and an updateSubscription-call-with-metadata- write-not-entered invariance walk — pinning the user's email being written to provider-side metadata as a contract. -
docs/pluginsAddedpolar-subscription-id-reactivate-body-spec.md— 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 a newapps/web-e2e/tests/api/polar-subscription-id-reactivate-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/polar/subscription/[subscriptionId]/reactivate/route.ts— the first per-source-file POST smoke pinning 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 pinning 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:'not scheduled for cancellation'→ 400'Subscription is not scheduled for cancellation'). Distinct from EVERY prior POST smoke: no body parsing; no Content-Length 413 pre-check (distinct from polar/cancel sibling); THREE-string catch dispatcher (404 / 401 / 400); 400-from-catch contract; static success message (NOT conditional based on a body flag); same IDOR-protection chain as polar/cancel sibling (getCustomerId→ 403, private property extraction → 500,getPolarSubscriptionownership check → merged 404). The smoke spec pins a bare 401-envelope assertion, a strict envelope-shape assertion, a success-branch-key non-disclosure assertion, a gate-before-post- auth invariant across seven candidate messages, a parameterised-vs-baseline status-stability comparison, a side-channel walk, a cross-method probe, a no-body-parse contract walk on malformed JSON, an IDOR-protection-chain-not- entered invariance walk, 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, and a body-completely-ignored invariance walk (the strongest no-body-parse contract in the rollout) — pinning two FIRST contracts no prior smoke covers. -
docs/pluginsAddedpolar-subscription-id-cancel-body-spec.md— 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 a newapps/web-e2e/tests/api/polar-subscription-id-cancel-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/polar/subscription/[subscriptionId]/cancel/route.ts— the first per-source-file POST smoke pinning a Content-Length 413 pre-check (the handler readsrequest.headers.get('content- length')BEFORE the body parse and returns 413 if declared length > 1KB; FIRST 413 contract in the rollout) AND the first per-source-file POST smoke pinning an IDOR-protection chain (aftergetCustomerId→ 403, retrieves subscription and explicitly checks ownership; merged 404 message'Subscription not found or access denied'for both not-found AND ownership- mismatch). Distinct from EVERY prior POST smoke: Content-Length 413 pre-check; IDOR-protection chain; private property access via(polarProvider as any).polar; helper-function injection (getPolarSubscriptiontakesformatErrorMessageANDlogger); TWO-string error-message-detection catch dispatching to 404 / 401 / 500; conditional success message; body- parse fault tolerance with size-error detection. The smoke spec pins a bare 401-envelope assertion, a strict envelope-shape assertion, a success-branch-key non-disclosure assertion, a gate-before-post-auth invariant, 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, a malformed-JSON-body invariance walk, an IDOR-protection-chain-not-entered invariance walk, a cancelSubscription-call-not-entered invariance walk, and a catch-branch-error- message-detection-not-entered invariance walk — pinning two FIRST contracts no prior smoke covers. -
docs/pluginsAddedlemonsqueezy-cancel-body-spec.md— 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 a newapps/web-e2e/tests/api/lemonsqueezy-cancel-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/lemonsqueezy/cancel/route.ts— the first per-source-file POST smoke pinning an email-gated auth contract (!session?.user?.email-- FIRST per-source-file POST smoke gating on session email) AND the first per-source-file POST smoke pinning acodefield in the 401 envelope (THREE-key envelope{ error, message, code: 'AUTH_REQUIRED' }; FIRST per-source-file POST smoke pinning enum-typed code in 401) AND the first per-source-file POST smoke pinning atimestampfield in success AND catch envelopes. 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(FIRST per- source-file POST smoke pinning a 4-key catch envelope); conditional success message based oncancelAtPeriodEndflag;timestampfield in success AND catch envelopes;safeErrorMessageextracted into catch envelope'smessagefield (NOT intoerrorfield). The smoke spec pins 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-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, and a catch- branch-four-key-envelope-not-echoed invariance walk — pinning three FIRST contracts no prior smoke covers. -
docs/pluginsAddedstripe-payment-methods-create-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-payment-methods-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/stripe/payment-methods/create/route.ts— the first per-source-file POST smoke pinning a Zodparse(NOTsafeParse) contract (createPaymentMethodSchema.parse(body)THROWS on validation failure and the outer catch detectserror instanceof z.ZodError; EVERY prior POST smoke usessafeParse; FIRST throw-on- invalid Zod contract) AND the first per- source-file POST smoke pinning a Stripe- error-echo contract (error instanceof Stripe. errors.StripeError→ 400 with raw stripe error message echoed; 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 (six SDK calls -- the most complex orchestration in any per-source-file POST smoke); formatted response payload (extracts subset of fields with card sub-object, NOT raw provider object). The smoke spec pins a canonical 401-envelope assertion, a strict envelope-shape assertion, a success-branch-key non-disclosure assertion, a gate-before-post-auth invariant, a Zod-throw- catch-not-entered invariance walk, a stripe- error-echo-catch-not-entered invariance walk, 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, and a catch-branch-generic-500-not-echoed invariance walk — pinning two FIRST contracts no prior smoke covers and the most complex Stripe SDK orchestration in the rollout. -
docs/pluginsAddedstripe-payment-intent-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-payment-intent-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/stripe/payment-intent/route.ts— the first per-source-file POST smoke pinning a NO-body-validation contract (the handler destructures{ amount, currency = 'usd', metadata, planId }and passes them straight tostripeProvider.createPaymentIntent(...)with NO validation; EVERY prior POST smoke has at least one body-validation gate -- FIRST trust-the-body POST contract in the rollout) AND the second per-source-file POST smoke pinning a raw payment-provider object as the success payload (after stripe-setup-intent-body-spec). Distinct from setup-intent: body destructure withcurrency = 'usd'default; caller-controlledmetadata: { userId, planId, ...metadata }spread (caller'smetadata.userIdOVERRIDES session userId because spread is AFTER); GET sibling with?payment_intent_id=query-param- required check. Distinct from every prior POST smoke: NO-body-validation; bare 401 envelope; raw PaymentIntent object payload; caller- controlled metadata spread. The smoke spec pins 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-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('attacker_user_id') 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 createPaymentIntent-and- getCustomerId-not-entered invariance walk, and a catch-branch-not-entered invariance walk — pinning the trust-the-body contract no prior smoke covers and the CRITICALclient_secret- leak security invariant as applied to PaymentIntent. -
docs/pluginsAddedsponsor-ads-user-id-renew-body-spec.md— 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 a newapps/web-e2e/tests/api/sponsor-ads-user-id-renew-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/sponsor-ads/user/[id]/renew/route.ts— the first per-source-file POST smoke pinning 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 — 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. -
docs/pluginsAddedstripe-setup-intent-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-setup-intent-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/stripe/setup-intent/route.ts— the first per-source-file POST smoke pinning a zero-argumentPOST()handler signature (norequestparameter at all; FIRST zero-arg POST contract in the rollout) AND the first per-source-file POST smoke pinning a raw payment-provider object as the success payload (returns the Stripe SetupIntent object verbatim, NO wrapper envelope -- making the no-client_secret-leak assertion a CRITICAL security invariant). Distinct from EVERY prior POST smoke: zero-argumentPOST()signature; bare 401 envelope{ error: 'Unauthorized' }(UNIQUE);!session?.usergate (matches stripe- checkout); raw provider-object success payload (no wrapper envelope); single-line catch (the simplest catch in any per-source-file POST smoke); only one load-bearing call. The smoke spec pins a bare 401-envelope assertion, a strict envelope-shape assertion (exactlyerrorkey, no other keys), 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), a body-IGNORED invariance walk pinning that the zero-arg handler ignores body content, a side-channel walk, a cross- method probe, a createSetupIntent-and-provider- not-entered invariance walk, and a catch- branch-not-entered invariance walk — pinning the simplest auth-gated POST contract in the rollout and a CRITICAL no-client_secret-leak security invariant. -
docs/pluginsAddedsponsor-ads-user-id-cancel-body-spec.md— 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 a newapps/web-e2e/tests/api/sponsor-ads-user-id-cancel-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/sponsor-ads/user/[id]/cancel/route.ts— the first per-source-file POST smoke pinning a body-parse-fault-tolerant contract (await request.json().catch(() => ({})) ?? {}-- malformed JSON ORnullOR empty body silently coalesces to{}; FIRST silent-coalesce contract in the rollout) AND a conditional Zod validation contract (.omit({ id: true }) .safeParse(body)withif (!parsed.success && body.cancelReason !== undefined)gate). Distinct from EVERY prior POST smoke: silent-coalesce body-parse contract; conditional Zod validation with.omit; default-fallback string forcancelReason('Cancelled by user'); three- branch outer catch with mixed exact-string + substring detection (error.message === 'Sponsor ad not found'→ 404,error.message.includes ('Cannot cancel')→ 400, default → 500). The smoke spec pins 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, a silent- coalesce-body-parse-without-400 invariance walk, a parameterised-vs-baseline status-stability comparison, a side-channel walk, a cross-method probe, a conditional-Zod-validation-not-entered invariance walk, an ownership-and-cancelSponsorAd- not-entered invariance walk, a three-branch-outer- catch-not-entered invariance walk, and a no- cancelReason-leak assertion — pinning four FIRST contracts no prior smoke covers. -
docs/pluginsAddedauth-change-password-body-spec.md— 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 a newapps/web-e2e/tests/api/auth-change-password-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/auth/change-password/route.ts— the first per-source-file POST smoke that pins a rate-limit-FIRST gate posture -- the rate-limit check fires BEFORE the auth gate. 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. Distinct from EVERY prior POST smoke: rate-limit-FIRST gate posture (returns 429 withretryAfterfield; FIRST per-source-file POST smoke pinning aretryAfterfield);'Unauthorized. Please sign in.'401 message (UNIQUE imperative- phrased); OAuth-account check (FIRST per- source-file POST smoke pinning OAuth-account- restriction); dual bcrypt.compare gates (current- password verification AND duplicate-password prevention; FIRST per-source-file POST smoke pinning a dual bcrypt.compare contract); cross- field Zod.refinevalidation (FIRST per- source-file POST smoke pinning a cross-field validation contract); email-send fault tolerance (sendPasswordChangeConfirmationEmail wrapped in try/catch, does NOT fail the password change). 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. The smoke spec pins an imperative-phrased 401-envelope assertion, a strict envelope-shape assertion, a success- branch-key non-disclosure assertion, a gate- before-post-auth invariant (8-message non- disclosure set), a 429-envelope-includes- retryAfter assertion, a parameterised-vs- baseline status-stability comparison, a side- channel walk, a cross-method probe, a malformed- JSON-body invariance walk, a Zod-validation- chain-not-entered invariance walk, a bcrypt- compare-gates-not-entered invariance walk, and an OAuth-account-check-and-db-update-and-email- send-not-entered invariance walk — the first rate-limit-FIRST POST smoke the docs tree publishes. -
docs/pluginsAddeditem-comments-id-method-spec.md— 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 a newapps/web-e2e/tests/api/item-comments-id-method.spec.tsspec covering 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 })-- plain-text body, NOT JSON; FIRST plain-text 401 contract in the rollout). Distinct from the comment-create POST sibling: plain-text 401 envelope (NOT JSON); plain-text 404 / 403 envelopes for client-profile / tenant errors; MIXED-envelope contract (auth / profile / tenant errors return plain-text, body- validation errors PUT-only return JSON; FIRST per-source-file smoke pinning a mixed plain-text- JSON envelope contract on the same handler);
three-step ownership chain via Drizzle query with
embedded
userId+tenantId+deletedAt IS NULLfilters; DELETE returns 204 No Content; PUT body validation pins a partial-update validation contract (FIRST per-source-file PUT smoke). The smoke spec pins a doubled header walk (~10 × PUT/DELETE) + PUT body walk (~12 bodies); a canonical plain-text 401 envelope assertion on both methods; a no-JSON-prefix invariant for unauth bodies; a gate-before-post-auth invariant with mixed plain + JSON message-set non- disclosure; 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; a Drizzle-ownership- query-not-entered invariance walk for both methods; and an updateComment-and-deleteComment- not-entered invariance walk pinning that DELETE must NEVER return 204 and PUT must NEVER return a comment payload — the first per-source-file PUT + DELETE smoke pinning a plain-text 401 envelope, expanding the rollout's mutating- method coverage beyond the JSON-envelope family for the first time.
- JSON envelope contract on the same handler);
three-step ownership chain via Drizzle query with
embedded
-
docs/pluginsAddedsponsor-ads-checkout-body-spec.md— 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 a newapps/web-e2e/tests/api/sponsor-ads-checkout-body.spec.tsspec covering 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, 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: multi-provider switch dispatch (FIRST per-source-file POST smoke pinning a three-way provider dispatch via env var);success: falseenvelope on every error branch (distinct from the quartet's two-key{ error, message }envelopes); open-redirect validation viavalidateRedirectUrl(successUrl)+validateRedirect Url(cancelUrl)(FIRST per-source-file POST smoke pinning an open-redirect-prevention contract); three-stage post-auth gate stack 404 → 403 → 400 (UNIQUE forbidden branch -- no other checkout has one); 2×3getPriceId(interval, provider)matrix lookup (FIRST per-source-file POST smoke pinning a price-matrix lookup);!session?.user?.idgate (matches lemonsqueezy; distinct from polar + solidgate + stripe's!session?.user); generic 500 on outer catch with no detail leak (distinct from stripe's three-key envelope and solidgate'ssafeErrorMessageextraction); POST-only export (distinct from the quartet which all exportGET+POST). The smoke spec pins a canonical success- false 401-envelope assertion{ success: false, error: 'Unauthorized' }, a strict envelope-shape assertion (exactlysuccess+errorkeys), a success-branch-key non-disclosure assertion, a gate- before-post-auth invariant across SEVEN candidate static messages, a parameterised-vs-baseline status- stability comparison, a side-channel walk, a cross- method probe (GETjoinsPUT/PATCH/DELETEbecause the route is POST-only), 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 (nodata.checkoutUrlon the unauth branch), a catch-branch-not-entered invariance walk, a no-redirect-leak assertion pinning XSS-shapedsuccessUrl/cancelUrlvalues must NEVER be echoed (open-redirect prevention contract), and a provider-name non-disclosure assertion pinning thatdata.providerand the literal strings'stripe'/'lemonsqueezy'/'polar'must NEVER appear in the unauth response. -
docs/pluginsAddedstripe-checkout-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-checkout-body.spec.tsspec covering 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, completing the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe). Distinct from ALL three siblings: three-way mode ternary mapping ('one_time' → 'payment','subscription' → 'subscription', unknown →'setup'-- UNIQUE setup fallback); trial-amount validation (FIRST per-source-file POST smoke pinning trial-config validation); helper-function pipeline (buildCheckoutLineItemscreateBaseCheckoutParams+applySubscriptionConfigfrom co-located./helpers-- FIRST per-source-file POST smoke pinning a multi-helper assembly pipeline);safeErrorMessage(NOTsafeErrorResponse) in catch, returns THREE keys (error,message,details: <dev-only-stack>); Stripe SDK direct call via publicgetStripeInstance()method (FIRST per-source-file POST smoke pinning a direct-SDK-instance contract via a public method, NOT private propertyas anylike polar's one_time branch);!session?.usergate (matches polar + solidgate; distinct from lemonsqueezy's!session?.user?.id). The smoke spec pins 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, a parameterised-vs- baseline status-stability comparison, a side- channel walk, a cross-method probe, a malformed- JSON-body invariance walk, a trial-config- validation-not-entered invariance walk, a mode- ternary-not-entered invariance walk, 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, and a no- redirect-leak assertion pinning that XSS-shapedsuccessUrl/cancelUrlvalues must NEVER be echoed — completing the auth-gated checkout quartet (Solidgate + Polar + LemonSqueezy + Stripe).
-
docs/pluginsAddedlemonsqueezy-checkout-body-spec.md— 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 a newapps/web-e2e/tests/api/lemonsqueezy-checkout-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/lemonsqueezy/checkout/route.ts— the third per-source-file POST smoke for an auth-gated payment-provider checkout endpoint (after solidgate-checkout-body and polar-checkout- body). Distinct from BOTH siblings:!session?.user?.idgate (NOT!session?.user); custom validator returning{ isValid, errors[] }viavalidateCheckoutRequestBody(body)(NOT Zod like solidgate; NOT simpleif (!field)like polar); per-call try/catch aroundrequest.json()like solidgate; dev-only PII- sanitizedconsole.log(FIRST per-source-file POST smoke pinning this contract); FOUR-string- scan catch with THREE different status codes (400 / 500 / 503);ERROR_TYPESenum-typed error field (VALIDATION_ERROR,CONFIGURATION_ERROR,PAYMENT_SERVICE_ERROR,INTERNAL_ERROR); GET export with NO auth gate (Q-010-style finding; cross-method probe pins this divergence from POST);success: truediscriminant in success payload (distinct from polar + solidgate's literalstatus: 200). The smoke spec pins 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, an allowed-pre-delivery-error static- string allow-list assertion (5-code set), a parameterised-vs-baseline status-stability comparison, a side-channel walk, a cross-method probe, a JSON-parse-failure-AFTER-auth-gate invariance walk, a validation-chain-not-entered invariance walk, a createCustomCheckout-not- entered invariance walk, and a four-string-scan- catch-not-entered invariance walk pinning that NONE of the three enum-typed error codes from the catch may appear on the unauth branch — the third per-source-file POST smoke for an auth- gated payment-provider checkout endpoint, expanding payment-provider checkout coverage from two providers to three (Solidgate + Polar + LemonSqueezy). -
docs/pluginsAddedpolar-checkout-body-spec.md— 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 a newapps/web-e2e/tests/api/polar-checkout-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/polar/checkout/route.ts— the second per-source-file POST smoke for an auth-gated payment-provider checkout endpoint (after solidgate-checkout-body). Distinct from solidgate-checkout: branching mode dispatch (subscription / one_time -- FIRST per-source-file POST smoke pinning a mode-dispatched two-branch POST contract); NO Zod validation (uses simpleif (!productId)check); NO try/catch aroundrequest.json()(malformed JSON cascades to outer catch); 503 error-message detection (outer catch scanserror.messagefor three payment- setup-incomplete strings, downgrades 500 → 503 with custom message -- FIRST per-source-file POST smoke pinning a 503-via-error-message-scan contract); private property access viaas any(one_timebranch reaches into(polarProvider as any).polar-- FIRST per- source-file POST smoke pinning a private-property- bypass contract); GET export companion (with ONE-key 401 envelope, distinct from POST's TWO- key). The smoke spec pins 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, 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, 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 echodata.urlordata.id, a 503-payment-setup-incomplete-not-triggered-on- unauth invariance walk, and a no-redirect-leak assertion pinning that XSS-shapedsuccessUrl/cancelUrlvalues must NEVER be echoed — the second per-source-file POST smoke for an auth-gated payment-provider checkout endpoint, expanding payment-provider checkout coverage from one provider to two (Solidgate + Polar). -
docs/pluginsAddedstripe-webhook-body-spec.md— 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 a newapps/web-e2e/tests/api/stripe-webhook-body.spec.tsspec covering 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 (Polar + LemonSqueezy + Solidgate + Stripe). The simplest of the four handlers: single-header signature check viastripe-signature(unique header name); NO JSON parse (matches lemonsqueezy); NOvalidateWebhookPayloadcheck (distinct from polar); NO idempotency check (distinct from solidgate); NO event-type-string-fallback in the switch dispatcher (matches ONLY theWebhookEventTypeenum values, including the UNIQUEBILLING_PORTAL_SESSION_UPDATEDStripe- specific case); POST-only export; same 400-default catch. The smoke spec pins a first-gate signature-header-presence-rejection assertion, a strict envelope-shape assertion, 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 (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, a signature- verification-call-gated-by-header-check invariant, and 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 — the fourth and final per-source-file webhook POST smoke the docs tree publishes, completing the four-provider webhook quartet. -
docs/pluginsAddedsolidgate-checkout-body-spec.md— 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 a newapps/web-e2e/tests/api/solidgate-checkout-body.spec.tsspec covering 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 Stripe / LemonSqueezy / Polar / Solidgate checkout endpoints with a single< 500assertion each; this spec drills into the Solidgate handler specifically. Distinct from the closest analogue (polar-subscription-portal-body -spec.md) 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; (b) ZodsafeParseAFTER the auth gate -- thecheckoutSchema.safeParse(json)and the surroundingtry/catcharoundrequest.json()fire only AFTERauth(); (c) FIVE-key success envelope -- success returns{ data: { id, url }, status, message }with a literalstatus: 200field embedded in the body, separate from the HTTP status -- UNIQUE; (d) 500 catch (NOT 400) -- outer catch returns 500 with{ error, message, details }(dev-only stack); polar-webhook usessafeErrorResponse(..., 400)-- UNIQUE; (e) POST -only export -- GET / PUT / PATCH / DELETE are NOT exported; method-resolution returns 405. The spec pins a canonical two-key 401-envelope assertion, a strict envelope-shape invariance walk pinningObject.keys(body).sort() === ['error', 'message'], a no-Zod-issue-leak invariance walk pinning that schema details (amount,successUrl,cancelUrl,mode,'Invalid request body','Invalid JSON') must NEVER appear in the unauth response, a no-success-key- leak invariance walk pinning thatdata/id/url/ literalstatus: 200must NEVER appear, a no-redirect-leak assertion pinning that caller- suppliedsuccessUrl/cancelUrlvalues must NEVER be echoed, a malformed-JSON-pre-gate-non- downgrade assertion, a catch-branch-non-entry walk pinning that the unauth branch must NEVER reach the 500 outer catch, a 1-message static-string allow-list assertion (only'Unauthorized'is reachable), a side-channel walk, a cross-method probe (GET / PUT / PATCH / DELETE), and a 401- status-invariance walk pinning that every documented bypass-key shape rounds-trips to the same 401 as the empty-body baseline — the first per-source-file POST smoke for an auth-gated payment-provider checkout endpoint the docs tree publishes, expanding the rollout's payment- provider checkout coverage from a single< 500smoke to a deep body-surface walk on the Solidgate handler. -
docs/pluginsAddedsolidgate-webhook-body-spec.md— 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 a newapps/web-e2e/tests/api/solidgate-webhook-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/solidgate/webhook/route.ts— the third per-source-file webhook POST smoke (after polar + lemonsqueezy). Distinct from BOTH: two-header signature fallback (x-signature || solidgate-signature-- UNIQUE to Solidgate); manual JSON parse like polar but NOvalidateWebhookPayloadcheck; in-memory idempotency Set with 24-hour TTL (FIRST webhook smoke pinning an idempotency contract; duplicates return 200{ received: true }-- FIRST webhook smoke with TWO 200-success branches); switch dispatcher accepting BOTH enum AND string values for 9 event types; GET export with informative message (UNIQUE: polar and lemonsqueezy export only POST); same 400-default catch. The smoke spec pins a first-gate two-header-fallback rejection assertion, a fallback-header-acceptance assertion, a strict envelope-shape assertion, 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, a side-channel walk, a GET-200-with-informative-message assertion, a cross-method probe, a signature-verification- call-gated-by-header-check invariant, and a switch-statement-dispatcher-gated-by-signature- verification invariant — the third per- source-file webhook POST smoke the docs tree publishes, expanding payment-provider webhook coverage from two providers to three (Polar + LemonSqueezy + Solidgate). -
docs/pluginsAddeditem-comments-rating-query-spec.md— 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 a newapps/web-e2e/tests/api/item-comments-rating-query.spec.tsspec covering 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-bodyPOST). 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. 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. The spec emits a ~57-path bulk-loop walk asserting< 500plus eleven hand-written scenarios: a canonical-envelope 200 zero-rating assertion, a strict envelope-shape assertion (nosuccess/data/errorkeys), a Number-cast invariant pinning thataverageRatingandtotalRatingsare bothnumber(NOT raw Drizzleavg(...)strings), 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), and a graceful-degrade catch-branch invariance walk pinning that no error path surfaces a 5xx. -
docs/pluginsAddedlemonsqueezy-webhook-body-spec.md— 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 a newapps/web-e2e/tests/api/lemonsqueezy-webhook-body.spec.tsspec covering 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: different signature header (x-signature-- lowercase, single field, vs polar'swebhook-signature+webhook-timestampwebhook-id); NO manual JSON parse (the handler reads the raw body viaawait request.text()and passes it as a STRING tolemonSqueezyProvider.handleWebhook(body, signature)); simpler 2-tier rejection chain (only'No signature provided'and'Webhook not processed'-- vs polar's 4-tier chain); switch-statement event dispatcher (8 mapped handlers + defaultconsole.log); same 400-default catch as polar but via rawNextResponse.jsoncall (NOTsafeErrorResponse(...)). The smoke spec pins a first-gate signature-header-presence- rejection assertion{ error: 'No signature provided' }, a strict envelope-shape assertion 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 polar-shape-headers-ignored assertion pinning that polar'swebhook-signaturedoes NOT satisfy LemonSqueezy'sx-signaturegate, a side-channel walk, a cross-method probe, a signature-verification-call-gated-by-header- check invariant, and a switch-statement- dispatcher-gated-by-signature-verification invariant pinning that invalid signatures must NEVER trigger any of the 8 event handlers — the second per-source-file webhook POST smoke the docs tree publishes, expanding payment-provider webhook coverage from one provider (Polar) to two (LemonSqueezy + Polar).
-
docs/pluginsAddeditem-votes-status-query-spec.md— 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 a newapps/web-e2e/tests/api/item-votes-status-query.spec.tsspec covering 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-bodyPOST) with the bare{ error }envelope (nosuccess: falsewrapper) — distinct from the canonical{ success: false, error: 'Unauthorized' }envelope used by the siblingitem-votes-cast-bodyPOST. 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 — the route is invariant to any query-string the caller appends. The spec emits a ~57-path bulk-loop walk asserting< 500plus eleven hand-written scenarios: a canonical-envelope 401 assertion pinning{ error: 'Authentication required' }, a strict envelope-shape assertion (Object.keys(body) === ['error'], nosuccesskey), a gate-before- post-auth invariance walk pinning none of the three candidate post-auth static messages may surface, a vote-record non-disclosure walk pinning that none of the six record keys (id,userId,itemId,voteType,createdAt,updatedAt) may surface, 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 (fabricated session-token cookies +X-Forwarded-For/X-Real-IP/Authorization/X-User-Idheaders), a cross-method probe (POST / PUT / PATCH / DELETE), a client-profile-lookup-not-entered invariance walk, and a vote-record-read-not-entered invariance walk — the first auth-gated non-admin per-source-file query smoke the docs tree publishes that pins anull-or-record success payload contract. -
docs/pluginsAddeditem-comments-create-body-spec.md— 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 a newapps/web-e2e/tests/api/item-comments-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/items/[slug]/comments/route.ts— the first non-admin per-source-file POST smoke that usescheckDatabaseAvailability()fromapps/web/lib/utils/database-check.tsas the load-bearing FIRST gate (BEFOREauth()) -- whenDATABASE_URLis missing, the helper returns a 503{ error: 'Database not configured', code: 'DATABASE_UNAVAILABLE', message: '...' }envelope (first POST smoke pinning this helper- emitted shape with a 503 status), the first non-admin POST smoke that uses the'Authentication required'401 message (distinct from'Unauthorized'used by the sibling votes-cast POST), and the second non-admin POST smoke that pins theisUserBlocked(clientProfile.status)moderation-status gate. In the e2e test environmentDATABASE_URLIS configured so the db-availability gate passes through and the auth gate fires for unauthenticated requests. The POST handler combines acheckDatabaseAvailability()gate (load-bearing FIRST gate),auth()lookup, a!session?.usergate (→ 401'Authentication required'), 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(...)lookup (not found → 404'Client profile not found'), theisUserBlocked(...)moderation-status gate (if true → 403 with dynamic block-reason message), the load-bearingcreateComment(...)write, agetCommentWithUserById(comment.id)post-write lookup (if null → 500'Failed to retrieve comment'-- the first POST smoke pinning a post- write null-check 500 envelope), success payload{ success: true, comment }with status 200, and outer catch 500'Failed to create comment'. The smoke spec pins a canonical-envelope authentication-required 401 assertion, a strict envelope-shape assertion, a success-branch-key non-disclosure assertion, a gate-before-post-auth invariant, 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, 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), and a createComment-and-post-write-lookup-not- entered invariance walk — the first checkDatabaseAvailability-helper-gated POST smoke the docs tree publishes. -
docs/pluginsAddeditem-votes-cast-body-spec.md— 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 a newapps/web-e2e/tests/api/item-votes-cast-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/items/[slug]/votes/route.ts— the first non-admin per-source-file POST smoke 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.ts(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()+ slug param resolution, a!session?.user?.idgate (→ 401{ success: false, error: 'Unauthorized' }), JSON body parse, vote-type enum validation,getClient ProfileByUserId(...)lookup (not found → 404'Client profile not found'), theisUserBlocked(...)moderation-status gate (if true → 403 with the dynamicgetBlockReasonMessagemessage), existing-votes lookup + replace logic, the load-bearingcreateVote({ userId, itemId, voteType })write,getVoteCountForItem(slug), success payload{ success: true, count, userVote: type }with status 200, and outer catchconsole.error+ 500'Internal server error'. The smoke spec pins a canonical-envelope bare- message 401 assertion, a strict envelope-shape assertion, a success-branch-key non-disclosure assertion, a gate-before-post-auth invariant, 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, 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), and a createVote-and- getVoteCountForItem-not-entered invariance walk — the first moderation-status-gated POST smoke the docs tree publishes that pins the load-bearing moderation invariant on a public, auth-gated vote- casting endpoint. -
docs/pluginsAddedpolar-webhook-body-spec.md— 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 a newapps/web-e2e/tests/api/polar-webhook-body.spec.tsspec covering 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, a manual JSON parse (failure → 400'Invalid JSON payload'), avalidateWebhookPayload(body)structure check (failure → 400'Invalid webhook payload'), awebhook-signatureheader presence check (missing → 400'No signature provided'), the load-bearingpolarProvider.handleWebhook(...)signature-verification call, a!webhookResult. receivedcheck (400'Webhook not processed'), the load-bearingrouteWebhookEvent(...)event- routing call on the success branch, success payload{ received: true }with status 200, and outer catchsafeErrorResponse(error, 'Webhook processing failed', 400). The smoke spec pins a first-gate JSON-parse-rejection assertion, a second-gate validate-payload- rejection assertion, a third-gate signature- header-presence-rejection assertion, a strict envelope-shape assertion 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, a side-channel walk, a cross-method probe, and 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 }— the first per-source- file webhook POST smoke the docs tree publishes, expanding the rollout into the payment-provider webhook layer for the first time. -
docs/pluginsAddeditem-views-record-body-spec.md— 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 a newapps/web-e2e/tests/api/item-views-record-body.spec.tsspec covering 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 smoke spec pins a canonical bot envelope assertion{ success: true, counted: false, reason: 'bot' }, a strict envelope-shape assertion (Object.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, 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, a malformed-JSON-body invariance walk pinning the gate-before-body-read order, 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, a bot-branch-non-disclosure-on-the- non-bot-branch assertion, and an owner-exclusion- not-entered invariance walk pinning that anonymous requests can NEVER receivereason: 'owner'regardless of UA ORsubmitted_bybody-field bypass attempts — the first bot-detection-graceful-degradation POST smoke the docs tree publishes that pins the bot gate as the load-bearing invariant on a public, non-auth-gated endpoint. -
docs/pluginsAddedextract-body-spec.md— 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 a newapps/web-e2e/tests/api/extract-body.spec.tsspec covering 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 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 admin/items/import) to surface the FIRST validation issue as the 400 envelope'serrorfield. In the e2e test environmentPLATFORM_API_URLis NOT configured, so EVERY POST request lands on the feature-disabled branch -- making the spec a pinning of the feature-disabled envelope as the load-bearing invariant. The smoke spec pins a 200-with- feature-disabled-envelope assertion, a strict envelope-shape assertion, a no-error-key assertion on the feature-disabled branch, a feature-disabled-before-post-feature-disabled invariant, 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', and an external-fetch- proxy-not-entered invariance walk pinning that the response must always includefeatureDisabled: truein the test environment — the first non-admin-tree per-source-file reference the docs tree publishes, expanding the rollout beyond theadmin/**route family for the first time. -
docs/pluginsAddedadmin-location-index-manage-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-location-index-manage-body.spec.tsspec covering 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.tsalready 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:'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. For an unauthenticated request the FIRST branch of the helper fires returning 401{ success: false, error: 'Unauthorized' }(canonical envelope withsuccess: falseAND short'Unauthorized'message). The smoke spec pins a canonical-envelope bare-message 401 assertion, a strict envelope- shape assertion, a success-branch-key non- disclosure assertion that NONE ofdataorclearedkeys must appear in any unauth response andsuccessmust befalse, 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, a malformed-JSON-body invariance walk, an action-enum-dispatch-not- entered invariance walk, and 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 — the first destructive-action-enum-dispatch POST admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-navigation-update-method-spec.md— 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 a newapps/web-e2e/tests/api/admin-navigation-update-method.spec.tsspec covering 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'), anitemsarray check, a per-item structure validation loop (label / path required), a per-item path-format XSS-prevention validation, thenconfigManager.updateNestedKey ('custom_header'|'custom_footer', items)for the load-bearing works.yml write. Returns{ success: true, type, items }on success (echoing bothtypeanditemsfrom the input). The smoke spec pins 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 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.', and a configManager-update-not-entered invariance walk pinning that the unauth response must NEVER echo atypeoritemskey from the input — the first per-item-XSS-prevention navigation-update PATCH admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-twenty-crm-config-save-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-twenty-crm-config-save-body.spec.tsspec covering 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), 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. Returns{ success: true, message: 'Configuration saved successfully', data: <savedConfig> }on success. The companionadmin-twenty-crm-config-query.spec.tscovers the GET surface of the same route. The smoke spec pins a canonical-longer 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, a malformed-JSON-body invariance walk, a validation-chain-not-entered invariance walk, and 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 — the first audit- logged CRM-config-save POST admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-categories-git-query-spec.md— 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 the existingapps/web-e2e/tests/api/admin-categories-git-query.spec.tsspec covering 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, NOT bare — a deliberate inconsistency between the unauth and post-auth configuration-error branches). The spec walks one bulk loop (~50 query permutations) and eleven hand-written scenarios pinning the bare 401 envelope, status invariance across query permutations, per-key isolation walks for?userId=/?token=/?bypass=/?repo=&branch=&owner=/?path=key families, side-channel isolation forAccept/ cookie / IP headers, and gate-before-config-validation / gate-before-Git-service invariants — the first GitHub-API-backed admin-tree GET smoke the docs tree publishes. -
docs/pluginsAddedadmin-tags-all-query-spec.md— 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 the existingapps/web-e2e/tests/api/admin-tags-all-query.spec.tsspec covering 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 message), 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 }), success payload{ success: true, data: tags }with status 200, and outer catchconsole.error+ 500'Failed to fetch tags'. The spec walks one bulk loop (~50 query permutations) and eight hand-written scenarios pinning the bare 401 envelope, status invariance across query permutations, per-key isolation walks for?locale=/?userId=/?token=/?bypass=/?repo=&branch=&commit=key families, and gate-before-locale-narrowing / gate-before-Git-CMS-read invariants — the first dead-branch type-narrowing Git-CMS query smoke the docs tree publishes. -
docs/pluginsAddedadmin-settings-update-method-spec.md— 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 a newapps/web-e2e/tests/api/admin-settings-update-method.spec.tsspec covering 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 returning 401{ error: 'Unauthorized' }(BARE envelope, NOsuccesskey, SHORT message), a single-field required check (if (!key)→ 400'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 input key and value), and outer catchconsole.error+ 500'Failed to update settings'. The companionadmin-settings-query.spec.tscovers the GET surface of the same route. The smoke spec pins 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, a malformed-JSON-body invariance walk, a required-key-check-not-entered invariance walk, and a configManager-update-not- entered invariance walk pinning that the unauth response must NEVER echo akeyorvaluefrom the input — the first cached-session-lookup config-write PATCH admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-categories-git-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-categories-git-create-body.spec.tsspec covering 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). Two-field required check, DATA_REPOSITORY env-var validation chain (missing / malformed), GH_TOKEN env-var validation, then the 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 ('already exists'→ 409 echoing raw error.message, elsesafeErrorResponse(error, 'Failed to create category via Git')). The smoke spec pins a canonical-longer-bare-envelope 401 assertion, a strict envelope-shape assertion (nosuccesskey), 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, a malformed- JSON-body invariance walk, a required-field-check- not-entered invariance walk, an env-var-validation- chain-not-entered invariance walk, and a Git- service-call-not-entered invariance walk — the first Git-CMS-write POST admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-featured-items-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-featured-items-create-body.spec.tsspec covering 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's POST handler does NOT call!isAdminat any point; it requires an authenticated user with a tenant. Two-step gate with tenant-first ordering (BEFORE body parse — distinct fromadmin/notificationsPOST which runsgetTenantId()AFTER body parse). The POST handler runs a two-field required check, an already- featured check via inline Drizzleselect(with tenant scoping) returning 400 (NOT 409)'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. Returns{ success: true, data: <featuredItem>, message: 'Item featured successfully' }with status 200. Outer catch isconsole.error+ 500'Failed to create featured item'. The smoke spec pins 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, 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', and a Drizzle-insert-not-entered invariance walk — the seventh Q-010b auth-gate- divergence finding the docs tree publishes. -
docs/pluginsAddedadmin-notifications-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-notifications-create-body.spec.tsspec covering 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, then!tenantIdaftergetTenantId()AFTER body parse + required-fields check → 403'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. Four- field required check (type,title,message,userId). Inline Drizzle insert with JSON- stringifieddatafield. Success payload withnotificationsuccess-key (NOTdata) and status 200 (NOT 201). The companionadmin-notifications-query.spec.tscovers the GET surface of the same route. The smoke spec pins 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, 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, and a Drizzle-insert-not-entered invariance walk — the sixth Q-010b auth-gate-divergence finding the docs tree publishes (joiningadmin-roles-query-spec.md,admin-roles-active-query-spec.md,admin-roles-create-body-spec.md,admin-featured-items-id-method-spec.md, and the broaderadmin-by-id.spec.tscoverage). -
docs/pluginsAddedadmin-roles-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-roles-create-body.spec.tsspec covering 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 on no body / empty name / out-of-range lengths, 409 on duplicate ID, or 201 on valid bodies. The POST handler additionally has a stable-ID-derivation step (namenormalized via.normalize('NFKD'), diacritic stripping, lowercasing, 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. The smoke spec pins 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", and a length-validation deterministic-fire invariant — the fifth Q-010b auth-gate-divergence finding the docs tree publishes (joiningadmin-roles-query- spec.md,admin-roles-active-query-spec.md,admin-featured-items-id-method-spec.md, and the broaderadmin-by-id.spec.tscoverage). -
docs/pluginsAddedadmin-collections-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-collections-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/collections/route.ts(the collection-level collection-create endpoint) — 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/${slug}`)slug-aware, in addition toawait invalidateContentCaches()). Sibling ofadmin-categories-create-body-spec.md— they share the SAME canonical-longer 401 envelope, the SAME three- branch outer catch chain ('already exists'/'must'/safeErrorResponse(...)fallback), and the SAME non-datasuccess-payload key (collectionhere,categorythere). Returns{ success: true, collection:, message: 'Collection created successfully' } with status 201. The companionadmin-collections-query.spec.tscovers the GET (paginated list) surface of the same route. The smoke spec pins a canonical-longer 401-envelope assertion (vs the bare-envelope sibling routes), a strict envelope-shape assertion, a success-branch-key non-disclosure assertion, a gate-before-post-auth invariant pinning that the four static post-auth messages must NEVER appear in any unauth response, a parameterised-vs-baseline status-stability comparison, a side-channel walk, a cross-method probe, 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 a
collectionkey, and therevalidatePathside-effects must NEVER fire, and 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.
- cache-invalidation-not-entered invariance walk
pinning that the unauth response status must NOT be
201, must NOT contain a
-
docs/pluginsAddedadmin-companies-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-companies-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/companies/route.ts(the collection-level company-create endpoint) — 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 on: NO existence check,createCompany(validatedData)call, and status-201 success branch with{ success: true, data: <company> }. The companionadmin-companies-query.spec.tscovers the GET surface of the same route. The smoke spec pins 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 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, a uniqueness-check-409-not-entered invariance walk, a createCompany-call-not-entered invariance walk, and a unique-constraint-outer-catch-not-entered invariance walk — the first bare-envelope-Zod-parse()-with-details-envelope collection-level POST admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-clients-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-clients-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/clients/route.ts(the collection-level client-create endpoint) — 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 (raw.email ?? raw.userId, distinct from prior POST smokes), a single-field required check,getUserByEmail(email)lookup, an inner-try/catch user-create branch that returns 400 with dynamically-interpolated'Failed to create user: <err.message>'message on failure, a get-or- create fallback validation, thencreateClientProfile(clientData)with defaults, an optional CRM sync side-effect, 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). The companionadmin-clients-query.spec.tscovers the GET surface of the same route. The smoke spec pins a bare 401- envelope assertion, a strict envelope-shape assertion (nosuccesskey), 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, and a createClientProfile-call-not-entered invariance walk — the first bare-envelope-with-get-or-create-user- side-effect collection-level POST admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-tags-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-tags-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/tags/route.ts(the collection-level tag-create endpoint) — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the hybrid bare-Unauthorized+success: false401 envelope with atagsuccess-payload key (NOTdata) — distinct from the canonical-longer- envelopeadmin/categoriesandadmin/collectionsPOST smokes. The POST handler runs a two-field required check, callstagRepository.create({ id, name, isActive: isActive ?? true })(defaultsisActivetotrueif not provided), runsawait invalidateContentCaches(), and returns{ success: true, tag: <tag> }with status 201 (NOmessagekey — distinct fromadmin/categoriesPOST andadmin/collectionsPOST). The outer catch uses a three-branch chain ('already exists'→ 409,'required' | 'must be'→ 400, else fixed-message 500'Failed to create tag'fallback — NOTsafeErrorResponse(...)). The companionadmin-tags-query.spec.tscovers the GET surface of the same route. The smoke spec pins a hybrid 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, a malformed-JSON-body invariance walk, a required-field-check-not-entered invariance walk, a create-call-not-entered invariance walk, and a three-branch-outer-catch-not-entered invariance walk — the first hybrid-envelopetag-key collection-level POST admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-categories-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-categories-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/categories/route.ts(the collection-level category-create endpoint) — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines a single-step inline!session?.user?.isAdmingate with a single required-field validation (if (!createData.name)→ 400'Category name is required') AND a three-branch outer-catch chain (error.message.includes('already exists')→ 409 echoing raw message,error.message.includes('must be')→ 400 echoing raw message,safeErrorResponse( error, 'Failed to create category')fallback) AND acategorysuccess-payload key (NOTdata) —{ success: true, category: <category>, message: 'Category created successfully' }with status 201. Distinct fromadmin/itemsPOST which uses a five- field guard with TWO 409 pre-create duplicate checks AND adatasuccess-key; distinct fromadmin/usersPOST which uses an eight-step body validation chain AND adatasuccess-key AND anerror.message-pass- through outer catch; distinct fromadmin/collectionsPOST which uses a two-field guard AND acollectionsuccess-key. The companionadmin-categories-query. spec.tscovers the GET (paginated list) surface of the same route. The smoke spec pins a canonical- longer 401-envelope assertion, a strict envelope- shape assertion, a success-branch-key non-disclosure assertion that NONE ofdata,category,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 ('Category name is required','Failed to create category','Category 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 malformed-JSON-body invariance walk, a required-field-validation-not-entered invariance walk pinning that EVERY missing-name probe round-trips to the same 401 status, a create-call-+- cache-invalidation-not-entered invariance walk pinning that the unauth response status must NOT be 201 and must NEVER echo'Category created successfully', and a two-branch-catch-not-entered invariance walk pinning that the unauth response must equal the canonical 401 envelope rather than any catch-branch shape — the firstcategory-success- key collection-level admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-users-create-body-spec.md— 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 a newapps/web-e2e/tests/api/admin-users-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/users/route.ts(the collection-level user-create endpoint) — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines the two-step!session?.user→!session.user.isAdmingate 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- interpolated message on 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. The companionadmin-users-query.spec.tscovers the GET (paginated list) surface of the same route. The smoke spec pins a hybrid 401-envelope 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 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', and a create-call-not-entered invariance walk pinning that the unauth response status must NOT be 201 — the first eight-step-validation collection-level POST admin-tree smoke the docs tree publishes (complementing the existing query-surface coverage of the same users-collection route). -
docs/pluginsAddedadmin-items-create-body-spec.md— 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 with a newapps/web-e2e/tests/api/admin-items-create-body.spec.tsspec covering thePOSTexport ofapps/web/app/api/admin/items/route.ts(the collection-level item-create endpoint) — the first POST-only collection-level admin-tree smoke the docs tree publishes that combines a five-field required-validation chain with TWO 409 Conflict pre-create duplicate checks using dynamically-interpolated messages 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!isAdmingate / canonical longer 401 envelope as its GET sibling. CRM sync is gated byprocess.env.TWENTY_CRM_ENABLED === 'true'(NOTE: strict-equals comparison, distinct fromadmin/items/[id]/route.tsPUT which uses!== 'false'). The smoke spec pins 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 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 pinning that the unauth response must NEVER match the dynamic/^Item with (ID|slug) '/regex prefixes, 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, and a Location-Index- side-effect-not-entered invariance walk pinning that a body withlocationdoes NOT change the unauth status — the first POST-only collection- level admin-tree smoke the docs tree publishes (complementing the existing query-surface coverage of the same items-collection route). -
docs/pluginsAddedadmin-roles-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-roles-id-method.spec.tsspec covering the admin single-role CRUD endpoint atapps/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 with a DELETE?hard=truequery-parameter branch AND a three-step manual PUT body validation chain with FIXED error messages. Distinct fromadmin/categories/[id]which has the DELETE-?hard=truebranch but a single-step gate; distinct fromadmin/users/[id]which has the two- step gate but NO DELETE-?hard=truebranch and uses an eight-step PUT validation chain. All three handlers share the SAME hybrid 401 envelope ({ success: false, error: 'Unauthorized' }— matchingadmin/users/[id]andadmin/featured- items/[id]) and the SAMEconsole.error+ 500 catch posture. Each handler diverges on its post- gate surface: GET callsroleRepository.findByIdreturning 404 / 200; PUT parses body AFTER both gate steps, runs an existence check AFTER body parse (NOT before, distinct fromadmin/reports/[id]andadmin/companies/[id]), runs the three-step validation chain ((a) name- empty / (b) name-length / (c) description-length), callsroleRepository.update(id, ...), returns{ success: true, data: <role>, message: 'Role updated successfully' }; DELETE parsessearchParams.get('hard') === 'true', runs an existence check, branches onhardDeleteboolean (hardDelete === true→roleRepository.hardDelete(id); elseroleRepository.delete(id)), returns the soft- delete'Role deleted (marked as inactive)'or hard-delete'Role permanently deleted'message. The smoke spec pins per-method hybrid 401-envelope assertions, a NEVER-403 invariant, gate-before- post-auth across eleven candidate messages (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, a cross-method side- channel walk, a service-not-entered invariance walk across all four repository calls, a three-step- validation invariance walk pinning that EVERY step-(a)/(b)/(c) probe round-trips to the same 401 status, and 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'— the first two-step-gate-with-DELETE-?hard-query admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-tags-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-tags-id-method.spec.tsspec covering the admin single-tag CRUD endpoint atapps/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 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 hybrid envelope shape and aconsole.error+ 500 catch posture. Each handler diverges on its post- gate surface: GET callstagRepository.findByIdreturning 404 or 200; PUT runs name validation → 400'Tag name is required', callstagRepository.update(id, ...), runsawait invalidateContentCaches(), returns{ success: true, data, message: 'Tag updated successfully' }, with three catch branches plus 500 fallback; DELETE callstagRepository.delete, runs cache invalidation, returns{ success: true, message: 'Tag deleted successfully' }, with one catch branch plus 500 fallback. The smoke spec pins per-method hybrid 401-envelope assertions, gate-before-post-auth across seven candidate messages, a per-id-shape status-stability comparison, a PUT body-permutation status-stability comparison, a cross-method side-channel walk, a malformed-JSON-body invariance walk for PUT, a service-not-entered invariance walk, a cache- invalidation-side-effect-not-entered invariance walk, and a three-branch-catch-chain-not-entered invariance walk — the first hybrid-envelope-with-3-branch-error.message. includes-catch admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-collections-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-collections-id-method.spec.tsspec covering the admin single-collection CRUD endpoint atapps/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: (a) GET callscollectionRepository.findById(id)returning 404'Collection not found'if missing or{ success: true, data: <collection> }; (b) 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 plus the always-emitted new-slug + index revalidation, returning{ success: true, data: <updated>, message: 'Collection updated successfully' }; (c) 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). The smoke spec pins per-method canonical-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 (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 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, and a gate-before- 409-/-'must'-400-catch invariance walk pinning that the unauth status MUST be 401 (NOT 400 / 409) — the first Zod-safeParse(...)-with-flatten()-envelope admin-tree smoke the docs tree publishes (joining the prior Zod-parse()admin-companies-id-method- spec.md, the validation-lessadmin-featured-items- id-method-spec.md, and the inline-untypedadmin- items-id-method-spec.mdandadmin-categories-id- method-spec.mdtriple-method smokes the sub-rollout previously published). -
docs/pluginsAddedadmin-featured-items-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-featured-items-id-method.spec.tsspec covering the admin single-featured-item CRUD endpoint atapps/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: (a) GET runs an inline Drizzleselect()with tenant scoping returning 404'Featured item not found'ifresult.length === 0or{ success: true, data: <featured-item> }; (b) 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' }; (c) 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). The smoke spec pins 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, 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 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, and a soft-delete-update-not-entered invariance walk pinning that the unauth response must NEVER echo'Featured item removed successfully'— the first non-admin-gated triple-method admin-tree smoke the docs tree publishes (joiningadmin-roles-query-spec.md,admin-roles-active-query-spec.md, and the broaderadmin-by-id.spec.tscoverage of similar tenant- only-gated routes as the fourth admin route flagged by Q-010b). -
docs/pluginsAddedadmin-companies-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-companies-id-method.spec.tsspec covering the admin single-company CRUD endpoint atapps/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 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. PUT is the first PUT smoke that pairs the existence-check-FIRST ordering with Zodparse()instead ofsafeParse(), thedetails: [...]400-validation-envelope key, two pre-update uniqueness checks, and an outer-catch unique-constraint translation chain. The smoke spec pins per-method bare 401-envelope assertions, a strict envelope-shape assertion 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 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 (getCompanyById/getCompanyByDomain/getCompanyBySlug/updateCompany/deleteCompany), 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, and a per-handler catch-message-divergence walk — the first Zod-parse()-with-details-envelope admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-comments-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-comments-id-method.spec.tsspec covering the admin single-comment CRUD endpoint atapps/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 envelope shape, and the SAMEconsole.error+ 500'Internal Server Error'catch posture. Each handler diverges on its post-gate surface: (a) 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> }; (b) 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' }; (c) 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). The smoke spec pins per-method 403-envelope assertions across GET / PUT / DELETE, a NEVER-401 invariant across all three methods, 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 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 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, and a tenant-resolution-not- entered invariance walk pinning that the unauth response must NEVER echo'Tenant not found'for GET / PUT — the first 403-on-unauth triple- method admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-sponsor-ads-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-sponsor-ads-id-method.spec.tsspec covering the admin single-sponsor-ad endpoint atapps/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 three sibling[id]/approve/[id]/reject/[id]/cancelPOST-only action routes which the smoke layer already covers separately; with this entry the sponsor-ad area smoke coverage is complete at four routes). 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). The smoke spec pins 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', and a per- handler catch-message-divergence walk — the first GET + DELETE-only dual-method admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-reports-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-reports-id-method.spec.tsspec covering the admin single-report CRUD endpoint atapps/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; this route returns 403'Forbidden'on the unauth branch instead). Both handlers share acheckDatabaseAvailability()pre- gate that runs BEFORE the auth gate, a single-step!session?.user?.isAdmingate that returns 403{ success: false, error: 'Forbidden' }, a strict envelope shape, a dynamic[id]segment, and a dev-gatedconsole.errorcatch. PUT is the first PUT smoke where the existence check runs BEFORE the body parse, then validatesstatus/resolutionagainst theReportStatus/ReportResolutionenums (with dynamically- interpolated 400 messages), then runs a conditional moderation-action chain (removeContent/warnUser/suspendUser/banUser) based onresolution, then a finalgetReportById(id), returning a four-key{ success: true, message, data, moderationResult }payload. The smoke spec pins per-method 403- envelope assertions, a NEVER-401 invariant, the cross-method envelope-equality assertion, gate- before-post-auth across five static plus regex- prefix dynamic 400 messages, gate-before-service across the entire moderation chain, gate-before- enum-validation, and gate-before-moderation-chain — the first 403-on-unauth admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-categories-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-categories-id-method.spec.tsspec covering the admin single-category CRUD endpoint atapps/web/app/api/admin/categories/[id]/route.ts— the second triple-method admin-tree smoke the docs tree publishes (after the first triple- methodadmin/items/[id]smoke), 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, but each has its own divergent 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. The smoke spec pins per- method canonical-longer 401-envelope assertions, a cross-method envelope-equality assertion, 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, and a per-handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response — the first triple-method admin smoke with a DELETE-only query-parameter branch and a query-flag-driven success-message dichotomy the docs tree publishes. -
docs/pluginsAddedadmin-collections-id-items-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-collections-id-items-method.spec.tsspec covering the admin collection-items endpoint atapps/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; this is the first that combinesGET+POSTon a nested path). Both 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: (a) GET — 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'); (b) 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 distinct from every prior admin POST smoke which uses at most three), catches withsafeErrorResponse(error, 'Failed to assign collection items'). The smoke spec pins per-method canonical-longer 401-envelope assertions, 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, and a side-effects-not-entered invariance walk pinning that theinvalidateContentCaches()+revalidatePath(...)chain is unreachable on the unauth branch — the first nested-[id]/<sub-resource>dual-method admin smoke the docs tree publishes. -
docs/pluginsAddedadmin-users-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-users-id-method.spec.tsspec covering the admin single-user CRUD endpoint atapps/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. The smoke spec pins per-method hybrid 401-envelope assertions across GET / PUT / DELETE, a strict envelope-shape assertion, 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, 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 round-trips to the same 401 status, and 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 — the first hybrid-envelope two- step-gated triple-method admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-clients-clientid-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-clients-clientid-method.spec.tsspec covering the admin single-client profile CRUD endpoint atapps/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; (2) bare{ error: 'Unauthorized' }envelope with NOsuccesskey on the 401 branch — distinct from the canonical-longer envelope of the sibling triple-methodadmin/items/[id]route; (3) direct query-function calls (getClientProfileById/updateClientProfile/deleteClientProfilefrom@/lib/db/queries) instead of a repository class; (4)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; (5) GET success payload{ success: true, data: <client> }; (6) PUT success payload{ success: true, data: <client> }(NOmessagekey — distinct from the siblingadmin/items/[id]PUT which includes'Item updated successfully'); (7) DELETE success payload{ success: true, message: 'Client deleted successfully' }(NOdatakey); (8) 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; (9) method-resolution surface withGET/PUT/DELETE-only export. The smoke spec pins 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 pinning that all three handlers emit byte-identical 401 envelopes, 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, and a per-handler catch-message-divergence walk — the first bare- envelope-no-success-key triple-method admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-items-id-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-items-id-method.spec.tsspec covering the admin single-item CRUD endpoint atapps/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, and the SAME{ success: false, error: ... }envelope shape, but each has its own divergent post-gate surface: (a) GET — no body parse, callsitemRepository.findById(id)with a 404'Item not found'branch, returns{ success: true, data: <item> }, catches withsafeErrorResponse(error, 'Failed to fetch item'); (b) 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 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' }, catches withsafeErrorResponse(error, 'Failed to update item'); (c) DELETE — no body parse, builds the same audit-user, callsitemRepository.delete(id, auditUser), optionally removes from the Location Index, returns{ success: true, message: 'Item deleted successfully' }(NOTE: NOdatakey — distinct from GET / PUT success payloads), catches withsafeErrorResponse(error, 'Failed to delete item'). The smoke spec pins 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, a service-not-entered invariance walk across all three repository calls, and a per- handler catch-message-divergence walk pinning that NONE of the three distinct catch messages must appear in any unauth response — the first triple-method admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-roles-id-permissions-method-spec.md— 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 with a newapps/web-e2e/tests/api/admin-roles-id-permissions-method.spec.tsspec covering the admin role-permissions endpoint atapps/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 the unique combination of (1) dual-method GET + PUT exports; (2)checkAdminAuth()helper-driven envelope with three distinct branches: 401'Unauthorized'(no session), 401'User ID not found'(session but no id), 403'Insufficient permissions'(session + id but!isAdmin) — distinct from every prior admin- tree route which returns 401 for both unauth AND non-admin-auth; (3) shorter'Unauthorized'401 envelope — distinct from the canonical longer'Unauthorized. Admin access required.'envelope every prior admin-id smoke pins; (4)success: falseenvelope key; (5) imperativeisValidPermission(permission)validation AFTER the gate, with side-channelinvalidPermissionsarray echoed in the 400 envelope — a UNIQUE envelope key no prior admin-tree smoke pins; (6) service-call surface withroleService.findById(id)(GET) androleService.updateRole(id, ...)(PUT) both delegating toRoleDbService; (7) method-resolution surface withGET + PUTexports (POST / PATCH / DELETE NOT exported). The smoke spec pins the gate-before-post-auth invariant that NONE of the eight post-gate messages must appear in the unauth response body, the gate-before- params-resolution invariant pinning that every id shape round-trips to the same 401 status across BOTH methods, the gate-before-body-parse invariant pinning that malformed JSON bodies do NOT surface a 400 on PUT, the gate-before-service invariant pinning that the unauth response does NOT echo adatakey from the service payload, the gate- before-validation invariant pinning that everypermissionsshape (missing + valid + empty + single-invalid + non-array string / null / numeric / object / numeric-array) round-trips to the same 401 status on PUT, the cross-method envelope- equality invariant pinning that GET / PUT return observably the same body on the unauth branch (sharedcheckAdminAuth()helper), the side-channelinvalidPermissionsnon-disclosure invariant pinning that the auth-branch validation echo NEVER appears on the unauth branch, and the first-branch landing invariant pinning that every probe from the cookie-less smoke harness lands on the FIRSTcheckAdminAuth()branch (the 401'Unauthorized'no-session envelope) — NOT the SECOND ('User ID not found') and NOT the THIRD ('Insufficient permissions'403) — the first dual-methodcheckAdminAuth()-helper admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-sponsor-ads-id-cancel-method-spec.md— 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 newapps/web-e2e/tests/api/admin-sponsor-ads-id-cancel-method.spec.tsspec covering the admin sponsor-ad cancellation endpoint atapps/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; (3) canonical longer 401 message andsuccess: falseenvelope key; (4) body parse via.catch(() => ({})); (5) Zod-safeParse(...)body validation AFTER the gate; (6) optional-onlycancelReasonwithmaxLength: 500constraint — a missing / undefined / nullcancelReasonwould pass validation on the auth branch — 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); (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. The smoke spec pins the gate-before-post- auth invariant that NONE of the four post-gate messages must appear in the unauth response body, the gate-before-params-resolution invariant pinning that every id shape round-trips to the same 401 status, the gate-before-body-parse invariant pinning that malformed JSON bodies do NOT surface a 400, the gate-before-service invariant pinning that the unauth response does NOT echo adatakey from the service payload, and the gate-before-Zod-validation invariant pinning that everycancelReasonshape (missing + empty + null + valid + 500-char-boundary- 501-char-too-long + numeric) round-trips to the same 401 status — the first optional-Zod-field admin-tree smoke the docs tree publishes.
-
docs/pluginsAddedadmin-sponsor-ads-id-reject-method-spec.md— 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 newapps/web-e2e/tests/api/admin-sponsor-ads-id-reject-method.spec.tsspec covering the admin sponsor-ad rejection endpoint atapps/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, 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!session?.user?.isAdmin || !session.user.id; (3) canonical longer 401 message'Unauthorized. Admin access required.'andsuccess: falseenvelope key with strict envelope-shape assertion; (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; (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; (10) method-resolution surface withPOST-only export. The smoke spec pins the gate-before-post- auth invariant that NONE of the four post-gate messages must appear in the unauth response body, the gate-before-params-resolution invariant pinning that every id shape round-trips to the same 401 status, the gate-before-body-parse invariant pinning that malformed JSON bodies do NOT surface a 400, the gate-before-service invariant pinning that the unauth response does NOT echo adatakey from the service payload, and the gate-before-Zod-validation invariant pinning that everyrejectionReasonshape (valid 70-char + 10-char-min boundary + 5- char-too-short + empty + null + numeric + 501-char- too-long + missing) round-trips to the same 401 status — the first Zod-safeParse(...)admin- tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-sponsor-ads-id-approve-method-spec.md— 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 newapps/web-e2e/tests/api/admin-sponsor-ads-id-approve-method.spec.tsspec covering the admin sponsor-ad approval endpoint atapps/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]/review); (2) compound single-ifgate!session?.user?.isAdmin || !session.user.id, observably equivalent to the single-step gate from the unauth client's perspective; (3) canonical longer 401 message'Unauthorized. Admin access required.'; (4)success: falseenvelope key on the 401 branch with strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; (5) params resolution AFTER the gate; (6) body parse inside its own try/catch AFTER params AND AFTER the gate —forceApprovedefaults 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' }; (10) method-resolution surface withPOST-only export. The smoke spec pins the gate-before-post- auth invariant that NONE of the four post-gate messages must appear in the unauth response body, the gate-before-params-resolution invariant pinning that every id shape round-trips to the same 401 status, the gate-before-body-parse invariant pinning that malformed JSON bodies do NOT surface a 400, the gate-before-service invariant pinning that the unauth response does NOT echo adatakey from the service payload, and the gate-before-flag-evaluation invariant pinning that everyforceApproveshape (true/false/string/numeric/null/missing) round-trips to the same 401 status — the first multi-error- code catch chain admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-notifications-id-read-method-spec.md— 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 newapps/web-e2e/tests/api/admin-notifications-id-read-method.spec.tsspec covering the admin single-notification mark-as- read endpoint atapps/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-path PATCH ofadmin/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; (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 — matching the siblingadmin/notifications/mark-all-read; (5) path-id surface — the handler readsidfromawait paramsAFTER the auth gate; (6) tenant-resolution surface AFTER params and AFTER the 400 missing-id 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-usernamecatch family; (9) method-resolution surface withPATCH-only export. The smoke spec pins the gate-before-post- auth invariant 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, the gate-before-params-resolution invariant pinning that every id shape (short slug, dashed slug, uuid, encoded slug, long padded slug) round-trips to the same 401 status, the gate-before- body-parse invariant pinning that malformed JSON bodies do NOT 400 with a JSON-parse error before the gate fires, and the gate-before-DB-update invariant pinning that thedb.update(notifications) ...returning()call is NOT entered on the unauth branch — the first dynamic-segment[id]PATCHadmin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-items-import-validate-body-spec.md— 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 newapps/web-e2e/tests/api/admin-items-import-validate-body.spec.tsspec covering the admin items-import-validate (dry-run) endpoint atapps/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) static-pathPOSThandler (sibling of the JSON-bodyadmin/items/importroute); (2) single-stepauth()chain matching the canonical longer message family; (3) canonical longer 401 message'Unauthorized. Admin access required.'; (4)success: falseenvelope key on the 401 branch with a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; (5) body parse viaawait request.formData()AFTER the gate — the first admin-tree smoke spec that documents aformData()-based body parse; (6) five-step file / mapping validation chain 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; (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. The smoke spec pins the gate-before-formData- parse invariant that malformed multipart bodies do NOT 400 with a parse error before the gate fires, the gate-before-validation invariant pinning that ALL FIVE 400 messages must NEVER appear in the unauth response body, the gate-before-service invariant pinning that none of the four success- branch keys (headers,suggestedMapping,validationResults,summary) must appear in the unauth response body, the gate-before-extension- whitelist invariant pinning that every extension shape (whitelisted.csv/.xlsx/.xlsplus non-whitelisted.txt/.json/.pdf/ extensionless) round-trips to the same 401 status, the gate-before-mapping-parse invariant pinning that everymappingshape (valid + invalid + broken + empty + missing) round-trips to the same 401 status, and the gate-before-default-fallback invariant pinning that everyduplicateStrategy/defaultStatusshape (valid + invalid + falsy) round-trips to the same 401 status — the first multipart/form-data admin-tree smoke the docs tree publishes. -
docs/pluginsAddedadmin-items-import-body-spec.md— 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 newapps/web-e2e/tests/api/admin-items-import-body.spec.tsspec covering the admin items-import-execute endpoint atapps/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) static-pathPOSThandler 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; (3) canonical longer 401 message'Unauthorized. Admin access required.'; (4)success: falseenvelope key on the 401 branch with a strict envelope-shape assertionObject.keys(body).sort() === ['error', 'success']; (5) body parse viaawait request.json()AFTER the gate; (6) two-step body validation chain 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; (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 theImportExecutionResult; (8)safeErrorResponse(error, 'Failed to execute import')catch matching theadmin/items/bulkandadmin/items/[id]/historycatch family; (9) method-resolution surface withPOST-only export. The smoke spec pins the gate-before-body- validation invariant that BOTH 400 messages must NEVER appear in the unauth response body, the gate- before-service invariant that theresultkey must NEVER appear in the unauth response body, the gate-before-default-fallback invariant pinning that everyduplicateStrategy/defaultStatusshape (valid + invalid + falsy) round-trips to the same 401 status, and the gate-before-streaming invariant pinning that 10-row and 100-row bodies round-trip to the same status as the empty-rows baseline — the first two-step-body-validation admin-tree smoke the docs tree publishes.
2026-05-03
-
docs/pluginsAddedadmin-items-id-history-query-spec.md— 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 newapps/web-e2e/tests/api/admin-items-id-history-query.spec.tsspec covering the admin item-audit-history endpoint atapps/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; (3) canonical longer 401 message matching the canonical-envelope family; (4)success: falseenvelope key on the 401 branch; (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 boolean second argumenttruetofindByIdopting the lookup into including soft-deleted items; (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); (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; (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. The smoke spec asserts the gate-before-existence-check invariant pinning that the'Item not found'404 message must NEVER appear in the unauth response body, the gate-before-query-parse invariant pinning that the dynamic 400 message must NEVER appear in the unauth response body, and the action-enum non-disclosure assertion that the six valid action names (created,updated,status_changed,reviewed,deleted,restored) must NEVER appear in the unauth response body via word-boundary regexes — the first dynamic-segment-GET-with-404 admin smoke the docs tree publishes. -
docs/pluginsAddedadmin-clients-bulk-method-spec.md— 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 newapps/web-e2e/tests/api/admin-clients-bulk-method.spec.tsspec covering the admin clients-bulk-action endpoint atapps/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) with the cross-method probe walking exactly three remaining methods (GET/POST/PATCH); (2) bare'Unauthorized'401 message with bare{ error: 'Unauthorized' }envelope (nosuccess: falsekey) — distinct from the canonical longer family ofadmin/items/bulk,admin/categories/reorder, andadmin/items/[id]/review, and the same bare-message family asadmin/users/check-email,admin/users/check-username, andadmin/notifications/mark-all-read; (3) single-stepauth()chain with bare-message envelope filling the previously-empty "single-step gate × bare envelope" quadrant in the admin-tree smoke matrix; (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 with one 400 message'Invalid request: clients array is required'on!Array.isArray(body.clients) || body.clients.length === 0; (6) per-client try/catch loop 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 —updateClientProfile/deleteClientProfileimported directly from@/lib/db/queries; (8) per-method success-branch payload divergence on 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 with each method'stry/catchreturning 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 matching theadmin/items/bulktwin-import posture (the second admin-tree route the smoke layer covers that imports BOTH helpers); (11) method-resolution surface withPUTANDDELETEexports. The smoke spec asserts the cross-method response-parity invariant (thePUTandDELETE401 envelopes must be byte-identical), the load-bearing invariant of the dual-method smoke layer. -
docs/pluginsAddedadmin-items-bulk-body-spec.md— 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 newapps/web-e2e/tests/api/admin-items-bulk-body.spec.tsspec covering the admin items-bulk-action endpoint atapps/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)); (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; (6) six-step body validation chain 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; (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; (10)safeErrorMessage+safeErrorResponsetwin-import surface — the only admin route the smoke layer covers that imports BOTH helpers; (11) method-resolution surface withPOST-only export. Pins the at-a-glance scenario tree (header / body bulk-loop walks asserting< 500; canonical-longer 401- envelope assertion; negative-property assertion on the success-branchresults/summarykeys; gate-before-body-validation invariant covering ALL six 400 messages; gate-before-catch, gate-before- body-parse, gate-before-bound-check, and gate- before-loop invariants; parameterised-vs-baseline status-stability; side-channel cookie /X-*header walk; cross-method probe; strict envelope- shape assertion). Cross-references the prior per-spec-file siblings and Spec 010 / Spec 009. -
docs/pluginsAddedadmin-items-id-review-body-spec.md— 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 newapps/web-e2e/tests/api/admin-items-id-review-body.spec.tsspec covering the admin item-review endpoint atapps/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. Documents the unique combination of (1)POSThandler with a dynamic[id]path parameter with{ params: Promise<{ id: string }> }resolved AFTER the gate AND AFTER the body validation; (2) single-stepauth()chain matching theadmin/categories/reordergate shape; (3) canonical longer'Unauthorized. Admin access required.'401 message; (4)success: falseenvelope key; (5) body parse viaawait request.json()AFTER the gate; (6) single-step body validation with the 400 message"Review status must be either 'approved' or 'rejected'"; (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; (9) method-resolution surface withPOST-only export. Pins the at-a-glance scenario tree (header / body bulk-loop walks asserting< 500; canonical-longer 401-envelope assertion; negative-property assertion on the success-branch keys; gate-before-body-validation, gate-before- catch, gate-before-body-parse, and gate-before- params-resolve invariants; parameterised-vs-baseline status-stability; side-channel cookie /X-*header walk; cross-method probe; strict envelope- shape assertion). Cross-references the prior per-spec-file siblings and Spec 010 / Spec 009. -
docs/pluginsAddedadmin-categories-reorder-method-spec.md— 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 newapps/web-e2e/tests/api/admin-categories-reorder-method.spec.tsspec covering the admin categories-reorder endpoint atapps/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. Documents the unique combination of (1)PUThandler withrequest: NextRequestbody-reading signature distinct from the barePATCH()ofadmin/notifications/mark-all-read; (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; (3) canonical longer'Unauthorized. Admin access required.'message matching theadmin/twenty-crm/*family, 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 ofadmin/notifications/mark-all-read(nosuccesskey); (5) body parse viaawait request.json()AFTER the gate distinct from the barePATCH()/POST()of the bare-handler routes which never read the body; (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'); (7)categoryRepository.reorder(categoryIds)call followed byinvalidateContentCaches(), with success-branch payload{ success: true, message: 'Categories reordered successfully' }; (8)safeErrorResponse(error, 'Failed to reorder categories')catch distinct from theconsole.error+'Internal server error'catch of the sibling check-email / check-username routes; (9) method-resolution surface withPUT-only export, so every other method (GET/POST/PATCH/DELETE) must round-trip to 405. 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; a negative-property assertion that the unauth response does NOT echo the success-branch'Categories reordered successfully'message; 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 to 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). -
docs/pluginsAddedadmin-notifications-mark-all-read-method-spec.md— 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 newapps/web-e2e/tests/api/admin-notifications-mark-all-read-method.spec.tsspec covering the admin mark-all-notifications-read endpoint atapps/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; (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; (4)'Tenant not found'403 envelope distinct from the sibling routes' bare'Forbidden'message; (5) direct Drizzle DB call without a repository abstraction distinct from the sibling routes' repository abstractions; (6) per-tenant scope on the success branch; (7) method-resolution surface withPATCH-only export. 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; 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 to 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 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). -
docs/pluginsAddedadmin-users-check-username-body-spec.md— 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 newapps/web-e2e/tests/api/admin-users-check-username-body.spec.tsspec covering the admin check-username endpoint atapps/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: documented field (usernamevsemail), body- validation message ('Username is required'vs'Email is required'), repository call (userRepository.usernameExistsvsuserRepository.emailExists), and catch-log prefix (the route path). 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 (cross-route field-validation regression, one-route-only auth-gate-removal regression, username-shape boundary fuzzing on the unauth branch). Documents the at-a-glance scenario tree including the first cross-route response- parity assertion the docs tree publishes (the bare-401 envelope ofadmin/users/check-usernamemust be byte-identical to the bare-401 envelope ofadmin/users/check-email). Includes username-shape boundary fuzzing (Unicode / RTL- override / null-byte / SQL injection / XSS / Cyrillic-homoglyph / zero-width-character / collation-sensitivity / leading-trailing-space). Cross-references to 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 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 lands as the load- bearing invariant of the cross-route smoke layer. -
docs/pluginsAddedadmin-users-check-email-body-spec.md— 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 newapps/web-e2e/tests/api/admin-users-check-email-body.spec.tsspec covering the admin check-email endpoint atapps/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']). Includes email-shape boundary fuzzing on the unauth branch (null-byte injection, 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 to 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 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.ts). -
docs/pluginsAddedadmin-items-export-query-spec.md— 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 newapps/web-e2e/tests/api/admin-items-export-query.spec.tsspec covering the admin items-export endpoint atapps/web/app/api/admin/items/export/route.ts— the per-tenant items dump counterpart to the sample-template route already covered byapps/web-e2e/tests/api/admin-items-export-sample-query.spec.ts. 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, (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; 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,admin-data-export-page-object.md,admin-item-form-page-object.md,admin-items-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 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. -
docs/pluginsAddedadmin-roles-active-query-spec.md— 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 newapps/web-e2e/tests/api/admin-roles-active-query.spec.tsspec covering the admin active-roles endpoint atapps/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). The spec emits one bulk-loop walk over ~85 paths asserting< 500plus 13 hand-written scenarios (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,admin-roles-page-object.md, and to Spec 010 — E2E Test Coverage and Spec 009 — Admin Dashboard. 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). -
docs/pluginsAddedadmin-roles-query-spec.md— 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 newapps/web-e2e/tests/api/admin-roles-query.spec.tsspec covering the admin-only roles listing endpoint atapps/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,admin/twenty-crm/config,admin/settings/map-status,admin/categories,admin/clients,admin/comments,admin/companies,admin/dashboard/stats,admin/featured-items,admin/geo-analytics,admin/items,admin/items/stats,admin/location-index,admin/navigation,admin/notifications,admin/reports,admin/reports/stats,admin/roles/stats,admin/settings,admin/tags,admin/tags/all,admin/twenty-crm/test-connection,admin/users, andadmin/users/statssmoke specs, 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 matching auth-gate question — 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. With this entry the per-spec-file docs rollout extends to 6-of-N, thetests/api/per-spec-file sub- rollout extends to 4-of-many, and the docs tree surfaces its first auth-gate-divergence finding via the question register. -
docs/questionsAdded Q-010b (Should /api/admin/roles and /api/admin/roles/active carry an explicit auth() gate?) — surfaces the auth-gate-divergence finding for human review with the recommended default of "yes, add the same two- step gate as the sibling /api/admin/roles/stats route" and four migration-path options. -
docs/pluginsAddedadmin-sponsor-ads-query-spec.md— 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 existingapps/web-e2e/tests/api/admin-sponsor-ads-query.spec.tsspec covering the admin-only sponsor-ads listing endpoint atapps/web/app/api/admin/sponsor-ads/route.ts— the first admin-tree route the smoke layer covers that documents the route-specific'Unauthorized. Admin access required.'error string paired with a request-bearingGET(request: NextRequest)handler signature and the widest documented query-param surface in the admin tree (pagination + enum filters + free-text search + order-targeting keys, all read AFTER the auth gate). The spec also pins the auth-gate- before-Zod-validation order via a deliberate'Unauthorized. Admin access required.' != 'Invalid query parameters'assertion that surfaces any future re-ordering as a 400 instead of a 401 on the unauth branch, plus a side-channel walk asserting that fabricatednext-auth.session-token/authjs.session-tokencookies andX-Forwarded-For/X-Real-IPheaders do NOT bypassauth(). 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. -
docs/pluginsAddedadmin-twenty-crm-config-query-spec.md— 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 newapps/web-e2e/tests/api/admin-twenty-crm-config-query.spec.tsspec that covers the admin-only Twenty CRM configuration endpoint atapps/web/app/api/admin/twenty-crm/config/route.ts— the first admin-tree route the smoke layer covers that documents the route-specific'Unauthorized. Admin access required.'error string combined with a bareGET()handler signature and the canonical{ success: false, error }envelope, plus a per- tenant CRM-credential non-disclosure contract pinned via a deliberate negative-string assertion that the unauth response body does NOT contain the masked-API-key regex (/\*{4}[A-Za-z0-9]{4}/), theTWENTY_CRM_API_KEY/TWENTY_CRM_BASE_URLenv-var names, or any of the config sub-field names. 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. -
docs/pluginsAddedadmin-settings-map-status-query-spec.md— the third per-source-file reference the docs tree publishes for any file underapps/web-e2e/tests/(and the first underapps/web-e2e/tests/api/), continuing the per- spec-file docs rollout after the now-closed (2-of-2)tests/smoke/rollout. Pairs with a newapps/web-e2e/tests/api/admin-settings-map-status-query.spec.tsspec that covers the admin-only map-provider configuration-status endpoint atapps/web/app/api/admin/settings/map-status/route.ts— the first admin-tree route the smoke layer covers that uses thegetCachedApiSession(req)wrapper (rather than the bareauth()call), the bare{ error }envelope (rather than the canonical{ success: false, error }envelope), and a per-env publishable-key non-disclosure contract pinned via a deliberate negative-string assertion that the unauth response body does NOT contain a Mapbox public access token (pk.*), Google Maps API key (AIza*), or either env-var name (NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN/NEXT_PUBLIC_GOOGLE_MAPS_API_KEY). 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. -
docs/pluginsAddedsmoke-navigation-spec.md— 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.mdand closing the smoke tree at 2-of-2 (thetests/smoke/directory has exactly two*.spec.tsfiles; both now have docs anchors). 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. Paired withapps/web-e2e/tests/smoke/navigation.spec.tsand the second consumer-layer reference in the rollout that documents (a) the four hand-writtentest()blocks that pin distinct user-flow primitives, with the per-scenario assertion divergence rationale (each scenario pins a structurally different invariant: count > 0 /h1visibility / URL match / link-click); (b) the whya[href*="/items/"]substring-CSS selector rationale (resilience to the page-object refactor surface, cross-route coverage matching every list / grid / card variant, selector simplicity); (c) the whygetByRole('link', { name: /sign in/i })accessibility-first locator rationale (tests the user-visible primitive via the accessibility tree, resilience to URL refactor, cross-locale coverage with thelocale: 'en-US'use-flag fromplaywright-config.md); (d) the why 30-secondexpect.toBeVisible({ timeout: 30_000 })override rationale (deliberate self-documenting pin against future contributors who lower the global default, cold-cache resilience for the home-page render, distinct from the navigation timeout); and (e) a "What it does not contain" six-bullet enumeration of the deliberate omissions. 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). -
docs/pluginsAddedsmoke-health-spec.md— 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 — seebase-page-object.mdandsignin-page-object.md). Where the page-object docs rollout documented the driver layer (the*.page.tsfiles that encapsulate per-page Locator and helper APIs), the per-spec-file docs rollout documents the consumer layer — the*.spec.tsfiles that import drivers / fixtures / helpers and turn them into assertion-bearing scenarios. Paired withapps/web-e2e/tests/smoke/health.spec.tsand 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 three load-bearing reasons (session agnosticism, independence fromglobal-setup.md, and a smaller import graph); (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; (c) awaitUntil: 'domcontentloaded'trade- off — the second-earliest of Playwright's four wait conditions, 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, andCache-Control: max-age304s; and (e) a most-universalbodyLocator pin for the rendered-DOM assertion, distinct from amain/[role="main"]/header/page.title()alternative. 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 inline 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). -
apps/web-e2e/tests/apiAddedadmin-clients-advanced-search-query.spec.ts— a query-param surface smoke for the admin-only advanced-client-search endpoint atapps/web/app/api/admin/clients/advanced-search/route.ts. The route is the first admin-tree route the smoke layer covers that documents a unique combination of FOUR distinct contracts: (1) the bare{ error: 'Unauthorized' }envelope on the unauthenticated 401 branch (NOT the canonical{ success: false, error: 'Unauthorized' }shape every other admin-tree route's gate emits, and NOT the role-context-specific'Unauthorized. Admin access required.'message the categories-git / items-import / items-import-validate routes emit) — the bare- envelope posture mirrors the categories-git route but with a bare'Unauthorized'message rather than the role-context suffix; (2) a richer-than-most query-param surface of 13+ documented keys (?page=/?limit=/?search=/?status=/?plan=/?accountType=/?provider=/?sortBy=/?sortOrder=/?createdAfter=/?createdBefore=/?updatedAfter=/?updatedBefore=) — every key parsed AFTER the gate so the unauth branch is invariant to the entire combinatorial surface; (3) the inline pagination clamp posture (Number()→Number.isFinite()→Math.floor()→Math.min(Math.max(…, 1), 100)), distinct from the admin-roles route'svalidatePaginationParams(searchParams)helper and the admin-categories route's Zod- schema-validated pagination posture, accepting every parseable integer (including negative / zero / non-integer values via the floor + clamp pipeline) and defaulting silently rather than emitting a 400; and (4) aparseDate(v)helper that normalises four distinct date- range filters (createdAfter/createdBefore/updatedAfter/updatedBefore) vianew Date(v)+Number.isNaN(d.getTime())pinning, silently returningundefinedfor NaN-valuedDateobjects rather than emitting a 400. The spec walks the unauthenticated branch and pins (a) the canonical 401 + bare envelope contract, (b) the negative-shape assertion that the body must NOT include asuccesskey (expect(body).not.toHaveProperty('success')), (c) the negative-shape assertion that the body's only key iserror(expect(Object.keys(body)).toEqual(['error'])), (d) the message-divergence assertion that the error must be the bare'Unauthorized'(NOT'Forbidden', NOT'Unauthorized. Admin access required.'), and (e) the status-invariance assertion that every documented and undocumented query-param permutation hits the same baseline status. Sweeps every documented query-param value permutation including pagination clamp targets (-1/0/999/999999/abc/1.5), status / plan / sortBy enum values plus invalid sentinels, OAuth provider values (google / github / facebook / twitter / microsoft), date-range filters with valid / invalid / empty values, SQL-injection-shaped search payloads, long search payloads, and the standard impersonation / token / bypass / cookie / IP / Accept-header / repeated-key side-channel sweeps that every admin-tree smoke spec runs. -
docs/pluginsAddedclient-trash-page-object.md— 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, paired withapps/web-e2e/page-objects/client/trash.page.tsand 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) 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); (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 with.*between the two substrings to allow 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. 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). 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. -
docs/pluginsAddedclient-submit-page-object.md— 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), paired withapps/web-e2e/page-objects/client/submit.page.tsand 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 (which lives inside a[role="dialog"]overlay rather than a per-route page); (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 — the first client-tree driver to combine all three anchor styles in a single class; (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 override the pre-filled values); (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 — the first client-tree driver to document a plan-selection mutator and the first to use a multi-substring alternation regex for label-drift tolerance. Documents the full surface for theClientSubmitPagedriver — the sevenreadonlyLocator fields, the navigation method, and the four composite helpers. Pinned toapps/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). Includes the "WhyClientSubmitPageextendsBasePage" three-reason analysis; the "WhyfillBasicInfofills URL first (and not name / description first)" three-reason analysis (the LinkInput component fetches metadata on blur, subsequent fills override the OG-prefilled values, the terminal Step 3 form-state validation expects all three fields filled); the "WhylinkUrlInputuses a bareinput[type="url"]element-selector" three-reason analysis (LinkInput does not bind to a stable id, the[type="url"]attribute is the production-source-stable hook,.first()defends against multi-URL forms); the "WhyselectTag(tagName)usesexact: true" three-reason analysis (tag names are short and may collide, exact preserves case-insensitivity by default, future tag rename surfaces as a clear test failure); the "WhyselectFreePlan()uses the OR-of-two-substring regex" three-reason analysis; cross-references to all four prior client-tree page-object docs and to the admin item-form driver as the modal-bound counterpart; and a "What it does not contain" five-bullet enumeration of the deliberate omissions (nogetByTestIdselectors, nosubmitFullFlow(data)composite, no paid-plan selection helpers, noassertStep(step)invariant, nogetCurrentStep(): Promise<number>accessor). -
apps/web-e2e/tests/apiAddedadmin-categories-git-query.spec.ts— a query- param surface smoke for the admin-only Git- repository-status / categories endpoint atapps/web/app/api/admin/categories/git/route.ts. The route is the first admin-tree route the smoke layer covers that documents a unique combination of FOUR distinct contracts: (1) a zero-argumentGET()handler signature (same posture as the notifications route, distinct from every other admin-tree route'sGET(request: NextRequest)posture); (2) the bare{ error: '...' }envelope (NOT the{ success: false, error: '...' }shape every other admin-gated route emits) — the ONLY admin- tree GET route that combines the bare-envelope shape with a role-context-specific'Unauthorized. Admin access required.'message (the settings route uses the bare envelope with a bare'Unauthorized'message; the admin- categories route uses the canonical envelope with the 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 tags/all / categories/all routes' Git-CMS file-system reader posture; and (4) THREE distinct configuration-error 500 envelopes after the gate (one per configuration prerequisite —DATA_REPOSITORYnot set / invalid format /GITHUB_TOKENnot set), each emitting the canonical{ success: false, error: '...' }envelope (NOT the bare envelope) — a deliberate inconsistency between the unauth-branch and the post-auth configuration-error branches that the route's handler structure makes invariant. The spec walks the unauthenticated branch and pins the canonical 401 + bare envelope contract plus negative-shape assertions that the body must NOT include asuccesskey, must NOT use the bare'Unauthorized'message, and must NOT use the'Forbidden'message. Sweeps Git-service- configuration override / impersonation / token / bypass / Git-ref-targeting / path-traversal / cache-bust / Accept-header / cookie-header (withX-GitHub-Tokenvariant) / repeated-key permutations. -
docs/pluginsAddedclient-submissions-page-object.md— the fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/, paired withapps/web-e2e/page-objects/client/submissions.page.ts. Continues the client-tree page-object docs rollout (4-of-6). Documents the first client- tree driver in the rollout that exposes (a) a named-row-resolved CRUD helper trio (viewSubmission(title)/editSubmission(title)/deleteSubmission(title)) mirroring the admin-tree tags / collections drivers' postures; (b) a named- row resolver via two-parent-walk (getSubmissionByTitle(title)walkspage.locator('h3').filter({ hasText: title }).first().locator('..').locator('..')) — 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"]) 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')) with a literal-union TypeScript parameter and start-anchor regex pattern; (e) a three-modal getter triplet (detailModalbare-.first(),editModal.filter({ has: this.page.locator('#name') })form-field-presence-scoped,deleteDialog.filter({ hasText: /delete/i })body-text-scoped) — the first client-tree driver to document multiple[role="dialog"]re-evaluating Locator getters with distinct scoping strategies; (f) a navigation-shelf header pair (heading,newSubmissionLink,trashLink); and (g) a search- input field (searchInput) pinned viainput[type="text"][placeholder*="earch"]— substring-on-placeholderselector dropping the leading capital. Pinned to the consuming specs atapps/web-e2e/tests/client/submissions.spec.ts(three flows) 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). Pinned to the co-tenant API smoke spec atapps/web-e2e/tests/api/admin-clients-stats-query.spec.ts. Linked fromdocs/index.mdunder the E2E references section. Subsequent rollouts in this client/ subtree will turn tosubmit.page.tsandtrash.page.ts. -
apps/web-e2e/tests/apiAddedadmin-clients-stats-query.spec.ts— query-param surface smoke for the admin-only enhanced-client- statistics endpoint atapps/web/app/api/admin/clients/stats/route.ts. 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) — distinct from thecheckAdminAuth()three-step gate theadmin/dashboard/stats,admin/users/stats,admin/clients/dashboard,admin/geo-analytics,admin/location-index, andadmin/roles/[id]/permissionssiblings use, the two-stepif (!session?.user)gate theadmin/roles/statssibling uses, and the single- stepif (!session?.user?.isAdmin)401-collapsed gate theadmin/items/statssibling uses. The unauthenticated branch returns 401 with the bare'Unauthorized'envelope; the catch returns'Failed to fetch client stats'(a route-specific message distinct from every other admin-tree stats route's catch). The handler signature is the bareGET()(norequestparameter) — symmetric withadmin/roles/statsandadmin/users/stats. Walks 80+ defensive query-key permutations covering pagination keys,?status=…per-status drill-down (the success response includes per-status counts —activeClients/inactiveClients/suspendedClients/trialClients), per-client drill-down (?clientId=…,?client_id=…), time- window filters for thegrowthsection'snewClientsToday/newClientsThisWeek/newClientsThisMonthfields (?from=…,?to=…,?since=…,?until=…,?days=…), content- projection keys for theoverview/growth/distributionsub-objects (?include=…,?fields=…,?select=…,?exclude=…),?isAdmin=…boolean filter,?sortBy=…/?sortOrder=…order-targeting keys,?search=…free-text filter with XSS-shaped / SQL-shaped values, admin-impersonation keys, magic-token bypass keys, admin-override keys, cache-busting keys,?locale=…/?lang=…i18n keys, repeated keys, and bogus / typo'd keys. Asserts every permutation round-trips to a status< 500(the route's two-step gate fires before anygetEnhancedClientStats()call), the canonical 401 /{ success: false, error: 'Unauthorized' }envelope on the no-arg unauth branch, status invariance across query permutations, status invariance under cookie /X-*header injection, and the route's unique combination of the bare'Unauthorized'first-step-gate message AND the catch's'Failed to fetch client stats'route- specific message (distinct from every other admin- tree stats route's envelope). Sits alongside the twenty prior admin-tree query-smoke specs (now 24 total). -
docs/pluginsAddedclient-settings-page-object.md— the third per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/, paired withapps/web-e2e/page-objects/client/settings.page.tsand 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 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-route navigation 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. Documents the full surface for theClientSettingsPagedriver — the fivereadonlyLocator fields and the singlenavigate()method. Pinned toapps/web-e2e/tests/client/settings.spec.ts(three flows — 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). Includes the "WhyClientSettingsPageextendsBasePage" three- reason analysis; the "Why a singlenavigate()(and not multiple)" three-reason analysis; the "Why three pre-bound link Locators" three-reason analysis; the "Why the heading is pinned tolevel: 1" three-reason analysis; cross- references to theclient-dashboard-page-object.mdandclient-profile-page-object.mdrollout precedents and to the related auth-treesignin-page-object.md/auth-fixture.mdreferences; and a "What it does not contain" six-bullet enumeration of the deliberate omissions. 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. -
apps/web-e2e/tests/apiAddedadmin-location-index-query.spec.ts— a query- param + method surface smoke for the admin-only location-index endpoint atapps/web/app/api/admin/location-index/route.ts. The route is the second admin-tree route the smoke layer covers that documents thecheckAdminAuth()three-step guard from@/lib/auth/admin-guard.tsAND the first admin-tree route covered by the smoke layer that exposes BOTH aGETAND aPOSThandler. The GET handler reads NO documented post-gate query params (the smallest documented post-gate query surface of any admin-tree route the smoke layer covers, contrasting/api/admin/clients/dashboard's eleven). The POST handler reads exactly one body field (action) with two valid destructive values ('rebuild're-indexes every item,'clear'truncates the index table); both action paths fire AFTER the gate. The spec walks the unauthenticated branches of BOTH handlers and pins the canonical 401 envelope plus a negative- shape assertion that the body must NOT echo the second-step'User ID not found'/ third-step'Insufficient permissions'/ post-gate'Invalid action.'messages. Sweeps GET permutations (impersonation / token / bypass / override /?action=-leak / Accept-header / cookie-header), POST permutations (every action value, missing action, body keys for impersonation / token / bypass), Content-Type fallback (text/plain, urlencoded), and the GET-vs-POST envelope-equivalence invariant. -
docs/pluginsAddedclient-profile-page-object.md— the second per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/client/, paired withapps/web-e2e/page-objects/client/profile.page.tsand 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). Documents the full surface for theClientProfilePagedriver — the ninereadonlyLocator fields and the two navigation methods. Pinned toapps/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). Includes the "WhyClientProfilePageextendsBasePage" three- reason analysis; the "Why two navigation methods (and not one with a parameter)" three-reason analysis; the "Why all input fields use camelCase id selectors" three-reason analysis; the "Why the form is page-level (and not modal-scoped)" three- reason analysis; cross-references to theclient-dashboard-page-object.mdrollout-template precedent and to the related admin-tree id- selector-posture variants; and a "What it does not contain" six-bullet enumeration of the deliberate omissions. -
apps/web-e2e/tests/apiAddedadmin-clients-dashboard-query.spec.ts— a query- param surface smoke for the admin-only clients- dashboard endpoint atapps/web/app/api/admin/clients/dashboard/route.ts. The route is 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. The helper folds three branches into one helper call: no session → 401'Unauthorized', missing user.id → 401'User ID not found', not admin → 403'Insufficient permissions'. The route reads ELEVEN documented post-gate query params (page,limit,search,status,plan,accountType,provider,createdAfter,createdBefore,updatedAfter,updatedBefore) — the largest documented post-gate query surface of any admin-tree route the smoke layer covers, exceeding the reports route's six query params and the items / featured-items routes' three. The four date-bound parameters use a per-boundparseDateBound(value, bound)helper that supports both YYYY-MM-DD and ISO 8601 formats. The spec walks the unauthenticated branch and pins the canonical 401 envelope plus a negative-shape assertion that the body must NOT echo the second- step'User ID not found'or third-step'Insufficient permissions'messages, then sweeps pagination / status / plan / accountType / provider / date-bound / impersonation / token / bypass / per-row-targeting / SQL-injection-themed search payload / Accept-header / cookie-header permutations. The sweep mirrors the shape of the sibling admin-gated query-smoke specs. -
docs/pluginsAddedclient-dashboard-page-object.md— per-source-file reference for the Playwright e2e suite's authenticated-client dashboard driver paired withapps/web-e2e/page-objects/client/dashboard.page.ts. Opens the client-tree page-object docs rollout (1-of-6) mirroring the seventeen-file admin-tree rollout that completed atadmin-tags-page-object.md. Documents the smallest-possible-surface posture (only anavigate()method plus three pre-boundLocatorfields —heading/statsGrid/welcomeText), thegetByRole('heading', { name: /dashboard/i })locale-tolerant case-insensitive substring resolver for the dashboard heading, the.grid.grid-cols-1.md\\:grid-cols-2.lg\\:grid-cols-4Tailwind responsive class chain anchor for the stats grid, thegetByText(/welcome back/i)greeting-string-tolerant resolver, the.first()strict-mode-correctness append on every Locator field, and the cross-references tobase-page-object.md,auth-fixture.md(theclientPageauthenticated-page fixture consuming specs use),signin-page-object.md(the auth-tree driver consuming specs depend on for the authenticatedclientPagefixture's setup precondition),admin-dashboard-page-object.md(the admin-area dashboard sibling concept),discover-page-object.md(another smallest-possible-surface page-object posture this driver mirrors),e2e-tsconfig.md(theincludeglob),playwright-config.md(thebaseURLposture), andfixtures-index.md. Pinned to the consuming spec atapps/web-e2e/tests/client/dashboard.spec.ts(three flows: authenticated client can access dashboard, unauthenticated user is redirected to/auth/signin, dashboard heading visible) and the co-tenant API smoke spec atapps/web-e2e/tests/api/client-dashboard-stats-query.spec.ts. Linked fromdocs/index.mdunder the E2E references section. Subsequent rollouts in this client/ subtree will turn toprofile.page.ts,settings.page.ts,submissions.page.ts,submit.page.ts, andtrash.page.ts. -
apps/web-e2e/tests/apiAddedadmin-users-stats-query.spec.ts— query-param surface smoke for the admin-only user-statistics endpoint atapps/web/app/api/admin/users/stats/route.ts. Pins the route'scheckAdminAuth()shared three-step gate (the same gate theadmin/dashboard/stats,admin/geo-analytics,admin/clients/dashboard,admin/location-index, andadmin/roles/[id]/permissionssiblings use) with the unauthenticated branch returning 401 and the bare'Unauthorized'message — distinct from the second-step gate's'User ID not found'message (reachable only by an authenticated session withoutuser.id), the third-step gate's'Insufficient permissions'message (reachable only by an authenticated non-admin), the'Forbidden'message theadmin/roles/statsroute's two-stepauth()chain emits on its third step, and the'Unauthorized. Admin access required.'message the sponsor-ads route's purpose-built guard emits. The handler signature is the bareGET()(norequestparameter) — symmetric with theadmin/dashboard/stats,admin/geo-analytics,admin/clients/dashboard,admin/location-index, andadmin/roles/[id]/permissionssiblings that route throughcheckAdminAuth()— narrowing the request surface to zero. Walks 90+ defensive query- key permutations covering pagination keys (?page=…,?limit=…), per-role drill-down keys (?role=…,?roleId=…— the success response includesroleDistributionso a future contributor might add a per-role drill-down),?status=…active/inactive enum filter (the siblingadmin/usersroute accepts this), GDPR-consent filter (?gdprConsentGiven=…— also siblingadmin/users), per-plan filter (?subscriptionPlanId=…— also siblingadmin/users),?isAdmin=…boolean filter,?sortBy=…/?sortOrder=…order-targeting keys,?search=…free-text filter with XSS-shaped / SQL-shaped values, time-window filters (?from=…,?to=…,?since=…,?until=…,?days=…— the success response includesrecentRegistrationshard-coded to "last 30 days" today),?topActiveUsersLimit=…tuning override (thetopActiveUsersarray length ismaxItems: 10today), admin-impersonation keys (?userId=…,?asUser=…,?impersonate=…), magic-token bypass keys (?token=…,?secret=…,?api_key=…,?authorization=…,?session=…,?adminToken=…), admin-override keys (?bypass=…,?admin=…,?override=…,?force=…), cache-busting keys (?refresh=…,?cache=…,?nocache=…),?locale=…/?lang=…i18n keys, content- projection keys (?fields=…,?select=…,?include=…,?exclude=…), repeated keys, and bogus / typo'd keys. Asserts every permutation round-trips to a status< 500(the route's three-step gate fires before anyuserRepository.getStats()call), the canonical 401 /{ success: false, error: 'Unauthorized' }envelope on the no-arg unauth branch, status invariance across query permutations, status invariance under cookie /X-*header injection, and the route's unique combination of the bare'Unauthorized'first-step-gate message AND the bare-GET()handler signature (distinct from every other admin-tree route's envelope-and- signature combination). Sits alongside the nineteen prior admin-tree query-smoke specs (admin-categories-query.spec.ts,admin-clients-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-navigation-query.spec.ts,admin-notifications-query.spec.ts,admin-reports-query.spec.ts,admin-roles-stats-query.spec.ts,admin-settings-query.spec.ts,admin-sponsor-ads-query.spec.ts,admin-tags-all-query.spec.ts,admin-tags-query.spec.ts,admin-users-query.spec.ts). -
apps/web-e2e/tests/apiAddedadmin-navigation-query.spec.ts— query-param surface smoke for the admin-only navigation-config endpoint atapps/web/app/api/admin/navigation/route.ts. Pins the route's single-step!session?.user?.isAdmin→ 401{ error: 'Unauthorized' }gate (the bare-key envelope variant — without thesuccess: falsediscriminator key, distinct from the bare-message- with-success-key envelope{ success: false, error: 'Unauthorized' }theadmin/tagsroute emits) and the route's use ofgetCachedApiSession(req)(the cached-session helper that caches the session lookup per-request, symmetric with theadmin/settingsroute — distinct from theauth()chain every other admin-tree route uses). Walks 60+ defensive query-key permutations covering?type=…/?placement=…filter keys (the route does NOT read them today but a future contributor might add them as filters to scope the response to onlycustom_headeror onlycustom_footer), admin-impersonation keys (?asAdmin=…,?as=…,?asUser=…,?impersonate=…), magic-token bypass keys (?token=…,?secret=…,?api_key=…,?authorization=…,?session=…,?adminToken=…), admin-override keys (?bypass=…,?admin=…,?override=…,?force=…),?locale=…/?lang=…i18n keys (a future contributor might add localized navigation responses), cache-busting keys (especially relevant given the route readsconfigManager.getConfig()which may be cached —?refresh=…,?cache=…,?nocache=…,?ttl=0),?path=…XSS-shaped values (the PATCH handler validates each item's path viaisValidNavigationPath(path)to defend againstjavascript:/data:/vbscript:/ protocol-relative//evil.comschemes — the unauth-branch contract must stay invariant under XSS-shaped query values when applied to the GET branch), content-projection keys (?fields=…,?select=…,?include=…), pagination keys (?page=…,?limit=…— the route returns the full config arrays today, but a future contributor might add pagination for very long navigation lists), repeated keys, and bogus / typo'd keys. Asserts every permutation round-trips to a status< 500(the route's single-step gate fires before anyconfigManager.getConfig()call), the canonical 401 /{ error: 'Unauthorized' }envelope on the no-arg unauth branch, status invariance across query permutations, status invariance under cookie /X-*header injection, and the route's unique combination of the bare'Unauthorized'message AND the absence of asuccessdiscriminator key (distinct from every other admin-tree route's envelope). Sits alongside the eighteen prior admin-tree query-smoke specs (admin-categories-query.spec.ts,admin-clients-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-notifications-query.spec.ts,admin-reports-query.spec.ts,admin-roles-stats-query.spec.ts,admin-settings-query.spec.ts,admin-sponsor-ads-query.spec.ts,admin-tags-all-query.spec.ts,admin-tags-query.spec.ts,admin-users-query.spec.ts). -
docs/pluginsAddedadmin-tags-page-object.md— 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. Paired withapps/web-e2e/page-objects/admin/tags.page.tsand 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'sgetItemByName(name)resolver-only posture and from the collections driver'seditCollection(name)/deleteCollection(name)which uses a single- parent walk); (b) a<div>-anchored named-row resolver with a^${name}start-anchor regex — the broadest possible row-anchor in the admin tree (compared to the items driver's<h4>heading- anchor and the collections driver's named-cell heading-anchor) 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 (reflecting the production source's contract where the tag's stable id is auto-derived from the name in create mode but can be explicitly overridden). Documents the full surface for theAdminTagsPagedriver — the tworeadonlyLocator fields (heading,addTagButton), the four methods (navigate(),getTagByName(name),editTag(name),deleteTag(name)), the one composite helper (fillTagForm(data)), and the seven getters (tagFormModal,tagIdInput,tagNameInput,statusToggle,cancelButton,createTagButton,updateTagButton). Pinned toapps/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). Includes the "WhyAdminTagsPageextendsBasePage" three-reason analysis; the "WhygetTagByName(name)uses a^${name}start- anchor regex" three-reason analysis (the tags page renders rows where the tag name is the first text content, the bare<div>element-selector is the broadest row-anchor, the runtime-built RegExp does NOT include theiflag because tag names are case-sensitive in storage); the "WhytagFormModaluses a.fixed.inset-0.z-50Tailwind-overlay selector" three-reason analysis; the "WhytagIdInput/tagNameInputuse kebab-case id selectors" three-reason analysis; the "WhystatusToggleis modal-scoped" three- reason analysis; the "WhyfillTagForm(data)uses an optionalidparameter" three-reason analysis; and a "What it does not contain" five- bullet enumeration of the deliberate omissions. This entry completes the admin-tree page-object docs rollout; subsequent rollouts should turn to theapps/web-e2e/page-objects/auth/and remainingapps/web-e2e/page-objects/client/subtrees. -
apps/web-e2e/tests/apiAddedadmin-tags-all-query.spec.ts— a query-param surface smoke for the admin-only Git-CMS tags-listing endpoint atapps/web/app/api/admin/tags/all/route.ts. The route is the first admin-tree route the smoke layer covers that documents (1) thegetCachedItems({ lang })Git-based CMS reader — distinct from every other admin-tree route's database-backed posture (the helper reads from the per-locale tag list stored in the Git-based content repository cloned fromDATA_REPOSITORYinto.content/); (2) a?locale=query param with type-coercion validation — the only documented query key, with a defensivetypeof locale !== 'string'narrowing that can never fire today (sincesearchParams.get(...)always returnsstring | nulland the|| 'en'default coerces null to a string before the typeof check); and (3) the paired tags-data-route posture — this route is the read-only Git-CMS variant of the database- backed/api/admin/tagslisting route. The spec walks the unauthenticated branch and pins the canonical{ success: false, error: 'Unauthorized' }401 envelope (the bare'Unauthorized'message, NOT'Unauthorized. Admin access required.'/'Forbidden'), then sweeps?locale=(with English / French / Spanish / German / Arabic / Chinese variants) /?lang=/?language=/?l=/?page=/?limit=/?status=/?active=/?fields=/?refresh=/?userId=/?token=/?bypass=/?repo=/?branch=/?commit=(a Git-CMS-source bypass vector category) / Accept-header / repeated-key / cookie-header permutations against the no-arg baseline. The sweep mirrors the shape of the sibling admin-gated query-smoke specs. -
docs/pluginsAddedadmin-surveys-page-object.md— the sixteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/surveys.page.tsand 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 undefined (reading '…')failure 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. -
apps/web-e2e/tests/apiAddedadmin-tags-query.spec.ts— query-param surface smoke for the admin-only tag-listing endpoint atapps/web/app/api/admin/tags/route.ts. Pins the route's 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). 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). Walks 60+ query permutations covering pagination (?page=…,?limit=…), admin-impersonation keys (?asAdmin=…,?as=…,?asUser=…,?impersonate=…), magic-token bypass keys (?token=…,?secret=…,?api_key=…,?authorization=…,?session=…,?adminToken=…), admin-override keys (?bypass=…,?admin=…,?override=…,?force=…), status-filter keys (?isActive=…,?includeInactive=…), free-text filter keys (?search=…,?q=…), order-targeting keys (?orderBy=…,?sortBy=…,?sortOrder=…), per-row-targeting keys (?tagId=…,?id=…), content-projection keys (?fields=…,?select=…,?include=…), cache-busting keys (?refresh=…,?cache=…,?nocache=…), i18n keys (?locale=…,?lang=…), repeated keys, and bogus / typo'd keys. Asserts every permutation round-trips to a status< 500(the route's single-step gate fires before any service-layer call), the canonical 401 /{ success: false, error: 'Unauthorized' }envelope on the no-arg unauth branch, status invariance across query permutations, status invariance under cookie /X-*header injection, and the route's unique combination of the bare'Unauthorized'message AND thesuccess: falsediscriminator key (distinct from every other admin-tree route's envelope). Pinned to the co- tenant page-object reference atdocs/plugins/admin-surveys-page-object.mdvia theindex.mdcross-reference. Sits alongside the seventeen prior admin-tree query-smoke specs (admin-categories-query.spec.ts,admin-clients-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-notifications-query.spec.ts,admin-reports-query.spec.ts,admin-roles-stats-query.spec.ts,admin-settings-query.spec.ts,admin-sponsor-ads-query.spec.ts,admin-users-query.spec.ts). -
docs/pluginsAddedadmin-sponsorships-page-object.md— the fifteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/sponsorships.page.tsand 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. -
apps/web-e2e/tests/apiAddedadmin-sponsor-ads-query.spec.ts— query-param surface smoke for the admin-only sponsor-ads listing endpoint atapps/web/app/api/admin/sponsor-ads/route.ts. Pins the route's 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). Pins the AFTER-the-auth-gate ordering ofvalidatePaginationParams(searchParams)(a regression that swaps the order would surface as 400 instead of 401 on the unauth branch for invalid pagination) ANDquerySponsorAdsSchema.safeParse(queryParams)(a regression that swaps the order would surface as 400 'Invalid query parameters' instead of 401 on the unauth branch when the query is malformed). Pins the?status=enum filter (valid values: pending_payment, pending, rejected, active, expired, cancelled), the?interval=enum filter (valid values: weekly, monthly), the?sortBy=enum (valid values: createdAt, updatedAt, startDate, endDate, status), the?sortOrder=enum (valid values: asc, desc), and the?search=free-text filter — all of which are read AFTER the auth gate. -
docs/pluginsAddedadmin-settings-page-object.md— the fourteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/settings.page.tsand 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); (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. Documents the full surface for theAdminSettingsPagedriver. Pinned toapps/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). Includes the "WhyAdminSettingsPageextendsBasePage" three-reason analysis; the "WhyopenSection(sectionName)uses a runtime-builtRegExp" three-reason analysis (the helper accepts any section name as a string parameter, theiflag is preserved, no per-section TypeScript union because the section list is more fluid than items / reports status lists); the "Whyswitchesuses the[role="switch"]ARIA-role selector" three-reason analysis (HeroUI'sSwitchemitsrole="switch", the WAI-ARIAswitchrole is screen-reader- canonical, future migration to native checkbox would be a production-source change); the "Whyselectsuses the bareselectelement selector" three-reason analysis (native<select>elements in Header / Footer sections, HeroUISelectopens to[role="listbox"]popup that's not the trigger, two-Locator pair documents the canonical settings-form contract); the "Why nocloseSection(sectionName)helper" three-reason analysis (no consuming spec closes a section, HeroUI accordion uses the same trigger button for open/close, future state-aware helpers can compose on top); cross-references to all thirteen prior admin-tree page-object docs; and a "What it does not contain" six-bullet enumeration of the deliberate omissions. -
apps/web-e2e/tests/apiAddedadmin-settings-query.spec.ts— a query-param surface smoke for the admin-only settings-fetching endpoint atapps/web/app/api/admin/settings/route.ts. The route is the first admin-tree route the smoke layer covers that documents (1) thegetCachedApiSession(req)cached-session helper — a custom variant ofauth()that caches the session lookup per-request (distinct from every other admin-tree route's bareauth()posture); and (2) a bare{ error: '...' }envelope (NOT the{ success: false, error: '...' }shape every other admin-tree route emits) — a single-key envelope without thesuccessdiscriminant. The spec walks the unauthenticated branch and pins the canonical{ error: 'Unauthorized' }401 envelope PLUS a negative-shape assertion that the body must NOT include asuccesskey, then sweeps?section=/?key=/?expand=/?refresh=/?userId=/?token=/?bypass=/ Accept-header / repeated- key / cookie-header /X-Forwarded-User-header permutations against the no-arg baseline. The route reads fromconfigManager.getConfig()(a YAML- config-file-backed singleton) rather than from a database — distinct from every other admin-tree route's async DB query posture. The spec is unique in that it pins both the 401 status AND the bare-envelope shape (rejecting both'Unauthorized. Admin access required.'and'Forbidden'alternatives, plus the{ success: false, error }envelope shape every sibling admin-tree route emits). The sweep mirrors the shape of the siblingadmin-categories-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-notifications-query.spec.ts,admin-reports-query.spec.ts,admin-roles-stats-query.spec.ts,admin-users-query.spec.tssmoke specs. -
docs/pluginsAddedadmin-roles-page-object.md— the thirteenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/roles.page.tsand 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')). Pinned toapps/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 to all twelve prior admin-tree page-object docs and to the consuming spec. -
apps/web-e2e/tests/apiAddedadmin-roles-stats-query.spec.ts— a query-param surface smoke for the admin-only role-statistics endpoint atapps/web/app/api/admin/roles/stats/route.ts. The route is admin-gated viaauth()+ a two-step check that resolves the unauthenticated and authenticated-non-admin branches into distinct status codes (401 vs 403) — distinct from the siblingadmin/clients/admin/comments/admin/companies/admin/usersroutes' single-step!session?.user?.isAdmin→ 401 'Unauthorized' gate AND from theadmin/reportsroute's single-step!session?.user?.isAdmin→ 403 'Forbidden' gate. The handler signature is the bareGET()(norequestparameter) — distinct from every other admin-tree route's signed handler signature; this is the strongest possible protection against query-param-driven bypass regressions because a contributor who wants to add a query-param-driven bypass must first widen the handler signature. The 401 envelope carries the bare'Unauthorized'message (NOT'Unauthorized. Admin access required.'like the sponsor-ads route, NOT the bare'Forbidden'like the reports route). The spec walks the unauthenticated branch with 60+ query permutations covering pagination keys, status / isAdmin / role-targeting filters, impersonation keys (?as=,?asUser=,?impersonate=), magic-token bypass keys (?token=,?secret=,?api_key=,?authorization=), admin-override keys (?bypass=,?admin=,?override=,?force=), per-role-targeting keys (?roleId=,?roleName=), time-range filters, cache-busting keys (?refresh=,?fresh=,?cache=), i18n keys (?locale=,?lang=), repeated-key permutations, and bogus / typo'd keys, then pins the canonical{ success: false, error: 'Unauthorized' }401 envelope and verifies that the message does NOT echo any other admin-tree route signature. -
docs/pluginsAddedadmin-reports-page-object.md— the twelfth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/reports.page.tsand 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). Documents the full surface for theAdminReportsPagedriver — the tworeadonlyLocator fields (heading,searchInput), the three methods (navigate(),selectStatusTab(status),searchReports(term)), and the three getters (reviewDialog,reviewButtons,reportCards). Pinned toapps/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). Includes the "WhyAdminReportsPageextendsBasePage" three-reason analysis; the "WhyselectStatusTab(status)usesgetByRole('button')" three-reason analysis (the reports page emits the status filter as<button>elements, symmetric with the bulk-action toolbar's button posture, a future migration to[role="tab"]would be a production- source change); the "WhyselectStatusTab(status)uses a prefix-match^${status}regex" three-reason analysis (the status-tab labels include per-tab counts likePending (12), disambiguation against future per-action buttons, symmetric with the items driver's posture); the "WhyreviewDialoguses a bare[role="dialog"]selector" three-reason analysis (HeroUI's per-versionaria-modaldrift,.first()is the strict-mode-correctness defence, no production-source change required); the "WhyreviewButtonsis a multi-resolution Locator" three- reason analysis; the "WhyreportCardsuses a.border-l-4Tailwind-utility selector" three- reason analysis (production source does not emit ARIA roles on report cards, theborder-l-4utility is the per-card visual anchor, future migration to[role="article"]is a production-source change); cross-references to all eleven prior admin-tree page-object docs; and a "What it does not contain" five-bullet enumeration of the deliberate omissions (nogetByTestIdselectors, no per-card Locator- factory beyond thereviewButtonsmulti-resolution Locator, noclickReview(reportId)/dismissReport(reportId)/resolveReport(reportId, notes)flow helpers, noassertCardCount(n)/assertEmptyState()invariant helpers, noclearSearch()reset helper). -
apps/web-e2e/tests/apiAddedadmin-reports-query.spec.ts— a query-param surface smoke for the admin-only reports-listing endpoint atapps/web/app/api/admin/reports/route.ts. The route is admin-gated viaauth()+session.user.isAdminbut with a unique 403-on-missing-session contract — distinct from every other admin-gated route in the smoke layer. The single-step gate!session?.user?.isAdminfolds the missing-session and missing-admin-bit branches into a single 403 response with the bare'Forbidden'message — distinct from the notifications route's two-step gate (which emits 401 'Unauthorized' for missing session and 403 'Forbidden' for missing admin bit). The handler signature isGET(request: Request)(the bareRequesttype, not the Next-specificNextRequesttype) and reads SIX documented query params after the gate (page,limit,search,status,contentType,reason) — the largest documented post-gate query surface of any admin-tree route the smoke layer covers. The route uses inlineNumber()parsing +Math.max()/Math.min()clamps for thepage/limitparams (distinct from thevalidatePaginationParams(...)utility the sibling routes use) and inlineVALID_*.includes(...)checks against the schema's enum constants for thestatus/contentType/reasonparams (distinct from the Zod-schema posture). The route runscheckDatabaseAvailability()BEFORE the auth gate and emits an explicitruntime = 'nodejs'Next.js export. The spec walks the unauthenticated branch and pins the canonical{ success: false, error: 'Forbidden' }403 envelope (NOT 401, NOT'Unauthorized. Admin access required.'), then sweeps?page=/?limit=/?search=(with SQL-injection-themed payloads) /?status=/?contentType=/?reason=/?userId=/?token=/?bypass=/ Accept-header / repeated-key / cookie-header permutations against the no-arg baseline. The spec is unique in that it pins 403 (NOT 401) plus the bare'Forbidden'(NOT'Unauthorized'/ NOT'Unauthorized. Admin access required.') error message — a regression that switches the gate to the two-stepsession?.user?.idthensession.user.isAdminpair would surface here as a status divergence between the expected 403 and the unexpected 401. The sweep mirrors the shape of the siblingadmin-categories-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-notifications-query.spec.ts,admin-users-query.spec.tssmoke specs. -
docs/pluginsAddedadmin-notifications-page-object.md— the eleventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/notifications.page.ts. The notifications driver is the first admin-tree driver that does NOT extendBasePage— by design, because header-chrome dropdowns do not need the page-navigation helpersBasePageprovides. Documents the four-readonly-Locator-field core surface (bellButton,dropdown,refreshButton,closeButton), the two-action surface (open()/close()), and the five-getter dropdown-content surface (markAllReadButton,unreadBadge,notificationItems,viewAllButton,emptyState). Pinned to the consuming spec atapps/web-e2e/tests/admin/notifications.spec.tsand the co-tenant smoke atapps/web-e2e/tests/api/admin-notifications-query.spec.ts. -
apps/web-e2eAddedapps/web-e2e/tests/api/admin-clients-query.spec.ts— query-param surface smoke spec for the admin-only client-profiles-listing endpoint atapps/web/app/api/admin/clients/route.ts. Pins the single-stepsession?.user?.isAdmingate's 401 + bare{ error: 'Unauthorized' }envelope on the unauth branch across pagination, search, status, plan, accountType, and provider param permutations, plus the per-bypass-key invariants (?asAdmin=…,?token=…,?bypass=…,?override=…) for future contributors. -
docs/pluginsAddedadmin-items-page-object.md— the tenth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/items.page.tsand 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 the 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 parameter, search-flow mutators, per-row resolution, per-row action-menu interactions, and per-row selection; (c) a two-modal-getter posture —rejectModalandbulkConfirmDialog, both pinned to the[role="dialog"][aria-modal="true"]composite- attribute selector withhasTexttext filters; (d) a<input>-id-bound modal-scoped input getter —rejectionReasonInputresolves viathis.rejectModal.locator('#rejectionReason'), the first admin-tree driver to scope anid-selector through a parent modal-Locator getter; (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"]) defending against the production-source's inconsistent capitalisation between the lowercase HeroUI default and the capitalised English translation; (g) a partial-aria-label-substring- anchored toolbar selector ([role="toolbar"][aria-label*="ulk"]) using a case-sensitive sub-word substringulkthat survives bothBulkandbulkcapitalisation drift while remaining strict enough to disambiguate against any other toolbar; and (h) exact-match^approve$/^reject$/^delete$regexes for the per-action bulk triggers (distinct from thebulkDeselectButton's substring/deselect/iposture which tolerates the currentDeselect alllabel and a futureDeselectlabel). Documents the full surface for theAdminItemsPagedriver — the elevenreadonlyLocator fields (heading,addItemButton,searchBar,itemsList,pagination,selectAllCheckbox,bulkActionBar,bulkApproveButton,bulkRejectButton,bulkDeleteButton,bulkDeselectButton), the nine methods (navigate(),selectStatusTab(status),searchItems(term),clearSearch(),getItemByName(name),openActionsMenu(itemName),clickAction(actionName),selectItem(itemName), plus the inheritedgoto()/gotoLocalized()/waitForPageReady()/getTitle()), and the three getters (rejectModal,rejectionReasonInput,bulkConfirmDialog). Pinned to the four consuming spec files — the largest spec-fan-out of any admin- tree driver to date:apps/web-e2e/tests/admin/items.spec.ts,apps/web-e2e/tests/admin/items-crud.spec.ts,apps/web-e2e/tests/admin/items-filter.spec.ts, andapps/web-e2e/tests/admin/items-review.spec.ts. Includes the "WhyAdminItemsPageextendsBasePage" three-reason analysis; the "WhysearchBarusesgetByRole('searchbox')" three- reason analysis (the items page emits<input type="search">, the featured-items driver emits<input type="text">, thesearchboxrole surfaces a screen-reader-accessible "search" announcement); the "WhygetItemByName(name)uses a double-..parent walk" three-reason analysis (the items list is not<table>-rendered, the per-row container is two parents up from the per- item<h4>heading, the double-..walk is robust against future production-source changes); the "WhyselectStatusTab(status)uses a five-element TypeScript union" three-reason analysis (the five status-tab labels are the only canonical values, type-narrowing surfaces typos at compile time, future status additions are explicit); the "Whypaginationuses a multi-attribute OR-selector" three-reason analysis (HeroUI emits lowercasearia-label="pagination", localised translation may capitalise, OR-selector tolerates both); the "WhybulkActionBaruses a[aria-label*="ulk"]partial substring" three-reason analysis (capitalisation drift tolerance, disambiguation against other toolbars, no production-source change required); the "WhybulkApprove/bulkReject/bulkDeleteuse exact-match^…$regexes" three-reason analysis; the "WhybulkDeselectButtonuses a substring (not exact- match) regex" three-reason analysis (production- source label isDeselect all, future shortened label is plausible, toolbar-scope is the second- line defence); the "WhyrejectModalandbulkConfirmDialogare getters" three-reason analysis; cross-references to all nine prior admin-tree page-object docs and to the public-tree drivers; and a "What it does not contain" five- bullet enumeration of the deliberate omissions (nogetByTestIdselectors, no per-row Locator- factory beyondgetItemByName(name), noclickReject(itemName, reason)composite flow helper, noassertItemPresent(name)/assertItemAbsent(name)invariant helpers, noclickPaginationPage(page)/nextPage()/prevPage()pagination helpers). -
apps/web-e2e/tests/apiAddedadmin-notifications-query.spec.ts— a query-param surface smoke for the admin-only notifications- listing endpoint atapps/web/app/api/admin/notifications/route.ts. The route is the first admin-tree route the smoke layer covers that documents a two-step session gate — distinct from every other admin- tree route's single-step gate. The handler signature is the zero-argument Next 16 form (the route does not take aNextRequestargument and reads nosearchParamsat all today). The route applies two distinct checks in order — firstsession?.user?.id(401 with the bare'Unauthorized'message if missing), thensession.user.isAdmin(403 with the bare'Forbidden'message if missing) — distinct from every other admin-tree route's single-step gate. The spec walks the unauthenticated branch and pins the canonical{ success: false, error: 'Unauthorized' }401 envelope (NOT 403), then sweeps?page=/?limit=/?unreadOnly=/?status=/?type=/?since=/?until=/?userId=/?token=/?bypass=/ Accept-header / repeated-key / cookie-header permutations against the no-arg baseline. The spec is unique among the admin-tree query-smoke specs in that it pins both the 401 status AND the'Unauthorized'(not'Forbidden', not'Unauthorized. Admin access required.') error message — the two-step gate emits distinct messages depending on which gate fired. A regression that switches the gate order (e.g. checksisAdminbeforeid, which would silently bypass the 401 status becausesession?.user?.isAdminon anullsession resolves toundefinedand the negation catches it as "not admin", returning 403 instead of 401) would surface here as a status divergence between the expected 401 and the unexpected 403. The sweep mirrors the shape of the siblingadmin-categories-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-query.spec.ts,admin-items-stats-query.spec.ts,admin-users-query.spec.tssmoke specs. -
docs/pluginsAddedadmin-item-form-page-object.md— the ninth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/item-form.page.tsand 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. Documents the full surface for theAdminItemFormPagedriver — the nineteen per-modalreadonlyLocator fields and the nineasynchelper methods. Pinned toapps/web-e2e/tests/admin/items-crud.spec.ts(a full create-then-edit-then-delete flow over the admin items management surface). -
apps/web-e2e/tests/apiAddedadmin-items-query.spec.ts— query-param surface smoke for the admin-gated items list endpoint atapps/web/app/api/admin/items/route.ts. The route reads seven documented query params (page,limit,status,search,categories,tags,sortBy,sortOrder) after thesession?.user?.isAdminadmin gate fires, so every call from the spec's unauthenticated context round-trips to a 401 with the canonical{ success: false, error: 'Unauthorized. Admin access required.' }envelope regardless of the query string. The spec pins (1) a 401 baseline assertion, (2) a "stable status across query permutations" assertion, (3) per-param "does NOT bypass the admin gate" assertions for each of the seven documented params plus the impersonation / token / bypass / format / Accept-header / repeated-key / NextRequest cookie side channels, and (4) a< 500no-server-error sweep across the full path table. Mirrors the shape of the sibling admin-gated query smokes (admin-categories-query.spec.ts,admin-collections-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-export-sample-query.spec.ts,admin-items-stats-query.spec.ts,admin-users-query.spec.ts). -
docs/pluginsAddedadmin-data-export-page-object.md— the eighth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/data-export.page.tsand 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 (the^…$anchors are required because the format-button accessible names are short three- to four-character tokens and a substring regex would match accidentally on other buttons), (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 (distinct from every other admin-tree driver's.first()-pinned Locator postures because the data-export widget intentionally renders multiple export trigger buttons), 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, so Playwright resolves the first matching half and a future production-source change that adds therole="progressbar"ARIA attribute lights up the accessibility-tree-canonical posture without breaking the existing positional fallback. Documents the full surface for theAdminDataExportPagedriver — the sixreadonlyLocator fields (heading,csvButton,jsonButton,includeMetadataCheckbox,exportButtons,progressBar) and thenavigate()shortcut that closes over the inheritedgoto('/admin'). Pinned toapps/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); the "WhyAdminDataExportPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgoto, global header / footer / nav-link chrome surfaced for free, post- navigationwaitForPageReadystabiliser); the "WhycsvButton/jsonButtonuse^CSV$/^JSON$exact-match regexes" three-reason analysis (the format-trigger accessible names are short three- to four-character tokens, the/icase-insensitivity flag is preserved, symmetric with the public-tree view-toggle driver's exact-match posture); the "WhyincludeMetadataCheckboxuses the#include-metadataid-selector" three-reason analysis (production- source-stable id-binding,getByRole('checkbox')would resolve too broadly,getByLabel('Include metadata')would lock to the English locale); the "WhyexportButtonsis a multi- resolution Locator" three-reason analysis (multiple export triggers, count-and-iterate in the consuming spec, composable filtering); the "WhyprogressBaruses a composite-or selector chain" three-reason analysis (current production source not ARIA-tagged, future-state production source should be ARIA- tagged, Playwright resolves the first matching half); cross-references to all seven prior admin-tree page-object docs and to the public-tree drivers; and a "What it does not contain" five-bullet enumeration of the deliberate omissions (nogetByTestIdselectors, no per-format download flow helper, noenableMetadata()/disableMetadata()setter helpers, noassertProgress(percent)invariant helper, no format-equivalence helper that switches between CSV and JSON). -
apps/web-e2e/tests/apiAddedadmin-items-export-sample-query.spec.ts— a query-param surface smoke for the admin-only sample-template-export endpoint atapps/web/app/api/admin/items/export/sample/route.ts. The route is admin-gated viaauth()+session.user.isAdmin(NOT the session-only gate the siblingadmin/featured-itemsroute uses) and reads a single Zod-validated query param after the gate (format, an enum of'csv' | 'xlsx'with a'csv'default). The spec walks the unauthenticated branch and pins the canonical{ success: false, error: 'Unauthorized. Admin access required.' }401 envelope, then sweeps?format=/?userId=/?token=/?bypass=/?filename=(with path-traversal + null-byte-injection variants) /?metadata=/ Accept-header / repeated-key / cookie-header permutations against the no-arg baseline so any future contributor who introduces query-string-based admin bypass —?asUser=true,?token=…,?as=admin,?bypass=1, or any other dangerous- passthrough — surfaces immediately as a status divergence between the no-arg 401 and a parameter-laden non-401. The sweep mirrors the shape of the siblingadmin-categories-query.spec.ts,admin-collections-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-stats-query.spec.ts,admin-users-query.spec.ts,items-export-query.spec.ts,items-export-settings-query.spec.tssmoke specs. -
docs/pluginsAddedadmin-featured-items-page-object.md— the seventh per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/featured-items.page.tsand 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). Documents the full surface for theAdminFeaturedItemsPagedriver — the fourreadonlyLocator fields (heading,addButton,searchInput,activeOnlyToggle), thenavigate()shortcut that closes over the inheritedgoto('/admin/featured-items'), thesearch(term)/clearSearch()async mutator helpers, and thefeaturedItemModal/statsCardslate-binding getters. Pinned toapps/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); the "WhyAdminFeaturedItemsPageextendsBasePage" three- reason analysis (page-route navigation via the inheritedgoto, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "WhysearchInputusesgetByRole('textbox').first()" three-reason analysis (the page renders a single<textbox>today,.first()defends against future per-section textboxes, thedata-testidposture would force a production-source change); the "WhyactiveOnlyToggleuses the#active-onlyid-selector" three-reason analysis (production-source-stable id-binding,getByRole( 'checkbox')would resolve too broadly,getByLabel( 'Active only')would lock to the English locale); the "WhyfeaturedItemModalandstatsCardsare getters" three-reason analysis (late-binding against modal mount/unmount lifecycle, symmetric with the modal- getter posture across the admin-tree page-object directory, the stats-grid Locator participates in the same late-binding contract); the "Whysearch(term)andclearSearch()are async methods" three-reason analysis (consuming specs always type into / clear the input, the pair of helpers documents the canonical search-flow contract, the underlyingLocator.clear()posture is the platform-canonical reset); cross-references to all six prior admin-tree page- object docs and to the public-tree drivers; and a "What it does not contain" five-bullet enumeration of the deliberate omissions (nogetByTestIdselectors, no per-row Locator getters, noaddFeaturedItem(...)/editFeaturedItem(...)/deleteFeaturedItem(...)flow helpers, noassertActiveOnly/assertActiveAllinvariant helper, nogetStatsValue(label)helper) that future contributors must respect when they add new helpers to keep the driver minimal. Continues the rollout of the per-source-file admin page-object references — ten admin-tree page objects remain (data-export, item-form, items, notifications, reports, roles, settings, sponsorships, surveys, tags). Updatesdocs/index.mdwith the standard one- paragraph entry that lists all prior admin-tree page- object docs as cross-references and pins the consuming spec, the five-flow envelope, the change protocol (update the doc in the same PR, update this log, cross-checke2e-tsconfig.md,playwright-config.md,fixtures-index.md, runpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the featured- items spec subset, a Spec 010 cross-link if the change introduces a new shared concept, and a reviewer pass), and follows the same posture as the six prior admin- tree page-object index entries. -
apps/web-e2e/tests/apiAddedadmin-featured-items-query.spec.ts— the ninth per- route admin-API query-surface smoke spec (afteradmin-by-id,admin-categories-query,admin-collections-query,admin-comments-query,admin-companies-query,admin-dashboard-stats-query,admin-geo-analytics-query,admin-items-stats-query, andadmin-users-query), pinned to theapps/web/app/api/admin/featured-items/route.tshandler. The first per-route admin-API smoke spec the suite publishes that targets a session-gated admin-tree route (gated bysession?.user?.idrather thansession?.user?.isAdmin— distinct from every prior admin-tree query-surface spec the suite publishes, which all target admin-isAdmin-gated routes). Pins the unauth-branch contract (always 401 with the canonical{ success: false, error: 'Unauthorized' }envelope, distinct from the{ success: false, error: 'Unauthorized. Admin access required.' }envelope theadmin/categoriesroute emits and from the{ success: false, error: 'Forbidden' }envelope theadmin/commentsroute emits) across a sweep of the three documented query keys (page,limit,active) and a speculative-bypass sweep (?userId=,?token=,?bypass=,?fields=,?itemSlug=,?q=,?from=…,?to=…,?deleted=…,?orderBy=,?category=) that catches any future regression that reads a query param before the session gate. Includes the standard 18 invariant assertions (< 500per parametrised path, exact-401-envelope for the no-arg baseline, status-stable across permutations, pagination-validators-do-not-fire-on-unauth,?active=does not bypass,?userId=does not bypass,?token=does not bypass,?bypass=does not bypass,?fields=does not bypass,?itemSlug=does not bypass,?q=does not bypass,?from=…&to=…does not bypass,?deleted=…does not bypass,?orderBy=does not bypass,?category=does not bypass, status stable across three permutations, Accept header does not branch, repeated query keys do not bypass, NextRequest-typed signature stable across cookie / IP side channels) that mirror the sibling admin-API query-surface specs. -
docs/pluginsAddedadmin-dashboard-page-object.md— the sixth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/dashboard.page.tsand 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 — bulk-actions, clients, collections, comments, companies — document, and distinct from every public- tree driver in the suite which has no tab-based navigation surface today). Documents the full surface for theAdminDashboardPagedriver — the threereadonlyLocator fields (mainContent,tabList,refreshButton), thenavigate()shortcut that closes over the inheritedgoto('/admin'), and theselectTab(tabName)async method that closes over atabList-scopedgetByRole('tab', { name: tabName, exact: false }).click()chain. Pinned toapps/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); the "WhyAdminDashboardPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgoto, global header / footer / nav-link chrome surfaced for free, post- navigationwaitForPageReadystabiliser); the "WhymainContentuses#main-content" three-reason analysis (production-source-stable id-binding for the skip-link target,getByRole('main')would resolve too broadly, thedata-testidposture would force a production-source change); the "WhytabListusesgetByRole('tablist')" three-reason analysis (accessibility-tree-canonical posture, production- source consistency with the per-tabgetByRole('tab')posture, thedata-testidposture would force a production-source change); the "WhyrefreshButtonusesgetByRole('button', { name: /refresh/i }).first()" three-reason analysis (case-insensitive substring-match tolerates production-source rephrasing,.first()defends against multi-button pages, thedata-testidposture would force a production-source change); the "WhyselectTabis an async method" three-reason analysis (consuming specs always click the resolved tab, thetabList-scoped selector is the load-bearing invariant, theexact: falseposture is the canonical Playwright shortcut for case-insensitive substring- match); cross-references to all five prior admin-tree page-object docs and to the public-tree drivers; and a "What it does not contain" five-bullet enumeration of the deliberate omissions (nogetByTestIdselectors, no per-tab Locator getters, no per-stat Locators, noclickRefresh()helper, noassertTabSelected(tabName)helper) that future contributors must respect when they add new helpers to keep the driver minimal. Continues the rollout of the per-source-file admin page-object references — eleven admin-tree page objects remain (data-export, featured-items, item-form, items, notifications, reports, roles, settings, sponsorships, surveys, tags). Updatesdocs/index.mdwith the standard one-paragraph entry that lists all prior admin-tree page-object docs as cross-references and pins the consuming spec, the four-flow envelope, the change protocol (update the doc in the same PR, update this log, cross-checke2e-tsconfig.md,playwright-config.md,fixtures-index.md, runpnpm tsc --noEmit, run a smoke-subset Playwright run targeting the dashboard spec subset, a Spec 010 cross- link if the change introduces a new shared concept, and a reviewer pass), and follows the same posture as the five prior admin-tree page-object index entries. -
apps/web-e2e/tests/apiAddedadmin-categories-query.spec.ts— the eighth per- route admin-API query-surface smoke spec (afteradmin-by-id,admin-collections-query,admin-comments-query,admin-companies-query,admin-dashboard-stats-query,admin-geo-analytics-query,admin-items-stats-query, andadmin-users-query), pinned to theapps/web/app/api/admin/categories/route.tshandler. Pins the unauth-branch contract (always 401 with the canonical{ success: false, error: 'Unauthorized. Admin access required.' }envelope, distinct from the bare{ error: 'Unauthorized' }envelope theadmin/companiesroute emits and from the{ success: false, error: 'Forbidden' }envelope theadmin/commentsroute emits) across a sweep of the five documented query keys (page,limit,includeInactive,sortBy,sortOrder) and a speculative-bypass sweep (?userId=,?token=,?bypass=,?fields=,?categoryId=,?q=,?from=…,?to=…,?deleted=…) that catches any future regression that reads a query param before the admin gate. Includes the standard 18 invariant assertions (< 500per parametrised path, exact-401- envelope for the no-arg baseline, status-stable across permutations, pagination-validators-do-not-fire-on- unauth,?includeInactive=does not bypass,?sortBy=does not bypass,?sortOrder=does not bypass,?userId=does not bypass,?token=does not bypass,?bypass=does not bypass,?fields=does not bypass,?categoryId=does not bypass,?q=does not bypass,?from=…&to=…does not bypass,?deleted=…does not bypass, status stable across three permutations, Accept header does not branch, repeated query keys do not bypass, NextRequest-typed signature stable across cookie / IP side channels) that mirror the sibling admin-API query-surface specs.
2026-05-02
-
docs/pluginsAddedadmin-companies-page-object.md— the fifth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/companies.page.tsand 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). Documents the full surface for theAdminCompaniesPagedriver — the tworeadonlyLocator fields (heading,addCompanyButton), the seven per-element getters (companyFormModal,companyNameInput,cancelButton,createCompanyButton,updateCompanyButton,deleteConfirmModal,confirmDeleteButton), and the singlenavigate()shortcut that closes over the inheritedgoto('/admin/companies'). Pinned toapps/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 inheritedgoto, 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); the "Why.first()oncompanyFormModal(and not ondeleteConfirmModal)" three-reason analysis (multi-instance selector, text-filter disambiguation, modal-mount lifecycle differences); the "WhycompanyNameInputuseslocator('input').first()" three-reason analysis (no production-source-stable placeholder, no accessible-name binding, single-input form contract); the "WhyconfirmDeleteButtonuses an exact-match/^delete$/iregex" three-reason analysis (HeroUI Modal title is the modal's accessible name, case-insensitive flag tolerates capitalisation drift, modal-scope second-line defence); the "Why two distinct submit-button getters" three-reason analysis (per-mode accessible names, per-mode test assertions, future- proof against per-mode loading states); the "WhycompanyFormModalis a getter" three-reason analysis; the failure matrix; the per-line walkthrough; and the read / write surface table mapping every caller to the fields they touch. -
apps/web-e2e/tests/apiAddedadmin-companies-query.spec.ts— the deep query-param surface smoke for the admin-gated companies-listing endpoint atapps/web/app/api/admin/companies/route.ts. Mirrors theadmin-collections-query.spec.ts/admin-comments-query.spec.ts/admin-dashboard-stats-query.spec.ts/admin-geo-analytics-query.spec.ts/admin-items-stats-query.spec.ts/admin-users-query.spec.ts/client-dashboard-stats-query.spec.tsshape; pins the "admin gate fires before anysearchParams.get(...)/ repository call" invariant by walking the route's four documented query params (page,limit,q,status) plus standard admin-impersonation / magic-token / admin-override / field-projection / cache-busting / format-negotiation / locale / multi-tenancy / time- range / sort / soft-delete-filter / company-targeting (by id / slug / domain) / repeated / bogus-key / Cookie-header probe sets (~95 deep paths). Adds 16 deep tests on top of the per-path 4xx baseline: the deterministic 401 with the bare-error envelope{ error: 'Unauthorized' }assertion (the route uses the legacy bare-error envelope rather than the unified{ success: false, error }envelope theadmin/commentsroute uses; the 401 status is the same posture as theadmin/collectionsandadmin/usersroutes for the unauth case); a stable- status-across-permutations assertion; eight "does NOT bypass the admin gate" assertions for?q=…,?page=…&limit=…,?status=…,?userId=…,?token=…,?bypass=…,?fields=…,?companyId=…; three "introduces no specific bypass" assertions for?from=…&to=…,?sortBy=…,?deleted=…; a stable- status-across-param-permutations assertion; an Accept- header invariance assertion; a repeated-keys invariance assertion; a NextRequest-typed handler signature stability assertion sweeping known-bogus Cookie / X-Forwarded-For / X-Real-IP headers. Closes the "deep query-surface walk" gap on the admin companies-listing route under Spec 010 — E2E Test Coverage and adds the first deep-query-surface smoke for theq-keyed search-input convention (where every other admin-route smoke pinned to date uses thesearch-keyed convention) — locking the production-source naming divergence into the test suite as a regression guard. -
docs/pluginsAddedadmin-comments-page-object.md— the fourth per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/comments.page.tsand 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). Documents the full surface for theAdminCommentsPagedriver — the tworeadonlyLocator fields (heading,searchInput), the two per-action methods (searchComments(term),clearSearch()), the two per-element getters (deleteCommentDialog,deleteButtons), and the singlenavigate()shortcut that closes over the inheritedgoto('/admin/comments'). Pinned toapps/web-e2e/tests/admin/comments.spec.ts(four flows over the admin comments-management surface); the "WhyAdminCommentsPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgoto, 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); 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; 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" three-reason analysis (HeroUIcolor="danger"prop + Tailwind utility-class fallback, future-proof against HeroUI prop reshuffling, the consuming spec uses an inline svg-children selector); the failure matrix; the per-line walkthrough; and the read / write surface table mapping every caller to the fields they touch. -
apps/web-e2e/tests/apiAddedadmin-comments-query.spec.ts— the deep query-param surface smoke for the admin-gated comments-listing endpoint atapps/web/app/api/admin/comments/route.ts. Mirrors theadmin-collections-query.spec.ts/admin-dashboard-stats-query.spec.ts/admin-geo-analytics-query.spec.ts/admin-items-stats-query.spec.ts/admin-users-query.spec.ts/client-dashboard-stats-query.spec.tsshape; pins the "admin gate fires before anysearchParams.get(...)/ drizzle query" invariant by walking the route's three documented query params (page,limit,search) plus standard admin-impersonation / magic-token / admin-override / field-projection / cache-busting / format-negotiation / locale / multi-tenancy / time- range / rating-filter / soft-delete-filter / sort / comment-targeting / item-targeting / repeated / bogus- key / Cookie-header probe sets (~85 deep paths). Adds 16 deep tests on top of the per-path 4xx baseline: the deterministic 403 with the canonical{ success: false, error: 'Forbidden' }envelope assertion (the single-step gate collapses unauthenticated and authenticated-non-admin into the same 403, distinct from theadmin/usersroute's two-step 401-then-403 split AND distinct from theadmin/collectionsroute's single-step 401 gate); a stable-status-across- permutations assertion; eight "does NOT bypass the admin gate" assertions for?search=…,?page=… &limit=…,?userId=…,?token=…,?bypass=…,?format=…,?fields=…,?commentId=…; two "introduces no specific bypass" assertions for?rating=…and?status=…; a stable-status-across- param-permutations assertion; an Accept-header invariance assertion; a repeated-keys invariance assertion; a bare-Request-typed handler signature stability assertion (this route uses bareRequest, distinct from theadmin/collectionsroute'sNextRequest-typed handler) sweeping known-bogus Cookie / X-Forwarded-For / X-Real-IP headers. Closes the "deep query-surface walk" gap on the admin comments-listing route under Spec 010 — E2E Test Coverage and adds the first deep-query-surface smoke for any bare-Request-typed handler in the suite — every other query-surface smoke pinned to date is for either aNextRequest-typed admin handler or a session-gated client / payment / public route. -
docs/pluginsAddedadmin-collections-page-object.md— the third per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/collections.page.tsand 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. Documents the full surface for theAdminCollectionsPagedriver — the tworeadonlyLocator fields (heading,addCollectionButton), the nine per-form modal getters (collectionFormModal,collectionIdInput,collectionNameInput,collectionIconInput,collectionDescriptionInput,activeToggle,cancelButton,createButton,saveButton), the three named-row helpers (getCollectionByName,editCollection,deleteCollection), the per-form fill helper (fillCollectionForm), and the singlenavigate()shortcut that closes over the inheritedgoto('/admin/collections'). Pinned toapps/web-e2e/tests/admin/collections.spec.ts(five flows over the admin collections-management surface); the "WhyAdminCollectionsPageextendsBasePage" three-reason analysis (page-route navigation via the inheritedgoto, global header / footer / nav-link chrome surfaced for free, post-navigationwaitForPageReadystabiliser); the "WhygetByPlaceholder(...)for every form-input field" three-reason analysis (HeroUI's<Input>does not pair with a visible<label>,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; the "Why placeholder-only inputs (no per-inputaria-labelordata-testid)" three-reason analysis; the failure matrix; the per-line walkthrough; and the read / write surface table mapping every caller to the fields they touch. -
apps/web-e2e/tests/apiAddedadmin-collections-query.spec.ts— the deep query-param surface smoke for the admin-gated collections-listing endpoint atapps/web/app/api/admin/collections/route.ts. Mirrors theadmin-dashboard-stats-query.spec.ts/admin-geo-analytics-query.spec.ts/admin-items-stats-query.spec.ts/admin-users-query.spec.ts/client-dashboard-stats-query.spec.tsshape; pins the "admin gate fires before anysearchParams.get(...)/collectionRepository.findAllPaginated(...)call" invariant by walking the route's six documented query params (page,limit,includeInactive,search,sortBy,sortOrder) plus standard admin-impersonation / magic-token / admin-override / field-projection / cache-busting / format-negotiation / locale / multi-tenancy / time-range / aggregation / repeated / bogus-key / NextRequest-cookie probe sets (~80 deep paths). Adds 17 deep tests on top of the per-path 4xx baseline: the deterministic 401 with the canonical{ success: false, error: "Unauthorized. Admin access required." }envelope assertion (the single-step gate collapses unauthenticated and authenticated-non-admin into the same 401, distinct from theadmin/usersroute's two-step 401-then-403 split); a stable-status-across-permutations assertion; six "does NOT bypass the admin gate" assertions for?search=…,?sortBy=…,?sortOrder=…,?includeInactive=…,?page=…&limit=…,?userId=…; a "does NOT bypass the admin gate" assertion for the higher-than-usual?limit=1000 ceiling sweep (this route is unique in allowing per-page limit up to 1000 because collections are loaded from Git); two "does NOT introduce a magic-token / admin-override bypass" assertions; two "does NOT introduce a content-negotiation / field-projection bypass" assertions; one "does NOT introduce a single-collection- targeting bypass" assertion (the route's listing surface vs the[id]per-collection endpoint); a "stable status across param permutations" assertion sweeping three orthogonal parameter sets; a "does NOT branch on Accept header" assertion; a "repeated query keys do NOT bypass the gate" assertion; and a "NextRequest-typed handler signature stable" assertion that sweeps fabricated session-cookie / forwarded-IP headers to defend against any future cookie-or-IP- driven auth bypass. -
docs/pluginsAddedadmin-clients-page-object.md— the second per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/clients.page.tsand continuing the rollout theadmin-bulk-actions-page-object.mdtemplate established. Documents the full surface for theAdminClientsPagedriver — the tworeadonlyLocator fields (heading,addClientButton), the four per-page modal getters (clientFormModal,deleteConfirmModal,confirmDeleteButton,cancelDeleteButton), and the singlenavigate()shortcut that closes over the inheritedgoto('/admin/clients'). Pinned toapps/web-e2e/tests/admin/clients.spec.ts(four flows over the admin clients-management surface); the "WhyAdminClientsPageextendsBasePage" three- reason analysis (page-route navigation via the inheritedgoto, 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, locale-tolerant via the case-insensitive regex, strict-mode safety via.first()against multi-button surfaces); the "WhyclientFormModalanddeleteConfirmModalare getters and notreadonlyfields" three-reason analysis (late-binding against the modal mount/unmount lifecycle, symmetry with 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 for disambiguation, host-app's CSS-utility convention); the "Why^delete$anchored regex forconfirmDeleteButton" three-reason analysis (defends against the modal heading collision, defends against future "Cannot delete" warning button, 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 the modal getter, symmetric with public-tree modal drivers); the failure matrix; the per-line walkthrough; and the read / write surface table mapping every caller to the fields they touch. -
apps/web-e2e/tests/apiAddedadmin-users-query.spec.ts— the deep query-param surface smoke for the admin-gated user-listing endpoint atapps/web/app/api/admin/users/route.ts. Mirrors theadmin-dashboard-stats-query.spec.ts/admin-geo-analytics-query.spec.ts/admin-items-stats-query.spec.ts/client-dashboard-stats-query.spec.tsshape; pins the "session+admin gate fires before anysearchParams.get(...), validator, oruserRepository.findAll(...)call" invariant by walking the route's eight documented query params (page,limit,search,role,status,sortBy,sortOrder,includeInactive) plus standard admin-impersonation / magic-token / admin-override / field-projection / cache-busting / format-negotiation / locale / multi-tenancy / time-range / aggregation / repeated / bogus-key / NextRequest-cookie probe sets (~80 deep paths). Adds 19 deep tests on top of the per-path 4xx baseline: the deterministic 401 with the canonical{ success: false, error: "Unauthorized" }envelope assertion (note: this route is two-step gated — session 401 then admin 403, unlikeadmin/items/stats's single 401 envelope); a stable-status-across-permutations assertion; eight "does NOT bypass the admin gate" assertions for?search=…,?role=…,?status=…,?sortBy=…,?sortOrder=…,?includeInactive=…,?page=…&limit=…,?userId=…; two "does NOT bypass the gate, and the length validator does NOT fire on the unauth branch" assertions for oversize?search=…(>100 chars trips a 400 on auth) and oversize?role=…(>50 chars trips a 400 on auth); three "does NOT introduce a query-token / admin-override / content-negotiation / field-projection bypass" assertions; a "stable status across param permutations" assertion sweeping three orthogonal parameter sets; a "does NOT branch on Accept header" assertion; a "repeated query keys do NOT bypass the gate" assertion; and a "NextRequest-typed handler signature stable" assertion that sweeps fabricated session-cookie / forwarded-IP headers to defend against any future cookie-or-IP-driven auth bypass. -
docs/pluginsAddedadmin-bulk-actions-page-object.md— the first per-source-file reference the docs tree publishes for any file underapps/web-e2e/page-objects/admin/, paired withapps/web-e2e/page-objects/admin/bulk-actions.page.tsand establishing the template the remaining sixteen admin-tree page-object docs (one per source file) will mirror. Documents the full surface for theAdminBulkActionsPagedriver — the eightreadonlyLocators (heading,selectAllCheckbox,bulkActionBar,approveButton,rejectButton,deleteButton,clearSelectionButton,confirmDialog), theitemCheckboxesgetter, and the singlenavigate()shortcut that closes over the inheritedgoto('/admin/items'). Pinned toapps/web-e2e/tests/admin/bulk-actions.spec.ts(five flows over the items-listing bulk surface); the "WhyAdminBulkActionsPageextendsBasePage" three- reason analysis (page-route navigation via the inheritedgoto, 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,.first()pin against per-row select-all duplicates); the "Why[role="toolbar"]forbulkActionBar" three-reason analysis; the "WhygetByRole('button', { name: /…/i })for the four action buttons" three-reason analysis; the "Why[role="dialog"][aria-modal="true"]forconfirmDialog" three-reason analysis (modal vs non-modal disambiguation, strict-mode safety against tooltip / toast libraries,.first()pin against parallel modals); the "WhyitemCheckboxesis a getter and not areadonlyfield" three-reason analysis; the "Whyaria-label*=\"Select\" iand notgetByRole('checkbox', { name: /select/i })foritemCheckboxes" three-reason analysis; the failure matrix; the per-line walkthrough; and the read / write surface table mapping every caller to the fields they touch. -
apps/web-e2e/tests/apiAddedadmin-items-stats-query.spec.ts— the deep query-param surface smoke for the admin-gated item-stats endpoint atapps/web/app/api/admin/items/stats/route.ts. Mirrors theadmin-dashboard-stats-query.spec.ts/admin-geo-analytics-query.spec.ts/client-dashboard-stats-query.spec.tsshape; pins the "admin gate fires before anysearchParams.get(...)/itemRepository.getStats(...)call" invariant by walking the route's three documented query params (search,categories,tags) plus standard admin-impersonation / magic-token / admin-override / status-filter / time-range / fields-projection / cache-busting / format-negotiation / locale / multi-tenancy / aggregation / repeated / long / bogus-key probe sets. Adds 13 deep tests on top of the per-path 4xx baseline: a deterministic 401 with the canonical{ success: false, error: "Unauthorized. Admin access required." }envelope assertion; a stable-status-across-permutations assertion; six "does NOT bypass the admin gate" assertions for?search=…,?categories=…,?tags=…,?userId=…,?token=…,?bypass=…; four "does NOT change the unauth branch" assertions for?status=…,?from=…&to=…,?format=…,?categories=,,,empty-only comma payloads; and two "does NOT branch on Accept header" / "keeps the response status stable across param permutations" assertions. Stays under the<500ceiling on every probe so the spec is green whether the route returns the canonical 401 or a future hardened 403. -
docs/pluginsAddedpublic-pages-page-object.md— the 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 and closing the gap that left thepublic-pages.page.tssource as the only public-tree page-object source file without a per-source-file doc reference. Documents the full surface across bothPublicPagesPage(the route- driver class with theheading/mainContent/breadcrumbLocators and the six route shortcuts to/collections,/categories,/tags,/cookies,/pricing,/sponsor) andErrorPage(the error- surface class with theheading/errorCode/goHomeButton/goBackButtonLocators); the spec-context cross-links to Spec 010 — E2E Test Coverage and the consuming specs atapps/web-e2e/tests/public/collections.spec.ts,apps/web-e2e/tests/public/sponsor.spec.ts, andapps/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 structurally a content page that happens to be a 404 / 403, the two classes share the sameBasePageimport andPage, 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, strict-mode- correctness against<h2>/<h3>siblings, locale- stable selector); the "Whybreadcrumbuses an OR-of-two-paths" three-reason analysis (canonicalaria-label="breadcrumb"with case-insensitive flag, structural fallback<nav><ol>,.first()strict-mode- correctness); the "WhyerrorCodeusesgetByText(/404\|403/)" three-reason analysis (error code as primary user-facing discriminator, regex form vs. string form, no.first()required); the "WhygoHomeButtonusesrole="link"" three-reason analysis (<a href="/">canonical,.first()strict-mode- correctness, case-insensitive substring); the "WhygoBackButtonusesrole="button"" three-reason analysis (<button onClick={() => history.back()}>canonical, two-word safety lock,.first()strict-mode- correctness); the failure matrix of 27 mistakes; the per-line walkthrough table; the read / write surface tables; and the 13-steppublic-pages.page.ts-change checklist tying any change to a spec audit, abase-page-object.mdcross-check, a production-source cross-check on each of the six routes and the error template, ae2e-tsconfig.md/playwright-config.md/fixtures-index.mdcross-check, dualpnpm tsc --noEmitruns, a smoke-subset Playwright run targeting--grep "Collections\|Categories\|Tags\|Cookies\|Pricing\|Sponsor\|Error", adocs/log.mdentry, a Spec 010 cross-link, and a reviewer pass. -
apps/web-e2e/tests/apiAddedadmin-geo-analytics-query.spec.ts— the deep query-param surface smoke for the admin-gated geo-analytics endpoint atapps/web/app/api/admin/geo-analytics/route.ts. Walks ~80 query-string permutations across the admin-impersonation key family (?userId=/?user_id=/?adminId=/?as=admin/?asAdmin=true/?impersonate=admin), the magic-token bypass family (?token=/?secret=/?api_key=/?authorization=/?session=/?adminToken=), the admin-override key family (?bypass=/?admin=/?override=/?force=), the geo-filter family (?country=/?city=/?serviceArea=), the distribution-tuning override family (?topCitiesLimit=/?topCountriesLimit=), the heatmap-density family (?heatmapResolution=/?heatmapBuckets=/?gridSize=), the remote-filter family (?includeRemote=/?excludeRemote=/?onlyRemote=), the time-range family (?from=/?to=/?since=/?until=), the content-projection family (?fields=/?select=/?include=/?exclude=), the cache-busting family (?refresh=/?force=/?fresh=/?cache=/?nocache=), the content-negotiation family (?format=json/geojson/csv/xml), the i18n family (?locale=/?lang=), the multi-tenancy family (?tenant=/?tenantId=/?org=), the bounding-box family (?bbox=/?bounds=/?viewport=), and the empty-value / repeated-key / injection-style / long-value / bogus-key permutations. Pins the zero-argument-handler / always-401-on-the- unauth-branch invariant via the same shape as the siblingadmin-dashboard-stats-query.spec.ts,client-dashboard-stats-query.spec.ts,client-geo-stats-query.spec.ts,client-items-coordinates-query.spec.ts, and other query-surface smoke specs. Adds 14 dedicated invariant tests on top of the loop: status-stable across permutations, 4xx with stable success- discriminator on the unauth branch, no query-userId-bypass / no query-token-bypass / no query-admin-override, geo-filter / distribution-tuning / heatmap-density / remote-filter / time-range / viewport- filter / format-negotiation params do NOT change the unauth branch, status stability across param permutations, and Accept-header invariance. The deeperadmin-protected-extra.spec.tssmoke also covers this route at the broad< 500level; this spec adds the deep query-surface walk on top of that. -
docs/pluginsAddedprofile-dropdown-page-object.md— the 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. 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 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()for the bottom-most logout item; 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 full file annotated chunk-by-chunk; 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; the "Why the trigger button uses#user-menu-button" three-reason analysis; the "Why the logout button uses.last()" three-reason analysis; the "WhyisOpen()checks the exact'true'string" three- reason analysis; the "WhyclickMenuItemtakes aRegExpnot astring" three-reason analysis; the failure matrix of 22 mistakes; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and the 12-stepprofile-dropdown.page.ts- change checklist. -
apps/web-e2eAddedapps/web-e2e/tests/api/admin-dashboard-stats-query.spec.tssmoke spec for the unauth GET branch of the admin- gated/api/admin/dashboard/statsendpoint served byapps/web/app/api/admin/dashboard/stats/route.ts. Pins the deterministic 4xx (typically 401) status across 60+ query-param permutations (admin-impersonation keys?userId=/?adminId=/?as=/?asAdmin=/?impersonate=, magic-token bypass keys?token=/?secret=/?api_key=/?authorization=/?adminToken=, admin-override keys?bypass=/?admin=/?override=/?force=, analytics-tuning override keys?userGrowthMonths=/?activityTrendDays=/?topItemsLimit=/?recentActivityLimit=for edge-cases like0/-1/999999/NaN/Infinity, time-range filter keys?from=/?to=/?since=/?until=, content-projection keys?fields=/?select=/?include=, cache-busting keys, format- negotiation keys, locale / tenant keys, empty values, repeated keys, special-character / injection-style values, long values, bogus keys, combined permutations), plus six bypass-resistance invariants (the unauth branch is invariant to bogus query parameters,?userId=…does not bypass the admin gate,?token=…does not introduce a query-token auth bypass,?bypass=…does not introduce a query-admin-override, analytics-tuning params do not change the unauth branch, time-range params do not change the unauth branch,?format=csvdoes not introduce a content-negotiation bypass) and Accept-header invariance. Closes a gap in Spec 009 and Spec 010, and complementsprotected.spec.ts's broad-coverage< 500smoke against the same route. -
docs/pluginsAddednewsletter-page-object.md— the 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. 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; the "Why the email input uses.first()" three-reason analysis; the "Why the submit button uses..traversal" three-reason analysis; the "Why the error message usestext-red-600, text-red-400" three-reason analysis; the "WhyhasSuccessToast()collapses errors tofalse" three- reason analysis; the failure matrix of 21 mistakes; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and the 12-stepnewsletter.page.ts-change checklist. -
apps/web-e2eAddedapps/web-e2e/tests/api/polar-subscription-portal-body.spec.tssmoke spec for the unauth POST branch of the session-gated/api/polar/subscription/portalendpoint served byapps/web/app/api/polar/subscription/portal/route.ts. Pins the deterministic 401 status and the{ error: 'Unauthorized' }envelope across 40+ body permutations ({ userId },{ user_id },{ uid },{ id },{ customerId },{ customer_id },{ polarCustomerId },{ customer },{ subscriptionId },{ planId },{ priceId },{ token },{ secret },{ api_key },{ authorization },{ session },{ sessionToken },{ admin },{ asAdmin },{ bypass },{ impersonate },{ returnUrl },{ return_url },{ successUrl },{ cancelUrl },{ email },{ tenant },{ tenantId },{ org }, XSS / path- traversal / null-byte / SQL-injection-style values, empty values, falsy values, long values, combined-keys permutation), plus seven bypass-resistance invariants (POST without explicit body responds without server error, POST returns 401 with stable{ error: string }envelope, POST is invariant to bogus body keys,{ userId }does not bypass the session gate,{ customerId }does not bypass the per-session customer resolution,{ token }does not introduce a body-token auth bypass,{ admin }does not introduce a body-admin-override) and an open- redirect-leak guard ({ returnUrl: '<attacker.example>' }must NOT echo the attacker URL in the unauth response body) and Accept-header invariance. Closes a gap in Spec 010 and complementspayment-checkouts.spec.ts's broad-coverage< 500smoke against the same route. -
docs/pluginsAddedmap-page-object.md— the 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. 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 "Why the class extendsBasePage" walkthrough; the "Why the view-toggle usesaria-label*="map" i" walkthrough; the "WhyisPageRendered()accepts the empty-state path" walkthrough; the failure matrix; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and themap.page.ts-change checklist. -
apps/web-e2eAddedapps/web-e2e/tests/api/client-items-coordinates-query.spec.tssmoke spec for the unauth GET branch of the session-gated/api/client/items/coordinatesendpoint served byapps/web/app/api/client/items/coordinates/route.ts. Pins the deterministic 401 status and the{ success: false, error }envelope across 70+ query- param permutations (?userId=,?clientId=,?token=,?country=,?lat=,?lng=,?bbox=,?radius=,?slug=,?itemId=,?format=,?fields=, cache-busting, per-tenant, admin-override, special-character payloads, repeated keys, long values, bogus keys), plus six bypass-resistance invariants (the unauth branch is invariant to bogus query parameters,?userId=…does not bypass the session gate,?token=…does not introduce a query-token auth bypass,?admin=…does not introduce a query-admin-override, spatial- filter params do not change the unauth branch, single- item-lookup?slug=…/?itemId=…keys do not change the unauth branch,?format=geojsondoes not introduce a content-negotiation bypass) and Accept-header invariance. Closes a gap in Spec 010. -
docs/pluginsAddeditem-detail-page-object.md— the 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. 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.tsandapps/web-e2e/tests/public/votes-and-comments.spec.ts; the "Why the class extendsBasePage" walkthrough; the "Why the vote button uses an OR-of-two-aria-labels selector" walkthrough; the "Why the favorite button uses anaria-label*="favorites"substring selector" walkthrough; the "WhygetVoteCount()returnsPromise<string>" walkthrough; the "WhyisVoted()checks the exact'Remove upvote'label" walkthrough; the failure matrix; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and theitem-detail.page.ts-change checklist. -
apps/web-e2eAddedapps/web-e2e/tests/api/version-sync-query.spec.tssmoke spec for the GET branch of the public/api/version/syncendpoint served byapps/web/app/api/version/sync/route.ts. Pins the canonical six-key envelope (syncInProgress,lastSyncTime,timeSinceLastSync,timeSinceLastSyncHuman,uptime,timestamp) and theCache-Control: no-cache, no-store, must-revalidate/Content-Type: application/jsonheader contracts across 40+ query-param permutations (cache-busting, per-tenant, per-user-impersonation, locale, format / fields / select / include filters, special-character payloads, repeated keys, long values, bogus keys), plus three correlation invariants (syncInProgress/lastSyncTime/timeSinceLastSync/timeSinceLastSyncHuman) and Accept-header invariance. Closes a gap in Spec 010. -
docs/pluginsAddedlanguage-switcher-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class LanguageSwitcherstandalone class with noextendsclause, thereadonly page: Pagefield that the standalone class restates and is also consumed insideselectLanguageto construct the per-locale option Locator at call-time against page-level scope because the dropdown may be portal- rendered, thereadonly button: Locatorpinned to the exact Englisharia-label="Select language"literal with.first()for strict-mode safety AND the deliberate non-localization that lets a user landing on a page in a language they cannot read still find the switcher, the constructor that pre-binds the trigger Locator in a single pass without asuper(page)call, theopen()minimal "open the dropdown" primitive, theselectLanguage(fullName: string)composite 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, thegetCurrentLocaleCode(): Promise<string>accessor with thetextContent()?.trim().toUpperCase() ?? ''chain whose.toUpperCase()casing-fold tolerates future production-source casing drift and whose?? ''pins the public return type toPromise<string>, theisOpen(): Promise<boolean>accessor that readsaria-expandedand returns the strict-equality comparisonexpanded === 'true'); 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; 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, strict- equality survives a futurearia-label="Choose region"related-control regression, no production-source change required); the "Why per-locale options pinaria-label="Switch to ${fullName}"" walkthrough that pins the localized-display-name UX convention, consuming-spec mental model, and no-production-source- change rationale; the "Why the option Locator does not carry.first()" walkthrough that pins the intentional asymmetry against the trigger's.first()pin; the "Why.first()on the trigger button" walkthrough that pins the three failure modes of dropping it; the "Why the constructor usesthis.page.locator(…)and not the inheritedheaderscope" walkthrough; the "WhygetCurrentLocaleCode()upper-cases the result" walkthrough; the "WhyisOpen()checksaria-expanded === 'true'" walkthrough; the failure matrix covering every language-switcher-page- level mistake; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and thelanguage-switcher.page.ts- change checklist with the spec audit +BasePagecross-check + production-source cross-check +next-intlconfiguration cross-check +e2e-tsconfig.mdcross-check +playwright-config.mdcross-check +fixtures-index.mdcross-check + dualpnpm tsc --noEmit+ smoke-subset Playwright run +docs/log.md+ Spec 010 cross-link + reviewer pass. -
apps/web-e2e/tests/apiAddeditems-export-settings-query.spec.tsquery-parameter smoke spec for the public items-export-settings endpoint served byapps/web/app/api/items/export/settings/route.ts. Pins the route's "public, zero-argument, single-key envelope, byte-identical body across query permutations" invariant by walking the public GET surface across every plausible query-key shape: the no-arg baseline, the obvious?format=/?type=keys that the adjacent/api/items/exportroute reads (a regression that confused the two routes is the obvious bypass shape), the?userId=/?asUser=/?impersonate=per-user-override keys, the?tenant=/?tenantId=/?org=per-tenant- override keys, the?token=/?secret=/?api_key=magic-token keys, the?refresh=/?force=/?fresh=/?cache=/?nocache=cache-busting keys, the?locale=/?lang=i18n keys, the?fields=/?select=/?include=selection keys, the?env=/?stage=environment-override keys, empty values, repeated keys, special-character values, long values (500-character repeats), and bogus / typo'd keys. Adds seven explicit assertion tests on top of the parameterised loop: the canonical{ export_enabled: boolean }single-key envelope shape withObject.keys(body)exact-equality check that catches body-shape drift (rename toenabled, wrap in{ success: true, data: {...} }, siblingexport_formatkey), the byte-identical-body invariant across query permutations usingawait response.text()exact-string equality (a stronger contract than status-only assertions because it catches a regression that branches on a query param to gate the boolean), the no-?token=-override assertion (no per-user feature-flag override exists today), the no-?tenant=-override assertion (the flag is host-wide today, sourced fromworks.ymlviagetExportEnabled()), the response-shape stability assertion across permuted parameter sets, and the no-Accept-header-branching assertion that pins the route's content-type toapplication/jsonregardless of the request's Accept header (a regression that adds content negotiation mirroring the adjacent/api/items/exportroute's actual?format=key would change the body type on the per-Accept branch). Mirrors the siblingclient-dashboard-stats-query.spec.ts,client-geo-stats-query.spec.ts,stripe-payment-methods-list-query.spec.ts,lemonsqueezy-list-query.spec.ts,subscription-query.spec.ts,payments-query.spec.ts,plan-status-query.spec.ts, and other zero-argument query smoke specs — but the items-export-settings route is the only one whose response payload is a single-key boolean feature-flag envelope today, making the invariant-shape assertion doubly load- bearing because the frontend's conditional-render logic reads the boolean directly without a deeper schema validation step. -
docs/pluginsAddedstar-rating-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class StarRatingstandalone class with noextendsclause, thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage, thereadonly container: Locatorpinned to the dual[role="radiogroup"][aria-label="Rating"]exact-match selector with.first()for strict-mode safety against future sibling radio groups, the constructor that pre-binds the single container Locator in a single pass without asuper(page)call, thestar(n: number): Locatorlocator-factory that interpolates 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 in the page footer with the substring match accommodating both singular"1 star"and plural"2 stars"/.../"5 stars"shapes and the Locator-return shape preserving composability withexpect(...).toBeVisible()chains, therate(n: number)composite "click the nth star" primitive, thegetValue(): Promise<number>accessor with the load-bearing reverse iterationi = 5..1that returns the highest checked star to handle the host app's HeroUI fill-up-to-N pattern correctly and thereturn 0no-rating 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 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 when ratings are disabled or the comment form does not surface); the "Why the class does not extendBasePage" walkthrough; the "Why[role="radiogroup"][aria-label="Rating"]exact match" walkthrough; the "Why.first()on the container Locator" walkthrough; the "Whystar(n)returns aLocatorinstead of clicking" walkthrough; the "Whyaria-label*="N star"substring match (and not exact)" walkthrough that pins plural-form variance, future locale variance, and future a11y-label expansion as the three reasons; the "Why reverse iteration ingetValue()" walkthrough that pins the highest-checked-wins semantics for the fill-up-to-N pattern, short-circuit on the most-likely-rating common case, and symmetric-to-visual rendering; the "Whyreturn 0as the no-rating sentinel" rationale; the failure matrix covering every star-rating-page-level mistake; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and thestar-rating.page.ts-change checklist with the spec audit +BasePagecross-check + production-source cross-check +discover-page-object.mdcross-check +fixtures-index.mdcross-check +e2e-tsconfig.mdcross-check +playwright-config.mdcross-check + dualpnpm tsc --noEmit+ smoke-subset Playwright run +docs/log.md+ Spec 010 cross-link + reviewer pass. -
apps/web-e2e/tests/apiAddedclient-geo-stats-query.spec.tsquery-parameter smoke spec for the authenticated client geo-stats endpoint served byapps/web/app/api/client/geo-stats/route.ts. Pins the route's "session-gated, 401 before any service-layer call" invariant by walking the unauthenticated GET surface across every plausible query-key shape: the no-arg baseline, the obvious?userId=/?user_id=/?uid=/?id=/?clientId=admin-impersonation key shapes that a future "admin-views-other-user's-geo-stats" feature might add, the?token=/?secret=/?api_key=/?authorization=magic-token bypass keys, the?country=/?city=/?region=/?area=/?serviceArea=/?coverage=geographic-filter keys that a future per-region scoping feature might add, the?lat=/?lng=/?bbox=/?radius=spatial-filter keys that a future "items near a point" feature might add, the?period=/?range=/?window=time-window keys, the?limit=/?offset=/?page=/?topN=pagination keys for thetop_cities/top_countriesarrays, the?fields=/?select=/?include=selection keys, the?refresh=/?force=/?fresh=/?cache=/?nocache=cache-busting keys, the?format=content-negotiation keys (json/xml/csv/geojson/kml), the?locale=/?lang=/?currency=i18n keys, the?status=/?type=/?sort=/?order=/?direction=filter and sort keys, the?tenant=/?tenantId=/?org=multi-tenancy keys, the?admin=/?asAdmin=/?bypass=/?impersonate=admin-override keys, empty values, repeated keys, special-character values, long values (500-character repeats), and bogus / typo'd keys. Adds three explicit assertion tests on top of the parameterised loop: the canonical 401 envelope shape ({ success: false, error: '<string>' }), the parameter-invariance assertion (no query-string permutation produces a non-401 status), the no-?userId=-bypass assertion (anonymous callers cannot impersonate other users), the no-?token=-bypass assertion (no magic-token auth exists today), the no-?admin=-override assertion (admin status is read from the session, never from the query string), the geographic-filter no-effect assertion (the route returns the full per-user payload today), and the response-shape stability assertion across permuted parameter sets. Mirrors the siblingclient-dashboard-stats-query.spec.ts,stripe-payment-methods-list-query.spec.ts,lemonsqueezy-list-query.spec.ts,subscription-query.spec.ts,payments-query.spec.ts, andplan-status-query.spec.tssmoke specs — all seven routes share the same "session-gated, 401 before any service-layer call" posture, but the client geo-stats route shares with the client dashboard-stats route the property that the handler signature is zero-argument AND uses therequireClientAuth()helper, making the unauth-branch 401 invariant doubly load-bearing because a regression that adds arequest: NextRequestargument and reads anysearchParamsvalue before the gate is the obvious shape of a future bypass — particularly tempting on a geo-stats endpoint where future contributors might add?country=…or?city=…filter keys to scope the payload to a sub-region. -
docs/pluginsAddedsort-menu-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class SortMenustandalone class with noextendsclause, thereadonly page: Pagefield that 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: Locatorpinned to the canonical ARIA-spec valuearia-haspopup="menu"via exact match for strict-mode-correctness against[role="menu"]popups, thereadonly menuContent: Locatordeliberately-exposed dropdown Locator pinned to[role="menu"], the constructor that pre-binds the two Locators in a single pass without asuper(page)call, theopen()minimal "open the dropdown" primitive, theselectOption(text: RegExp)composite primitive with the load-bearingRegExpparameter type and the dual-role[role="menuitemradio"], [role="menuitem"]selector that accommodates both single-select and free-action option shapes, thegetCurrentLabel(): Promise<string>accessor with thetextContent()?.trim() ?? ''chain that pins the public return type toPromise<string>); 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; the "Why the class does not extendBasePage" walkthrough; the "Whyaria-haspopup="menu"exact match and not a substring" walkthrough; the "Why[role="menu"]exact match formenuContent" walkthrough; the "Why.first()on every Locator" walkthrough; the "Why the dual-role selector inselectOption" walkthrough that pins the three reasons for the comma-separated[role="menuitemradio"], [role="menuitem"]selector; the "Whytext: RegExpand nottext: string" walkthrough that pins the locale-invariance posture; the "Why?.trim() ?? ''ongetCurrentLabel" rationale; the failure matrix covering every sort-menu-page-level mistake; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and the change checklist that ties any change to a spec audit, a base-page-object cross-check, a production-source cross-check, a discover-page-object cross-check, an e2e-tsconfig cross-check, a playwright-config cross-check, a fixtures-index cross-check, dualpnpm tsc --noEmitruns, a smoke-subset Playwright run, a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link, and a reviewer pass. -
docs/indexAdded a new entry forsort-menu-page-object.mdto the docs index. -
apps/web-e2e/tests/apiAddedclient-dashboard-stats-query.spec.ts— the query-param surface smoke for the authenticated client dashboard-stats endpoint served byapps/web/app/api/client/dashboard/stats/route.ts, pinning therequireClientAuth()session-gate response invariant: the unauth GET surface returns 401 with the canonical{ success: false, error: 'Unauthorized. Please sign in to continue.' }envelope deterministically, regardless of which query keys the caller appends to the URL. The spec walks the unauth branch with 80+ parametrised query-string permutations covering the obvious bypass shapes — the?userId=/?user_id=/?uid=/?id=/?clientId=user- identity-override keys that a regression might wire as fallbacks forrequireClientAuth()'ssession.user.idresolution, the?token=/?secret=/?api_key=/?authorization=magic-token keys that a regression might wire as auth-bypass paths, the?from=/?to=/?startDate=/?endDate=/?period=/?range=/?window=date-range filter keys that the route ignores today, the?limit=/?offset=/?page=pagination keys for thetopItemsarray, the?fields=/?select=/?include=shape keys, the?refresh=/?force=/?fresh=/?cache=cache-busting keys, the?format=content-negotiation keys, the?locale=/?lang=/?currency=i18n keys, the?status=/?type=filter keys for thestatusBreakdownarray, the?sort=/?order=/?direction=sort-override keys, the?tenant=/?tenantId=/?org=multi-tenancy keys, the?admin=/?asAdmin=/?bypass=/?impersonate=admin-override keys that would be the obvious shape of a future "view another user's dashboard as admin" feature, and the empty / repeated / special-character / long / typo'd combinations. Each parametrised path is asserted to return a status< 500so the unauth branch's 401 is the only reachable response. Six dedicated no-bypass assertions then pin specific bypass shapes: the?userId=…no-impersonation invariant, the?token=…no-magic-auth invariant, the?admin=…no-query-admin-override invariant (especially load- bearing becauserequireClientAuth()'s comment notes admins are allowed to use client endpoints), the date- range-params no-shape-change invariant, the response- shape stability across three different parameter sets. Mirrors the siblingstripe-payment-methods-list-query.spec.ts,stripe-products-query.spec.ts,lemonsqueezy-list-query.spec.ts,subscription-query.spec.ts,payments-query.spec.ts, andplan-status-query.spec.tsposture — the client dashboard-stats route is the only one whose handler signature is zero-argument AND which uses therequireClientAuth()helper rather than the bareauth()call, so the unauth-branch 401 invariant is doubly load-bearing. -
docs/pluginsAddedshare-button-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class ShareButtonstandalone class with noextendsclause, thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage, thereadonly trigger: Locatorpinned viapage.locator('button').filter({ hasText: /share/i }).first()because the host app's button has noaria-labeltoday, the fourreadonlymenu-item Locators pinned via[role="menuitem"]with per-item case-insensitive regex filters —/copy link/i,/twitter|x \(/iwith the dual-substring posture that survives the X rebrand by matching either legacy"Twitter"or post-rebrand"X (formerly Twitter)"via thex \(disambiguator,/facebook/i,/linkedin/i— the constructor that pre-binds the five Locators in a single pass without asuper(page)call, theopen()minimal "open the dropdown" primitive, thecopyLink()composite "open then click Copy Link" primitive that is the only deterministic per-platform action method today because the per-platform entries open externalwindow.open(...)URLs that require a popup-verification harness); 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(both tests soft-skip withtest.skip(true, …)when the trigger is not visible so the spec degrades gracefully on environments / CMS-content combinations where the item-detail page does not surface a share button); the "Why the class does not extendBasePage" walkthrough that pins the three load-bearing reasons (composition over inheritance, reusability on non-item-detail surfaces like a future profile / collection / per-tag share button, constructor parity with non-page widget drivers); the "Whyfilter({ hasText: /share/i })and not anaria-label" walkthrough; the "Why[role="menuitem"]and not adata-testid" walkthrough; the "Why the Twitter regex uses/twitter|x \(/i" walkthrough; the "Why.first()on every Locator" walkthrough; the "Why theiflag on every regex" walkthrough; the "Why onlyopen()andcopyLink()action methods" walkthrough; the failure matrix covering every share-button-page-level mistake; the per-line walkthrough table; the read / write surface summary; the read / write surface failure modes table; and the change checklist that ties any change to a spec audit, a base-page-object cross-check, a production-source cross-check, a discover-page-object cross-check, an e2e-tsconfig cross-check, a playwright-config cross-check, a fixtures-index cross-check, a per-platform popup-verification harness cross-check if a future per-platform action method is added, dualpnpm tsc --noEmitruns, a smoke-subset Playwright run, a docs/log.md entry, a Spec 010 — E2E Test Coverage cross-link, and a reviewer pass. -
docs/indexAdded a new entry forshare-button-page-object.mdto the docs index. -
apps/web-e2e/tests/apiAddedstripe-products-query.spec.ts— the query-param surface smoke for the public Stripe-products endpoint served byapps/web/app/api/stripe/products/route.ts, pinning theNEXT_PUBLIC_STRIPE_DYNAMIC_PRICINGflag-gate response invariant: the disabled-flag GET surface returns 400 with the canonical{ error: 'Dynamic pricing is not enabled', message: …}envelope deterministically, regardless of which query keys the caller appends to the URL. The spec walks the disabled-flag branch with 80+ parametrised query-string permutations covering the obvious bypass shapes — the?dynamic=/?dynamicPricing=/?force=/?override=flag-flip keys that a regression might wire as fallbacks forisStripeDynamicPricingEnabled(), the?productId=/?priceId=/?id=filter keys that a regression might wire as before-the-gate filters, the?stripeKey=/?sk=/?apiKey=dangerous-passthrough keys that a regression might forward to the Stripe SDK, the?token=/?secret=/?api_key=/?authorization=magic-token keys that a regression might wire as auth-bypass paths, the?provider=switch that the wider repo's LemonSqueezy / Polar / Solidgate providers might tempt a future contributor to wire here, the?account=/?stripeAccount=/?connect=Stripe-Connect account-override keys that a regression might forward to the SDK as thestripeAccountoption, the?currency=/?locale=/?lang=i18n keys, the?refresh=/?cache=/?fresh=cache-busting keys, the?expand=/?include=Stripe SDK expansion keys, the?format=content-negotiation keys, the?sort=/?order=/?direction=sort-override keys, the?fields=/?select=shape keys, the?tenant=/?tenantId=/?org=multi-tenancy keys, the?active=/?archived=product-state filter keys, the?sponsorAds=response-shape gate, and the empty / repeated / special-character / long / typo'd combinations. Each parametrised path is asserted to return a status in the canonical 200/400/500 set so the spec coexists on every CI runner regardless of which gate fires first (disabled-flag 400 vs enabled-without-key 500 vs enabled-and-configured 200). Six dedicated no-bypass assertions then pin specific bypass shapes: the?dynamic=…flag-bypass invariant, the?stripeKey=…no-passthrough invariant, the?token=…no-magic-auth invariant, the?provider=…no-provider-switch invariant, the?account=…no-Connect-override invariant, the?productId=…no-shape-change invariant. A response-shape stability check across three different parameter sets confirms the route's gate fires before any branching on potential future query schemas. Mirrors the siblingstripe-payment-methods-list-query.spec.ts,lemonsqueezy-list-query.spec.ts,subscription-query.spec.ts,payments-query.spec.ts, andplan-status-query.spec.tsposture — the Stripe products route is the only one of the six whose gate is flag-driven (not session-driven) and whose handler signature is zero-argument today. -
docs/pluginsAddedscroll-to-top-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class ScrollToTopstandalone class with noextendsclause, thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage, the singlereadonly button: Locatorpinned via thepage.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, the constructor that pre-binds the single button Locator without asuper(page)call, thescrollDown(pixels = 500)primitive that runswindow.scrollBy(0, pixels)inside the page context with a default that comfortably clears the production source's ~300-pixel threshold, theclick()primitive that clicks the floating button, and 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 seven "Why X" walkthroughs (the class does not extendBasePagebecause the scroll-to-top button is a global floating widget rendered on every page in every role tree and a future admin-shell or client-shell consumer would reuse it, exactaria-labelselector over substring because the label is the canonical accessibility primitive a screen reader announces and there is no plausible label variant requiring substring-tolerance, no.first()on the Locator because the single-instance invariant means future regressions should surface as strict-mode violations,page.evaluate(() => window.scrollBy(0, px), pixels)overpage.mouse.wheelorpage.keyboard.press('PageDown')for deterministic scroll distance and threshold-test ergonomics,pixels = 500default for comfortable threshold clearance and documentation-by-default,getScrollYreadswindow.scrollYinstead of React state for production-source-first signal and no reach-in to React internals); the failure matrix covering every scroll-to-top-page-level mistake (type-only import drop, accidentalextends BasePageadd,readonlydrop, substringaria-label*=swap,data-testidswap, accidental.first()add,page.mouse.wheelswap with 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; the read / write surface summary that maps every caller (the consuming spec, 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, 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. Cross-linked fromdocs/index.mdand adjacent to the existingview-toggle-page-object.md/theme-toggle-page-object.md/signin-page-object.md/search-bar-page-object.md/discover-page-object.md/base-page-object.mdper-source-file references. -
apps/web-e2e/tests/apiAddedstripe-payment-methods-list-query.spec.ts— the query-param surface smoke for the authenticated Stripe-payment-methods-list endpoint served byapps/web/app/api/stripe/payment-methods/list/route.ts, the fifth query-smoke spec in thesubscription-query.spec.ts/payments-query.spec.ts/plan-status-query.spec.ts/lemonsqueezy-list-query.spec.tsfamily. The route is unique among the five because its handler signature is zero-argument today (export async function GET(), reading nosearchParamsat all). The spec exhaustively exercises every plausible query key a regression might introduce —?type=(filter by payment-method type),?limit=(pagination limit),?starting_after=/?ending_before=(Stripe's canonical cursor keys),?customer=/?customerId=/?stripeCustomerId=(admin-impersonation candidates),?userId=/?user_id=/?uid=/?id=(per-user override candidates),?token=/?secret=/?api_key=/?authorization=/?session=(magic-token bypass candidates),?stripeKey=/?stripe_key=/?sk=/?apiKey=/?secretKey=(dangerous Stripe-key passthrough candidates),?provider=(cross-provider bypass candidates),?refresh=/?force=/?fresh=/?cache=/?nocache=(cache-busting),?expand=/?include=(Stripe SDK expansion forwarding candidates),?format=(content-negotiation),?currency=/?locale=/?lang=(response-transformation candidates),?sort=/?order=/?direction=(sort-override candidates),?fields=/?select=(column-projection candidates),?tenant=/?tenantId=/?org=(multi-tenancy candidates),?account=/?stripeAccount=/?connect=(Stripe Connect account-override candidates), empty-value variants for each of the load-bearing keys, repeated keys, special-character values (URL-encoded<script>,' OR 1=1, path traversal,%00), 500-character long values, and bogus typo'd keys — pinning that every parameter permutation round-trips to the same canonical 401 envelope{ success: false, error: 'Unauthorized' }on the unauthenticated GET branch because the auth gate fires before any potentialsearchParamsparsing or Stripe SDK call. Includes nine targeted assertions (the no-arg 401-with-canonical-envelope shape, the identity assertion across with/without bogus params, thecustomer=no-bypass assertion that pins the "the customer id is gated by the session" invariant that prevents arbitrary-customer payment-method enumeration, thestripeKey=no-forwarding assertion that pins the "the Stripe key is server-side only" invariant, thetoken=no-bypass assertion, theprovider=no-switching assertion, theaccount=no-Connect-override assertion that pins the "platform Stripe account exclusively" invariant, thetype=no-filter-change assertion, and the response shape stability assertion across permutations). -
docs/pluginsAddedview-toggle-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class ViewTogglestandalone class with noextendsclause, thereadonly page: Pagefield that the standalone class restates because it does not inherit fromBasePage, the four button Locatorsreadonly listButton/gridButton/masonryButton/mapButtoneach pinned via thepage.locator('button[aria-label*="…" i]').first()case-insensitive substring selector, the constructor that pre-binds the four button Locators in a single pass without asuper(page)call, the three symmetric-shapeselectList()/selectGrid()/selectMasonry()click primitives, theisActive(button: Locator)predicate that reads the supplied button'sclassattribute and returns whether thescale-105Tailwind utility-class substring is present with a?? 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 seven "Why X" walkthroughs (the class does not extendBasePagebecause the view toggle is a listing-mounted control row not a page-shaped surface and a future admin-shell list/grid switch would reuse it,aria-label*="…" ioverdata-testidbecause it tolerates view-label phrasing variants like"View as list"/"List view"/"Show as a list"and theiflag tolerates casing drift,.first()on every button Locator for strict-mode safety against future stacked toggles, theiflag on every substring selector for locale-style / production-source / per- tenant casing drift survival, noselectMap()method today because the map-view is feature-gated behindfeatures/map-view.mdand symmetric posture preserves a future addition the day the map mode becomes always-on while the exposedmapButtonfield permits direct-Locator interaction,isActive()reads thescale-105substring because it is the production-source-first visual signal that tolerates future class-list expansion and future-proofs against additivearia-pressedadoption,?? falseon the class-list scan to type-narrow toPromise<boolean>and mirror the siblingtheme-toggle-page-object.md'sisDarkMode()); the failure matrix that maps each view-toggle mistake (dropimport type, add anextends BasePageclause, dropreadonlyfrom any of the five fields, switch any button to anaria-label="…"exact match, drop theiflag from any substring selector, drop.first()on any button, swap anyaria-label*=for adata-testid, add aselectMap()method that unconditionally clicks, drop themapButtonfield, read the active-state from React state oraria-pressed, replacescale-105substring with abg-primarysubstring that false-positives on hover, drop the?? falsefromisActive(), file move, rename,.tsxextension, CRLF line endings) onto the layer that surfaces each one; the per-line walkthrough table; 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, 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, aplaywright-config.mdcross-check, 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 cross-link if a new shared concept is introduced, and a reviewer pass. -
apps/web-e2e/tests/apiAddedlemonsqueezy-list-query.spec.ts— the first smoke spec for the authenticated/api/lemonsqueezy/listendpoint's query-param surface served byapps/web/app/api/lemonsqueezy/list/route.ts. The route is a session-gated GET handler — it returns the caller's paginated LemonSqueezy checkouts (with admin-impersonation via?customerEmail=…permitted once the caller is authenticated). The auth gate fires before anysearchParamsparsing or LemonSqueezy provider call; on the unauth branch the route returns 401 +{ error: 'Unauthorized', message: 'Authentication required', code: 'AUTH_REQUIRED' }deterministically. The spec pins this contract via a ~110-entry parametrised matrix spanning every "the route reads this only after the gate" category (?status=allowlist + invalid + empty,?limit=in-range + above-cap + below-floor + non-finite,?page=valid + invalid,?customerEmail=admin-impersonation attempts + invalid emails,?dateFrom=/?dateTo=ISO 8601 + invalid + reversed range,?storeId=arbitrary-store-id attempts, every?userId=/?user_id=/?uid=identity-override attempt, every?token=/?secret=/?api_key=/?authorization=magic- token attempt, every?lemonsqueezyKey=/?lemon_squeezy_key=/?lsk=/?apiKey=caller-supplied-key attempt, every?provider=stripe/?provider=polar/?provider=solidgateprovider- switch attempt, every cache-busting key, every expansion key, every pagination cursor key, every format / currency / locale / sort / fields key, every multi-tenancy key, every empty value, every repeated- key permutation, every special-character payload (<script>, SQL injection, path traversal, null bytes), every long-input string (500-charactercustomerEmail/storeId/token), and every bogus / typo'd combination), each asserting< 500; one canonical-envelope test that asserts the unauth GET surface returns exactly 401 with astringerrorfield and astringcodefield; one no-bypass invariance test that pins?customerEmail=…against the baseline 401; one no-bypass invariance test that pins?storeId=…against the baseline 401; one no-bypass invariance test that pins?lemonsqueezyKey=…/?lemon_squeezy_key=…/?lsk=…/?apiKey=…against the baseline 401 to catch any future caller-supplied-key forwarding; one no-bypass invariance test that pins?token=…/?secret=…/?api_key=…/?authorization=…against the baseline 401; one provider-switch invariance test that pins?provider=stripe/?provider=polar/?provider=solidgateagainst the baseline 401; one validator-order test that asserts the auth gate fires before the Zod validator (so an out-of-allowlist?status=…value still produces 401 instead of 400 on the unauth branch); and one param-permutation envelope-shape test that asserts every parametrised path returns the{ error: string }envelope. Mirrors the shape ofsubscription-query.spec.ts,payments-query.spec.ts, andplan-status-query.spec.ts(the three sibling session-gated route smokes) but is the first smoke spec to pin the "auth gate fires before the Zod validator" invariant on a route whose query schema issafeParse-validated. -
docs/pluginsAddedsearch-bar-page-object.md— the 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. Documents the at-a-glance summary table of every load-bearing element (the type-only Playwright import, theexport class SearchBarstandalone class with noextendsclause, thereadonly page: Pagefield that the standalone class restates, thereadonly input: Locatorpage.locator('input[placeholder*="Search" i]').first()case-insensitive substring selector, thereadonly clearButton: Locatorpage.locator('button', { hasText: '×' }).first()multiplication-sign-glyph selector, the constructor that pre-binds both Locators without asuper(page)call, thesearch(term)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()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 seven "Why X" walkthroughs (the class does not extendBasePagebecause the search input is page-mounted not page-shaped and a future admin-search would reuse it,placeholder*="Search" ioverdata-testidbecause it tolerates placeholder evolution and case drift,hasText: '×'over a CSS class because the multiplication-sign glyph U+00D7 survives every translation pass while the lower-case Latin xxU+0078 would silently miss,.first()on both Locators for strict-mode safety against future stacked inputs / clear buttons,fill()overpressSequentially()because the production-source debounces on the React value not per-keystroke events,clear()over aclearButton.click()because the clear button is hidden when the input is empty so clicking it would flake,?? ''ongetValue()to keep the public return type pinned tostringagainst any future Playwright API change); the failure matrix that maps each search-bar mistake (dropimport type, add anextends BasePageclause, dropreadonlyfrom any field, switchinputto aplaceholder="Search"exact match, drop theiflag from theplaceholdersubstring, drop.first()oninput, swap the placeholder substring for adata-testid, switchclearButtontohasText: 'x'Latin lower-case x, switchclearButtonto a CSS class selector, drop.first()onclearButton, switchsearch()fromfill()topressSequentially(), switchclear()to aclearButton.click(), drop the?? ''ongetValue(), file move, rename,.tsxextension, CRLF line endings) onto the layer that surfaces each one; the per-line walkthrough table; 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,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, andclear-button-glyph-missesfailures; and thesearch-bar.page.ts-change checklist that ties any change to a spec audit, 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, aplaywright-config.mdcross-check, 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 cross-link if a new shared concept is introduced, and a reviewer pass. -
apps/web-e2e/tests/apiAddedlocation-cities-query.spec.ts— the first smoke spec for the public/api/location/citiesendpoint's query-param surface served byapps/web/app/api/location/cities/route.ts. The route is a zero-query-param GET handler — it reads zerosearchParamsand exposes exactly two well-formed branches:404+{ success: false, error: 'Location features are disabled' }whengetLocationEnabled()returnsfalse(the most-likely branch on a clean local-dev baseline), and200+{ success: true, data: string[] }when the feature is on. The spec pins this contract via a 75-entry parametrised matrix spanning every "the route does not read this" category (?city=/?country=/?countryCode=/?region=/?state=/?province=/?q=/?search=/?term=/?prefix=/?limit=/?offset=/?page=/?perPage=/?cursor=/?sort=/?order=/?direction=/?locale=/?lang=/?format=/?fields=/?include=/?expand=/?tenant=/?tenantId=/?org=/?refresh=/?cache=/?force=/?fresh=/?nocache=/?token=/?secret=/?api_key=/?authorization=/ special-character values for XSS / SQL-injection / path-traversal / null bytes / 500-character long values / typo'd keys / repeated keys), each asserting< 500; one per-call envelope test that asserts the status is exactly200or404and the body shape matches the success envelope (success: true, data: string[]with every entry astring) or the feature-disabled envelope (success: false, error: string); one invariance test that compares 14 representative parametrised responses to the no-arg baseline so a regression that begins reading?city=…/?q=…/?limit=…surfaces immediately; one filter-override test that asserts a nonsensical?city=__definitely-not-a-real-city__filter does not change the response so a future filtering wire-up that grants caller-controlled override ofgetDistinctCities()is caught; and one parallel sweep test that confirms every parametrised query in the matrix is below 500. Mirrors the shape oflocation-countries-query.spec.ts(its sibling in theapps/web/app/api/location/subtree) and rounds out the location-API smoke matrix. -
docs/pluginsAddeddiscover-page-object.md— the 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. 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 typePlaywright type-only 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: Locatordual-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; thegetItemCount()method; theclickFirstItem()method 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 six "Why X" walkthroughs (the class extendsBasePagebecause/discover/[N]is a navigable page in the URL sense sogoto/waitForPageReadyare useful for free, shared page-shell Locators are useful, constructor parity with sibling page-shaped page objects;a[href*="/items/"]and not adata-testidbecause no production-source change required, locale invariance against the six locale prefixes, slug invariance; dual-substringaria-label*for pagination because production-source case drift tolerance, strict-mode safety from the unique landmark, zero-false-positive substring narrowness;getByRole('heading', { level: 1 })for heading because locale invariance, single accessible-name source of truth, production-source-first discipline;pageNum = 1default because most-common call site shortest, explicit-page-number documentation for pagination tests, type-narrowedPromise<void>posture;.first()onclickFirstItembecause strict-mode collision avoidance, 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 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, abase-page-object.mdcross-check, a production-source cross-check, ane2e-tsconfig.mdcross-check, aplaywright-config.mdcross-check, afixtures-index.mdcross-check, dualpnpm tsc --noEmitruns, a smoke-subset Playwright run, adocs/log.mdentry, a Spec 010 cross-link, and a reviewer pass. -
e2eAddedapps/web-e2e/tests/api/location-countries-query.spec.ts— the query-param surface smoke for the public unauthenticated/api/location/countriesendpoint served byapps/web/app/api/location/countries/route.ts. Pins the route's zero-query-param contract: the handler reads zerosearchParamskeys today and returns one of two well-formed envelopes —200+{ success: true, data: string[] }when the location feature is enabled andgetDistinctCountries()resolves the distinct-country list;404+{ success: false, error: 'Location features are disabled' }whengetLocationEnabled()is false. The catch-and-500 fallback ('Failed to fetch countries') must never fire on a clean baseline. The spec parametrises across 50+ query strings (?country=…filter overrides,?countryCode=/?code=/?iso=ISO-code keys,?city=/?q=/?search=/?term=/?prefix=search keys,?limit=/?offset=/?page=/?perPage=/?cursor=pagination keys,?sort=/?order=/?direction=sort keys,?locale=/?lang=i18n keys,?format=content-negotiation keys,?fields=/?include=/?expand=sparse-fieldset keys,?tenant=/?tenantId=/?org=multi-tenancy keys,?refresh=/?cache=/?force=cache-busting keys,?token=/?secret=/?api_key=auth-bypass keys,<script>/' OR 1=1//etc/passwd/ NUL-byte special-character values, 500-character long values, bogus-key combinations, repeated keys) plus four targeted invariant tests (canonical 200/404 envelope shape with the success-branchdataarray of strings; the response is invariant across every parametrised query string compared to the baseline; caller-supplied filter overrides do NOT bypass the data-layer call; final 5xx-free sweep across the matrix). A regression that begins reading?country=…/?q=…/?limit=…would produce a divergent response and surface immediately as a status / body divergence. The route's siblingsapps/web/app/api/location/cities/route.ts,apps/web/app/api/location/coordinates/route.ts(already smoked bylocation-coordinates-query.spec.ts), andapps/web/app/api/location/search/route.ts(already smoked bylocation-search-query.spec.ts) share the sameapps/web/app/api/location/subtree; this spec is the first smoke for the/api/location/countriessurface. -
docs/index.mdInsert the newdiscover-page-objectentry above thetheme-toggle-page-objectentry in the plugins section. Maintains alphabetical-then-recency ordering inside the plugins block. -
docs/pluginsAddedtheme-toggle-page-object.md— the 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. 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 typePlaywright type-only 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 six "Why X" walkthroughs (the class does not extendBasePagebecause composition over inheritance against the header surface, reusability on non-page surfaces, and constructor parity with non-page widgets;aria-label*="Current theme"and not adata-testidbecause no production-source change required, theme-label invariance, and strict-mode resilience against a future second theme switch with.first();.first()on the toggle button against future admin-shell / per-option / portal-rendered duplicates; parsing thearia-labelsubstring instead of querying state because of black-box discipline, storage drift survival, and theme-set extensibility; role+regex name for the option buttons for locale invariance and strict-mode resilience;isDarkMode()reads<html>'s class because of TailwinddarkMode: 'class', server-render parity, and the no-flicker guarantee); 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 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, abase-page-object.mdcross-check, a production-source cross-check, ane2e-tsconfig.mdcross-check, aplaywright-config.mdcross-check, afixtures-index.mdcross-check, dualpnpm tsc --noEmitruns, a smoke-subset Playwright run, adocs/log.mdentry, a Spec 010 cross-link, and a reviewer pass. -
e2eAddedapps/web-e2e/tests/api/user-currency-query.spec.ts— the query-param surface smoke for the unauthenticated user-currency detection endpoint served byapps/web/app/api/user/currency/route.ts. Pins the route's always-200 graceful-degradation contract: the handler reads exactly one query parameter (provider), runs it throughvalidateProvider()which lowercases / trims / falls back to'smart'for any value not in the canonical seven-element allowlist ('cloudflare' | 'vercel' | 'cloudfront' | 'fastly' | 'generic' | 'auto' | 'smart'), and otherwise reads CDN country headers (Cf-IPCountry,X-Vercel-IP-Country, …) to derive the currency. Without those headers — the default e2e-runner shape — the response is always{ currency: 'USD', country: null, detected: false }with status 200. The spec parametrises across 60+ query strings (the seven canonical providers; case-insensitive variants; whitespace-padded variants; out-of-allowlist providers; empty / null providers; repeated providers;country=,countryCode=,currency=overrides that the route reads zero of today; user-id and auth-token bypass keys; cache busting / format / locale / tenant keys; special-character / long-value / bogus combinations) plus seven targeted invariant tests (canonical 200 fallback envelope shape; invariance across all seven canonical providers; invariance across invalid providers; the?country=…-does-NOT-bypass-detection invariant; the?currency=…-does-NOT-bypass-derivation invariant; the ISO 4217 supported-currency-set invariant; param-permutation shape stability). Mirrors the siblingcurrent-user-query/payments-query/subscription-querysmoke shape but inverted to the always-200 contract because the currency route is the only GET handler in the user tree that is not session-gated. -
indexInserted thetheme-toggle-page-object.mdentry at the head of the plugins section (abovesignin-page-object) with the same exhaustive single-line summary shape every recent index entry uses. -
docs/pluginsAddedsignin-page-object.md— the 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 typePlaywright type-only import that mirrors the base-class discipline; theimport { BasePage }runtime import — the only runtime import in the file; theexport class SignInPage extends BasePagenamed export; the seven pre-bound Locator fields with the form-scoping posture foremailInput/passwordInput/forgotPasswordLink, the unscoped role+regex-namesubmitButton, and the.first()-pinnederrorAlert/successAlert; the constructor that uses a localauthForm = page.locator('form').filter({ has: page.locator('#email') })to scope every form-relative Locator to the sign-in form; thenavigate()method that wrapsgoto('/auth/signin'); thesignIn(email, password)form-fill kernel that submits viapasswordInput.press('Enter')instead of clicking the button; thesignInAndWaitForRedirect(...)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 four "Why X" walkthroughs (form-scoping every form Locator vs unscoped against newsletter / sign-up form#emailcollisions, role+regex namesubmitButtonvs CSS attribute / form-scoped role / text selector alternatives that fail on locale-coverage or button-floats-outside-form refactors, Enter-key submission vs button click against real-user semantics and button-state flakes,.bg-red-50.first()/.bg-green-50.first()vs unscoped against stacked-banner strict-mode collisions); the kernel-vs-wrapper rationale forsignIn/signInAndWaitForRedirect(failure-path specs need the kernel without an awaited redirect; happy-path specs need the wrapper with a 60-second cold-start-tolerant timeout); the failure matrix covering every signin-page-level mistake (type-only import drop, inheritance drop,readonlydrop, form-scoping drop, CSS / text submitButton swap,href*=→href=forgotPasswordLink swap,.first()drop, Enter → click swap, timeout tightening below 30s or raising above 60s, global-state field, file move, rename,.tsxextension, CRLF line endings); 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 to the fields they touch; and thesignin.page.ts-change checklist with cross-checks againstbase-page-object.md, the production sign-in form components underapps/web/components/auth/**and the route underapps/web/app/[locale]/auth/signin/,auth-fixture.md,e2e-tsconfig.md,e2e-package-manifest.md,playwright-config.md, andglobal-setup.md. Linked fromdocs/index.md. Spec 010 cross-link. -
apps/web-e2eAddedtests/api/payments-query.spec.ts— a smoke spec covering the query-param surface of the authenticated user-payments endpoint atapps/web/app/api/user/payments/route.ts. The handler is session-gated (auth()early-returns 401 for unauthenticated callers, then resolves a Stripe customer id from the session-bound user record before listing invoices and subscriptions) and declares no parameters at all — not_request, notrequest: NextRequest, not acontextobject — so the route reads zero query params. Mirrors the siblingsubscription-query.spec.tsshape because both routes share the sameauth() → getCustomerId() → stripe.list()chain and the same zero-query-param contract. The spec walks 70+ query-string permutations (impersonation keys?userId=/?user_id=/?uid=/?id=, customer-bypass keys?customerId=/?customer=/?stripeCustomerId=, invoice-id / subscription-id filters, status filters?status=paid|pending|draft|open|void|uncollectible, magic-token keys?token=/?secret=/?api_key=/?authorization=/?session=, dangerous Stripe-key passthrough keys?stripeKey=/?stripe_key=/?sk=that must NEVER be honoured, cache-bust, expand / pagination keys mirroring Stripe's own shape, content-negotiation keys, currency / locale keys, multi-provider switch keys?provider=stripe|polar|lemonsqueezy|solidgate, date-range filters, multi-tenancy, empty / repeated / special-character / long values) and asserts status invariance plus the five load-bearing "no bypass" contracts (?userId=does not impersonate,?customerId=does not bypass the session-bound customer-resolution step,?stripeKey=does not forward a caller-supplied Stripe key,?token=does not introduce a query-token auth bypass, parameterised vs no-arg calls produce identical 401 envelopes). Spec 010 cross-link. -
docs/pluginsAddedgitignore.md— the 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 (# dependencies,# turbo,# testing(covering the security-critical**/auth-states/pattern protecting the persisted NextAuth session cookies documented inauth-fixture.mdandglobal-setup.md),# next.js,# docusaurus,# production,# misc,# debug,# env files(covering the security-critical.env*glob plus!.env.examplere-include — the single most important block for the workspace's secret posture),# vercel,# typescript,# content(the.contentGit-CMS directory cloned at runtime fromDATA_REPOSITORYplus theanalyze/bundle-analyzer output),# vscode AI rules,# cache,# OpenAPI backups(three patterns exhaustively covering the generate-openapi script's backup output), and# claude(the per-checkout Claude Code state directory)); the full file annotated section-by-section; the five "Why X" walkthroughs (single workspace-root file vs per-package files,**/auth-states/security posture vs bareauth-states/,.env*plus!.env.exampledefence-in-depth vs positive include list,.contentper-deployment customisation chokepoint,.claudeper-developer state); the OpenAPI backup multi-pattern rationale; the failure matrix that maps every gitignore-level mistake to the workflow that surfaces it (pnpm install,pnpm dev,pnpm build,pnpm test:e2e, contributor sign-up, security regression on the.env*and**/auth-states/blocks); the per-section walkthrough table; and the.gitignore-change checklist with cross-checks againstauth-fixture.md,global-setup.md,e2e-test-data.md,playwright-config.md,workspace-root-manifest.md, andturbo-config.md. Linked fromdocs/index.md. Spec 010 cross-link. -
apps/web-e2eAddedtests/api/subscription-query.spec.ts— a smoke spec covering the query-param surface of the authenticated user-subscription endpoint atapps/web/app/api/user/subscription/route.ts. The handler is session-gated (auth()early-returns 401 for unauthenticated callers, then resolves a Stripe customer id from the session-bound user record before listing subscriptions) and declares no parameters at all — not_request, notrequest: NextRequest, not acontextobject — so the route reads zero query params. The spec walks 70+ query-string permutations (impersonation keys?userId=/?user_id=/?uid=/?id=, customer-bypass keys?customerId=/?customer=/?stripeCustomerId=, dangerous Stripe-key passthrough keys?stripeKey=/?stripe_key=/?sk=that must NEVER be honoured, magic-token keys, status filters, expand / pagination keys mirroring Stripe's own shape, cache-bust, content-negotiation, currency / locale, multi-provider switch keys?provider=stripe|polar|lemonsqueezy|solidgate, multi-tenancy, empty / repeated / special-character / long values) and asserts status invariance plus the five load-bearing "no bypass" contracts (?userId=does not impersonate,?customerId=does not bypass the session-bound customer-resolution step,?stripeKey=does not forward a caller-supplied Stripe key,?token=does not introduce a query-token auth bypass, parameterised vs no-arg calls produce identical 401 envelopes). Spec 010 cross-link. -
docs/pluginsAddedbase-page-object.md— the 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 under the four role trees (apps/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 typePlaywright type-only import that stays out of the runtime bundle, theBasePagesingle named export inherited by 30+ subclasses today, the four pre-bound Locatorspage/header/footer/navLinkswith thefirst()posture onheaderandfooteragainst Next 16 stacked-layout headers, thegoto()suite-wide navigation primitive with thewaitUntil: 'domcontentloaded'override, thegotoLocalized()locale-aware variant that special-cases'en'to bare paths, thewaitForPageReady()re-await primitive, thegetTitle()shortcut); the full file annotated chunk-by-chunk; the four "Why X" walkthroughs for the load-bearing choices (type-only import vs runtime import,first()vs unscoped header / footer selection, header-scoped vs page-scoped link enumeration,domcontentloadedvsload/networkidle); the'en'-special-case rationale forgotoLocalized(); the failure matrix covering every base-class-level mistake; the per-line walkthrough table; and thebase.page.ts-change checklist with cross-checks againstfixtures-index.md,e2e-tsconfig.md,e2e-package-manifest.md,playwright-config.md, andauth-fixture.md. Linked fromdocs/index.md. Spec 010 cross-link. -
apps/web-e2eAddedtests/api/plan-status-query.spec.ts— a smoke spec covering the query-param surface of the authenticated user-plan-status endpoint atapps/web/app/api/user/plan-status/route.ts. The handler is session-gated (auth()early-returns 401 for unauthenticated callers) and declares its parameter as_request: NextRequest— underscored to mark it deliberately unused. The spec walks 60+ query-string permutations (impersonation keys?userId=/?user_id=/?uid=/?id=, plan-spoof keys?planId=/?effectivePlan=/?plan=, magic-token keys?token=/?secret=/?api_key=/?authorization=/?session=, cache-bust keys, content-negotiation keys, field-selection keys, point-in-time-query keys, warning-window keys, multi-tenancy keys, localisation keys, empty / repeated / special-character / long values) and asserts status invariance plus the three load-bearing "no bypass" contracts (?userId=does not impersonate,?token=does not introduce a query-token auth bypass,?plan=does not spoof the effective plan). Spec 010 cross-link. -
docs/pluginsAddedfixtures-index.md— the 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). Documents the singleexport { test, expect } from './auth.fixture're-export statement that turns thefixtures/directory into a single addressable import target so a future spec can writeimport { test, expect } from '../../fixtures'instead of'../../fixtures/auth.fixture'; the three load-bearing properties of the re-export (forwarding bothtestANDexpectto prevent the soft-failure aggregation anti-pattern, the relative./auth.fixturesource path that resolves throughmoduleResolution: "bundler"without going through anypathsmapping, the bare names 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 (composition with future fixture modules, internal-restructuring absorption, JavaScript ecosystem lingua franca) 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 drop, intent becomes opaque, accidental additions surface) against the lowest-coupling named-re-export shape; the failure matrix covering every barrel-level mistake; the per-line walkthrough table; and theindex.ts-change checklist with cross-checks againstauth-fixture.md,e2e-tsconfig.md, ande2e-package-manifest.md. Linked fromdocs/index.md. Spec 010 cross-link. -
apps/web-e2eAddedtests/api/geocode-query.spec.ts— a smoke spec covering the query-param surface and the POST body-resilience surface of the admin-only geocoding endpoint atapps/web/app/api/geocode/route.ts. The handler is admin-gated (auth()early-returns 401 for unauthenticated callers, then 403 for non-admin), so every assertion in the spec pins the unauthenticated branch's 401 contract: the GET handler declares no parameters at all and reads zero query keys, so the matrix walks 30+ query-string permutations (cache-bust keys, content-negotiation keys, localisation keys, provider-pinning keys, geocode-as-query attempts, empty/repeated/special-character/long values) and asserts status invariance; the POST suite pins the gate-then-parse order — a regression that flipped to parse-then-gate would surface as a 400 instead of a 401 on malformed bodies. Spec 010 cross-link. -
docs/pluginsAddede2e-package-manifest.md— the 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 document the manifest of a host-app or library workspace member, this one documents the manifest of a test-only workspace 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 thepnpm --filterglob and 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 inheritance; the five Playwrightscripts.*entriestest:e2e/test:e2e:ui/test:e2e:chromium/test:e2e:headed/test:e2e:debugplus the no-opscripts.lintecho that lets workspace-widepnpm -r lintwalk this member without a per-package opt-out; the fourdevDependencies@ever-works/tsconfigworkspace:*,@playwright/test^1.58.2,@faker-js/faker^10.1.0,dotenv^16.4.7,typescript^5); the file-contents walkthrough; the per-field walkthrough that pins each field to a concrete responsibility; the deliberately-absent fields matrix (nodescription/homepage/repository/bugs/author/keywords/engines/packageManager/type/main/types/exports/bin/peerDependencies/files/dependencies/scripts.dev/scripts.build/scripts.start/pnpm.*/prettier); the consumer table mapping each reader (pnpm install, `pnpm --filter @ever-works/web-e2e -
apps/web-e2e/tests/apiAddeditem-votes-query.spec.ts— the query-param surface smoke forGET /api/items/[slug]/votesdefined byapps/web/app/api/items/[slug]/votes/route.ts. The route'sGEThandler signature isGET(request: Request, context: { params: Promise<{ slug: string }> })—requestis declared but never read inside the body (norequest.url, norequest.headers, nosearchParams.get(...)); the handler awaitscontext.paramsandauth()together, then callsgetVoteCountForItem(slug)and (when signed in)getClientProfileByUserId(...)/getVoteByUserIdAndItemId(...). The route therefore must be invariant to any query parameter the caller appends — present, absent, empty, repeated, special-character, or long. The existingitem-votes-public.spec.tscovers the no-arg unknown-slug 5xx-resilience contract; this new spec walks the query-param surface so a regression that introduces arequest.url-based wiring (which a future "filter votes by date range" or "include per-vote breakdown" feature might tempt a future contributor into adding) is caught immediately as a status divergence between the no-arg and parameter-laden branches. The route contract is deliberately permissive on the catch path: success is{ success: true, count: number, userVote: 'up' | 'down' | null }with status 200; thetry / catchblock degrades to the same{ success: true, count: 0, userVote: null }envelope with status 200 (logging the error in development only), so there is no 5xx branch on this route. The matrix accepts< 500as the dominant happy path and pins the 200-only contract in the dedicated tests at the bottom. The query enumeration covers the?userId=/?include=/?fields=/?select=/?expand=/?refresh=/?force=/?fresh=/?format=/?locale=/?lang=/?since=/?from=/?until=/?direction=/?type=obvious-future-wiring keys, the empty-value / repeated-key / special-character / long-value / bogus-key edge variants, and the deliberate?type=upoverlap with the POST body'stype: 'up' | 'down'field that proves URL params do not influence the GET response. Three dedicated tests at the bottom pin the canonical envelope shape, the status-invariance across no-arg and parameter-laden branches, and the response-shape stability across param permutations — anchoring the bulk-loop's< 500matrix to the stricter 200-with-{ success, count, userVote }shape on the happy path. -
docs/pluginsAddedauth-fixture.md— the 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, with what failure modes, and what guarantees a spec can rely on when it importstestfrom this file instead of from@playwright/test. Documents the at-a-glance summary table of every load-bearing element (the file-scopedeslint-disable react-hooks/rules-of-hooksdirective 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;fs/pathNode imports for therequireAuthState()existsSynccheck and thepath.resolve(__dirname, '..', ...)absolute-path computation; theimport { ADMIN_STATE_FILE, CLIENT_STATE_FILE } from '../helpers/test-data'so the fixture never types the literal'auth-states/admin.json'; theADMIN_STATE_PATH/CLIENT_STATE_PATHresolved-once-at-module-load absolute paths with the__dirname-anchored shape that surviveswebServer.cwd: '../..'; therequireAuthState(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; the fourbase.extend<AuthFixtures>(...)factories withadminContextdepending onbrowserandadminPagedepending onadminContext— the only shapes that load the storage state at context creation; the per-testawait 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 imported-test-but-not-expectanti-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; the "Why a fixture instead of atest.beforeEach()hook" walkthrough that pins the three failure modes of the hook approach 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 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, aglobal-teardown.mdcross-check, ane2e-test-data.mdcross-check, aplaywright-config.mdcross-check, ane2e-tsconfig.mdcross-check, every authenticated spec underapps/web-e2e/tests/admin/andapps/web-e2e/tests/client/(they all import{ test, expect }from this file), dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run of the admin / client spec set, 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. -
apps/web-e2e/tests/apiAddedinternal-db-init-query.spec.ts— a query-param surface smoke spec forGET /api/internal/db-init(the development-only database-initialization endpoint served byapps/web/app/api/internal/db-init/route.tsthat triggersinitializeDatabase()— auto-migration plus seeding — when the host app is inNODE_ENV=developmentand hard-blocks with403and{ error: 'Not available in production' }otherwise). Pins the route's status surface against future regressions that might introduce a?force=bypass for the production guard (which a future "allow init in staging" feature might tempt a contributor into adding by flipping the hard-codedNODE_ENV !== 'development'check), a?env=query-string spoof for the server-sideprocess.env.NODE_ENV, a?seed=/?reset=/?drop=destructive toggle, a?token=/?secret=/?api_key=query-token authentication bypass, or any other caller-controlled flag that would change the production-mode behaviour. Walks the route's three-branch contract (200on the dev happy path,403on the production guard,500on a throwninitializeDatabase()error mapped throughsafeErrorResponse) and asserts< 600 && >= 200on every parameterised path because all three branches are part of the route's contract; pins the more precise allowed-set[200, 403, 500]on the no-arg path; asserts status-equality between the no-arg case and a parameter-laden case to pin the "every unknown query key is silently ignored" invariant; and asserts three load-bearing security invariants explicitly:?force=truedoes NOT bypass the production guard,?env=development/?env=productiondo NOT spoofNODE_ENV, and?token=anything/?secret=anything/?authorization=Bearer+anythingdo NOT introduce a query-token auth bypass. The matrix covers the obvious bypass keys (?force=,?bypass=,?override=), env-spoof keys (?env=,?NODE_ENV=), destructive toggles (?seed=,?reset=,?drop=,?recreate=), migration toggles (?migrate=,?skip-migration=), dry-run toggles (?dryRun=,?dry-run=), logging toggles (?verbose=,?debug=,?logLevel=), cache-busting keys (?refresh=,?nocache=), content-negotiation keys (?format=), multi-tenancy keys (?tenant=,?tenantId=), magic-token keys (?token=,?secret=,?api_key=,?authorization=), the empty-value case for each, repeated keys, special characters that would tempt regex / LIKE / SQL-injection wiring (including a?token=' OR 1=1classic), long values to guard against future regex-based indexing bugs, and bogus / typo'd keys. Cross-references Spec 010 — E2E Test Coverage, themethod-guards.spec.tsspec that already covers the route indirectly, and theapps/web/app/api/internal/db-init/route.tssource file the spec is paired with. -
docs/pluginsAddede2e-test-data.md— the 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(...)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 and the RFC 6761 reserved@test.localTLD;TEST_DATA.generateItemName()andTEST_DATA.generateItemUrl()(the latter using the IANA RFC 2606 reservedexample.comapex);REQUIRED_ENV_VARSas constwhitelist consumed bypromptForMissingEnv();PUBLIC_ROUTES13-rowas consttable of every public route the navigation shell links to;AUTH_STATE_DIR,ADMIN_STATE_FILE,CLIENT_STATE_FILEtemplate-composed path constants); 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(), Faker's real-world TLDs, per-worker static emails); the "WhyPUBLIC_ROUTESis areadonlyarray of objects" rationale (thenamefield is the test description, survives a route rename, and theas constposture lets specs type-check against literal values); the failure matrix that maps eachtest-data.tsmistake (drop the empty-string check → 30-second timeout instead of fail-fast, switch getters to property assignments → punishes credential-free specs, add?? ''→ silent''propagation, switch client TLD from@test.localto@example.com→ real-MX-records risk, switch URL apex fromexample.comto a real domain → accidental traffic, drop theDate.now()prefix → suite-lifetime collision risk, add a required env-var without updatingREQUIRED_ENV_VARS→ pre-flight prompt skips it, dropas const→ typos become silent test failures, public-route drift between the navigation andPUBLIC_ROUTESin either direction, hard-code'auth-states'instead of importing → multi-file rename becomes lossy, switchCLIENT_PASSWORDto a generator → failed sign-ups become harder to reproduce, move the file out ofhelpers/→Cannot find module, exportrequireEnv→ multiple opinions of "missing env-var" 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, adocs/log.mdentry, a Spec 010 — E2E Test Coverage cross-link if the change introduces a new shared concept, and a reviewer pass. -
apps/web-e2e/tests/apiAddedsurveys-exists-query.spec.ts— a query-param surface smoke spec forGET /api/surveys/exists(the navigation-shell surveys-existence-probe served byapps/web/app/api/surveys/exists/route.tsthat decides whether the "Surveys" link belongs in the header, the same way the siblingcategories/existsandcollections/existsprobes decide whether the "Categories" / "Collections" links belong there). Pins the route's status surface against future regressions that might introduce a?status=filter (which a future "show draft surveys" feature might tempt a contributor into adding by flipping the hard-codedSurveyStatusEnum.PUBLISHED), a?lang=filter, a?refresh=cache-bust, a?limit=override (which a contributor might wire to flip the hard-codedlimit: 1), or a non-200 status on an unknown?type=value (which a future "throw on unknown survey type" change might add). Walks the route's coercion contract (typeParam === SurveyTypeEnum.ITEM ? ITEM : GLOBALbyte-for-byte ternary so every non-'item'value — including'ITEM','global','',null, typos — falls through to the GLOBAL branch). Asserts< 500on every parameterised path; asserts the canonical{ exists: boolean }shape on the no-arg path; asserts status-equality between the no-arg case and a parameter-laden case to pin the "every unknown query key is silently ignored" invariant; asserts the GLOBAL-branch invariance across no-arg /?type=global/?type=unknown/?type=ITEM(case variant must NOT match) /?type=(empty); and asserts that the ITEM and GLOBAL branches both return the same canonical envelope shape. The matrix covers every?type=case variant (item/ITEM/Item/iTeM/GLOBAL/Global), unknown?type=values (location,tag,category,unknown,null), empty / whitespace?type=values, the obvious filter-by-state keys (?status=,?published=), pagination keys (?limit=), i18n keys (?locale=,?lang=), cache-busting keys (?refresh=,?force=,?fresh=,?nocache=), validation keys (?strict=,?validate=), projection keys (?include=,?fields=,?select=,?expand=), content-negotiation keys (?format=), multi-tenancy keys (?tenant=,?tenantId=), valid keys combined with unknown keys, repeated?type=keys (thesearchParams.get('type')first-value semantics), special characters in?type=and?include=(would tempt regex / LIKE / SQL-injection wiring), long values to guard against future regex-based indexing bugs, and bogus / typo'd keys. Cross-references Spec 010 — E2E Test Coverage, thecategories-exists-query.spec.tsandcollections-exists-query.spec.tssibling specs, thefeature-existence.spec.tsspec that already covers the surveys-exists endpoint indirectly, theapps/web/lib/types/survey.tsenum source, and theapps/web/app/api/surveys/exists/route.tssource file the spec is paired with. -
docs/pluginsAddedglobal-teardown.md— the 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 file documents the 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) and wired intoplaywright-config.mdvia the always-resolvedglobalTeardown: path.resolve(__dirname, './global-teardown.ts')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// Placeholder for future cleanup (e.g., test database reset)marker comment that prevents the file from being deleted as dead code, theexport default globalTeardown;shape Playwright's runner imports as(await import('./global-teardown.ts')).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 both the file and the config field, 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, per-run client account deletion viaTEST_DATA.generateClientEmail(), per-run Stripe / Polar / LemonSqueezy sandbox fixture cleanup,apps/web-e2e/test-results/directory cleanup on success, and test-database snapshot reset); 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); the "WhyglobalTeardownis not allowed to throw" rationale that pins the recommended per-buckettry / catch+console.errorpattern; the "WhyglobalTeardownruns once, not per-worker" rationale that pins the global-shared cleanup buckets 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'), addprocess.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, aplaywright-config.mdcross-check, anapps/web-e2e/helpers/test-data.tscross-check, ane2e-tsconfig.mdcross-check, dualpnpm tsc --noEmitruns (e2e + workspace root), a smoke-subset Playwright run that confirms the runner starts (noENOENT), exits cleanly (teardown returns within 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. Added per-source-file reference link todocs/index.mdunder the plugin-package per-source-file section right above the matchedglobal-setup.mdentry. Cross-references Spec 010 — E2E Test Coverage, theapps/web-e2e/playwright.config.tsglobalTeardown:field, theapps/web-e2e/global-setup.tsmatched pre-flight file,apps/web-e2e/helpers/test-data.tsfor the constants a future teardown will use,apps/web-e2e/tsconfig.jsonfor theinclude: ["./**/*.ts"]glob that picks up this file, and thedocs/log.mdrunning change log itself. -
apps/web-e2e/tests/apiAddedcollections-exists-query.spec.ts— a query-param surface smoke spec forGET /api/collections/exists(the navigation-shell existence-probe served byapps/web/app/api/collections/exists/route.tsthat decides whether the "Collections" link belongs in the header, the same way the siblingcategories/existsprobe decides whether the "Categories" link belongs there). Pins the route's status surface against future regressions that might introduce a?fresh=cache-busting wiring, a?strict=validation that throws, an?include=inactivetoggle (which a future "show archived collections" feature might tempt a future contributor into adding by flipping the hard-codedincludeInactive: falseargument), or a per-locale 404 (which a hypothetical i18n-aware variant might add). Walks the route's two-branch contract (happy path returns{ exists, count }with200; the catch branch — which today is the only legitimate non-200 path — returns{ exists: false, count: 0, error: 'Failed to check collections existence' }with500). Asserts on< 600 && >= 200for every parameterised path because both200and500are legitimate route branches; asserts the canonical{ exists: boolean, count: number }shape on the no-arg path; asserts status-equality between the no-arg case and a parameter-laden case to pin the "every unknown query key is silently ignored" invariant; asserts that?includeInactive=truedoes not flip the repository'sincludeInactiveflag (the route hard-codesfalsetoday); and asserts that the?locale=enand?locale=empty-string cases round-trip to the same status as the no-arg case (the route reads zero query input, so all three must land in the same branch). The matrix covers the obvious i18n keys (?locale=,?lang=), cache-busting keys (?refresh=,?force=,?fresh=,?nocache=), validation keys (?strict=,?validate=), projection keys (?include=,?fields=,?select=,?expand=,?includeInactive=), content-negotiation keys (?format=), filter-by-state keys (?status=,?active=), multi-tenancy keys (?tenant=,?tenantId=), the empty-value case for each, repeated keys, special-character values that would tempt a future regex / LIKE / path-injection wiring, long values to guard against future regex-based indexing bugs, and bogus / typo'd keys. Cross-references Spec 010 — E2E Test Coverage, thecategories-exists-query.spec.tssibling spec for the categories-existence probe, and theapps/web/app/api/collections/exists/route.tssource file the spec is paired with. -
docs/pluginsAddedglobal-setup.md— the 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, this file locks the suite's pre-flight boundary — what the runner does once before the first test, in what order, with what failure modes) and the type-checking companion toe2e-tsconfig.md(which scopes the type-checker's walk to include this file). Documents the ordered pre-flight sequence —promptForMissingEnv()first (walksREQUIRED_ENV_VARS = ['SEED_ADMIN_EMAIL', 'SEED_ADMIN_PASSWORD'], throws onprocess.env.CIto prevent CI hangs, prompts on a TTY usingreadline/promiseswith an empty-answer guard and atry / finallyclose),baseURLresolution fromconfig.projects[0]?.use?.baseURL ?? 'http://localhost:3000', 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 (~500 ms × 1 vs ~500 ms × 2, ~150 MB × 1 vs ~150 MB × 2), the admin sign-in flow (/auth/signin→#email/#password→ clickgetByRole('button', { name: /sign in/i })→waitForURL(/\/(admin|client\/dashboard)/)→storageState({ path: 'auth-states/admin.json' })→[global-setup] Admin auth state saved), the client sign-up flow (per-runTEST_DATA.generateClientEmail()→/auth/register→#name/#email/#password→press('Enter')instead of click →waitForURL(/\/client\/dashboard/, { timeout: 120_000, waitUntil: 'domcontentloaded' })→storageState({ path: 'auth-states/client.json' })→[global-setup] Client auth state saved), 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; the "WhystorageState({ path })instead of cookies-only" rationale; the "Why the admin flow accepts both/adminand/client/dashboard" role-tolerance rationale; the "Why the client flow usesdomcontentloadedinstead ofload" analytics-pixel rationale; the "Why theauth-states/directory is per-suite, not per-worker" cost rationale; 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 cross-link, and a reviewer pass. -
apps/web-e2eAdded a query-param surface smoke spec forGET /api/categories/exists(categories-exists-query.spec.ts) — the public categories-existence probe served byapps/web/app/api/categories/exists/route.tsthat the navigation shell hits on every render to decide whether the "Categories" link belongs in the header. The handler reads exactly one query param —?locale=— viarequest?.nextUrl?.searchParams?.get('locale') || 'en'and forwards it tofetchItems({ lang: locale }). The spec enumerates every plausible query-param shape a future contributor might add (?locale=en/fr/es/de/ar/zh/pt/ja, the obvious?lang=alias,?lang=and?locale=together to confirm?locale=continues to win,?refresh=/?force=/?fresh=/?nocache=cache-busting keys that the route does not honour,?strict=/?validate=keys that would tempt a future throw-on-invalid-locale wiring,?include=/?fields=/?select=/?expand=projection keys,?format=,?status=/?active=filter-by-state keys,?tenant=/?tenantId=multi-tenancy scoping keys, plus empty values that exercise the|| 'en'fallback, repeated keys, special-character payloads, long payloads, and bogus typo'd keys) and asserts the bulk-loop< 500contract (the route has two success branches — the happyfetchItemsresolution and the catch-and-empty fallback that maps every thrown error to{ exists: false, count: 0 }with status 200), the canonical{ exists: boolean, count: number }envelope shape on the happy path, the status-invariance between the no-arg and parameter-laden branches, a multi-permutation shape-stability assertion, and a dedicated?locale=en/?locale=/ no-arg three-way status-equality assertion that pins the|| 'en'fallback semantics. The spec guards against regressions that introduce a?fresh=cache-busting wiring, a?strict=locale validation that throws, or a per-locale 404 (which a future "treat unknown locales as missing" feature might tempt a future contributor into adding), and pairs withglobal-setup.mdin the same change so the per-source-file documentation set and the e2e coverage advance together. -
docs/pluginsAddedplaywright-config.md— the 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, this file locks the suite's runtime behaviour). Documents the at-a-glance summary of every load-bearing field (dotenv.config({ path: '../web/.env.local' })cross-app env loading, theBASE_URLoverride hatch with the'http://localhost:3000'default, theisCIboolean gate, thetestDir: './tests'andoutputDir: './test-results'artefact boundaries,fullyParallel: true,workers: isCI ? 2 : 1,retries: isCI ? 2 : 0, the per-environment reporter set withopen: 'never'on CI, 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(build && starton CI,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 "WhyBASE_URLis the only env-var override surface" rationale; the per-CI-vs-local branch matrix that maps eachisCI ? X : Ybranch to its trade-off; the "Why the three browser projects" cost / benefit matrix; the "WhywebServer.cwdis the monorepo root" rationale; the "Whystdout: 'pipe'andstderr: 'pipe'" self-diagnosing rationale; the failure matrix that maps eachplaywright.config.tsmistake (droppeddotenv.config(...)→ cryptic 500s, separateapps/web-e2e/.env.local→ drift,BASE_URLfallback dropped → cannot target deployed previews,fullyParallel: false→ ~3× wall-clock,workers > 2on CI → resource contention flakes,retries: 0on CI → un-mergeable flake amplification,githubreporter dropped → no inline annotations,htmlreporter dropped → unreproducible flakes,open: 'always'on CI → CI hangs on no-display,timeoutreduction → cold-render flakes,globalSetupdropped → unseeded specs,use.trace: 'off'on CI → un-diagnosable CI-only flakes,use.locale: 'en-GB'→ date-format breakage,use.timezoneId: 'UTC'→ timestamp-render breakage, project drop → engine-specific regressions slip past CI, 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 →EADDRINUSE,stdout: 'ignore'→ silent host-app errors) onto the layer that surfaces each one; the per-line walkthrough table; and theplaywright.config.ts-change checklist that ties any flip back to ae2e-tsconfig.mdcross-check, apnpm tsc --noEmitrun, a smoke-subset run, the per-CI-vs-local both-modes verification, adocs/log.mdentry, a Spec 010 cross-link, and a reviewer pass. -
apps/web-e2eAdded a query-param surface smoke spec forGET /api/items/[slug]/comments(item-comments-query.spec.ts) — the public per-item comments-list endpoint served byapps/web/app/api/items/[slug]/comments/route.ts. The handler signature isGET(request: Request, { params }: ...)—requestis declared but never read; the handler only awaitsparams.slug, callscheckDatabaseAvailability()(which short-circuits to an empty list when no DB is configured), and otherwise callsgetCommentsByItemId(slug). The spec enumerates every plausible query-param shape a future contributor might add (?limit=/?offset=/?page=/?pageSize=pagination keys,?sort=/?order=/?orderBy=sort keys,?rating=/?minRating=/?maxRating=filter keys,?include=/?fields=/?select=/?expand=projection keys,?userId=/?status=/?moderation=filter keys,?refresh=/?force=/?fresh=cache-busting keys,?format=,?locale=/?lang=,?since=/?from=/?until=time-window keys, plus empty values, repeated keys, special-character payloads, long payloads, and bogus typo'd keys) and asserts the bulk-loop< 500contract (the route has three success branches — DB-unavailable short-circuit, happy-path data-layer query, and catch-and-empty fallback — that all legitimately return200 OK), the canonical{ success, comments }envelope shape on the happy path, the status-invariance between the no-arg and parameter-laden branches, and a multi-permutation shape-stability assertion. The spec guards against regressions that introduce arequest.url-based wiring (which a future "filter by rating", "include only top-level", "sort by helpfulness", or "paginate" feature might tempt a future contributor into adding), and pairs withplaywright-config.mdin the same change so the per-source-file documentation set and the e2e coverage advance together. -
docs/pluginsAddede2e-tsconfig.md— the 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 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 (thetests/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); theexclude: ["node_modules"]resilience rationale; the deliberate divergences fromapps/web/tsconfig.json(no@/*alias, no**/*.tsxglob, no.next/types/**/*.tsor.next/dev/types/**/*.tsentries, noscripts/generate-openapi.tsentry, the leading-./anchor on the include glob); the per-line walkthrough that pins each line to a documentation impact; 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; and thetsconfig.json-change checklist that ties any flip back to atsconfig-presets.mdcross-check, adocs/log.mdentry, a Spec 010 cross-link, the dualpnpm tsc --noEmitruns (e2e + workspace root), the Playwright smoke run, and a reviewer pass. -
apps/web-e2eAdded a query-param surface smoke spec forGET /api/reference(reference-query.spec.ts) — the public Scalar API reference UI served byapps/web/app/api/reference/route.ts. The spec enumerates every plausible query-param shape a future contributor might add (?theme=,?layout=/?sidebar=/?showSidebar=,?spec=/?url=/?source=,?tag=/?operation=/?path=,?format=,?locale=/?lang=,?refresh=/?force=/?fresh=/?nocache=,?dark=/?darkMode=/?colorMode=, plus empty values, repeated keys, special-character payloads — including SSRF-shaped?spec=URLs — long payloads, and bogus typo'd keys) and asserts the bulk-loop< 500contract, the status-invariance between the no-arg and parameter-laden branches, a multi-permutation status-stability assertion, and a dedicated guard against a future?spec=SSRF wiring (the handler today is the library-provided constantApiReference(config)from@scalar/nextjs-api-referenceclosed over a staticconfigobject, so any caller-supplied URL must be ignored). The spec guards against regressions that swap the constant handler for arequest.url-based wiring, and pairs withe2e-tsconfig.mdin the same change so the per-source-file documentation set and the e2e coverage advance together. -
docs/pluginsAddedweb-app-tsconfig.md— the 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 theextends: "@ever-works/tsconfig/nextjs.json"chain that locks the workspace-wide TypeScript posture (target: ES2017,module: esnext,moduleResolution: bundler,strict: true,noEmit: true,jsx: react-jsx, thenextLSP plugin, thedom/dom.iterable/esnextlib set,allowJs: true,skipLibCheck: true,incremental: true,isolatedModules: true) 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.ts,**/*.ts,**/*.tsx,.next/types/**/*.tsfornext buildtyped-link declarations,scripts/generate-openapi.tsfor the OpenAPI-generation script, and.next/dev/types/**/*.tsfor the Next 16 dev-server variant); theexclude: ["node_modules"]resilience rationale; the per-line walkthrough that pins each line to a documentation impact; 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; and thetsconfig.json-change checklist that ties any flip back to atsconfig-presets.mdcross-check, adocs/log.mdentry, a Spec 002 cross-link, the dualpnpm tsc --noEmitruns, and a reviewer pass. -
apps/web-e2eAdded a query-param surface smoke spec forGET /api/items/[slug]/votes/count(item-vote-count-query.spec.ts) — the public per-item vote-count endpoint served byapps/web/app/api/items/[slug]/votes/count/route.ts. The spec enumerates every plausible query-param shape a future contributor might add (?userId=,?include=/?fields=/?select=,?expand=,?refresh=/?force=/?fresh=,?format=,?locale=/?lang=,?since=/?from=/?until=,?direction=, plus empty values, repeated keys, special-character payloads, long payloads, and bogus typo'd keys) and asserts the bulk-loop< 500contract, the canonical{ success, count }envelope shape on the happy path, the status-invariance between the no-arg and parameter-laden branches, and a multi-permutation shape-stability assertion. The spec guards against regressions that introduce arequest.url-based wiring (the handler signature isGET(request, context)—requestis declared but never read; the handler only awaitscontext.paramsand callsgetVoteCountForItem(slug)), and pairs withweb-app-tsconfig.mdin the same change so the per-source-file documentation set and the e2e coverage advance together.
2026-05-01
-
docs/pluginsAddednpmrc-config.md— the 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, 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), 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), the failure matrix that pins each setting flip to a concrete user-visible failure, the per-line walkthrough that pairs each line with the documentation impact, and the.npmrc-change checklist that ties any flip back to adocs/log.mdentry, a Spec 002 cross-link, and a reviewer pass. -
apps/web-e2eAdded a query-param surface smoke spec forGET /api/user/profile/location(user-profile-location-query.spec.ts) — the auth-gatedclientProfileId-derived location lookup served byapps/web/app/api/user/profile/location/route.ts. The spec enumerates every plausible query-param shape a future contributor might add (?userId=,?clientProfileId=,?id=,?include=,?fields=/?select=,?expand=,?refresh=/?force=/?fresh=,?format=,?locale=/?lang=,?privacy=, plus empty values, repeated keys, special-character payloads, long payloads, and bogus typo'd keys) and asserts the bulk-loop< 500contract and the canonical typed{ error }envelope shape on the unauthenticated branch. The spec guards against regressions that introduce arequest.nextUrl.searchParams.get(...)call (the handler signature isexport async function GET()— norequestparameter, no query-key reads), and pairs withnpmrc-config.mdin the same change so the per-source-file documentation set and the e2e coverage advance together. -
docs/pluginsAddedworkspace-root-manifest.md— the 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, this page documents the workspace-coordination posture itself — the eleven top-levelscripts.*entries 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 (engines.node>=20.19.0,packageManagerexact pinpnpm@10.31.0enforced by Corepack), the version-pinning posture for transitive dependencies viapnpm.overrides(@types/react19.2.7,@types/react-dom19.2.3,esbuild0.27.0,esbuild-register3.6.0,@opentelemetry/api1.9.0), thepnpm.publicHoistPattern: ['@opentelemetry/*']hoist rule 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 gatespostinstallhooks duringpnpm installunder pnpm 10's deny-by-default hardening, 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 —name,version,private,license,packageManagerwith the Corepack-shim rationale,engines.nodewith the Next.js 16 /apps/web/scripts/check-env.jsESM-API /node:testparity rationale, the elevenscripts.*entries with theirturbo run <task>delegations and the--filter=@ever-works/<name>shortcut rationale, the twodevDependencies.turbo/prettierranges with their exact-version rationales (Turborepo 2.x cache-key semantics +$schemaenforcement +persistent: truehonouringdependsOn; Prettier 3.x'soverridesmatcher syntax), thepnpm.publicHoistPatternrule and why it must coexist with the@opentelemetry/apioverride, 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). -
apps/web-e2eAdded a Playwright smoke spec for the query-param surface of the auth-gated current-tenant endpoint served byapps/web/app/api/tenant/route.tsatapps/web-e2e/tests/api/tenant-query.spec.ts. The handler signature isexport async function GET()(norequestparameter) and it reads zero query keys; the spec walks ~50 query strings —?tenantId=,?id=,?slug=,?include=,?fields=/?select=,?expand=,?refresh=/?force=/?fresh=,?format=,?locale=/?lang=, empty values, repeated keys, SQL-injection-style escapes (',<script>,..,%00), and 500-character payloads — assertingstatus < 500for each so a regression that introduces asearchParams.get(...)call is caught immediately as a typed-envelope-shape failure rather than as a session-bearing test flake. Pins the unauthenticated branch's typed{ tenant: null }envelope on the 401 path against accidental drops tonullor{ error: 'Unauthorized' }, and asserts that the bogus-query response and the no-arg response have identical envelope shapes. -
docs/pluginsAddedturbo-config.md— the per-source-file reference for the monorepo's Turborepo task pipeline paired withturbo.jsonat the repo root, the second root-level config reference afterpnpm-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 vs. two uncached, one persistent taskdev, local.turbo/cache backend, no remote-cache wired today, Prettier*.jsonoverride pinning JSON to spaces withtabWidth: 2); the file-contents walk-through (the full JSON file with one row per task entry); 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, the 19-entryenvallow-listANALYZE,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 mapping 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 mapping 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, apnpm-workspace.mdcross-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). -
apps/web-e2eAddedtests/api/config-features-query.spec.ts— query-param surface smoke for the/api/config/featuresendpoint, mirroring the pattern set bytests/api/feature-existence-query.spec.ts. The route's handler signature isexport async function GET()— norequestparameter and zerosearchParams.get(...)calls — so any query string the caller appends must be silently ignored. The spec walks ~70 query variants (locale-style keys?locale=,?lang=; tenant-style keys?tenant=,?tenantId=,?org=; filter keys?feature=,?features=; caller-controlled flag overrides?ratings=/?comments=/?favorites=/?featuredItems=/?surveys=; pagination keys?limit=,?offset=,?page=,?pageSize=; sort keys?sort=,?order=,?direction=; type-ahead keys?q=,?search=,?prefix=,?filter=; cache-bypass knobs?cache=,?fresh=,?nocache=,?bypass=,?_=<timestamp>; empty values, repeated keys, special-character percent-encoded values for%,/,\,',",<,>, null byte,;,--, long values up to 2,000 chars, prototype-pollution attempts via?__proto__=and?constructor=) and asserts each must respond with one of the two documented status codes (200 success, 500 catch-and-degrade) — anything else (502, 503, framework crash) indicates a regression. Three dedicated tests pin the canonical envelope shape ({ ratings: boolean, comments: boolean, favorites: boolean, featuredItems: boolean, surveys: boolean }on either branch, with all five flags hard-coded tofalseon the catch path), the per-branchCache-Controlheader (public, s-maxage=300, stale-while-revalidate=600on success vs.no-cacheon the 500 path so a degraded response is not pinned by a CDN for five minutes), the no-arg-vs-bogus-args status invariance, and the caller-controlled-flag-override ignorability (?ratings=falsedoes not flip the envelope's flag — the response reflects the server's view, not the client's suggestion). -
docs/index.mdAdded theturbo-config.mdentry to the monorepo / packages section so the new pipeline reference is discoverable from the top-level docs navigation, sitting immediately afterpnpm-workspace.mdas the second of the two root-level config references. -
docs/pluginsAddedpnpm-workspace.md— the 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. Where the package-level references document what each package contributes to the workspace graph, this page documents how the graph is declared in the first place — the single YAML file that pnpm reads before any package manifest is touched. The page documents the at-a-glance summary (pathpnpm-workspace.yamlat the repo root, YAML 1.2 format, singlepackagestop-level key, two globs"apps/*"and"packages/*", eight resolved members acrossapps/web,apps/docs,apps/web-e2eand 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, the"apps/*"glob and what it does and does not match, the"packages/*"glob and what it does and does not match); 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, apackages.mdcross-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). -
apps/web-e2eAddedtests/api/current-user-query.spec.ts— query-param surface smoke for the/api/current-userendpoint, mirroring the pattern set bytests/api/health-database-query.spec.ts,tests/api/version-query.spec.ts,tests/api/feature-existence-query.spec.ts, and the other*-query.spec.tsfiles. The handler signature isexport async function GET()(norequestparameter), so the spec walks the obvious query-param keys a future contributor might add (refresh,force,provider,tenantId,locale,lang,format,verbose,debug,fields,include,exclude) plus empty values, repeated keys, SQL-injection-shaped values (%27,%22,%3B,%2D%2D,'OR'1'='1,DROP+TABLE+users), an XSS-shaped value (<script>alert(1)</script>), a path-traversal-shaped value (../../etc/passwd), long values, and bogus typo'd keys. Asserts a tighter contract than the other query-smoke specs: the route is intentionally public (returnsnullrather than401when unauthenticated) so the only valid status is200, and any 4xx — not just 5xx — is a regression. Also pins the unauthenticated response envelope (the JSON literalnull, not{}, not{ user: null }, not the safe-user shape withnullfields), the authenticated envelope shape (idis a string,isAdminis a boolean — the only tworequiredfields per the route's swagger doc), the same-status invariant across baseline and parameterised URLs, the SQL-injection invariant (the route runsauth()only with no SQL interpolation, so injection-shaped values cannot reach any downstream layer), and the Authentication-spec sensitive-field-non-exposure contract that forbidspassword,passwordHash,hashedPassword,salt,token,accessToken,refreshToken,idToken,jwt,session,sessionToken,iat,exp,jti,sub, andsecretfrom appearing in the safe-user shape. -
docs/pluginsAddedtsconfig-presets.md— the 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 same wayeslint-config.mdpairs with the twopackages/eslint-config/*files andplugin-tsconfigs.mdpairs with the three plugin-packagetsconfig.jsonfiles. Whereplugin-tsconfigs.mdcovers the downstream plugin tsconfigs that extendbase.json, this page covers the upstream preset package itself — the three preset files plus the manifest that publishes them. The page documents 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; the per-field walk-through of each preset file (twelve compiler options onbase.jsonplus anexcludeentry, two-line override onnextjs.json, two-line override onplaywright.json); the per-field walk-through ofpackage.jsonplus 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; the consumer table mapping each of the six current consumers to itsextendstarget with the rationale; the deliberateapps/docsout-of-scope note; the cross-cutting concerns walkthrough (target: 'ES2017',module+moduleResolutionpair semantics,strictsub-flags,incrementalcache mechanics); the "How the leaves diverge from the base" matrix; the failure matrix that maps each preset-level mistake onto the layer that surfaces it; and the public-surface change checklist with the Constitution-Check note for Article II (TypeScript-Only) and Article III (Public-Surface Stability). -
apps/web-e2eAddedtests/api/health-database-query.spec.ts— query-param surface smoke for the/api/health/databaseendpoint, mirroring the pattern set bytests/api/version-query.spec.ts,tests/api/feature-existence-query.spec.ts, and the other*-query.spec.tsfiles. The handler signature isexport async function GET()(norequestparameter), so the spec walks the obvious query-param keys a future contributor might add (refresh,force,schema,database,table,timeout,check,probe,format,verbose,debug,locale,lang) plus empty values, repeated keys, SQL-injection-shaped values (%27,%22,%3B,%2D%2D,'OR'1'='1,DROP+TABLE+users), long values, and bogus typo'd keys. Asserts a tighter contract than the other query-smoke specs (< 500): the route's two valid branches are 200 (healthy) and 500 (unhealthy on a missing-database CI environment), and every parameterised URL must respond with the same status as the no-arg baseline — any URL-driven status drift is a real regression. Also pins the response envelope shape (statusone-of'healthy'/'unhealthy',databaseone-of'connected'/'disconnected',timestampan ISO-8601 string) and the SQL-injection invariant (the route runs a hard-codeddb.execute(sql\SELECT 1 as test`)` with no parameter binding, so injection-shaped values cannot reach the SQL layer). -
docs/index.mdAdded the Shared TypeScript Presets entry under the "For Contributors & AI Agents" section so the new plugin docs page is reachable from the docs index. -
docs/pluginsAddedeslint-config.md— the per-source-file reference for the workspace's shared ESLint flat config preset, paired withpackages/eslint-config/nextjs.mjsandpackages/eslint-config/package.json, the same wayplugin-tsconfigs.mdpairs with the three plugin-packagetsconfig.jsonfiles. Whereplugin-tsconfigs.mdcovers the workspace's TypeScript posture, this page covers the workspace's lint posture — the rules, the parser, the ignored globs, and thetsconfigPathparameter every consumer threads through. The page is organised as a per-block walkthrough of the three flat-config blocks the factory returns (block 1:ignoresfor**/node_modules/**,**/.next/**,**/out/**,**/build/**,**/dist/**, and**/*.config.{js,ts,mjs}with each pattern's rationale; block 2: JS/TS shared rules for*.{js,jsx,ts,tsx}withreact-hooks/rules-of-hooks: 'error'as the load-bearing rule,react-hooks/exhaustive-deps: 'warn'as the hint level, the deliberate'no-unused-vars': 'off'to defer to the TS-aware variant, and'no-console': 'off'to allow the structured-logging convention used by the API routes; block 3: TS-only typed rules for*.{ts,tsx}with the typed@typescript-eslint/parserthreadingparserOptions.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: '@ever-works/eslint-config',version: '0.0.0',private: true,license: AGPL-3.0, the single sub-pathexports."./nextjs"that forces consumers to import via the full path, the four direct dependencies and 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 scheduled in Spec 002; 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 aplugin-tsconfigs.mdcross-check, anauthoring-a-plugin.mdcross-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). Cross-linked fromplugin-tsconfigs.mdand fromdocs/index.md. -
apps/web-e2eAddedtests/api/version-query.spec.ts— a query-param surface smoke for the public version endpoints (/api/versionGET,/api/version/syncGET,/api/version/syncPOST). The existingversion.spec.tscovers the canonical no-arg / no-body happy paths; this spec walks ~50 query-string variations (?branch=,?refresh=,?force=,?clone=,?commit=,?sha=,?ref=,?repository=,?format=,?short=,?long=,?locale=,?lang=, empty values, repeated keys, special-character values that would tempt a future shell-quoting bug if a contributor ever swappedisomorphic-gitfor a shellgitinvocation, 500-character values, and bogus / typo'd unknown keys) and asserts each variation returns a non-5xx response, plus per-endpoint envelope-shape assertions (/api/version:{commit, message, ...}always at 200 withcommitalways a non-empty string andmessagealways a string on both the success and the graceful-degrade branches;/api/version/syncGET:{syncInProgress, lastSyncTime, timeSinceLastSyncHuman, uptime, timestamp}always at 200 withsyncInProgress: boolean,lastSyncTime: string | null,uptime: number >= 0), an "identical with and without bogus query parameters" status-code invariant for both GETs, and a POST/api/version/sync"ignores query parameters" invariant that proves the body-only handler does not regress to reading the URL. Closes the query-surface gap for these three endpoints in Spec 010. -
docs/pluginsAddedplugin-tsconfigs.md— the per-source-file reference for the three byte-identicaltsconfig.jsonfiles in the plugin-system packages, paired withpackages/plugin-sdk/tsconfig.json,packages/plugin-runtime/tsconfig.json, andpackages/plugin-demo/tsconfig.json, the same waysdk-package-manifest.mdpairs withpackages/plugin-sdk/package.json,runtime-package-manifest.mdpairs withpackages/plugin-runtime/package.json, andplugin-demo-package-manifest.mdpairs withpackages/plugin-demo/package.json. Where the package-manifest references cover how each package is wired into Node's resolution algorithm andpackage.json#exports, this page covers how each package's TypeScript compiler is configured whenpnpm tsc --noEmitruns against its sources. The page is organised as a field-by-field reference (extends: "@ever-works/tsconfig/base.json",compilerOptions.jsx: "react-jsx",compilerOptions.types: ["react"],include: ["src/**/*"],exclude: ["node_modules", "dist"]) with each field paired with its purpose, the practical consequence for plugin authors, and the change-event-class it implies for any third-party plugin author copying this configuration as a starting point; the per-flag walkthrough of the inherited base config (target: "ES2017",lib,allowJs,skipLibCheck,strict,noEmit,esModuleInterop,module,moduleResolution: "bundler",resolveJsonModule,isolatedModules,incremental) that pins each one to a documentation impact; thereact-jsxautomatic-runtime rationale (the SDK'splugin.tsreferencesReact.ComponentTypetypes so the JSX scope must be open even where no JSX is authored, runtime'sSlotHost.tsxand demo'sHeader.tsxauthor literal JSX, all three packages need the same JSX flag); thetypes: ["react"]whitelist semantics (transitive@types/node/@types/jest/ DOM-polyfill packages cannot leak ambient types into the plugin's compilation, plugin authors who needprocess.envambient typing must explicitly add"node"to their owntypesarray); theinclude-and-excluderationale that locks the package boundary atsrc/and forward-guards against a futuredist/build step (a one-off script inpackages/plugin-demo/scripts/is intentionally outside the type-check guarantee, which is the forcing function for "move undersrc/" or "stay outside the package's public surface"); the "How the three packages diverge from this baseline" matrix that lists every hypothetical override (types: ["react", "node"]for a Node-aware demo,declaration: truefor IDE pre-warm,outDir: "./dist"for a future build step,composite: truefor project-references parallelism,lib: ["esnext"]for a Node-only plugin, widenedincludefor a CLI-helper plugin, narrowedexcludefor co-located Vitest 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 leaking into IntelliSense from a removedtypeswhitelist,Output file 'dist/index.js' has not been built from source file 'src/index.ts'from an accidentalnoEmit: false,Compiler option 'isolatedModules' may not be used with 'composite'from acomposite: trueoverride that didn't dropisolatedModules, the demo'sCannot find module 'react/jsx-runtime'symptom of a React-18 lockfile downgrade while keepingjsx: "react-jsx", 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 (changes to JSX runtime / React peer-dep range / entry-file extension propagate tosdk-package-manifest.md,runtime-package-manifest.md, andplugin-demo-package-manifest.mdin the same commit), anAuthoring a Plugincross-check, apackages.mdcross-check, the dualpnpm tsc --noEmitruns (workspace-root + per-package, because Turborepo's cache may mask a regression that only shows up in the per-package 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 III (Public-Surface Stability). Cross-linked from the three package-manifest references' Cross-references sections and fromdocs/index.md. -
apps/web-e2eAddedtests/api/feature-existence-query.spec.ts— a query-param surface smoke for the four public feature-existence endpoints (/api/categories/exists,/api/collections/exists,/api/surveys/exists, and/api/items/export/settings). The existingfeature-existence.spec.tscovers the no-arg / single-canonical-arg happy path; this spec walks ~80 query-string variations (?locale=,?type=,?limit=,?offset=,?page=,?pageSize=,?q=,?search=,?filter=,?prefix=,?sort=,?order=,?direction=,?lang=, empty values, repeated keys, special-character values, 500-character values, and bogus / typo'd unknown keys across all four endpoints) and asserts each variation returns a non-5xx response, plus a per-endpoint envelope-shape assertion (categories:{exists, count}always at 200; collections: same envelope at 200 or 500 with the optionalerrorstring; surveys:{exists}always at 200; items/export/settings:{export_enabled}always at 200), plus an "identical with and without bogus query parameters" invariant for the three endpoints whose handlers do not read the request URL. Closes the query-surface gap for these four endpoints in Spec 010. -
docs/pluginsAddedplugin-demo-package-manifest.md— the per-source-file reference for the demo plugin package manifest that pairs withpackages/plugin-demo/package.jsonthe same waysdk-package-manifest.mdpairs withpackages/plugin-sdk/package.json,runtime-package-manifest.mdpairs withpackages/plugin-runtime/package.json, andplugin-demo.mdpairs with the bundled reference plugin's TypeScript sources underpackages/plugin-demo/src/. Where the Reference Plugin page documents the three TypeScript files (config.ts,Header.tsx,index.tsx), this page documents the package-level contract — thepackage.jsonfields that decide how the demo plugin is wired into the workspace and how a downstream plugin author must wire their own package the same way. The page is organised as a field-by-field reference (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-sdk(workspace),dependencies.zod,peerDependencies.react(required, nopeerDependenciesMetabecause the demo always ships a slot component), and thedevDependenciesset) with each field paired 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"); a failure matrix that maps each demo-level manifestation (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) onto the layer that surfaces it; and a public-surface change checklist that ties any field change to a cross-check againstplugin-demo.md,sdk-package-manifest.md,runtime-package-manifest.md(the three manifests move in lock-step onversion, Zod range, React peer range, andsideEffectsflag),packages.md, 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). Cross-linked fromdocs/index.mdPlugins section. -
apps/web-e2eAddedtests/api/location-listing-query.spec.ts— a Playwright API smoke spec that closes the query-param surface coverage gap for the public no-arg location-listing endpoints/api/location/citiesand/api/location/countries. The existinglocation.spec.tscovers the no-arg happy path for both endpoints; the new spec walks the query-param surface so a regression that introduces a typo'drequest.nextUrl.searchParams.get(...)call (which a future filter-by-country-prefix or filter-by-locale change might tempt a future contributor into adding) is caught immediately as a non-200 / non-404 / 5xx response. Both routes are intentionally no-arg — theGET()function signature isexport async function GET()— so the route's contract is that any query string is silently ignored, and the spec enumerates every plausible-future-typed key family (?city=/?country=typo'd from/api/location/coordinates;?prefix=/?q=/?search=/?filter=typed for type-ahead search;?limit=/?offset=/?page=/?pageSize=typed for pagination;?sort=/?order=/?direction=typed for sort wiring;?locale=/?lang=typed for i18n; empty-value forms; repeated keys; special-character values like%25/%2F/%5C/%27that would tempt a future regex / LIKE-prefix wiring; long values'x'.repeat(500); bogus / typo'd keys). The assertion contract is intentionally narrow — every URL must respond with a<500status, and the no-arg envelope must be either{ success: false, error: 'Location features are disabled' }(404 branch when the feature gate is off, the most-likely branch in local dev) or{ success: true, data: string[] }(200 branch when the feature is on and the data layer succeeds). The tworesponds identically with and without bogus query parametersassertions pin the contract that the route never reads the request URL, so the status code with any query string must match the no-arg status code exactly. -
docs/pluginsAddedruntime-package-manifest.md— the per-source-file reference for the runtime package manifest that pairs withpackages/plugin-runtime/package.jsonthe same waysdk-package-manifest.mdpairs withpackages/plugin-sdk/package.json,runtime-public-surface.mdpairs withpackages/plugin-runtime/src/index.ts, and the per-source-file reference set underdocs/plugins/already documents every TypeScript file inpackages/plugin-sdk/src/andpackages/plugin-runtime/src/. Where the Runtime Public Surface Reference documents the TypeScript barrel, this page documents the package-level contract — thepackage.jsonfields that decide which sub-paths are importable, how React is reached (peer dependency, required — unlike the SDK where it is optional), how the SDK is reached (workspace dependency viaworkspace:*), how Zod is reached (runtime dependency, required), and how bundlers tree-shake the runtime's React-aware<SlotHost />re-export off server bundles when the host imports through the narrowed./registry/./loader/./testingsub-paths. The page is organised as a field-by-field reference (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-sdk(workspace),dependencies.zod,peerDependencies.react(required, nopeerDependenciesMeta), and thedevDependenciesset) with each field paired with its purpose, why-it-matters note, and the change-event-class it implies for host-app authors; a 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); a 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 it; and a public-surface change checklist that ties any field change to a cross-check againstruntime-public-surface.md,sdk-package-manifest.md,packages.md, 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). Cross-linked fromdocs/index.mdPlugins section. -
apps/web-e2eAddedtests/api/location-search-query.spec.ts— a Playwright API smoke spec that closes the query-param surface detail coverage gap for the public/api/location/searchendpoint served byapps/web/app/api/location/search/route.ts. The existinglocation.spec.tscovers the no-params 400, the single-paramcity/country/ radius branches, and an invalid-coordinates 400; the new spec walks the full query- param surface detail (the radius branch'sparseFloatfinite-number checks, theparseInt(radius, 10)default-50 fallback, theradius=0/ negative /NaN400, thenear_lat=NaN/near_lng=NaN/infinity400, the only-one-of-the-pair fall-through to city / country, theif (city)/if (country)truthy guards, the percent-encoded UTF-8 city / country values, the whitespace-only city / country values that pass the truthy check, the branch-priority order radius > city > country, the unknown / typo'd parameter names that hit the no-params 400, and the repeated query keys that take the first value viasearchParams.get(name)) so a regression in any of those branches is caught explicitly. The assertion contract is intentionally narrow — every URL must respond with a<500status, the body must be valid JSON when present, and the JSON envelope must contain at least one ofsuccess/error/datakeys; whendatais present,data.slugsmust be an array anddata.distances(when present) must be a non-array object. 4xx-other and 5xx are never allowed because the route never validates beyond what the matrix above describes. -
docs/pluginsAddedsdk-package-manifest.md— the per-source-file reference for the SDK package manifest that pairs withpackages/plugin-sdk/package.jsonthe same waysdk-public-surface.mdpairs withpackages/plugin-sdk/src/index.ts,runtime-public-surface.mdpairs withpackages/plugin-runtime/src/index.ts,manifest.mdpairs withmanifest.ts,capabilities.mdpairs withcapabilities.ts,slots.mdpairs withslots.ts,providers.mdpairs withproviders.ts,plugin.mdpairs withplugin.ts,loader.mdpairs withloader.ts,registry.mdpairs withregistry.ts,slot-host.mdpairs withSlotHost.tsx,testing.mdpairs withtesting.ts, andplugin-demo.mdpairs with the bundled reference plugin underpackages/plugin-demo/src/. Where the SDK public-surface page documents the TypeScript barrel, this page documents the package-level contract — thepackage.jsonfields that decide which sub-paths are importable, how React is reached (peer dependency, optional), how Zod is reached (runtime dependency, required), and how bundlers tree-shake the SDK's type-only exports down to nothing. The page is organised as a field-by-field reference (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 each field paired with its purpose, why-it-matters note, and the change-event-class it implies for plugin authors; a 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); a 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 it; and a public-surface change checklist that ties any field change to a cross-check againstsdk-public-surface.mdandpackages.md, 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). Cross-linked fromdocs/index.mdPlugins section. -
apps/web-e2eAddedtests/api/location-coordinates-query.spec.ts— a Playwright API smoke spec that closes a coverage gap for the public/api/location/coordinatesendpoint served byapps/web/app/api/location/coordinates/route.ts. The existinglocation-coordinates.spec.tscovers the no-query-param happy path and two basic filter cases; the new spec walks the query-param surface (thesearchParams.get('city')/searchParams.get('country')reads, thecity.trim().toLowerCase()normalisation againstentry.cityNormalized/entry.countryNormalized, theif (city)/if (country)truthy guards, the!entry.isRemotefilter, theNumber(entry.latitude)/Number(entry.longitude)coercion, the 404-on-feature-disabled short-circuit, and the catch-and-500 fallback) so a regression in any of those branches is caught explicitly. The spec enumerates well-formed values (Paris,paris,PARIS,New%20York, percent-encoded UTF-8 likeS%C3%A3o%20PauloandBogot%C3%A1), whitespace-only values that pass the truthy check but normalise to an empty string (single space, double space,%09tab,%0Anewline), missing-key cases, and the combinedcity+countryshape. The assertion contract is intentionally narrow — every URL must respond with a JSON body matching{ success: true, data: [] | array }(200 branch, feature enabled) or{ success: false, error: string }(404 branch, feature disabled). 4xx-other and 5xx are never allowed because the route never validates the value and the data-layer call must not crash before the response renderer. -
docs/pluginsAddedruntime-public-surface.md— the per-source-file reference for the runtime barrel that pairs withpackages/plugin-runtime/src/index.tsthe same waysdk-public-surface.mdpairs withpackages/plugin-sdk/src/index.ts,manifest.mdpairs withmanifest.ts,capabilities.mdpairs withcapabilities.ts,slots.mdpairs withslots.ts,providers.mdpairs withproviders.ts,plugin.mdpairs withplugin.ts,loader.mdpairs withloader.ts,registry.mdpairs withregistry.ts,slot-host.mdpairs withSlotHost.tsx,testing.mdpairs withtesting.ts, andplugin-demo.mdpairs with the bundled reference plugin underpackages/plugin-demo/src/. The page is organised as a per-line walkthrough of the 15-line barrel: the JSDoc preamble's three pinned invariants (React-aware-only-in-SlotHostso a server action that importsPluginRegistrydoes not drag React into the server graph and the unit-test harness that importscreateTestRegistrydoes not need a JSDOM environment; owns-registry-loader-host so anything beyond register / enable / disable / load / render belongs in a host-app module that wraps the registry rather than in this package; cross-link todocs/architecture/plugin-system.mdso the architecture is the rationale and the barrel is the contract); line 9 thePluginRegistryvalue re-export and why it must cross the value boundary (anexport typewould erase the class at compile time andnew PluginRegistry({…})would fail at runtime); line 10 theloadPluginsandmergeConfigSourcesvalue re-exports plus the explicit reasonmergeConfigSourcesis a value re-export rather than a runtime-only helper (so a host app that builds config sources in a non-standard way like an admin REST handler that reads from a key-vault rather than a Postgres row can callmergeConfigSourcesdirectly to enforce the precedence contract without going throughloadPlugins, removing the temptation to reimplement the merge in the host app and accidentally reverse the precedence order); line 11 thePluginConfigSourcesandLoadPluginsResulttype-only re-exports plus the never-throws-for-plugin-level-config-failures invariant onLoadPluginsResult(every rejection lands inresult.rejectedso a host app can render a per-plugin admin UI that distinguishes "loaded successfully" from "loaded but rejected" without wrapping the loader call in a try / catch); line 12 theSlotHostvalue re-export and the Fragment-only zero-DOM output; line 13 theSlotHostPropstype-only re-export with theslotIdconstraint that catches typos at the call site; and line 14 thecreateTestRegistryvalue re-export with the explicit no-export typecompanion line because the helper's options object ({ plugins: DirectoryPlugin[] }) is an inline anonymous type and test consumers that want to refer to it by name should declare a local alias rather than expand the public surface here. The page also documents 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 value-vs-type contract that locks moving a name across theexport { ... }/export type { ... }boundary as a breaking change and points at@typescript-eslint/consistent-type-exportsas the lint rule the runtime turns on alongside the SDK; the failure matrix that maps barrel-level mistakes (Cannot find module '@ever-works/plugin-runtime/internal'from a non-public sub-path import,'LoadPluginsResult' is not exportedfrom a value-vs-type mis-import,PluginRegistry is not a constructorfrom a bundler tree-shaking the registry value re-export,<SlotHost />rendering an empty Fragment when the host layout passes a different registry instance than the oneloadPluginspopulated, plugin admin UI showing the plugin disabled when the host app storedLoadPluginsResult.registeredbut ignored enable state from the registry, full-runtime-pulled-in regression when thesideEffects: falseflag is dropped frompackage.json, React leaking into a server bundle when a host action imports from the barrel instead of the narrowed./registrysub-path) onto the layer that catches it (Node module resolution, TypeScript withverbatimModuleSyntax, the consumer call site, the<SlotHost />runtime, the admin dashboard, the bundle analyzer, the public page bundle-size budget under Spec 018, the server action bundle-size budget); and the public-surface change checklist that ties any addition / removal back to Spec Kit, the matching per-source reference page, thedocs/log.mdentry, thepnpm tsc --noEmitverification step, and Article VIII (No removal) for any name that needs to leave the barrel. Cross-link fromdocs/index.mdand fromdocs/plugins/packages.mdso the new doc is discoverable from both the docs index and the package overview alongside the SDK / runtime / demo source links. -
e2e/apiAddeditems-engagement-query.spec.ts— the public query-param surface smoke forGET /api/items/engagementthat pairs with the four obvious branches already initems-engagement-and-favorites.spec.tsthe same waysponsor-ads-public.spec.tspairs with the no-arg coverage already infeature-existence.spec.ts,featured-items-query.spec.tspairs with thefeatured-itemsno-arg case initems.spec.ts,items-export-query.spec.tspairs with theitems-exportno-arg case indiscovery.spec.ts, anditems-popularity-scores.spec.tspairs with thepopularity-scoresno-arg case indiscovery.spec.ts. The spec parametrises the route's singleslugsrequired comma-separated query parameter (split(',').map(s => s.trim()).filter(Boolean)-parsed with no upper limit beyond the 200-entry abuse-prevention ceiling) across the missing-param branch (returns400+{ error: 'Missing required parameter: slugs' }from thesearchParams.get('slugs') === nullcheck), the present-but-empty / whitespace-only / comma-only branches (?slugs=,?slugs=%20,?slugs=,,,— all produce an empty parsed list and return200+{ metrics: {} }via theslugs.length === 0guard), the single-known-or-unknown-slug case, the multi-slug happy path, the surrounding / interior whitespace case (the route trims each entry and drops trimmed-empty entries viafilter(Boolean)), the URL-encoded slug-content case (%2F,%2B,%25,%26are passed through verbatim to the data layer), the at-the-ceiling 200-slug case, the one-above-the-ceiling 201-slug case (the off-by-one boundary on theslugs.length > 200guard that the existing 250-slug case initems-engagement-and-favorites.spec.tsdoesn't pin explicitly), the extra-unknown-query-params case (the route only readsslugsfromsearchParams), and the repeatedslugskeys case (searchParams.getreturns the first occurrence; the rest are silently ignored — the route does not callsearchParams.getAll). Status< 500is the only asserted contract for the parametrised cases — the route has three distinct success branches that all legitimately return200 OKwith different payloads (thecheckDatabaseAvailability()short-circuit returning{ metrics: {} }, the happy-pathgetEngagementMetricsPerItem(slugs)query with theMap-to-plain-object conversion, and thetry / catchempty-fallback that handles internal errors by warning in dev and still returning{ metrics: {} }), and asserting on the body would pin the spec to a single branch and break under the others. Two extra small assertions pin the deterministic branches: the no-arg case must produce a 4xx with the missing-param envelope (or the DB-fallback{ metrics: {} }short-circuit if a future refactor swaps the order — the assertion is permissive on which envelope but strict on the 4xx-or-200 bracket so the JSON shape stays valid), and the two-slug happy path must always produce a 200 with ametricsplain-object envelope (not array, not null) so a future change that turned the route into a 4xx / 5xx response on a well-formed request would be caught explicitly. -
docs/pluginsAddedsdk-public-surface.md— the per-source-file reference for the SDK barrel that pairs withpackages/plugin-sdk/src/index.tsthe same waymanifest.mdpairs withmanifest.ts,capabilities.mdpairs withcapabilities.ts,slots.mdpairs withslots.ts,providers.mdpairs withproviders.ts,plugin.mdpairs withplugin.ts,loader.mdpairs withloader.ts,registry.mdpairs withregistry.ts,slot-host.mdpairs withSlotHost.tsx,testing.mdpairs withtesting.ts, andplugin-demo.mdpairs with the bundled reference plugin underpackages/plugin-demo/src/. The page is organised as a per-line walkthrough of the 40-line barrel: the JSDoc preamble's three pinned invariants (framework-agnostic, thereactpeer-dependency-not-direct-dep stance, and the cross-link todocs/architecture/plugin-system.md); lines 11-12 the capability re-exports (CAPABILITIESandisCapabilityas values,Capabilityas a type-only re-export with the value-vs-type split thatisolatedModulesenforces); lines 14-15 the slot re-exports with the same shape; line 17 the manifest type re-exports (PluginManifest<C>andPluginConfig<C>); lines 19-30 the nine concrete capability provider interfaces and theCapabilityProviderMapmapped type re-exports; line 32 the only value re-export fromplugin.ts(defineDirectoryPlugin) and the inference path the factory's<C extends z.ZodTypeAny>signature creates; lines 33-39 the five plugin-shape type re-exports (DirectoryPlugin<C>,PluginContext<TConfig>,SlotComponentProps<TConfig>,PluginProviders,PluginSlots<TConfig>). The page also documents thepackage.json#exportssub-path map (.,./capabilities,./slots) and the deliberate decision to keepmanifest,providers,plugin,loader,registry, andSlotHostreachable only through the barrel (so adding a new capability or provider interface does not implicitly create a public sub-path); the value-vs-type contract that locks moving a name across theexport { ... }/export type { ... }boundary as a breaking change and points at@typescript-eslint/consistent-type-exportsas the lint rule the SDK turns on once the surface is stable; the failure matrix that maps barrel-level mistakes (Cannot find module '@ever-works/plugin-sdk/manifest'from a non-public sub-path import,'Capability' is not exportedfrom a value-vs-type mis-import,defineDirectoryPlugin is not a functionfrom a bundler tree-shaking a value re-export,ctx.configtyping asunknownwhen an author skips the factory, new capability not appearing in admin UI when the id is missing from theCAPABILITIEStuple, new manifest field silently ignored when the barrel re-export is missing, full-SDK-pulled-in regression when thesideEffects: falseflag is dropped frompackage.json) onto the layer that catches it (Node module resolution, TypeScript withverbatimModuleSyntax, the consumer call site, the admin dashboard, the bundle analyzer, the public page bundle-size budget under Spec 018); and the public-surface change checklist that ties any addition / removal back to Spec Kit, the matching per-source reference page, thedocs/log.mdentry, thepnpm tsc --noEmitverification step, and Article VIII (No removal) for any name that needs to leave the barrel. Cross-link fromdocs/index.mdand fromdocs/plugins/packages.mdso the new doc is discoverable from both the docs index and the package overview alongside the SDK / runtime / demo source links. -
e2e/apiAddedsponsor-ads-public.spec.ts— the public query-param surface smoke forGET /api/sponsor-adsthat pairs with the no-arg coverage already infeature-existence.spec.tsthe same wayfeatured-items-query.spec.tspairs with thefeatured-itemsno-arg case initems.spec.ts,items-export-query.spec.tspairs with theitems-exportno-arg case indiscovery.spec.ts, anditems-popularity-scores.spec.tspairs with thepopularity-scoresno-arg case indiscovery.spec.ts. The spec parametrises the route's singlelimitquery parameter (Number(...)-ed with default10,Number.isFinite ? Math.min(Math.max(1, Math.floor(value)), 50) : 10-clamped) across the [1, 50] valid range, beyond the upper clamp (51,999,10000), below the lower clamp (0,-5,-1), non-numeric /NaN/Infinity/-Infinity(which exercise theNumber.isFinitefallback path), float (truncated viaMath.floorbefore clamping), leading-whitespace /+sign, extra unknown query params (silently ignored), and repeatedlimitkeys (only the first occurrence is read bysearchParams.get). Status< 500is the only asserted contract — the route has three distinct success branches that all legitimately return200 OKwith different payloads (thecheckDatabaseAvailability()short-circuit returning{ success: true, data: [] }, the happy-pathsponsorAdService.getActiveSponsorAdsWithItemsquery, and thetry / catchempty-list fallback that handles internal errors by logging in development and still returning{ success: true, data: [] }), and asserting on the body would pin the spec to a single branch and break under the others. A separate small assertion on the no-arg path verifies that the JSON envelope shape ({ success: true, data: [...] }withdataan array) is preserved across all three branches so a future change that turned the route into a 4xx / 5xx response would be caught explicitly. -
docs/pluginsAddedplugin-demo.md— the per-source-file reference for the bundled reference / demo plugin that pairs withpackages/plugin-demo/src/index.tsx,packages/plugin-demo/src/config.ts, andpackages/plugin-demo/src/Header.tsxthe same waymanifest.mdpairs withmanifest.ts,capabilities.mdpairs withcapabilities.ts,slots.mdpairs withslots.ts,providers.mdpairs withproviders.ts,plugin.mdpairs withplugin.ts,loader.mdpairs withloader.ts,registry.mdpairs withregistry.ts,slot-host.mdpairs withSlotHost.tsx, andtesting.mdpairs withtesting.ts. The page documents 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 that ties each of the three source files to the SDK surface they consume; the per-line walk-through ofConfigSchemaandDemoConfig(the.default(true)/.default('Demo plugin loaded')calls that makez.infer<typeof ConfigSchema>non-optional and let the loader parse cleanly with no config sources at all); theDemoHeaderBadgeprops / render contract / disabled-config short-circuit (theif (!ctx.config.enabled) return null;line the admin enable / disable flow exercises through merged config sources rather than registry-level unregistration) 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>so the slot component cannot drift out of sync with the schema; the three call sites the demo plugin participates in (loader Zod parse + register, registry key under'demo', slot host render via<SlotHost slotId="header.right" />); the failure matrix that maps demo-plugin manifestations onto the loader / registry / slot-host failure surfaces (Zod-rejectedenabled: 'yes'/greeting: 42,templateRangemismatch, admin override flippingenabledpost-boot, duplicate-name throw); the replace-the-demo-plugin recipe that exercises the slot ordering guarantee, the admin toggle, and thedefaultEnabled: falselever without removing the reference package from tree (per the no-removal rule); and the evolution checklist that pairs every source-file change with the matching SDK reference page anddocs/log.mdentry. Cross-link fromdocs/index.mdso the new doc is discoverable from the docs index alongside the SDK / runtime / demo package links it complements. -
e2e/apiAddedfeatured-items-query.spec.ts— the query-param surface smoke for the publicGET /api/featured-itemsendpoint that pairs with the no-arg coverage already initems.spec.tsthe same wayitems-export-query.spec.tspairs with theitems-exportno-arg case indiscovery.spec.tsanditems-popularity-scores.spec.tspairs with thepopularity-scoresno-arg case indiscovery.spec.ts. The spec parametrises the route's two query parameters: thelimitparameter (Number.parseInt-ed with default6,Math.min(Math.max(value, 1), 50)-clamped,Number.isFinitefallback to default forNaN) across the [1, 50] valid range, beyond the upper clamp (51,999,10000), below the lower clamp (0,-5), non-numeric / empty (abc,NaN, empty string), float (6.5,49.9), and leading-whitespace /+sign (%2010,%2B10) cases that exercise every branch ofNumber.parseInt+ clamp + finiteness fallback; and theincludeExpiredparameter (strict=== 'true'check) across the literal'true'flip and every other value that keeps the default-on path ('false','1','0', empty,'TRUE'). Combinedlimit+includeExpiredcases verify the two parameters stay independent. Status< 500is the only asserted contract — the route has two distinct success branches (DB-available query vs.checkDatabaseAvailability()-short-circuit /getTenantId() === null-short-circuit, both legitimately returning200 OKwith different payloads) plus a catch-and-empty fallback, and asserting on the body would pin the spec to a single branch and break under the others. -
docs/pluginsAddedproviders.md— the parallel per-export capability-provider reference that pairs withpackages/plugin-sdk/src/providers.tsexactly the waymanifest.mdpairs withmanifest.ts,capabilities.mdpairs withcapabilities.ts,slots.mdpairs withslots.ts,loader.mdpairs withloader.ts,registry.mdpairs withregistry.ts,slot-host.mdpairs withSlotHost.tsx,testing.mdpairs withtesting.ts, andplugin.mdpairs withplugin.ts. The page is one section per public export ofproviders.ts: each of the nine concrete provider interfaces (AuthProvider,PaymentProvider,AnalyticsProvider,SearchProvider,ContentSource,MapsProvider,NewsletterProvider,NotificationsProvider,AIProvider) with one sub-section per member documenting its type, nullability, and per-member type-system notes (the(string & {})literal-with-fallback trick onPaymentProvider.idthat keeps the union open without giving up autocomplete on the three built-in literals; thePromise<unknown[]>widening contract onSearchProvider.searchthat defersItem-shape assertion to the host; thePromise<unknown | undefined>absent-vs-error distinction onContentSource.getItemwhereunknownis success,undefinedis 404, and a thrown error is the third case; thevoid | Promise<void>sync-or-async pattern on optional hooks that lets a synchronous backend declare without anasyncwrapper; the{ ok; reason? }result envelope onNewsletterProviderthat surfaces provider-specific failures as data so the host renders them without a try/catch on the request path; themarkRead(string[])batch-baked-into-the-type contract onNotificationsProvider; the deliberately-minimal v1AIProvider.completeshape that a futureAIProvider<TStream extends boolean = false>extension can grow without breaking), theCapabilityProviderMapmapped type that binds every member ofCapabilityto its provider interface and typesPluginRegistry.get<C>/list<C>/PluginProvidersgenerically including the'ui-slot' = neverlockout that turns anyproviders: { 'ui-slot': anything }attempt into a TypeScript compile error and the[K in Capability]?: K extends keyof CapabilityProviderMap ? CapabilityProviderMap[K] : never;mapped-type expression that catches an unknown-capability key the same way; 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 which calls are async; and a nine-row failure matrix that maps every observable failure mode (missing required interface member, extra unknown member excess-property check,'ui-slot'provider attempt as a TypeScript compile error, provider attached for an undeclared capability as the same compile-time category error via the[K in Capability]?: …mapped type,setupthrow routed toLoadPluginsResult.rejected[name].reason: 'setup', fan-outforwardthrow swallowed by the host wrapper, single-lookup throw propagated through normaltry/catch, runtime malformed shape caught by the host's per-call re-narrowing, two enabled plugins on the same single-lookup capability resolved as "first-registered wins") onto the layer that surfaces it. The page bookends Spec 002's per-source-file SDK reference set so every public export of everypackages/plugin-sdk/**andpackages/plugin-runtime/**source file now has a paireddocs/plugins/<file>.mdpage with the same> When the SDK adds, removes, or renames an export update **this** page in the same changeanti-drift contract. Cross-linked fromdocs/plugins/plugin.md,docs/plugins/capabilities.md,docs/plugins/manifest.md,docs/plugins/registry.md,docs/plugins/loader.md,docs/plugins/slots.md,docs/plugins/slot-host.md,docs/plugins/testing.md,docs/plugins/lifecycle.md,docs/plugins/authoring-a-plugin.md,docs/plugins/testing-a-plugin.md,docs/plugins/packages.md, anddocs/index.md; the parallel pagedocs/plugins/capabilities.mdretains the runtime contract angle (lookup style, fan-out vs. single, dispatch order) while this new page owns the TypeScript shape angle (per-member type-system notes, theCapabilityProviderMapmapped-type expression, and the compile-time failure modes), and the two pages cross-link to make the split explicit so a reader implementing a provider knows to read this one and a reader deciding which capability to declare knows to read the other. -
spec-002Updateddocs/spec/002-plugin-architecture/tasks.md's T-010 to enumeratedocs/plugins/providers.mdalongside the other thirteendocs/plugins/**pages and to document the same anti-drift / per-member / read-write / failure-matrix cross-reference contract this new page satisfies — completing the per-source-file SDK doc set so everypackages/plugin-sdk/**andpackages/plugin-runtime/**source file is paired with exactly onedocs/plugins/<file>.mdreference under Spec 002. -
apps/web-e2eAddedtests/api/items-export-query.spec.ts— ten cases that exercise the query-param surface ofapps/web/app/api/items/export/route.ts(the Zod-validatedformatenumexportQuerySchema: both valid valuescsv/xlsx, the empty-string rejection, the unknown-value rejections, the case-sensitivity check, and the unknown-extra-key passthrough). Complements the single happy-path entry already smoked indiscovery.spec.tsso a regression in the schema, the default-on-omit fallback, the rate-limit short-circuit, or thegetExportEnabled()feature-flag gate surfaces as a failing case rather than a silent change in export behaviour. No-5xx contract; payload shape andContent-Typeare intentionally not asserted because the response is either a 403 / 4xx JSON envelope or a binary CSV / XLSX stream depending on whether the export feature flag is on for the active config repository. -
docs/pluginsAddedplugin.md— the parallel per-export plugin definition reference that pairs withpackages/plugin-sdk/src/plugin.tsexactly the waymanifest.mdpairs withmanifest.ts,capabilities.mdpairs withcapabilities.ts,slots.mdpairs withslots.ts,loader.mdpairs withloader.ts,registry.mdpairs withregistry.ts,slot-host.mdpairs withSlotHost.tsx, andtesting.mdpairs withtesting.ts. The page is one section per public export ofplugin.ts: thedefineDirectoryPluginfactory (and its inference-only semantics — the function returns its argument unchanged and never validates / mutates anything; validation is the loader's job and registration is the registry's job), theDirectoryPlugin<C>interface (with per-field sub-sections formanifest,setup,teardown,slots,providersthat document the runtime contract for each hook including the silent-rejection / propagated-throw distinctions, the "where it runs" / "use it for" / "do not use it for" / "what happens if it throws" framing established by the earlier per-source-file references), thePluginContext<TConfig>runtime context (one sub-section per field —config,name,enabled, optionallogger— including the always-trueinvariant forenabledinsidesetup, the explicit "whereconfigcomes from" three-step trace throughmergeConfigSources→ Zod parse →ctx.config, and theconsole-vs-ctx.loggerguidance), theSlotComponentProps<TConfig>slot-component contract (singlectxfield, no extra props from<SlotHost />, request-scoped data viaheaders()/cookies()/ context providers above the host), and thePluginProvidersandPluginSlots<TConfig>typed maps (mapped-type internals including the'ui-slot' = neverlockout that catchesproviders: { 'ui-slot': anything }at compile time and thePartial<Record<SlotId, ...>>shape that catches unknown slot ids the same way). The page also documents a nine-row failure matrix that lists every observable failure mode in the loader / registry /<SlotHost />layers a plugin returns into (hand-rolled plugin losesCinference at the TypeScript layer, duplicatenameis the only manifest-level propagated throw viaregister,manifest.configrejection routes throughLoadPluginsResult.rejected[name].reason: 'config'silently, invalid / unmatchedtemplateRangeroutes the same way withreason: 'templateRange', throwingsetupis plugin-local withreason: 'setup', throwingteardownis swallowed bydisable, slot-component throw bubbles through React, and the two TypeScript-only failures —'ui-slot'provider attempt and unknownSlotId— are caught at compile time), a read / write surface summary that mirrors themanifest.mdandregistry.mdtables and maps every caller (plugin author,loadPlugins,PluginRegistry.register,PluginRegistry.disable,<SlotHost />,createTestRegistry, slot components) to the fields they touch, three worked examples (minimaldefineDirectoryPlugincall, asetuphook reading the typedctx.config, a slot component readingprops.ctx), and a five-step "how to add a new plugin field" checklist that mirrors the patterns established incapabilities.md,slots.md,loader.md,registry.md,slot-host.md,testing.md, andmanifest.md— bookending the SDK with the same anti-drift contract every per-source-file SDK / runtime page now satisfies. Cross-links added inauthoring-a-plugin.md,lifecycle.mdSee also,loader.mdSee also,registry.mdSee also,slot-host.mdSee also,testing.mdSee also,testing-a-plugin.mdSee also,packages.mdSee also,capabilities.mdSee also,slots.mdSee also,manifest.mdSee also, anddocs/index.md. Spec 002T-010task list grew from "eleven pages" to "twelve pages" and adds an explicit "doc and SDK cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.md,slots.md,loader.md,registry.md,slot-host.md,testing.md, andmanifest.md). -
apps/web-e2eAddedapi/items-popularity-scores.spec.ts(15 cases) closing the query-param surface of the publicGET /api/items/popularity-scoresdebug endpoint served byapps/web/app/api/items/popularity-scores/route.ts. The single happy-path entry (/api/items/popularity-scoreswith no query) was already smoked bydiscovery.spec.ts; this spec exercises the route'sparseInt+Math.min(value, 100)clamp onlimit(valid integers5/20, beyond-clamp values999/10000, empty string falling back to the'20'default, non-integerabc, negative-5, zero, plus combinedlimit=200&locale=de) and thelocaledefault / unknown-locale fallback (en,fr,zh,__no_such_locale__) so a regression in the route's parameter parsing surfaces as a failing case rather than a silent change in scoring output. Same conservative no-5xx contract as the rest of the smoke layer — payload shape is intentionally not asserted because the score breakdown varies with the active data repository / database state.E2E-TESTS.mdupdated with the entry and the continual-improvement total annotation (~292 → ~307 across 47 → 48 spec files). -
docs/pluginsAddedmanifest.md— the parallel per-field manifest reference that pairs withpackages/plugin-sdk/src/manifest.tsexactly the wayregistry.mdpairs withregistry.ts,loader.mdpairs withloader.ts,slot-host.mdpairs withSlotHost.tsx, andtesting.mdpairs withtesting.ts. The page is one section per field (name,version,description,templateRange,capabilities,config,defaultEnabled,adminToggleable,homepage) plus an eight-row failure matrix covering every observable manifest-level outcome (duplicatename→ the only propagated throw, invalid semver intemplateRange→ silent rejection with reasontemplateRange, mismatchedtemplateRange→ same, emptycapabilities→ emptylist<C>index, Zod-rejectedconfig→ silent rejection with reasonconfig,defaultEnabledvs DB row → DB wins,adminToggleable: falsevs programmaticdisable→ mutation succeeds (UI hint, not authz), non-URLhomepage→ not validated). It documents thePluginManifest<C>interface, thePluginConfig<C>type alias the SDK ships, the registry / loader /<SlotHost />reads every field powers (manifest.name→ React key, registry primary key,plugin_settingsrow id;manifest.capabilities→ registrylist<C>index;manifest.config→loadPluginsZod gate;manifest.templateRange→ boot-time semver compatibility check), and the rename-is-a-breaking-change contract that previously lived only in source comments. The page closes with a five-step "how to add a new manifest field" checklist that mirrors the patterns established incapabilities.md,slots.md,loader.md,registry.md,slot-host.md, andtesting.md— bookending the surface so every per-source-file SDK / runtime page is now covered by a matching anti-drift reference. Cross-links added inauthoring-a-plugin.md,lifecycle.md,loader.md,registry.md,slot-host.md,testing.md,testing-a-plugin.md,capabilities.md,slots.md,packages.md, anddocs/index.md. Spec 002T-010task list grew from "ten pages" to "eleven pages" and adds an explicit "doc and SDK cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.md,slots.md,loader.md,registry.md,slot-host.md, andtesting.md). -
apps/web-e2eAddedapi/client-item-restore.spec.ts(1 case) closing the last/api/client/**per-id surface that was previously implicit rather than explicit:POST /api/client/items/[id]/restore, the soft-delete restore action served byapps/web/app/api/client/items/[id]/restore/route.ts. The matching CRUD surface (GET / PATCH / DELETE /api/client/items/[id]) is already smoked viaclient-protected.spec.ts; this spec closes the per-id action sub-route. Same conservative no-5xx pattern as the rest of the smoke layer — uses an intentionally non-existent UUID so the spec never depends on data-repository content.E2E-TESTS.mdupdated with the entry and the continual-improvement total annotation (~291 → ~292 across 46 → 47 spec files). -
apps/web-e2eAddedapi/nextauth-discovery.spec.ts(9 cases) closing the NextAuth catch-all (/api/auth/[...nextauth]) public discovery surface: GETproviders,csrf,session,signin,signout,errorplus POSTsignout(no CSRF), POSTcallback/credentials(empty body), and GETcallback/<unknown-provider>— no-5xx contract for every entry. Closes the last NextAuth-managed surface that was implicit rather than explicit (the custom/api/auth/change-passwordhelper sits inauth-change-password.spec.ts). Also addedpublic/seo-manifests.spec.ts(4 cases) for the public SEO / manifest surface generated byapp/{robots,sitemap,opengraph-image}.{ts,tsx}and the static favicon:/robots.txt(withUser-agentcontent sanity check),/sitemap.xml(XML prolog sanity check),/opengraph-image,/favicon.ico— no-5xx contract. Same conservative pattern as the rest of the smoke layer so the specs stay valid across local / CI environments.E2E-TESTS.mdupdated with both entries and the continual-improvement total annotation. -
docs/pluginsAddedtesting.md— the parallel per-helper testing reference that pairs withpackages/plugin-runtime/src/testing.tsexactly the wayregistry.mdpairs withregistry.ts,loader.mdpairs withloader.ts, andslot-host.mdpairs withSlotHost.tsx. The page is one section per helper (createTestRegistry({ plugins })is the only one in v1) plus a six-row failure matrix covering every observable outcome (Zod-rejected schema → silent drop, throwingsetup→ loader records as rejected but helper still resolves, duplicate-name → the only propagated throw out of the helper, empty-array → empty registry no-op,defaultEnabled: false→ registered but not enabled, slot component throws on render → bubbles through React when<SlotHost />calls it). It documents the four thingscreateTestRegistrydoes in order (new PluginRegistry()withpersistEnabledundefined, map each plugin to a{ plugin }envelope,await loadPlugins(...), return the loaded registry) and the explicit non-goals that previously lived only in source comments — the helper is not a registry constructor, not a config harness, not a rejection inspector, not a persistence harness, not a render harness, not async-cleanup-aware — so test authors can pick the right tool the first time. It also documents the dual import surface (from '@ever-works/plugin-runtime'versusfrom '@ever-works/plugin-runtime/testing') declared in the runtime'spackage.jsonexportsmap, the read / write surface summary that maps callers (plugin package unit tests, capability composition tests, slot composition tests, admin enable / disable tests, config-required plugins, rejection-asserting tests, persistence-callback tests) to the methods they're allowed to invoke, three worked Vitest examples (happy-path register-and-slot, config-required plugin via directloadPlugins, disable-then-empty round-trip) — the same three paths thatapps/web-e2e/tests/plugins/admin-toggle.spec.tsandapps/web-e2e/tests/plugins/slots.spec.tscover at the Playwright layer (per Spec 002 / T-009), and a five-step "how to add a new test seam" checklist that mirrors the patterns established incapabilities.md,slots.md,loader.md,registry.md, andslot-host.md. Cross-links added inauthoring-a-plugin.md,lifecycle.mdSee also,testing-a-plugin.mdSee also,packages.mdSee also,capabilities.mdSee also,slots.mdSee also,loader.mdSee also,registry.mdSee also,slot-host.mdSee also, anddocs/index.md. Spec 002T-010task list grew from "nine pages" to "ten pages" and adds an explicit "doc and runtime cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.md,slots.md,loader.md,registry.md, andslot-host.md). -
docs/pluginsAddedslot-host.md— the parallel per-component SlotHost reference that pairs withpackages/plugin-runtime/src/SlotHost.tsxexactly the wayregistry.mdpairs withregistry.tsandloader.mdpairs withloader.ts. The page is one section per prop (slotId,registry,fallback?) plus a six-row failure matrix covering every observable outcome (no contributors, all contributors disabled, one or more enabled contributors, contributed component throws, duplicate plugin name — already caught one level up byPluginRegistry.register, unknownslotIdtyped throughany). It documents the four things<SlotHost />does in order (callslotsFor, fall back tofallback ?? nullon empty, wrap each contribution in a Fragment keyed bypluginName, return with no extra DOM wrapper) and the server-friendliness contract that previously lived only in source comments — no"use client", no client-only hooks, noreact-domimport, only a synchronous registry read — which means a layout that uses<SlotHost />stays a server component even when its contributed slot components opt into client rendering. It also documents the anti-patterns (the host is not a wrapper element, not a client component, not a reactivity boundary, not an error-boundary, not a way to pass extra props to slot components) so layout authors do not have to read the source to rule them out. Three worked Vitest examples cover the happy-path render, the empty-fallback path, and the disable-then-empty round-trip — the same three paths thatapps/web-e2e/tests/plugins/slots.spec.tscovers at the Playwright layer (per Spec 002 / T-009). The page also documents the dual import surface (from '@ever-works/plugin-runtime'versusfrom '@ever-works/plugin-runtime/SlotHost') and a five-step "how to add a new prop" checklist that mirrors the patterns established incapabilities.md,slots.md,loader.md, andregistry.md. Cross-links added inauthoring-a-plugin.md,lifecycle.mdSee also,testing-a-plugin.mdSee also,packages.mdSee also,capabilities.mdSee also,slots.mdSee also,loader.mdSee also,registry.mdSee also, anddocs/index.md. Spec 002T-010task list grew from "eight pages" to "nine pages" and adds an explicit "doc and runtime cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.md,slots.md,loader.md, andregistry.md). -
docs/pluginsAddedregistry.md— the parallel per-API registry reference that pairs withpackages/plugin-runtime/src/registry.tsexactly the wayloader.mdpairs withloader.ts. One section per public method (new PluginRegistry({ persistEnabled? }),register,isEnabled,isRegistered,enable,disable,get<C>,list<C>,slotsFor,list_all) with the full TypeScript signature, the throws / no-throws contract, and the precise idempotence rules (enableon an already-enabled plugin is a no-op and does not invoke the persistence callback;disableon an already-disabled plugin does not invoketeardown). The page includes a read / write surface summary that maps callers (layouts / capability code / admin UI / boot / tests) to the methods they're allowed to invoke, plus an explicit eight-row failure matrix covering the throwing outcomes (duplicate-name onregister, unregistered name onenable/disable), the silent outcomes (already-enabled, already-disabled, unknown capability returningundefined/[], emptyslotsFor), and the propagating outcomes (throwingpersistEnabled, throwingteardown— including the "stays disabled in memory" semantics that allow safe retries). Two worked Vitest examples cover the disable-then-slotsFor-empty round-trip and the duplicate-name throw. The page also documents thedefaultEnabledprecedence (opts?.enabled ?? plugin.manifest.defaultEnabled ?? true) and the rationale for the underscore-casedlist_allname — both facts that previously lived only in the source comments. Cross-links added inauthoring-a-plugin.md,lifecycle.mdSee also,testing-a-plugin.mdSee also,packages.mdSee also,capabilities.mdSee also,slots.mdSee also,loader.mdSee also, anddocs/index.md. Spec 002T-010task list grew from "seven pages" to "eight pages" and adds an explicit "doc and runtime cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.md,slots.md, andloader.md). -
docs/pluginsAddedloader.md— the parallel per-API loader reference that pairs withpackages/plugin-runtime/src/loader.tsexactly the waycapabilities.mdpairs withproviders.tsandslots.mdpairs withslots.ts. One section per export (loadPlugins,mergeConfigSources,PluginConfigSources,LoadPluginsResult) with the full TypeScript signature, the precedence rule (env ⊆ db ⊆ override), and an explicit six-row failure matrix covering the five outcomes ("config passes Zod", "config fails Zod", "setup throws", "enabled: false+ valid config", "duplicate plugin name", "empty plugins array") that previously lived only in the source comments. The page also includes a worked Vitest example that callsloadPluginsdirectly to verify override precedence and the validation-failure path, plus a five-step "how to add a new loader feature" checklist that mirrors the patterns established incapabilities.mdandslots.md. Plugin authors and host-app integrators previously had to read the loader source to discover that a plugin whosesetup()throws appears in bothregisteredandrejected, that the merge is intentionally shallow (not deep), and that the loader does not abort on failure; that information now lives in one place. Cross-links added inauthoring-a-plugin.md,lifecycle.mdSee also,testing-a-plugin.mdSee also,packages.mdSee also,capabilities.mdSee also,slots.mdSee also, anddocs/index.md. Spec 002T-010task list grew from "six pages" to "seven pages" and adds an explicit "doc and runtime cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.mdandslots.md). -
docs/pluginsAddedslots.md— the parallel per-slot reference that pairs withpackages/plugin-sdk/src/slots.tsexactly the waycapabilities.mdpairs withproviders.ts. One section per canonical slot id (header.left,header.right,footer.center,home.before-listing,home.after-listing,item.detail.sidebar,item.detail.actions,item.detail.afterFooter,admin.layout.header.right,admin.settings.section,admin.dashboard.widgets,admin.items.row.actions,admin.items.toolbar,client.dashboard.widgets,client.settings.section) with the layout it renders into, the intended use case, and any composition caveats. Top of the page documents the{ ctx }component contract (props are fixed; render an accessible region; keep server-friendly; localise vianext-intl), the composition rules (registration-order, multi-contributor support, immediate disable,fallbacksemantics, fragment-only host), and a five-step "how to add a new slot" checklist. Cross-links added in the architecture page (Slots table now points at this reference as the source of truth),authoring-a-plugin.md,lifecycle.md,testing-a-plugin.md,capabilities.md,packages.md, anddocs/index.md. Spec 002T-010task list grew from "five pages" to "six pages" and adds an explicit "doc and SDK cannot drift" verification bullet for the new reference (matching the wording added forcapabilities.md). -
docs/pluginsAddedcapabilities.md— the missing per-capability reference that pairs withpackages/plugin-sdk/src/providers.ts. One section per canonical capability (auth,payment,analytics,search,content-source,maps,newsletter,notifications,ai,ui-slot) with the full TypeScript interface, the lookup style (single-provider viaregistry.getvs fan-out viaregistry.list), the rules the runtime applies when two enabled plugins declare the same capability, and a five-step "how to add a new capability" checklist that mirrors Spec 002. Plugin authors previously had to read the SDK source to discover thatanalytics/newsletter/notificationsare fan-out and thatauth/payment/search/content-source/maps/aiare single-lookup; that information now lives in one place. Cross-links added in the architecture page (Capabilitiestable now points at this reference as the source of truth),authoring-a-plugin.mdSee also,lifecycle.mdSee also,testing-a-plugin.mdSee also,packages.mdSee also, anddocs/index.md. Spec 002T-010task list grew from "four pages" to "five pages" and adds an explicit "doc and SDK cannot drift" verification bullet for the new reference. -
apps/web-e2eAddedpublic/per-survey-public.spec.ts(1 —GET /surveys/[slug]with an unknown slug; exercises thenotFound()/ disabled-feature branch with the same non-5xx contract as the rest of the smoke layer. Closes the last public-survey page surface that was implicit rather than explicit; the listing page is already covered bypublic/surveys.spec.ts, the dashboard owner flow bypublic/dashboard-surveys-protected.spec.ts, the admin per-slug pages bypublic/admin-by-id-pages-protected.spec.ts, and the REST surface byapi/surveys.spec.ts).E2E-TESTS.mdupdated with the new entry and the continual-improvement headline total annotation (now ~278 tests across 44 spec files). -
docs/pluginsAddedtesting-a-plugin.md(~6 KB) — author-facing guide that pairs with the existingauthoring-a-plugin.md. It documents the four-layer test pyramid for plugins (manifest/Zod schema, registry round-trip viacreateTestRegistry, slot rendering through<SlotHost />, and Playwright smoke specs), an explicit what not to do list (don't mockPluginRegistry, don't reach intoapps/web/**, don't assert on translatable copy), and an "override" recipe for schemas with non-default required fields. Each example imports from the published runtime exports (@ever-works/plugin-runtime/testing,@ever-works/plugin-runtime/SlotHost,@ever-works/plugin-runtime), so the doc andpackages/plugin-runtime/src/testing.ts/SlotHost.tsxcannot drift. Cross-links added inauthoring-a-plugin.md(replaces the inline "Add a Playwright spec" snippet with a pointer to the dedicated guide and the original Playwright example),packages.mdSee also section, andlifecycle.mdSee also section.docs/index.mdnow lists three plugin guides under the spec-driven pointers —Authoring a Plugin, the previously-unlinkedPlugin Lifecycle, and the newTesting a Plugin. Spec 002tasks.mdT-010task list grew from "three new pages" to the four canonicaldocs/plugins/**pages and now includesdocs/plugins/packages.md+docs/plugins/testing-a-plugin.md, with an explicit "doc and runtime cannot drift" verification bullet. -
docs/architectureplugin-system.mdstatus block updated from proposed to in-progress (Phase A scaffolding shipped in commit8b68d29a); the "two packages" section now reads "three packages" to include the existing@ever-works/plugin-demoreference plugin (with a note that it is not part of the runtime contract). The Slots table was extended from 9 rows to the full 15 canonical slot ids (home.after-listing,item.detail.actions,admin.layout.header.right,admin.items.row.actions,admin.items.toolbar,client.settings.section) and now points readers atpackages/plugin-sdk/src/slots.tsas the authoritative source so the doc and the SDK can never drift again. -
apps/web-e2eAddedapi/item-company-write.spec.ts(2 —POSTandDELETE/api/items/[slug]/companyfor a non-existent slug; the matchingGETis already covered inpayment-protected.spec.tsline 37, but the write surfaces of the per-item admin company-assignment route were not explicitly smoke-tested. Same no-5xx contract as the rest of the smoke layer — anonymous callers must receive a 4xx, never a 5xx).E2E-TESTS.mdupdated with the new entry. -
spec-018Added018-performance-budget(proposed): full spec/plan/tasks trio that converts Article V of the constitution into a measurable, CI-enforced contract — per-route gzip first-load JS budget, apnpm perf:bundlescript, a Lighthouse CI workflow, and a maintainer-facing dashboard page. Two PRs are scoped: PR 1 (bundle gate + docs) and PR 2 (Lighthouse CI). Two open questions recorded indocs/questions.md(Q-018a Lighthouse trigger surface; Q-018b budget file location). No code yet — this entry only adds the docs/spec scaffolding so future work stops "Article V is aspirational" being a true statement. -
spec-017Status flipped from in-progress to shipped in the spec index and inspec.md. All T-001..T-009 tasks landed in commitfe808cc3(feat: more on maps) on thedevelopbranch — sidebar + dedicated/maproute + header nav link + e2e coverage are live. Follow-up enhancements (sidebar virtualisation, mini-map embed on item-detail) tracked as separate iterations. -
questionsAddedQ-018a(Lighthouse trigger surface) andQ-018b(perf budget file location) under the new Spec 018 section. -
spec-017Added017-map-view(proposed → implemented in this PR): spec/plan/tasks for the listing map view + dedicated/maproute + headerMapnav link gated onsettings.header.map_enabled. AddsMapSidebar, extendsLayoutMapwith marker↔card sync and auto-fit bounds, addsapps/web-e2e/tests/public/map.spec.tsanddocs/features/map-view.md. No new dependencies. -
indexAdded a Maps & Location bullet todocs/index.mdKey Features that links to the new feature page. -
docs/featuresmaps-location.mdandguides/map-integration-guide.mdnow cross-link to the Map View feature page and Spec 017. -
READMERootREADME.mdTech Stack now mentions Mapbox GL JS / Google Maps and a new "Maps & Location" section documents the Map view config, env vars, and YAML location example. -
apps/web-e2eAdded two more smoke spec files closing the last notable per-slug surfaces that were not yet explicitly covered:public/per-slug-public.spec.ts(3 —/comparisons/[slug],/categories/[category],/tags/[tag]per-slug detail pages with an intentionally unknown slug; exercises each page'snotFound()/ disabled-feature branch with the same non-5xx contract used elsewhere in the smoke layer; complements the legacy(listing)versions inpublic/legacy-routing.spec.ts) andapi/item-comment-rating-by-id.spec.ts(2 —GETandPATCHof/api/items/[slug]/comments/rating/[commentId]for a non-existent comment id; closes the last/api/items/[slug]/**per-comment route that was not explicitly smoke-tested — sibling routes/api/items/[slug]/comments/ratingand.../comments/[commentId]are already covered byapi/item-public.spec.tsandapi/items-engagement-and-favorites.spec.ts). Same no-5xx contract as the rest of the smoke layer.E2E-TESTS.mdupdated with both entries and the continual-improvement headline total annotation (now ~277 tests across 43 spec files).
2026-04-30
apps/web-e2eAddedapi/item-votes-public.spec.ts(2 — publicGET /api/items/[slug]/votesnon-existent-slug contract: no-5xx status plus a non-error JSON envelope when the body parses). This closes the last public per-item GET surface that was implicit rather than explicit (the/votes/countroute is its sibling and was already covered byapi/item-public.spec.ts; the auth-gated/votesPOST/DELETE and/votes/statusGET sit initems-engagement-and-favorites.spec.tsandpayment-protected.spec.tsrespectively). Same no-5xx contract as the rest of the smoke layer.E2E-TESTS.mdupdated with the new entry and the continual-improvement headline total annotation.spec-001Added retroactiveplan.mdandtasks.mdso the monorepo-conversion spec now carries the full Spec Kit spec → plan → tasks trio. Both files state up front that they are retroactive and defer to the originals underdocs/plans/2026-03-08-monorepo-conversion*for historical sequencing per Article VIII of the constitution. The spec index (docs/spec/README.md) gained inline(plan, tasks)links on the 001 row and a clarifying line in Conventions explaining when a retroactive trio is reconstructed for parity.apps/web-e2eAdded three more smoke spec files closing the remaining admin-by-id and client / page-by-id gaps not covered by the earlier collection-level specs:api/admin-by-id.spec.ts(47 — admin per-[id]REST routes across categories, clients, collections (+ items helper), comments, companies, featured-items, items (+ history / review / full import), notifications read receipt, reports, roles (+ permissions sub-route), sponsor-ads (+ approve / cancel / reject), tags, users - settings POST),api/items-engagement-and-favorites.spec.ts(11 — public/api/items/engagement4 cases including missing-slugs, empty-slugs, unknown-slugs, and >200-slugs guard + auth-gated comment-by-id PUT / DELETE, vote toggle / clear, and favorites GET / POST +/favorites/[itemSlug]DELETE),public/admin-by-id-pages-protected.spec.ts(18 — admin per-id page routes/admin/clients/[id],/admin/surveys/[slug]/{edit, preview,responses},/admin/auth/signin, plus/client/**authenticated owner pages: dashboard, profile/[username], settings (basic-info / billing / location / portfolio / theme-colors / submissions/trash), security, sponsorships, submissions, submissions/trash). Same no-5xx contract as the rest of the smoke layer.E2E-TESTS.mdupdated with all three entries and the continual-improvement headline total annotation.apps/web-e2eAdded five more page-route smoke specs closing the remaining gaps in the public + protected page surface:public/pricing-success.spec.ts(2 —/pricing/successwith and without checkout query params),public/listing-paginated.spec.ts(6 —/discover/[page]with a high page number,/collections/paging[/page],/tags/paging[/page]),public/legacy-routing.spec.ts(5 — legacy nested catch-alls/categories/category/[...categorie],/tags/tag/[...tags], and the(listing)group's/tags/[...tag]),public/item-survey-public.spec.ts(2 — public per-item survey response page/items/[slug]/surveys/[surveySlug]for unknown slugs),public/dashboard-surveys-protected.spec.ts(3 — owner flow/dashboard/items/[itemId]/surveys[/preview|/responses]redirect-or-404 contract). Same no-5xx contract as the rest of the smoke layer.E2E-TESTS.mdupdated with all five entries and the continual-improvement headline total annotation.apps/web-e2eAdded eleven more smoke specs closing the largest remaining coverage gaps:api/admin-protected-extra.spec.ts(36 admin-only endpoints across every slice — categoriesall/git/reorder, clientsdashboard/stats/advanced-search/bulk, collections, comments, companies, featured-items, geo-analytics, itemsstats/bulk/export/export/sample/import/validate, location-index, navigation, notificationsmark-all-read, reportslist/stats, roleslist/active/stats, settingslist/map-status, sponsor-ads, tagslist/all, twenty-crmconfig/test-connection, userscheck-email/check-username/stats),api/client-protected.spec.ts(8/api/client/**endpoints — dashboardstats,geo-stats, items list /coordinates/stats, importsample/validate/POST),api/surveys.spec.ts(8 auth-gated CRUD + per-survey responses - per-response detail),api/payment-checkouts.spec.ts(28 auth-gated checkout / payment-method / setup-intent / subscription mutation routes across Stripe, LemonSqueezy, Polar, Solidgate + sponsor-ad lifecycle),api/auth-change-password.spec.ts(2 no-session / empty-body cases),api/location-coordinates.spec.ts(3 enabled / disabled feature-gate cases),api/user-profile-location.spec.ts(2 GET + PUT no-session cases),api/reports.spec.ts(2 no-session / empty-body cases),public/newsletter-unsubscribe.spec.ts(2 with / without token),public/integration.spec.ts(3/integration/{analytics,posthog, speed-insights}showcase pages), andpublic/admin-pages-protected.spec.ts(18/admin/**and/dashboard/**page routes redirect anonymous visitors without 5xx). Same no-5xx contract as the rest of the smoke layer.E2E-TESTS.mdupdated with all eleven entries and the continual-improvement headline total annotation.apps/web-e2eAdded six more API smoke spec files closing remaining coverage gaps in the public surface:api/feature-existence.spec.ts(/api/categories/exists,/api/collections/exists,/api/surveys/existswithtype=item|global,/api/items/export/settings),api/location.spec.ts(/api/location/countries,/cities,/searchwith no-params / city / country / valid-radius / invalid-coords variants — covers both the location-enabled 200/400 and location-disabled 404 contracts),api/item-public.spec.ts(public per-item GETs and POSTs against a non-existent slug — votes/count, comments listing, comments/rating, views POST, unauthenticated comments POST),api/cron-jobs.spec.ts(/api/cron/subscription-expirationand/api/cron/subscription-reminderswith no secret and with a wrong secret),api/stripe-public.spec.ts(/api/stripe/productsdynamic-pricing gate), andapi/payment-protected.spec.ts(13 auth-required user / Stripe / LemonSqueezy / sponsor-ads / payment account / per-item company / votes-status surfaces). Same no-5xx contract as the rest of the API smoke layer.E2E-TESTS.mdupdated with all six entries and the continual-improvement total annotation.spec-002Status moved from proposed to in-progress in the spec index now that Phase A (T-001/T-002/T-003 — SDK, runtime, and demo plugin scaffolds) has shipped. T-004..T-012 still remain.apps/web-e2eAdded API smoke specs for previously-uncovered endpoint surfaces:api/version.spec.ts(GET/api/version, GET and POST on/api/version/sync),api/webhooks.spec.ts(Stripe, LemonSqueezy, Polar, Solidgate webhook GET / unsigned-POST contracts — both must be 4xx, never 5xx),api/discovery.spec.ts(public sponsor-ads, items popularity-scores, items export, items/[slug] 404 contract),api/protected.spec.ts(10 auth-required endpoints across tenant, admin, user, client, current-user surfaces — must respond 4xx, not 5xx, when unauthenticated), andapi/method-guards.spec.ts(POST-only/api/extract,/api/verify-recaptcha,/api/geocode, plus/api/internal/db-initdev-gate and/api/cron/synccontract). Each spec only asserts "no 5xx" so it stays valid across local / CI environments.apps/web-e2e/E2E-TESTS.mdupdated with new entries and the headline total annotation.apps/web-e2eAdded smoke specs for previously-uncovered surfaces:auth/new-verification.spec.ts,public/docs.spec.ts,public/cms-page.spec.ts(the generic/pages/[slug]route),client/billing.spec.ts(dashboard billing auth + redirect), andapi/reference.spec.ts(Scalar API reference UI +openapi.json).apps/web-e2e/E2E-TESTS.mdupdated to list each new spec.docs/pluginsAddedpackages.md— a per-package overview of@ever-works/plugin-sdk,@ever-works/plugin-runtime,@ever-works/plugin-demo. Linked fromdocs/index.md.spec-002Phase A complete: scaffolded@ever-works/plugin-sdk,@ever-works/plugin-runtime, and@ever-works/plugin-demoper Spec 002 / T-001..T-003. All three packages typecheck cleanly. Noapps/webwire-up yet — that lands in Phase B (T-004..T-006).spec-006,spec-007,spec-008,spec-009,spec-011,spec-012,spec-013,spec-014,spec-015,spec-016Added implementation plans + task lists, completing the Spec Kit trio (spec.md+plan.md+tasks.md) for every retroactive feature spec from this run. Each plan documents the existing topology and the migration path to the plugin architecture (Spec 002).apps/web-e2eAdded smoke specs for previously-uncovered surfaces to close gaps in Spec 010:auth/forgot-password.spec.ts,auth/new-password.spec.ts,public/help.spec.ts,public/about.spec.ts,public/comparisons.spec.ts,public/sponsor.spec.ts.spec-003Added implementation plan + tasks for auth providers, documenting the existing topology and the migration path to plugin packages once Spec 002 stabilises.spec-004Added implementation plan + tasks for payment providers with the same pattern (current shape + plugin-migration target).spec-005Added implementation plan + tasks for i18n covering message catalogue management, locale switcher, RTL, and Docusaurus i18n.spec-016Added retroactive spec for the typed analytics events layer shipped in PR #692, sitting on top of Spec 008.spec-010Added implementation plan and task list for the e2e test coverage spec, including engineering backlog (resilience and speed passes).docs/pluginsAddedlifecycle.mdcovering boot, validation, registration, setup, runtime use, enable/disable/swap, teardown, events, versioning, and anti-patterns.claudeAdded a "Read first" block toCLAUDE.mdpointing to AGENTS.md,.specify/,docs/spec/, log, and questions.spec-002Added Spec Kit feature spec, plan, and tasks for the plugin / adapter architecture.spec-001Added retroactive spec for the monorepo conversion (the underlying plan documents indocs/plans/are kept untouched per Article VIII of the constitution).spec-003,spec-004,spec-005,spec-006,spec-007,spec-008,spec-009,spec-010,spec-011,spec-012,spec-013,spec-014,spec-015Added retroactive specs for the shipped or in-progress features (auth providers, payment providers, i18n, Git CMS, theming, analytics, admin dashboard, e2e test coverage, maps, newsletter, notifications, docs translation, Spec Kit adoption).constitutionCreated.specify/memory/constitution.mdwith ten durable principles (Plugin-First, TypeScript-Only, Spec-Before-Code, Documentation-First, Performance Budget, Latest Stable Frameworks, Reuse Before Build, No Removal Without Migration, Test Coverage Bar, Modular Packages).docs/.specifyAdded.specify/README.md, the constitution, and the spec / plan / tasks templates per the GitHub Spec Kit convention.agentsRewroteAGENTS.mdto enumerate the cross-cutting rules for any AI agent operating in this monorepo (Spec-Driven Development, plugin-first, TypeScript-only, performance budget, latest frameworks, reuse, no-removal, test bar, docs-first, modular packages, safety, continual-improvement runs).indexLinked.specify/,docs/spec/,docs/log.md, anddocs/questions.mdfromdocs/index.md.questionsCreateddocs/questions.mdto capture open questions with chosen defaults.
2026-04-26 (pre-Spec-Kit)
docs/architectureTranslation work landed for architecture pages (PR #681).docs/apiTranslation work landed for API pages (PR #680).docs/advanced-guidedocs/featuresdocs/paymentTranslations landed (PR #677).
2026-03-08
- Monorepo conversion design and plan landed in
docs/plans/2026-03-08-monorepo-conversion.mdanddocs/plans/2026-03-08-monorepo-conversion-design.md. These remain the definitive source for that effort and are now cross-linked fromdocs/spec/001-monorepo-conversion/spec.md.
How to add an entry
- Append a single line under the most recent date heading; create a new date heading for a new day.
- Keep entries in a stable bullet style (
- area: summary). - If the change implements or amends a spec, link the spec folder.
- If the change has a PR, mention the PR number in parentheses.