@ttt-productions/dev-standards 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/dev-standards",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",
@@ -29,8 +29,9 @@ A step can protect more than one area; overlap is intentional.
29
29
 
30
30
  - **Architecture:** 9, 10, 12, 16–18, 25, 31, 35, 38, 41, 51, 52, 55, 61, 63, 64, 80, 83, 96, 99
31
31
  - **Backend:** 10, 13, 20, 21, 23, 29, 32, 33, 36, 40, 49, 50, 56, 67, 77, 80, 87, 97, 98, 102, 105,
32
- 107, 108, 111, 112
33
- - **Frontend:** 1–3, 7, 15, 17, 21, 28, 35, 36, 57, 60, 65, 66, 68, 82, 91, 99, 101, 103, 104, 114
32
+ 107, 108, 111, 112, 121
33
+ - **Frontend:** 1–3, 7, 15, 17, 21, 28, 35, 36, 57, 60, 65, 66, 68, 82, 91, 99, 101, 103, 104, 114,
34
+ 121
34
35
  - **Media:** 4–6, 11, 14, 37, 39, 53, 58, 59, 83
35
36
  - **Quality / documentation:** 25, 27, 28, 30, 40, 42, 46, 63, 69, 73, 75, 88, 89, 90, 95, 117–120
36
37
 
@@ -798,9 +799,10 @@ RULE-STD-ENG-009. **Scope:** `src`.
798
799
  # Swallowed rejections — a user-initiated action's failure always reaches one surface.
799
800
  rg -n "\.catch\(\s*\(\s*\w*\s*\)\s*=>\s*(undefined|null|\{\s*\}|void 0)\s*\)" src -g '*.ts' -g '*.tsx' -g '!**/__tests__/**' -g '!src/test/**'
800
801
  ```
801
- **Expected:** the guards are green: every callable is declared once through the firebase-helpers
802
- factory kind that fixes its retry, surface, and capture, from the app's one callable policy, and is
803
- called through its declaration — a raw callable site opts out of the policy's filter and interceptions.
802
+ **Expected:** the guards are green: every callable is declared once through the factory kind from
803
+ query-core `./react`'s `createCallables` that fixes its retry, surface, and capture, from the app's
804
+ one callable policy, and is called through its declaration — a raw callable site opts out of the
805
+ policy's filter and interceptions.
804
806
  One rejection gets exactly one surface: no second capture or second destructive toast. A CALLABLE
805
807
  failure's copy comes only from the policy's one describer, so a raw SDK string never reaches the user;
806
808
  a READ failure renders the canonical load-error state, which owns the one "could not load" sentence
@@ -816,12 +818,14 @@ half is guard-enforced).
816
818
  ```bash
817
819
  <GUARD_SUITE> -t "every schema bound in the app, its Functions, and its Workers derives from a named limit|a stored enum value renders through its label map, never as raw text|no app module re-declares a constant the core package exports|no app module restates a core literal union"
818
820
  <GUARD_SUITE> -t "every core schema bound derives from a named constant outside the reviewed structural bounds|every free-text property of a core wire input is a declared text field"
821
+ <GUARD_SUITE> -t "app routes are built by the one route owner"
819
822
  rg -n "maxLength=\{[0-9]|minLength=\{[0-9]" src -g '*.tsx' -g '*.ts' -g '!**/__tests__/**'
820
823
  rg -n "\.length\s*[<>]=?\s*[0-9]{2,}|\.slice\(0,\s*[0-9]" src functions/src -g '*.ts' -g '*.tsx' -g '!**/__tests__/**' -g '!*.test.ts' -g '!*.test.tsx'
821
824
  ```
822
825
  **Expected:** the guards are green: every schema bound is a named limit, a text-field declaration, or an
823
826
  enum schema; a stored enum value renders through its label map (never raw, never CSS-transformed); no app
824
- module restates a package constant or union. The greps: no free-text input carries a literal
827
+ module restates a package constant or union; every page path is built through the app's one route
828
+ owner in `<APP_DATA_PACKAGE>`, never written as a route literal. The greps: no free-text input carries a literal
825
829
  `maxLength` — its cap is its `<APP_DATA_PACKAGE>` text-field declaration passed as the input's format;
826
830
  every remaining length attribute reads a named constant; each length or excerpt guard on user text
827
831
  imports its constant or is a reviewed structural literal; and a mode-varied value is never a literal
@@ -1475,6 +1479,22 @@ file in the repository with its license beside it; no stylesheet declares its ow
1475
1479
  nothing names a hosted font service.
1476
1480
  **Status:** PASS · FAIL · NOT RUN · BLOCKED
1477
1481
 
1482
+ ### 121. Admin actions outside the admin area go through the admin-actions affordance
1483
+ **Rules:** RULE-STD-FRONTEND-111, RULE-STD-BACKEND-011. **Scope:** `src`, read against the admin-only
1484
+ callables the guard derives. A FULL audit classifies every reviewed entry; a PARTIAL audit, the
1485
+ touched areas.
1486
+ ```bash
1487
+ <GUARD_SUITE> -t "admin actions outside the admin area go through the admin-actions affordance"
1488
+ ```
1489
+ **Expected:** the guard is green. Outside the admin area, every admin-only callable is reached only
1490
+ through the one shield-marked admin-actions affordance. Every other read of admin standing outside the
1491
+ admin area is a reviewed entry whose stated purpose is navigation to the admin area, an admin's viewing
1492
+ reach, or the admin's inbox. Read each reviewed entry against its code: an entry whose read now offers
1493
+ an action is the finding, and so is an entry whose code is gone. An action that the affordance offers
1494
+ stays reachable on the admin standing alone (step 70). No installed React package renders a control
1495
+ gated on admin standing.
1496
+ **Status:** PASS · FAIL · NOT RUN · BLOCKED
1497
+
1478
1498
  ## Pass / Fail
1479
1499
 
1480
1500
  Per [README.md](README.md) § Verdict.
@@ -36,7 +36,7 @@ rule's coverage in its additions; they are not listed here.
36
36
  | RULE-STD-ARCH-005 | Cross-tree trust contracts: one definition, one proof | codebase-hygiene 31; auth-boundary 13; testing-coverage 2 | — |
37
37
  | RULE-STD-ARCH-006 | Code from another product is adapted, never carried with its identity | **No audit step yet.** No shared step; an app's own step is the candidate. | — |
38
38
  | RULE-STD-ARCH-101 | `<APP_DATA_PACKAGE>` owns cross-boundary contracts | codebase-hygiene 16, 18, 31, 51, 56, 80; client-write-security-audit 8; data-integrity 2, 7, 8; dependency-audit 1, 4, 5; field-lineage 5; migration 1 | `isolatedRoots`, `tsconfigAliasesInRoot` |
39
- | RULE-STD-ARCH-102 | One named declaration; enforcement points derive | codebase-hygiene 31, 51, 61, 99; callable-functions 3; client-write-security-audit 8; data-integrity 7; dependency-audit 5; migration 8 | `definitionRedeclaration`, `schemaLengthLiterals`, `textFieldDeclarations`, `schemaBounds`, `enumLabels` |
39
+ | RULE-STD-ARCH-102 | One named declaration; enforcement points derive | codebase-hygiene 31, 51, 61, 99; callable-functions 3; client-write-security-audit 8; data-integrity 7; dependency-audit 5; migration 8 | `definitionRedeclaration`, `schemaLengthLiterals`, `textFieldDeclarations`, `schemaBounds`, `enumLabels`, `appRouteOwner` |
40
40
  | RULE-STD-ARCH-103 | Persist identity references, not snapshots | codebase-hygiene 35; data-integrity 2, 13; field-lineage 1, 2, 3, 5 | — |
41
41
  | RULE-STD-ARCH-104 | Canonical Firestore paths and compound-camelCase naming | codebase-hygiene 10, 12, 56, 80, 117; data-integrity 8; field-lineage 3; firestore-storage-rules 1 | `firestorePathAssembly`, `firestoreLiteralSegments`, `firestoreRetiredSegments`, `firestoreTestPaths`, `rulesMirrorCore` |
42
42
  | RULE-STD-ARCH-105 | Timestamps are epoch milliseconds via firebase-helpers | codebase-hygiene 9 | `firestoreTimestampNow` |
@@ -62,7 +62,7 @@ rule's coverage in its additions; they are not listed here.
62
62
  | RULE-STD-BACKEND-004 | Admin authority is explicit; the claim is metadata | codebase-hygiene 32, 49, 70; auth-boundary 4; callable-functions 4; firestore-storage-rules 4 | `adminClaimRead` |
63
63
  | RULE-STD-BACKEND-005 | Identity fields must match the authenticated caller | codebase-hygiene 77; auth-boundary 2, 12; callable-functions 2; client-write-security-audit 5, 11; firestore-storage-rules 6; testing-coverage 3 | — |
64
64
  | RULE-STD-BACKEND-010 | Callables enforce App Check attestation | codebase-hygiene 29; auth-boundary 3, 12, 13; callable-functions 6, 12 | `appCheck` |
65
- | RULE-STD-BACKEND-011 | Admin capability is self-sufficient; never coupled to a participant status | codebase-hygiene 70 | — |
65
+ | RULE-STD-BACKEND-011 | Admin capability is self-sufficient; never coupled to a participant status | codebase-hygiene 70, 121 | — |
66
66
  | RULE-STD-BACKEND-012 | Privileged route handlers and server modules carry the callable obligations | codebase-hygiene 23, 77; auth-boundary 12; callable-functions 12; client-write-security-audit 11 | — |
67
67
  | RULE-STD-BACKEND-013 | Named surface classes carry a rate-limit decision | codebase-hygiene 40, 77; auth-boundary 12; callable-functions 5 | `rateLimitSurfaces` |
68
68
  | RULE-STD-BACKEND-101 | All transaction reads before all transaction writes | codebase-hygiene 97; async-workflow 6; callable-functions 8; data-integrity 5 | — |
@@ -124,6 +124,7 @@ rule's coverage in its additions; they are not listed here.
124
124
  | RULE-STD-FRONTEND-108 | Auth-scoped subscribers reset on identity change | codebase-hygiene 35, 114; auth-boundary 14 | `authScopedKeys`, `signOutLiveness` |
125
125
  | RULE-STD-FRONTEND-109 | One owner per data-layer failure's user-facing copy; never a raw SDK string | codebase-hygiene 60; ux-audit 8 | `callableErrorCopy`, `readFailureCopy`, `errorInternalsRender` |
126
126
  | RULE-STD-FRONTEND-110 | An offered action mirrors the backend's predicate | codebase-hygiene 101, 108; auth-boundary 9 | — |
127
+ | RULE-STD-FRONTEND-111 | Admin actions outside the admin area go through the one admin-actions affordance | codebase-hygiene 121 | `adminActionSurface` |
127
128
  | RULE-STD-FRONTEND-201 | Every async control shows its own pending state through the shared primitives | codebase-hygiene 57, 65, 68; ux-audit 2, 5, 6, 11, 12 | `loadingFeedback` |
128
129
  | RULE-STD-FRONTEND-202 | ConsequenceDialog for consequential confirmations | codebase-hygiene 57, 65, 68, 104 | — |
129
130
  | RULE-STD-FRONTEND-203 | Accessibility baseline: landmarks, labels, focus, Escape | codebase-hygiene 68, 103; ux-audit 4, 5, 12 | `accessibleControls`; guard-enforced: the `@ttt-productions/dev-standards/node/a11y` axe run (default, high-contrast, reduced-motion) in the app's e2e stage, and the shared ESLint base's `jsx-a11y` recommended rules |
@@ -89,16 +89,16 @@ names, and exceptions arrive as its facts.
89
89
 
90
90
  ### 5. A callable is declared through the shared callable factories
91
91
  **Rules:** RULE-STD-ENG-002, RULE-STD-ENG-003, RULE-STD-FRONTEND-105. **Scope:** `packages/*/src`.
92
- `firebase-helpers/react`'s
93
- `createCallables` is the one factory set for a callable hook (action, reader, invoker, resumable); the
94
- SDK's callable primitives stay inside firebase-helpers.
92
+ `query-core/react`'s `createCallables` is the one factory set for a callable hook (action, reader,
93
+ invoker, resumable). It builds on query-core's session-aware mutation owner, `useSessionMutation`. The
94
+ SDK's callable primitives stay inside firebase-helpers, behind `callCallable`.
95
95
  ```bash
96
96
  rg -n "^[^/*]*\bhttpsCallable(FromURL)?\(" packages/*/src -g '*.ts' -g '*.tsx' | rg -v "packages/firebase-helpers/"
97
97
  rg -n "useMutation\(" packages/*/src -g '*.ts' -g '*.tsx' | rg -i "callable"
98
98
  ```
99
- **Expected:** the first prints nothing. The second prints only firebase-helpers' own factories; a
100
- package building its own callable mutation hook is the finding — it takes a hook built by
101
- `createCallables` from the app instead.
99
+ **Expected:** the first prints nothing. The second prints nothing: every callable hook is built by
100
+ `createCallables` on the session-aware owner. A package that builds its own callable mutation hook is
101
+ the finding; it takes a hook built by `createCallables` from the app instead.
102
102
  **Status:** PASS · FAIL · NOT RUN · BLOCKED
103
103
 
104
104
  ### 6. Imports inside a package name the defining file by its built extension
@@ -24,7 +24,7 @@ Cross-boundary contracts, canonical definitions, identity, paths, compound ids,
24
24
 
25
25
  **Rule.** Every definition — numbers (length limits, count caps, rate windows, price bounds), string unions and enums (kinds, statuses, item types), option and allowlist arrays, label and display-name maps, verbatim business copy (agreement statements, attestations), and typed shapes — is ONE named declaration in the package that owns the concept: `<APP_DATA_PACKAGE>` for an app's business truth, the owning generic package for a mechanism.
26
26
 
27
- **Apps:** this holds regardless of which layer consumes the definition today: there is no "frontend-only" or "backend-only" definition, because `src/` and `functions/src` are not importable by a second consumer, so a definition living there is a copy waiting to happen. The app owns wiring (routes, composition, layout) — never truths.
27
+ **Apps:** this holds regardless of which layer consumes the definition today: there is no "frontend-only" or "backend-only" definition, because `src/` and `functions/src` are not importable by a second consumer, so a definition living there is a copy waiting to happen. The app owns wiring (mounting pages, composition, layout) — never truths. An app's page paths are a declaration in `<APP_DATA_PACKAGE>`, built through its one route owner; a route literal anywhere else is drift.
28
28
 
29
29
  Enforcement points **derive**, never restate: zod `.max(MAX_X)`, React `maxLength={MAX_X}`, backend guards, `.slice(0, EXCERPT_LEN)`, and type positions referencing the canonical union (never an inline re-declared union). A literal at an enforcement point is drift by definition. Two compliance questions for any declaration: "updating this touches how many places?" (must be one) and "can a second consumer import this?" (if it would ever need to, it belongs in a package).
30
30
 
@@ -16,7 +16,7 @@ A comment these rules do not settle — bad in a way no rule names, or good but
16
16
  - An exception that does not name the rule ID and the reason.
17
17
  - One explanation repeated file after file.
18
18
 
19
- **References.** The required markers — each a comment this rule keeps, and a sweep never removes one: RULE-STD-ARCH-206 (`SANCTIONED EXCEPTION`), RULE-STD-BACKEND-002 (`.passthrough()` reason), RULE-STD-BACKEND-010 (App Check omission reason), RULE-STD-BACKEND-013 (in-file rate-limit decision), RULE-STD-BACKEND-119 (refuse-instead-of-clamp exception), RULE-STD-BACKEND-301 (the post-commit marker), RULE-STD-FRONTEND-102 (raw `staleTime` justification), RULE-STD-QUALITY-006 (reasoned `eslint-disable-next-line`, `prettier-ignore` reason), RULE-STD-QUALITY-007 (guard allowlist justification), RULE-STD-QUALITY-009 (a reviewed entry's why). A rule that adds a required comment joins this list in the same change. RULE-STD-ENG-002 (no "keep in sync" pairs); RULE-STD-COMMENT-012 (no security mechanism).
19
+ **References.** The required markers — each a comment this rule keeps, and a sweep never removes one: RULE-STD-ARCH-206 (`SANCTIONED EXCEPTION`), RULE-STD-BACKEND-002 (`.passthrough()` reason), RULE-STD-BACKEND-010 (App Check omission reason), RULE-STD-BACKEND-013 (in-file rate-limit decision), RULE-STD-BACKEND-119 (refuse-instead-of-clamp exception), RULE-STD-BACKEND-301 (the post-commit marker), RULE-STD-FRONTEND-002 (the utility an `overrides.css` rule beats), RULE-STD-FRONTEND-102 (raw `staleTime` justification), RULE-STD-QUALITY-006 (reasoned `eslint-disable-next-line`, `prettier-ignore` reason), RULE-STD-QUALITY-007 (guard allowlist justification), RULE-STD-QUALITY-009 (a reviewed entry's why). A rule that adds a required comment joins this list in the same change. RULE-STD-ENG-002 (no "keep in sync" pairs); RULE-STD-COMMENT-012 (no security mechanism).
20
20
 
21
21
  ### RULE-STD-COMMENT-002 — No dates
22
22
  **Scope.** both
@@ -64,7 +64,7 @@ How the client reads server state, builds cache keys, invalidates, calls callabl
64
64
  ### RULE-STD-FRONTEND-105 — One callable invocation path; never raw `httpsCallable`
65
65
  **Scope.** apps
66
66
 
67
- **Rule.** Every callable is declared once in `<CALLABLE_DECLARATIONS>`, through the firebase-helpers factory kind that fixes its retry, surface, and capture — an action, a reader, an invoker, or a resumable action — built from the one app callable policy (`<CALLABLE_POLICY>`). Screens and hooks call the declared hook, and the callable's name is written nowhere else. The factories ride the package primitive, which owns the **undefined-strip** (the SDK encodes `undefined` as `null`, which strict `.optional()` `<APP_DATA_PACKAGE>` schemas reject — the primitive drops undefined keys deep), and the policy owns the **error taxonomy**: the expected-answer monitoring filter, the standard error toast, and the answers that have their own surface (the app's addition lists them) — announced and never toasted, which a caller-owned surface skips through the policy's surfaced-elsewhere predicate. A hook that wraps both a callable and a non-callable leg (e.g. a Storage upload) toasts only the leg the callable can't see — gate its local `onError` on the policy's callable-rejection predicate returning false, or declare the call `{ showErrorToast: false }` and own the copy. A React Query READ calls its declared reader, which neither toasts nor captures, and its failure is a rendered state (RULE-STD-FRONTEND-109). **One rejection gets ONE toast, and that budget spans files**: a hook's declared action with its toast on and an `onError`/`catch` toast in a component that calls that hook are one surface between them, so exactly one side owns the copy — either the hook declares the toast off, or the call site drops its toast and lets the action own it.
67
+ **Rule.** Every callable is declared once in `<CALLABLE_DECLARATIONS>`, through the factory kind from query-core `./react`'s `createCallables` that fixes its retry, surface, and capture — an action, a reader, an invoker, or a resumable action — built from the one app callable policy (`<CALLABLE_POLICY>`). Screens and hooks call the declared hook, and the callable's name is written nowhere else. The factories ride firebase-helpers' `callCallable` primitive, which owns the **undefined-strip** (the SDK encodes `undefined` as `null`, which strict `.optional()` `<APP_DATA_PACKAGE>` schemas reject — the primitive drops undefined keys deep), and the policy owns the **error taxonomy**: the expected-answer monitoring filter, the standard error toast, and the answers that have their own surface (the app's addition lists them) — announced and never toasted, which a caller-owned surface skips through the policy's surfaced-elsewhere predicate. A hook that wraps both a callable and a non-callable leg (e.g. a Storage upload) toasts only the leg the callable can't see — gate its local `onError` on the policy's callable-rejection predicate returning false, or declare the call `{ showErrorToast: false }` and own the copy. A React Query READ calls its declared reader, which neither toasts nor captures, and its failure is a rendered state (RULE-STD-FRONTEND-109). **One rejection gets ONE toast, and that budget spans files**: a hook's declared action with its toast on and an `onError`/`catch` toast in a component that calls that hook are one surface between them, so exactly one side owns the copy — either the hook declares the toast off, or the call site drops its toast and lets the action own it.
68
68
 
69
69
  **Why.** A raw `httpsCallable` skips the undefined-strip (the schema rejects valid input) and the error taxonomy (double toasts, unfiltered monitoring noise, and answers that have their own surface toasted as errors).
70
70
 
@@ -72,7 +72,7 @@ How the client reads server state, builds cache keys, invalidates, calls callabl
72
72
  - Importing `httpsCallable`/`getFunctions` from `firebase/functions`, or `callCallable`, anywhere in `src/`; a callable's name written outside `<CALLABLE_DECLARATIONS>`, including as a bare string at a call site; a file-local `callable()` wrapper.
73
73
  - A callable hook that double-surfaces one rejection as two destructive toasts — in ONE file, or split across the hook module and its consuming component.
74
74
 
75
- **References.** RULE-STD-FRONTEND-109; RULE-STD-QUALITY-103; ttt-packages `docs/packages/firebase-helpers.md`.
75
+ **References.** RULE-STD-FRONTEND-109; RULE-STD-QUALITY-103; ttt-packages `docs/packages/query-core.md`, `docs/packages/firebase-helpers.md`.
76
76
 
77
77
  ### RULE-STD-FRONTEND-106 — Gated actions go through the canonical auth gate
78
78
  **Scope.** apps
@@ -153,3 +153,17 @@ Classification predicates are generic and come from `@ttt-productions/firebase-h
153
153
  - A picker whose selectable range disagrees with the backend's rule for the value.
154
154
 
155
155
  **References.** RULE-STD-ENG-002, RULE-STD-ENG-006; RULE-STD-FRONTEND-106 (the auth-readiness gate), RULE-STD-FRONTEND-206.
156
+
157
+ ### RULE-STD-FRONTEND-111 — Admin actions outside the admin area go through the one admin-actions affordance
158
+ **Scope.** both
159
+
160
+ **Rule.** **Apps:** outside the app's admin area, an admin action is offered only through the app's one shield-marked admin-actions affordance, never as an inline control beside a member's own actions. Outside the admin area, a read of the viewer's admin standing is allowed for three purposes only: navigation to the admin area, an admin's viewing reach (seeing what a member could not), or the admin's own inbox. Each such read is a reviewed entry naming its purpose, and every other read of admin standing outside the admin area is a violation. The backend stays the authority on every admin action (RULE-STD-BACKEND-004). **Packages:** a React package never renders a control gated on admin standing. It takes no admin flag; an admin action on a package's surface is the app's, offered through the app's affordance.
161
+
162
+ **Why.** An inline admin control looks like a member action. One page offers it and the next one doesn't, and every screen that gates on admin standing is one more place where that gate can drift away from the one owner. When a package carries an admin control, every app that installs it gets an admin action outside its admin affordance.
163
+
164
+ **Prohibits.**
165
+ - An admin action rendered outside the admin area other than through the admin-actions affordance; a second admin-actions affordance.
166
+ - A read of admin standing outside the admin area that is not a reviewed entry for navigation, viewing reach, or the admin's inbox.
167
+ - A React package component, prop, or config field that shows, hides, or enables a control on admin standing.
168
+
169
+ **References.** RULE-STD-ENG-002, RULE-STD-ENG-006; RULE-STD-ARCH-201; RULE-STD-BACKEND-004, RULE-STD-BACKEND-011; RULE-STD-FRONTEND-001, RULE-STD-FRONTEND-110.