@nurix/etna 0.19.66 → 0.19.67
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/bin/etna.js +10 -1
- package/data/harness-index.json +1 -5
- package/data/harnesses/code/CLAUDE.md +2 -0
- package/data/harnesses/code/agents/maintenance.md +3 -1
- package/data/harnesses/code/harness.json +7 -2
- package/data/harnesses/code/hooks/compliance.mjs +1 -0
- package/data/harnesses/code/hooks/lib/frozen.mjs +3 -0
- package/data/harnesses/code/hooks/lib/ownership.mjs +3 -1
- package/data/harnesses/code/skills/documentation/scripts/doc-lint.mjs +1 -1
- package/data/harnesses/delivery/agents/maintenance.md +3 -1
- package/data/harnesses/delivery/harness.json +2 -2
- package/data/harnesses/delivery/skills/documentation/scripts/doc-lint.mjs +1 -1
- package/data/harnesses/etna/agents/maintenance.md +3 -1
- package/data/harnesses/etna/harness.json +2 -2
- package/data/harnesses/etna/skills/documentation/scripts/doc-lint.mjs +1 -1
- package/lib/install/materialize.mjs +51 -57
- package/package.json +2 -2
- package/data/harnesses/apollo/.mcp.json +0 -8
- package/data/harnesses/apollo/.prettierignore +0 -29
- package/data/harnesses/apollo/.prettierrc.json +0 -3
- package/data/harnesses/apollo/.vscode/settings.json +0 -19
- package/data/harnesses/apollo/CLAUDE.md +0 -162
- package/data/harnesses/apollo/agents/bookkeeper.md +0 -28
- package/data/harnesses/apollo/agents/brainstorm.md +0 -144
- package/data/harnesses/apollo/agents/change-review.md +0 -251
- package/data/harnesses/apollo/agents/lld-mode.md +0 -58
- package/data/harnesses/apollo/agents/maintenance.md +0 -234
- package/data/harnesses/apollo/agents/performance-review.md +0 -223
- package/data/harnesses/apollo/agents/retro.md +0 -46
- package/data/harnesses/apollo/codex/agents/bookkeeper.toml +0 -26
- package/data/harnesses/apollo/codex/agents/brainstorm.toml +0 -142
- package/data/harnesses/apollo/codex/agents/change-review.toml +0 -249
- package/data/harnesses/apollo/codex/agents/lld-mode.toml +0 -52
- package/data/harnesses/apollo/codex/agents/maintenance.toml +0 -228
- package/data/harnesses/apollo/codex/agents/performance-review.toml +0 -222
- package/data/harnesses/apollo/codex/agents/retro.toml +0 -45
- package/data/harnesses/apollo/harness.json +0 -19
- package/data/harnesses/apollo/hooks/approved-packages.md +0 -44
- package/data/harnesses/apollo/hooks/awareness.mjs +0 -405
- package/data/harnesses/apollo/hooks/check-bookkeeping-clean.mjs +0 -19
- package/data/harnesses/apollo/hooks/check-changelog-presence.mjs +0 -57
- package/data/harnesses/apollo/hooks/check-completion-integrity.mjs +0 -71
- package/data/harnesses/apollo/hooks/check-ownership.mjs +0 -22
- package/data/harnesses/apollo/hooks/compliance.mjs +0 -457
- package/data/harnesses/apollo/hooks/hooks.json +0 -23
- package/data/harnesses/apollo/hooks/lib/frozen.mjs +0 -58
- package/data/harnesses/apollo/hooks/lib/ownership.mjs +0 -242
- package/data/harnesses/apollo/hooks/lib/state.mjs +0 -170
- package/data/harnesses/apollo/rules/changelog.md +0 -27
- package/data/harnesses/apollo/rules/database.md +0 -34
- package/data/harnesses/apollo/rules/frontend.md +0 -30
- package/data/harnesses/apollo/rules/general.md +0 -40
- package/data/harnesses/apollo/rules/nomenclature.md +0 -30
- package/data/harnesses/apollo/rules/pnpm.md +0 -57
- package/data/harnesses/apollo/rules/testing.md +0 -64
- package/data/harnesses/apollo/skills/boundary/SKILL.md +0 -75
- package/data/harnesses/apollo/skills/boundary/references/index.md +0 -16
- package/data/harnesses/apollo/skills/boundary/references/mechanisms.md +0 -99
- package/data/harnesses/apollo/skills/boundary/references/packs/async-messaging.md +0 -81
- package/data/harnesses/apollo/skills/boundary/references/packs/auth.md +0 -93
- package/data/harnesses/apollo/skills/boundary/references/packs/build-config-runtime.md +0 -93
- package/data/harnesses/apollo/skills/boundary/references/packs/cloudflare.md +0 -93
- package/data/harnesses/apollo/skills/boundary/references/packs/frontend.md +0 -93
- package/data/harnesses/apollo/skills/boundary/references/packs/generic-tail.md +0 -55
- package/data/harnesses/apollo/skills/boundary/references/packs/persistence.md +0 -93
- package/data/harnesses/apollo/skills/boundary/references/packs/wire-api.md +0 -87
- package/data/harnesses/apollo/skills/brainstorm/README.md +0 -56
- package/data/harnesses/apollo/skills/brainstorm/SKILL.md +0 -173
- package/data/harnesses/apollo/skills/brainstorm/references/doctrine.md +0 -98
- package/data/harnesses/apollo/skills/brainstorm/references/harvest.md +0 -86
- package/data/harnesses/apollo/skills/brainstorm/references/session-schema.md +0 -186
- package/data/harnesses/apollo/skills/brainstorm/scripts/detector.mjs +0 -460
- package/data/harnesses/apollo/skills/design/SKILL.md +0 -121
- package/data/harnesses/apollo/skills/design/components/charts.md +0 -76
- package/data/harnesses/apollo/skills/design/components/collections.md +0 -132
- package/data/harnesses/apollo/skills/design/components/feedback.md +0 -47
- package/data/harnesses/apollo/skills/design/components/hooks.md +0 -35
- package/data/harnesses/apollo/skills/design/components/index.md +0 -143
- package/data/harnesses/apollo/skills/design/components/inputs.md +0 -327
- package/data/harnesses/apollo/skills/design/components/layout.md +0 -52
- package/data/harnesses/apollo/skills/design/components/media.md +0 -66
- package/data/harnesses/apollo/skills/design/components/motion.md +0 -82
- package/data/harnesses/apollo/skills/design/components/navigation.md +0 -29
- package/data/harnesses/apollo/skills/design/components/overlays.md +0 -72
- package/data/harnesses/apollo/skills/design/components/setup.md +0 -40
- package/data/harnesses/apollo/skills/design/components/typography.md +0 -46
- package/data/harnesses/apollo/skills/design/components/utils.md +0 -37
- package/data/harnesses/apollo/skills/design/data/blueprints/browse.md +0 -38
- package/data/harnesses/apollo/skills/design/data/blueprints/creation-flow.md +0 -38
- package/data/harnesses/apollo/skills/design/data/blueprints/dashboard.md +0 -38
- package/data/harnesses/apollo/skills/design/data/blueprints/grid.md +0 -38
- package/data/harnesses/apollo/skills/design/data/blueprints/queue.md +0 -38
- package/data/harnesses/apollo/skills/design/data/blueprints/record-detail.md +0 -38
- package/data/harnesses/apollo/skills/design/data/blueprints/settings.md +0 -38
- package/data/harnesses/apollo/skills/design/data/charts/census.csv +0 -58
- package/data/harnesses/apollo/skills/design/data/checklists/browse.md +0 -21
- package/data/harnesses/apollo/skills/design/data/checklists/creation-flow.md +0 -20
- package/data/harnesses/apollo/skills/design/data/checklists/dashboard.md +0 -21
- package/data/harnesses/apollo/skills/design/data/checklists/grid.md +0 -21
- package/data/harnesses/apollo/skills/design/data/checklists/queue.md +0 -21
- package/data/harnesses/apollo/skills/design/data/checklists/record-detail.md +0 -21
- package/data/harnesses/apollo/skills/design/data/checklists/settings.md +0 -21
- package/data/harnesses/apollo/skills/design/data/chrome/board-column.md +0 -68
- package/data/harnesses/apollo/skills/design/data/chrome/data-table.md +0 -63
- package/data/harnesses/apollo/skills/design/data/chrome/side-panel.md +0 -60
- package/data/harnesses/apollo/skills/design/data/chrome/sidebar.md +0 -78
- package/data/harnesses/apollo/skills/design/data/chrome/stat-cards.md +0 -62
- package/data/harnesses/apollo/skills/design/data/chrome/topbar.md +0 -65
- package/data/harnesses/apollo/skills/design/data/elevation/ant-design-directional-shadow.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/apple-liquid-glass-material.md +0 -18
- package/data/harnesses/apollo/skills/design/data/elevation/atlassian-surface-shadow-pairing.md +0 -20
- package/data/harnesses/apollo/skills/design/data/elevation/fluent-2-key-ambient-ramp.md +0 -20
- package/data/harnesses/apollo/skills/design/data/elevation/github-primer-sparing-shadow.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/ibm-carbon-layer-and-border.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/linear-dark-surface-ladder.md +0 -17
- package/data/harnesses/apollo/skills/design/data/elevation/material-2-key-ambient-dp-ramp.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/material-3-tonal-elevation.md +0 -21
- package/data/harnesses/apollo/skills/design/data/elevation/neobrutalist-hard-offset.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/neumorphic-soft-dual-shadow.md +0 -18
- package/data/harnesses/apollo/skills/design/data/elevation/notion-warm-minimal-hairline.md +0 -17
- package/data/harnesses/apollo/skills/design/data/elevation/polaris-elevation-inset-bevel.md +0 -21
- package/data/harnesses/apollo/skills/design/data/elevation/radix-themes-alpha-ring-scale.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/shadcn-neutral-derived-radius.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/stripe-brand-tinted-dual-shadow.md +0 -18
- package/data/harnesses/apollo/skills/design/data/elevation/tailwind-default-utility-scale.md +0 -19
- package/data/harnesses/apollo/skills/design/data/elevation/vercel-geist-shadow-as-border.md +0 -20
- package/data/harnesses/apollo/skills/design/data/iconography.md +0 -199
- package/data/harnesses/apollo/skills/design/data/motion/accordion-expand.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/destructive-hold-confirm.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/drag-dismiss-momentum.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/empty-state-arrive.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/hover-lift.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/modal-center-arrive.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/number-ticker-roll.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/onboarding-bounce.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/panel-slide.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/press-feedback.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/progress-linear-loop.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/route-crossfade.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/rubber-band-overscroll.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/skeleton-shimmer.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/stagger-arrive.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/toast-enter-stack.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/tooltip-chain-instant.md +0 -18
- package/data/harnesses/apollo/skills/design/data/motion/trigger-anchored-reveal.md +0 -18
- package/data/harnesses/apollo/skills/design/data/tells.csv +0 -34
- package/data/harnesses/apollo/skills/design/data/voices/bright-collaborator.md +0 -29
- package/data/harnesses/apollo/skills/design/data/voices/direct-operator.md +0 -29
- package/data/harnesses/apollo/skills/design/data/voices/earnest-helper.md +0 -29
- package/data/harnesses/apollo/skills/design/data/voices/editorial-composed.md +0 -29
- package/data/harnesses/apollo/skills/design/data/voices/quiet-concierge.md +0 -29
- package/data/harnesses/apollo/skills/design/data/voices/warm-guide.md +0 -29
- package/data/harnesses/apollo/skills/design/references/compose.md +0 -23
- package/data/harnesses/apollo/skills/design/references/content.md +0 -28
- package/data/harnesses/apollo/skills/design/references/emission.md +0 -21
- package/data/harnesses/apollo/skills/design/references/existing-apps.md +0 -27
- package/data/harnesses/apollo/skills/design/references/foundations.md +0 -57
- package/data/harnesses/apollo/skills/design/references/intake.md +0 -40
- package/data/harnesses/apollo/skills/design/references/object-pass.md +0 -24
- package/data/harnesses/apollo/skills/design/references/ship-gate.md +0 -29
- package/data/harnesses/apollo/skills/design/references/ux-patterns/Readme.md +0 -195
- package/data/harnesses/apollo/skills/design/references/ux-patterns/ai-layer.md +0 -49
- package/data/harnesses/apollo/skills/design/references/ux-patterns/attributes-lifecycle.md +0 -43
- package/data/harnesses/apollo/skills/design/references/ux-patterns/container-choice.md +0 -44
- package/data/harnesses/apollo/skills/design/references/ux-patterns/creation-paths.md +0 -46
- package/data/harnesses/apollo/skills/design/references/ux-patterns/dashboards.md +0 -42
- package/data/harnesses/apollo/skills/design/references/ux-patterns/modes.md +0 -42
- package/data/harnesses/apollo/skills/design/references/ux-patterns/object-model.md +0 -46
- package/data/harnesses/apollo/skills/design/references/ux-patterns/page-archetype.md +0 -48
- package/data/harnesses/apollo/skills/design/references/ux-patterns/promotion-demotion.md +0 -37
- package/data/harnesses/apollo/skills/design/references/ux-patterns/settings.md +0 -42
- package/data/harnesses/apollo/skills/design/references/ux-patterns/standard-pages.md +0 -46
- package/data/harnesses/apollo/skills/design/references/ux-patterns/state-matrix.md +0 -41
- package/data/harnesses/apollo/skills/design/references/ux-patterns/table-mechanics.md +0 -48
- package/data/harnesses/apollo/skills/design/references/ux-patterns/verbs.md +0 -45
- package/data/harnesses/apollo/skills/design/references/ux-patterns/viewer-context.md +0 -40
- package/data/harnesses/apollo/skills/design/scripts/README-validator.md +0 -27
- package/data/harnesses/apollo/skills/design/scripts/select-chart.mjs +0 -622
- package/data/harnesses/apollo/skills/design/scripts/validate-design-md.mjs +0 -1198
- package/data/harnesses/apollo/skills/documentation/SKILL.md +0 -135
- package/data/harnesses/apollo/skills/documentation/references/examples.md +0 -279
- package/data/harnesses/apollo/skills/documentation/references/frontmatter.md +0 -44
- package/data/harnesses/apollo/skills/documentation/references/instruction-style.md +0 -50
- package/data/harnesses/apollo/skills/documentation/references/style-guide.md +0 -384
- package/data/harnesses/apollo/skills/documentation/references/untemplated.md +0 -38
- package/data/harnesses/apollo/skills/documentation/scripts/doc-lint.mjs +0 -140
- package/data/harnesses/apollo/skills/documentation/templates/Readme.md +0 -35
- package/data/harnesses/apollo/skills/documentation/templates/api-reference.md +0 -20
- package/data/harnesses/apollo/skills/documentation/templates/architecture-decision.md +0 -121
- package/data/harnesses/apollo/skills/documentation/templates/backlog.md +0 -33
- package/data/harnesses/apollo/skills/documentation/templates/behavioral-checklist.md +0 -81
- package/data/harnesses/apollo/skills/documentation/templates/bookkeeping.md +0 -18
- package/data/harnesses/apollo/skills/documentation/templates/changelog-entry.md +0 -57
- package/data/harnesses/apollo/skills/documentation/templates/changelog-index.md +0 -41
- package/data/harnesses/apollo/skills/documentation/templates/docs-readme.md +0 -72
- package/data/harnesses/apollo/skills/documentation/templates/domain-model.md +0 -30
- package/data/harnesses/apollo/skills/documentation/templates/environment-registry.md +0 -15
- package/data/harnesses/apollo/skills/documentation/templates/feature.md +0 -150
- package/data/harnesses/apollo/skills/documentation/templates/glossary.md +0 -11
- package/data/harnesses/apollo/skills/documentation/templates/implementation-map.md +0 -42
- package/data/harnesses/apollo/skills/documentation/templates/journal.md +0 -13
- package/data/harnesses/apollo/skills/documentation/templates/memory-main.md +0 -25
- package/data/harnesses/apollo/skills/documentation/templates/migration-runbook.md +0 -34
- package/data/harnesses/apollo/skills/documentation/templates/non-goals.md +0 -11
- package/data/harnesses/apollo/skills/documentation/templates/operations-procedure.md +0 -36
- package/data/harnesses/apollo/skills/documentation/templates/plan.md +0 -88
- package/data/harnesses/apollo/skills/documentation/templates/protected-files.md +0 -37
- package/data/harnesses/apollo/skills/documentation/templates/root-readme.md +0 -104
- package/data/harnesses/apollo/skills/documentation/templates/security-overview.md +0 -27
- package/data/harnesses/apollo/skills/documentation/templates/structure.md +0 -65
- package/data/harnesses/apollo/skills/documentation/templates/system-overview.md +0 -32
- package/data/harnesses/apollo/skills/documentation/templates/testing-strategy.md +0 -29
- package/data/harnesses/apollo/skills/schema-design/SKILL.md +0 -141
- package/data/harnesses/apollo/skills/test/SKILL.md +0 -104
- package/data/harnesses/apollo/skills/testing/SKILL.md +0 -166
- package/data/harnesses/apollo/skills/workflow-mapper/SKILL.md +0 -451
- package/data/harnesses/apollo/skills/workflow-mapper/references/patterns.md +0 -214
- /package/data/harnesses/{apollo → code}/docs/README.md +0 -0
- /package/data/harnesses/{apollo → code}/docs/history/CHANGELOG.md +0 -0
- /package/data/harnesses/{apollo → code}/docs/history/backlog.md +0 -0
- /package/data/harnesses/{apollo → code}/docs/memory/main.md +0 -0
- /package/data/harnesses/{apollo → code}/docs/structure.md +0 -0
- /package/data/harnesses/{apollo → code}/rules/cloudflare.md +0 -0
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Pack: package/workspace management, build config, CI, runtime process
|
|
2
|
-
|
|
3
|
-
Load when: the touch list exhibits `package.json` (deps, `exports`, scripts, `packageManager`), lockfiles, `.env*` files, tsconfig `paths`, Dockerfiles/compose, `.github/workflows/`, pino logger config, or Express servers behind proxies.
|
|
4
|
-
|
|
5
|
-
## env-var-reader-vs-declaration — HIGH · config-elsewhere
|
|
6
|
-
**Contract:** Every `process.env.X` read in code must have X declared in `.env.example`, each `.env.*`, CI `env:`/secrets, and every deploy dashboard — the reader and the declarations share only the literal name.
|
|
7
|
-
**Detect:** `process.env\.[A-Z_]+`, `.env.example`, `.env*`, `env:` / `secrets\.` in `.github/workflows/*.yml`, `loadEnv(`, `dotenv`
|
|
8
|
-
**Ships green, breaks:** Undefined var is `undefined`, not an error — `|| defaultValue` fallbacks silently run prod against the default; env values are ALWAYS strings, so `if (process.env.FEATURE_ENABLED)` is truthy for `"false"` and `"0"`; dotenv loads only locally, so a var present in `.env` but absent from the deploy dashboard works on every laptop and fails only in prod.
|
|
9
|
-
**Safe change:** Add the key to `.env.example` + every deploy target BEFORE merging the reader; validate all env at boot with a zod schema and crash on missing/malformed (fail-fast, no silent defaults); parse booleans explicitly (`=== 'true'`); grep workflows and dashboards when renaming a key.
|
|
10
|
-
|
|
11
|
-
## vite-client-env-prefix-gate — CRITICAL · trust-invariant
|
|
12
|
-
**Contract:** The `VITE_` prefix is the only gate deciding whether an env var is inlined verbatim into the public JS bundle; server secrets rely on NOT having the prefix.
|
|
13
|
-
**Detect:** `import.meta.env.VITE_`, `envPrefix` in `vite.config.*`, `define:` blocks, `VITE_` keys in `.env*`
|
|
14
|
-
**Ships green, breaks:** Prefixing a secret (`VITE_API_KEY`) ships it string-searchable in `dist/assets/*.js` with zero warning; `import.meta.env.VITE_*` is statically replaced at BUILD time — rotating the value in a deploy dashboard changes nothing until rebuild, and a build-once/promote-across-envs pipeline bakes the build env's values into every environment; widening `envPrefix` silently exposes every matching var; `define:` entries bypass the prefix gate entirely.
|
|
15
|
-
**Safe change:** Before adding `VITE_` to any var, confirm it is public; audit `dist/` with `grep -r "<secret>" dist/` after build; treat `envPrefix` and `define` changes as security-review items; never promote a client bundle across environments whose `VITE_` values differ.
|
|
16
|
-
|
|
17
|
-
## lockfile-manifest-sync — HIGH · generated-artifact
|
|
18
|
-
**Contract:** `pnpm-lock.yaml` (or `package-lock.json`/`yarn.lock`) is a generated artifact that must move in the same commit as any `package.json` dependency, `overrides`/`resolutions`, or `pnpm.overrides` change.
|
|
19
|
-
**Detect:** `pnpm-lock.yaml`, `package-lock.json`, `yarn.lock` (more than one present = hazard), `"overrides"`, `"resolutions"`, `"pnpm": { "overrides"` in `package.json`
|
|
20
|
-
**Ships green, breaks:** Hand-editing `package.json` without running install works locally (bare `pnpm install` silently updates the lock) then fails CI with `ERR_PNPM_OUTDATED_LOCKFILE` — pnpm defaults `--frozen-lockfile` to true when `CI=true`; adding an `overrides` entry without re-locking means the vulnerable version stays pinned and installed while review shows the override "applied"; two lockfiles in one repo means detect-from-lockfile tooling picks nondeterministically and installs a different tree per machine; CI using `npm install` instead of `npm ci` silently rewrites resolutions.
|
|
21
|
-
**Safe change:** Run the detected manager's install and commit the lockfile with the manifest change; keep exactly one lockfile (delete strays); CI installs use `pnpm install --frozen-lockfile` / `npm ci` / `yarn install --immutable`; after any `overrides` change, verify the resolved version with `pnpm why <pkg>`.
|
|
22
|
-
|
|
23
|
-
## pnpm-strict-layout-phantom-imports — HIGH · rendezvous-string
|
|
24
|
-
**Contract:** Every module specifier imported by a package must appear in THAT package's `dependencies` — the import string and the manifest meet only through node_modules layout, and pnpm's symlinked layout exposes only declared deps.
|
|
25
|
-
**Detect:** `import .* from ['"]` specifiers absent from the nearest `package.json` `dependencies`, `workspace:` protocol entries, `.npmrc` `public-hoist-pattern`/`shamefully-hoist`, `node-linker`
|
|
26
|
-
**Ships green, breaks:** An undeclared transitive import (phantom dep) typechecks (types hoisted or ambient) and works under npm's flat hoisting, then throws `ERR_MODULE_NOT_FOUND` at runtime under pnpm — or vice versa when a repo migrates managers; a cross-package workspace import without a `workspace:*` dep entry works via stale symlinks locally and breaks on fresh CI install; relying on `shamefully-hoist=true` masks all of this until someone removes it.
|
|
27
|
-
**Safe change:** Declare every imported package in the importer's own `package.json` (workspace siblings as `"@nurix/x": "workspace:*"`); never widen `public-hoist-pattern` to fix a missing-module error — add the dep; verify with a fresh `pnpm install` on a clean clone.
|
|
28
|
-
|
|
29
|
-
## package-exports-subpath-map — HIGH · rendezvous-string
|
|
30
|
-
**Contract:** Consumers' import specifiers (`@nurix/components/button`) must match literal keys in the package's `exports` map; the `files` allowlist and `types` conditions must cover the same paths.
|
|
31
|
-
**Detect:** `"exports"` and `"files"` in `package.json`, `from ['"]@nurix/[^'"]+/`, `"moduleResolution"` in `tsconfig.json`
|
|
32
|
-
**Ships green, breaks:** Any subpath not mapped in `exports` throws `ERR_PACKAGE_PATH_NOT_EXPORTED` at runtime — and tsc under `moduleResolution: "node10"` ignores `exports` entirely, so types resolve while Node throws (use `"bundler"`/`"nodenext"` to make tsc see the real contract); adding a new component file without an `exports` entry publishes fine and breaks every consumer import; a built `dist/` path referenced by `exports` but missing from the `files` allowlist installs cleanly and fails only on first import of the published tarball.
|
|
33
|
-
**Safe change:** Add the `exports` subpath (with `types` condition first in each condition block) in the same change as the new file; confirm `files` includes everything `exports` references; check the shipped surface with `npm pack --dry-run`; treat removing/renaming an exports key as a breaking change for all consumers.
|
|
34
|
-
|
|
35
|
-
## private-registry-auth-split — HIGH · config-elsewhere
|
|
36
|
-
**Contract:** `@nurix/*` installs authenticate via an ambient read token in `~/.npmrc` locally and via `NODE_AUTH_TOKEN` + `setup-node`'s `registry-url` in CI — two disconnected declarations of the same credential.
|
|
37
|
-
**Detect:** `@nurix:registry=`, `_authToken` in `.npmrc`, `registry-url:` in workflows, `NODE_AUTH_TOKEN` in step `env:`
|
|
38
|
-
**Ships green, breaks:** The npm registry returns **404 (not 401)** for restricted packages without valid auth — CI failures read as "package doesn't exist," which tempts agents into substituting public packages (forbidden here); `setup-node` with `registry-url` writes an `.npmrc` referencing `${NODE_AUTH_TOKEN}` that is resolved at INSTALL time, so the token must be in the `env:` of the step running `pnpm install`/`npm publish`, not on the setup-node step; local installs succeed off `~/.npmrc` so the CI gap is invisible until the pipeline runs.
|
|
39
|
-
**Safe change:** In CI, pair `setup-node` `registry-url: https://registry.npmjs.org` with `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` on every step touching the registry; on a 404 for `@nurix/*`, diagnose auth first — never swap packages; keep `publishConfig.access: restricted` on every `@nurix` package.
|
|
40
|
-
|
|
41
|
-
## publish-pipeline-artifact-and-loop — HIGH · lifecycle-protocol
|
|
42
|
-
**Contract:** CI publish sequencing: `prepack`/`prepublishOnly` builds the shipped artifact, the version-bump commit carries `[skip ci]`, `npm version` runs `--no-git-tag-version` in monorepo subdirs, and workspace deps are rewritten at pack time.
|
|
43
|
-
**Detect:** `"prepack"`, `"prepublishOnly"` in `package.json`, `npm version` in workflows, `[skip ci]` in bump-commit messages, `workspace:` in `dependencies`
|
|
44
|
-
**Ships green, breaks:** No `prepack` → the published tarball carries whatever stale `dist/` was on the runner (or none, silently trimmed by `files`); a bump commit without `[skip ci]` pushed to the trigger branch re-fires the publish workflow — infinite loop or a duplicate publish dying with `E403 cannot publish over previously published version`; `npm version` in a subdirectory of the git root silently skips the commit+tag step, so tag automation quietly stops; publishing a pnpm workspace package with `npm publish` ships literal `workspace:*` in dependencies — every consumer install fails with `EUNSUPPORTEDPROTOCOL` (only `pnpm publish`/`pnpm pack` rewrites the protocol to real versions).
|
|
45
|
-
**Safe change:** Build in `prepack` so published always matches source; `npm version --no-git-tag-version`, then commit with `[skip ci]` and tag explicitly (`git push origin HEAD:<ref> --follow-tags`); always publish pnpm workspaces with `pnpm publish`; verify content with `npm pack --dry-run` in CI before publish.
|
|
46
|
-
|
|
47
|
-
## tsconfig-paths-vs-bundler-alias — HIGH · dual-authority
|
|
48
|
-
**Contract:** tsconfig `compilerOptions.paths` (what the typechecker resolves) and Vite `resolve.alias` (what the bundle resolves) independently define the same aliases and must stay identical.
|
|
49
|
-
**Detect:** `"paths"` in `tsconfig*.json`, `resolve:` `alias` in `vite.config.*`, `vite-tsconfig-paths`
|
|
50
|
-
**Ships green, breaks:** `paths` never rewrites emitted JS — code compiled with tsc alone typechecks, then Node throws `ERR_MODULE_NOT_FOUND` on the alias at runtime; Vite does NOT read tsconfig `paths` (needs `vite-tsconfig-paths` or a duplicated `resolve.alias`), so an alias added on one side only means typecheck passes while the build/tests resolve a different file — worst case the two point at different targets (`src/` vs `dist/`) and the code you typechecked is not the code you shipped; the same drift applies to Vitest, which uses Vite resolution, not tsc.
|
|
51
|
-
**Safe change:** Single-source aliases via `vite-tsconfig-paths` (tsconfig as authority) or a script that derives one from the other; when adding an alias, update both in one commit and run both `tsc --noEmit` and `vite build`; for Node-run (unbundled) code, avoid `paths` entirely — use package subpath `imports` (`#alias`) which both sides honor.
|
|
52
|
-
|
|
53
|
-
## corepack-packagemanager-version-drift — MED · config-elsewhere
|
|
54
|
-
**Contract:** `packageManager` and `engines` in `package.json` declare the pnpm/Node versions, but the versions actually running in CI, Docker, and laptops are set independently (setup-node/action-setup inputs, `FROM node:` tags, whatever's installed).
|
|
55
|
-
**Detect:** `"packageManager"`, `"engines"` in `package.json`, `corepack enable`, `node-version:` in workflows, `FROM node:` in Dockerfiles, `engine-strict` in `.npmrc`
|
|
56
|
-
**Ships green, breaks:** `packageManager` is enforced ONLY when corepack is active — Node 22 bundles corepack disabled by default and newer Node lines drop it from the distribution, and `setup-node` never enables it — so a dev on an older pnpm regenerates the committed lockfile at a different `lockfileVersion` (wholesale churn, then `ERR_PNPM_OUTDATED_LOCKFILE` in CI); npm ignores `engines` entirely unless `engine-strict=true`, so a newer-Node-only syntax feature passes local dev and crashes the Node-22 Docker runtime; `pnpm/action-setup` errors on "Multiple versions of pnpm specified" when its `version` input contradicts `packageManager`.
|
|
57
|
-
**Safe change:** Treat `packageManager` as the single source: `corepack enable` in Dockerfiles/dev setup, omit `version` from `pnpm/action-setup` so it reads the field; bump Node in `engines`, workflows `node-version`, and `FROM node:` together; set `engine-strict=true` in `.npmrc` to make drift loud.
|
|
58
|
-
|
|
59
|
-
## gha-workflow-repo-rendezvous — HIGH · rendezvous-string
|
|
60
|
-
**Contract:** Workflows reference repo internals purely by literal strings — npm script names in `run:`, secrets/vars by name, `paths:` filters by directory — with no static link to what they name.
|
|
61
|
-
**Detect:** `run: (npm|pnpm|yarn) (run )?[a-z:-]+` in `.github/workflows/*.yml`, `secrets\.[A-Z_]+`, `vars\.[A-Z_]+`, `paths:` / `paths-ignore:` filters
|
|
62
|
-
**Ships green, breaks:** A referenced secret that doesn't exist interpolates to **empty string with no error** — deploys run with a blank API key and publish steps fail with a misleading 404/403 far downstream; renaming a `package.json` script breaks every `run:` line only when that workflow next executes (possibly a rarely-fired release workflow, weeks later); moving `packages/cli/` to a new path while the publish workflow filters `paths: ['packages/cli/**']` means the publish job silently never triggers again — the package just stops shipping; `node-version` drifting from `engines`/Dockerfile builds with a different Node than production runs.
|
|
63
|
-
**Safe change:** When renaming scripts, secrets, or moving package dirs, grep `.github/workflows/` for the old literal; assert required secrets non-empty at job start (`if [ -z "$TOKEN" ]; then exit 1; fi`); after moving a path-filtered directory, push a trivial change inside it and confirm the workflow fires.
|
|
64
|
-
|
|
65
|
-
## docker-arg-env-boundary — CRITICAL · config-elsewhere
|
|
66
|
-
**Contract:** Dockerfile `ARG` exists only at build time and `ENV` persists into the running container; app code reading `process.env.X` at runtime sees only `ENV` (or orchestrator-injected) values, and both are recorded in inspectable image metadata.
|
|
67
|
-
**Detect:** `^ARG `, `^ENV ` in Dockerfiles, `--build-arg` in workflows/compose, `_authToken`/`NPM_TOKEN`/`NODE_AUTH_TOKEN` near `RUN.*install`
|
|
68
|
-
**Ships green, breaks:** A value declared only as `ARG` builds fine but is `undefined` in `process.env` at runtime — config appears set in the Dockerfile yet the app runs on defaults; any `ARG` consumed by a `RUN` (e.g. `--build-arg NPM_TOKEN=` for the `@nurix` install) is permanently visible in `docker history` of that stage, and secrets put in `ENV` are readable via `docker inspect` by anyone who can pull the image; multi-stage copies quietly drop `ENV` set in earlier stages.
|
|
69
|
-
**Safe change:** Runtime config via `ENV` or orchestrator env, never bare `ARG`; registry tokens via BuildKit `RUN --mount=type=secret,id=npmrc,target=/root/.npmrc pnpm install` (never `--build-arg`); audit with `docker history --no-trunc` and `docker inspect` before pushing an image containing credentials.
|
|
70
|
-
|
|
71
|
-
## pid1-signal-delivery — HIGH · lifecycle-protocol
|
|
72
|
-
**Contract:** The container's PID 1 must actually receive SIGTERM for the app's graceful-shutdown handler (`server.close()`, draining) to run before the orchestrator's SIGKILL.
|
|
73
|
-
**Detect:** shell-form `ENTRYPOINT node ...` / `CMD npm start` (no JSON array) in Dockerfiles, `CMD ["npm"`, absence of `process.on('SIGTERM'`, `init:` in compose, `--init`
|
|
74
|
-
**Ships green, breaks:** Shell-form ENTRYPOINT/CMD makes `/bin/sh -c` PID 1, and sh does not forward SIGTERM to the node child — `docker stop` waits the default 10s then SIGKILLs, so shutdown hooks never run and in-flight requests are dropped on every single deploy while everything looks healthy; `CMD ["npm","start"]` has the same defect (npm does not reliably forward signals to its child); the failure is invisible in logs because the process dies before it can log.
|
|
75
|
-
**Safe change:** Exec form running node directly: `ENTRYPOINT ["node","dist/index.js"]`; register `SIGTERM`/`SIGINT` handlers that `server.close()` and exit; add `--init`/tini (or compose `init: true`) for zombie reaping; verify with `docker stop` and confirm the shutdown log line appears in under 10s.
|
|
76
|
-
|
|
77
|
-
## pino-redact-and-field-name-rendezvous — CRITICAL · rendezvous-string
|
|
78
|
-
**Contract:** pino `redact` paths, serializer keys, and downstream dashboard/alert queries all pin the EXACT field names your code passes to the logger — nothing checks they still match.
|
|
79
|
-
**Detect:** `redact:` in pino init, `stdSerializers`, `errorKey`, `logger\.(info|error|warn)\(`, field names referenced in Grafana/Datadog/alert configs
|
|
80
|
-
**Ships green, breaks:** `redact: ['req.headers.authorization']` stops matching the moment the field is renamed or re-nested — the secret/PII logs in cleartext with zero warning (fast-redact wildcards are single-level `*` only, no deep `**`; hyphenated keys need bracket notation `'req.headers["x-api-key"]'`); pino 10 serializes an Error only when it sits under the configured `errorKey` (default `err`) — `logger.error({ error: e })` skips `stdSerializers.err` and logs `{}` because Error properties are non-enumerable, silently destroying stack traces; renaming a logged field (`userId` → `user_id`) blanks every dashboard panel and alert keyed on the old name without firing anything.
|
|
81
|
-
**Safe change:** Treat logged field names as a public schema — grep `redact` config and dashboard/alert definitions before renaming; always log errors as `{ err }` (or set `errorKey` and use it consistently); after changing redact paths, assert on a captured log line that the secret is `[Redacted]`.
|
|
82
|
-
|
|
83
|
-
## express-trust-proxy-hop-count — CRITICAL · trust-invariant
|
|
84
|
-
**Contract:** `app.set('trust proxy', n)` must equal the real number of trusted proxy hops (Cloudflare, LB) so `req.ip`/`req.secure` derive from headers only as far as infrastructure you control actually sets them.
|
|
85
|
-
**Detect:** `trust proxy` in server setup, `X-Forwarded-For` / `CF-Connecting-IP` / `X-Forwarded-Proto` reads, `express-rate-limit` config, `req\.ip`, `req\.secure`
|
|
86
|
-
**Ships green, breaks:** Default (`false`) behind Cloudflare: `req.ip` is the CF edge IP for every request — rate limits collapse onto one bucket throttling all users, and IP audit logs are garbage; `trust proxy: true`: `req.ip` becomes the LEFTMOST `X-Forwarded-For` entry, which the client controls — rate-limit and IP-allowlist bypass by header injection; with trust proxy unset, `req.secure` is always false behind TLS-terminating Cloudflare, so an https-redirect middleware loops infinitely.
|
|
87
|
-
**Safe change:** Set the hop count as a number (`app.set('trust proxy', 1)` for Cloudflare directly in front; +1 per additional LB); prefer `CF-Connecting-IP` for client identity when Cloudflare is guaranteed in path; verify with a test endpoint echoing `req.ip` from a known client; re-check the number whenever a proxy layer is added or removed.
|
|
88
|
-
|
|
89
|
-
## port-and-health-rendezvous — MED · rendezvous-string
|
|
90
|
-
**Contract:** The port in `listen(PORT)`, Dockerfile `EXPOSE`, compose/K8s `ports:`, and the healthcheck URL+path all name the same endpoint from four disconnected files.
|
|
91
|
-
**Detect:** `\.listen\(`, `EXPOSE` in Dockerfiles, `ports:` / `healthcheck:` in compose, `HEALTHCHECK`, health route paths (`/health`, `/healthz`, `/ready`) in app routes vs infra config
|
|
92
|
-
**Ships green, breaks:** `EXPOSE` is pure documentation — changing the app port and updating only `EXPOSE` leaves the compose `ports:` mapping and healthcheck pointing at the old port (connection refused only in the deployed environment); a healthcheck probing `/health` when the app serves `/healthz` marks the container unhealthy → restart loop while the app itself is fine; `listen(PORT, '127.0.0.1')` inside a container is unreachable through the port mapping (must bind `0.0.0.0`/default), which works in local dev and fails only containerized.
|
|
93
|
-
**Safe change:** Single-source the port as `PORT` env consumed by app, compose, and healthcheck alike; when changing port or health path, grep Dockerfile, compose, K8s manifests, and proxy config for the old literal; never bind loopback in a container; after deploy, watch one full healthcheck interval pass green.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Pack: Cloudflare Workers platform
|
|
2
|
-
|
|
3
|
-
Load when: the touch list exhibits `wrangler.jsonc`/`wrangler.toml`, `env.X` binding reads, Durable Objects (`extends DurableObject`, `[[migrations]]`), KV/R2/D1/Queues bindings, cron triggers, service bindings, or Workers static assets.
|
|
4
|
-
|
|
5
|
-
## worker-binding-env-rendezvous — HIGH · rendezvous-string
|
|
6
|
-
**Contract:** The `binding` name strings in wrangler config (`kv_namespaces`, `r2_buckets`, `d1_databases`, `durable_objects.bindings`, `services`, `queues.producers`, `vars`) and the `env.NAME` property reads in Worker code meet on a literal identifier with no static link.
|
|
7
|
-
**Detect:** `"binding":` in `wrangler.jsonc`/`wrangler.toml`, `env\.[A-Z_]+`, `worker-configuration.d.ts`, `wrangler types`
|
|
8
|
-
**Ships green, breaks:** Renaming a binding in config (or code) compiles and deploys clean; at request time `env.OLD_NAME` is `undefined`, so the first `.get()`/`.prepare()`/`.send()` throws `TypeError: Cannot read properties of undefined` per-request — the Worker itself deploys and health-checks fine on untouched paths. A stale generated `worker-configuration.d.ts` makes TypeScript actively vouch for the dead name.
|
|
9
|
-
**Safe change:** Grep all `env.X` reads before renaming a binding; rename config and code in one commit; re-run `wrangler types` and commit the regenerated `worker-configuration.d.ts`; exercise the specific route that touches the binding, not just deploy success.
|
|
10
|
-
|
|
11
|
-
## wrangler-env-binding-non-inheritance — HIGH · config-elsewhere
|
|
12
|
-
**Contract:** Top-level bindings in wrangler config and the `env.staging`/`env.production` sub-configs are independent — bindings, `vars`, and secrets are explicitly non-inheritable, and each environment deploys a distinct Worker named `<name>-<env>`.
|
|
13
|
-
**Detect:** `"env":` block in `wrangler.jsonc`, `[env.` in `wrangler.toml`, `wrangler deploy --env`, `wrangler secret put ... --env`
|
|
14
|
-
**Ships green, breaks:** Adding a KV/D1/queue/var binding only at top level works in dev and in the default deploy, then 500s (undefined binding) only in the environment it was never copied into. Secrets are per-Worker: `wrangler secret put KEY` without `--env staging` sets it on the base Worker, and `my-worker-staging` silently has no secret. Routes must also be provided per environment.
|
|
15
|
-
**Safe change:** When adding any binding/var, add it to top level AND every `env.*` block in the same edit; run `wrangler secret put KEY --env <e>` for each environment; diff the deployed bindings per env (`wrangler deploy --env <e> --dry-run`); test each environment's URL, not just one.
|
|
16
|
-
|
|
17
|
-
## do-migrations-ledger — CRITICAL · lifecycle-protocol
|
|
18
|
-
**Contract:** Every Durable Object class rename/delete/create must be recorded as an entry in the append-only, ordered `[[migrations]]` list (`new_sqlite_classes`, `new_classes`, `renamed_classes`, `deleted_classes`, `transferred_classes`); once a Worker has a migration tag, all future deploys must carry tags.
|
|
19
|
-
**Detect:** `"migrations":` / `[[migrations]]`, `new_sqlite_classes`, `renamed_classes`, `deleted_classes`, `class_name` in `durable_objects.bindings`, `extends DurableObject`
|
|
20
|
-
**Ships green, breaks:** Renaming the exported class in code + config without a `renamed_classes` migration orphans every live object and its storage under the old class name — new requests mint fresh empty objects. `deleted_classes` deletes all Durable Objects of that class including all stored data — irreversible. The storage backend is permanent per class: you cannot enable SQLite storage on an existing deployed class — setting `new_sqlite_classes` on later migrations fails; a class born under `new_classes` (KV) is KV forever.
|
|
21
|
-
**Safe change:** Treat migrations as an append-only ledger — never edit or reorder past tags; use `new_sqlite_classes` for every new class; for renames, add `renamed_classes = [{from, to}]` in the same deploy as the code rename; never ship `deleted_classes` without confirming the data is disposable.
|
|
22
|
-
|
|
23
|
-
## do-storage-outlives-code — CRITICAL · serialized-shape
|
|
24
|
-
**Contract:** A Durable Object's `ctx.storage` key/value entries, its embedded SQLite schema (`ctx.storage.sql`), and its pending alarms persist across every deploy — new code must parse whatever old code wrote, forever.
|
|
25
|
-
**Detect:** `ctx.storage.put`, `state.storage.get`, `storage.sql.exec`, `setAlarm`, `blockConcurrencyWhile`, `async alarm(`
|
|
26
|
-
**Ships green, breaks:** Changing a stored value's shape or a `sql.exec` schema deploys green; existing objects wake up with old rows/keys and the new code misreads or crashes per-object (only the objects that have old data — staging with fresh objects looks fine). Scheduled alarms survive the deploy and fire into the new code with at-least-once semantics (retried up to 6 times, exponential backoff from 2s) — removing or renaming the `alarm()` handler while alarms are pending produces repeating runtime errors.
|
|
27
|
-
**Safe change:** Version stored payloads (a schema-version key) and migrate lazily on read, or run `storage.sql` DDL inside `blockConcurrencyWhile()` in the constructor; keep reads tolerant of the previous shape for at least one deploy; audit `setAlarm` sites before deleting/renaming `alarm()`.
|
|
28
|
-
|
|
29
|
-
## do-idfromname-key-scheme — CRITICAL · rendezvous-string
|
|
30
|
-
**Contract:** `namespace.idFromName(name)` deterministically maps a name string to one object and its storage — the string-construction scheme (e.g. `` `tenant:${id}` ``) IS the address of persisted state.
|
|
31
|
-
**Detect:** `idFromName(`, `idFromString(`, `getByName(`, `.get(id)` on a `DurableObjectNamespace`
|
|
32
|
-
**Ships green, breaks:** Any change to the name derivation (adding a prefix, switching from email to user-id, changing case/normalization) compiles fine and silently routes to brand-new empty objects; the old objects keep their data (and keep firing their alarms) but are never addressed again — state forks with no error anywhere.
|
|
33
|
-
**Safe change:** Centralize name construction in one function and treat it as frozen; if the scheme must change, ship a bridge that reads the old name's object and migrates its storage into the new one; verify against a pre-existing object, not a fresh one.
|
|
34
|
-
|
|
35
|
-
## compatibility-date-behavior-gate — HIGH · lifecycle-protocol
|
|
36
|
-
**Contract:** `compatibility_date` in wrangler config selects which default-on runtime behaviors your code runs under; bumping it is a semantic upgrade of dozens of flags, not metadata.
|
|
37
|
-
**Detect:** `compatibility_date`, `compatibility_flags`, `nodejs_compat` in `wrangler.jsonc`
|
|
38
|
-
**Ships green, breaks:** Concrete gates: ≥ `2022-10-31` switches to the spec-compliant WHATWG URL parser (`url_standard`) — URLs with odd slashes/whitespace parse differently; ≥ `2024-03-18` flips Queues `send()` default `contentType` from `v8` to `json` — structured-clone-only bodies now serialize differently; ≥ `2024-09-23` with `nodejs_compat` auto-enables `nodejs_compat_v2` — extra polyfills bundled, module resolution changes. A date bump made "to get one new API" silently drags in every other flag between the dates.
|
|
39
|
-
**Safe change:** Bump the date as its own commit with nothing else in it; read the compat-flags changelog for every flag whose default-on date falls in the interval; pin unwanted flips with explicit `no_*` flags (e.g. `no_nodejs_compat_v2`); run the full suite under `wrangler dev` (which honors the new date) before deploying.
|
|
40
|
-
|
|
41
|
-
## cron-triggers-config-drift — HIGH · config-elsewhere
|
|
42
|
-
**Contract:** `triggers.crons` in wrangler config and the `scheduled(controller, env, ctx)` handler agree on which schedules exist and (via `controller.cron` string matching) which code each fires — always evaluated in UTC.
|
|
43
|
-
**Detect:** `"triggers":`, `"crons":`, `async scheduled(`, `controller.cron ===`
|
|
44
|
-
**Ships green, breaks:** Omitting the `triggers`/`crons` key entirely does NOT remove deployed crons — currently deployed Cron Triggers are left in place, so deleted-from-config schedules keep firing zombie jobs forever; only an explicit `"crons": []` removes them. When `triggers` IS present, deploy replaces the whole set. Multiple crons dispatch to the one `scheduled` handler; dispatch is a string compare against `controller.cron` — editing the cron expression in config breaks the `===` in code with no error, the branch just never runs. All expressions are UTC; a "midnight" job drifts an hour across DST in local-time reasoning.
|
|
45
|
-
**Safe change:** Keep the `triggers` key always present (empty array when none); when editing an expression, update the matching `controller.cron` comparison in the same commit; reason about schedules in UTC only; after removing a cron, verify in the dashboard that it's gone.
|
|
46
|
-
|
|
47
|
-
## kv-eventual-consistency — HIGH · lifecycle-protocol
|
|
48
|
-
**Contract:** KV writers and readers agree the store is eventually consistent: writes (and deletes) take up to ~60 seconds to be visible in other locations, and reads are served from per-location caches.
|
|
49
|
-
**Detect:** `kv_namespaces`, `.put(` / `.get(` / `.delete(` on a KV binding, `cacheTtl`
|
|
50
|
-
**Ships green, breaks:** Read-after-write logic works in `wrangler dev` and usually within the same PoP, then intermittently reads stale or missing values in production. Negative lookups are cached too: create-key-then-read from a location that just saw it missing returns `null` for up to 60s. Passing `cacheTtl` above the 60s default extends staleness accordingly. Two concurrent writers last-write-wins with no conflict signal.
|
|
51
|
-
**Safe change:** Never gate correctness on KV read-after-write across requests; use a Durable Object or D1 for anything needing coordination or counters; treat KV as a cache of derived, idempotently-rebuildable data; if a key was read-as-missing, expect the miss to be cached ~60s.
|
|
52
|
-
|
|
53
|
-
## secrets-vars-dual-authority — HIGH · dual-authority
|
|
54
|
-
**Contract:** A Worker's plaintext `vars` are owned by wrangler config on every deploy, while secrets are owned by the out-of-band `wrangler secret put` channel (dashboard edits form a third writer) — all landing in the same `env` namespace.
|
|
55
|
-
**Detect:** `"vars":`, `keep_vars`, `wrangler secret put`, `.dev.vars`, `--secrets-file`, `"secrets":` (required-secrets validation)
|
|
56
|
-
**Ships green, breaks:** Dashboard-edited vars are silently wiped by the next `wrangler deploy` unless `keep_vars = true`. `.dev.vars` makes a never-uploaded secret work perfectly in `wrangler dev` while `env.KEY` is `undefined` in production — first noticed when auth/API calls fail (or worse, when code falls back via `env.KEY ?? devDefault`). A `vars` entry colliding with an existing secret name fails deploy with API error 10053 — loud, but only at deploy of the colliding env.
|
|
57
|
-
**Safe change:** Declare required secret names under the `secrets` config key so wrangler validates them at deploy; pick one writer for vars (config, with `keep_vars` decided explicitly); for every new `.dev.vars` entry, immediately run `wrangler secret put` per environment; never default-fallback a missing secret.
|
|
58
|
-
|
|
59
|
-
## service-binding-rpc-surface — HIGH · serialized-shape
|
|
60
|
-
**Contract:** A service binding's `service` + `entrypoint` names in the caller's config must match a separately-deployed Worker that exports that `WorkerEntrypoint` class, and every RPC method the caller invokes must exist on whatever version of the target is currently live.
|
|
61
|
-
**Detect:** `"services":` with `"service":`/`"entrypoint":` in `wrangler.jsonc`, `extends WorkerEntrypoint`, `env\.[A-Z_]+\.\w+\(` RPC calls
|
|
62
|
-
**Ships green, breaks:** The two Workers compile, typecheck, and deploy independently — renaming/removing an RPC method (or the entrypoint class) in the target deploys green, then every caller invocation throws `TypeError: The RPC receiver does not implement the method "x"` at runtime. Arguments/returns must be structured-clone-serializable (plus RPC stubs); passing a class instance throws `DataCloneError` only when the call executes. Deploy order is a live window: between target and caller deploys, one side speaks a method surface the other lacks.
|
|
63
|
-
**Safe change:** Treat the entrypoint as a public API — additive changes only, remove methods one release after all callers stop calling; share the entrypoint's type via a common package so typecheck spans the gap; deploy target-with-both-methods first, then callers, then remove; smoke-test the actual RPC call cross-worker, not each Worker alone.
|
|
64
|
-
|
|
65
|
-
## queues-body-serialization — HIGH · serialized-shape
|
|
66
|
-
**Contract:** Producer (`queues.producers` binding) and consumer (`queues.consumers` config) rendezvous on the queue name string, and the message body shape is an unversioned schema crossing between two independently-deployed Workers via serialization.
|
|
67
|
-
**Detect:** `"queues":` with `"producers":`/`"consumers":`, `.send(` / `.sendBatch(`, `contentType`, `message.body`
|
|
68
|
-
**Ships green, breaks:** Default `contentType` is `"json"` (compat date ≥ 2024-03-18; older dates default `"v8"`): `Date` silently becomes an ISO string, `Map`/`Set` become `{}`, `undefined` fields vanish — the consumer reads mangled data with no error. `"v8"`-serialized bodies are unreadable by pull-based HTTP consumers. Messages already sitting in the queue were serialized by the OLD producer — a consumer deploy expecting the new shape must still parse days-old messages (retention default 4 days, up to 14). Max message size 128 KB — a payload that grew past it fails only at `send()` time in production.
|
|
69
|
-
**Safe change:** Version the message envelope (`{v: 2, ...}`) and keep the consumer parsing v(n-1) until retention drains; deploy the tolerant consumer before the new producer; pin `contentType` explicitly; keep payloads small (IDs, not blobs).
|
|
70
|
-
|
|
71
|
-
## queues-retry-dlq-lifecycle — CRITICAL · lifecycle-protocol
|
|
72
|
-
**Contract:** Consumer batch semantics: a thrown handler fails the entire batch, each message retries up to `max_retries` (default 3), and messages exhausting retries go to the configured `dead_letter_queue` — or are permanently discarded if none is configured.
|
|
73
|
-
**Detect:** `max_retries`, `dead_letter_queue`, `max_batch_size`, `max_batch_timeout`, `message.ack()`, `message.retry(`, `batch.ackAll()`
|
|
74
|
-
**Ships green, breaks:** With no DLQ, messages that repeatedly fail are eventually discarded — one poison message plus 3 retries equals silent permanent data loss. A handler that processes messages 1–9 then throws on message 10 retries the WHOLE batch (default `max_batch_size` 10): messages 1–9 are redelivered and double-processed unless each was explicitly `ack()`ed. `ack()` is final regardless of whether the handler later throws.
|
|
75
|
-
**Safe change:** Always configure a `dead_letter_queue` (and a consumer or alert on it); `ack()` each message individually after its side effects commit; make handlers idempotent (dedupe on message id) — redelivery is at-least-once; use `retry({delaySeconds})` for backoff instead of throwing.
|
|
76
|
-
|
|
77
|
-
## workers-runtime-limits — HIGH · config-elsewhere
|
|
78
|
-
**Contract:** Code's resource consumption must fit caps declared nowhere in the code — plan tier plus optional `limits` keys in wrangler config: CPU 10 ms free / 30 s paid default (raise via `limits.cpu_ms`), 128 MB memory per isolate, subrequest caps per plan, script size 3 MB free / 10 MB paid after gzip, 6 simultaneous open connections, env vars 5 KB each.
|
|
79
|
-
**Detect:** `"limits":` / `cpu_ms` / `subrequests` in `wrangler.jsonc`, new heavy dependency in `package.json`, fan-out `Promise.all(...fetch`, large `vars` values
|
|
80
|
-
**Ships green, breaks:** All of these pass typecheck, tests, and `wrangler dev` (which doesn't enforce production caps) and fail only under production load: CPU overage kills the request mid-flight; a fan-out that grew past the subrequest cap throws "Too many subrequests"; the 6-connection cap silently queues the 7th concurrent `fetch` — un-consumed response bodies hold connections open and can deadlock a request that then awaits another fetch. A dependency bump pushing past the gzip size cap fails at deploy — but only on the free-plan project.
|
|
81
|
-
**Safe change:** After adding a dependency, check `wrangler deploy --dry-run --outdir` bundle size; consume or `cancel()` every response body; count worst-case subrequests per request path against the plan cap; load-test CPU-heavy paths on the real plan.
|
|
82
|
-
|
|
83
|
-
## d1-transaction-and-replica-consistency — HIGH · lifecycle-protocol
|
|
84
|
-
**Contract:** D1 client code and the database agree that atomicity exists ONLY via `batch()` — SQL `BEGIN TRANSACTION`/`SAVEPOINT` are rejected at runtime — and that with read replication, read-your-writes exists only inside a Sessions-API session.
|
|
85
|
-
**Detect:** `.batch(`, `.prepare(`, `.exec(`, `db.transaction(` (ORM), `withSession(`, `first-primary`, `first-unconstrained`
|
|
86
|
-
**Ships green, breaks:** ORM interactive-transaction APIs (e.g. Drizzle `db.transaction()` on D1) compile fine and fail at runtime. The tempting workaround — sequential awaited statements — is silently non-atomic: a mid-sequence failure leaves half-applied writes. `batch()` IS transactional (aborts or rolls back the entire sequence). With read replication enabled, `withSession()` defaults to `first-unconstrained` — write-then-read across separate requests can miss the write unless the bookmark is carried forward or `first-primary` is used.
|
|
87
|
-
**Safe change:** Route every multi-statement invariant through `batch()`; never emulate transactions with sequential awaits; when enabling read replication, thread session bookmarks through request flows or pin consistency-critical reads to `first-primary`.
|
|
88
|
-
|
|
89
|
-
## static-assets-shadow-worker-routes — HIGH · config-elsewhere
|
|
90
|
-
**Contract:** With `assets.directory` configured, the platform serves a matching static asset BEFORE invoking the Worker script — the asset manifest (build output) and the Worker's route handlers implicitly partition the URL space.
|
|
91
|
-
**Detect:** `"assets":` with `"directory":`, `not_found_handling`, `run_worker_first`, `"binding": "ASSETS"`
|
|
92
|
-
**Ships green, breaks:** A build-output file landing at the same path as a Worker route (e.g. a generated `/api/status` or a new `sitemap.xml` route vs an old static one) silently shadows the handler — the Worker code never runs for that path, no error anywhere. With `not_found_handling: "single-page-application"`, unmatched paths get `index.html` served as a fallback without invoking the Worker, with a 200 — so a new server-rendered or OAuth-callback route works in unit tests but browser navigations to it receive the SPA shell instead of the handler.
|
|
93
|
-
**Safe change:** List Worker-owned path prefixes in `run_worker_first: ["/api/*", "/oauth/*", ...]` and keep it updated with every new server route; check the build output directory for filename collisions with handler paths; verify new routes with a real browser navigation (not just curl of an XHR path).
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Pack: frontend / client state / UI build
|
|
2
|
-
|
|
3
|
-
Load when: the touch list exhibits Redux Toolkit (`createSlice`, `configureStore`), RTK Query tags, react-router route tables, Tailwind 4 CSS (`@theme`, `personality.css`, `globals.css`), Vite config (`resolve.alias`, env), `@nurix/components` imports, localforage/redux-persist, or date handling crossing the wire.
|
|
4
|
-
|
|
5
|
-
## redux-persist-shape-migration — CRITICAL · serialized-shape
|
|
6
|
-
**Contract:** State shapes frozen in the user's browser storage by redux-persist must be readable by the *current* reducers' `initialState` — `version`/`migrate` is the only bridge, and `whitelist`/`blacklist` reference reducer-map keys by literal string.
|
|
7
|
-
**Detect:** `persistReducer(`, `persistStore(`, `whitelist:`, `blacklist:`, `createMigrate(`, `version:`, `stateReconciler`, storage keys `persist:*`
|
|
8
|
-
**Ships green, breaks:** Default reconciler `autoMergeLevel1` replaces each persisted top-level slice wholesale — a field added to a persisted slice's `initialState` rehydrates as `undefined` for every returning user (fresh installs and dev with cleared storage look fine). Default `version` is `-1` and with no `migrate` supplied nothing transforms old shapes. Renaming a reducer-map key silently drops it from `whitelist` — the slice just stops persisting. The bad merged shape is re-persisted on the next write, so corruption sticks.
|
|
9
|
-
**Safe change:** Bump `version` + add a `createMigrate` step for every persisted-slice shape change; grep `whitelist`/`blacklist` when renaming reducer keys; test by rehydrating a captured copy of old storage, never a fresh state.
|
|
10
|
-
|
|
11
|
-
## vite-env-client-exposure — CRITICAL · trust-invariant
|
|
12
|
-
**Contract:** The `VITE_` name prefix (or `envPrefix`) is simultaneously the exposure gate into the public bundle and the rendezvous for `import.meta.env.VITE_X` reads — naming decides secrecy.
|
|
13
|
-
**Detect:** `import.meta.env`, `.env*`, `envPrefix`, `define:` in `vite.config.*`, grep `VITE_.*(SECRET|KEY|TOKEN|PASSWORD)` in `.env*`
|
|
14
|
-
**Ships green, breaks:** Any `VITE_`-prefixed value is string-inlined into shipped JS — `VITE_API_SECRET=…` is world-readable with zero warning (fails open). Non-prefixed vars silently read `undefined`. Env access is statically replaced at build: `import.meta.env[key]` dynamic access works in dev and returns `undefined` in production; `import.meta.env.BASE_URL` must appear verbatim (no destructuring, no aliasing to a variable) or it is never replaced.
|
|
15
|
-
**Safe change:** Audit `.env*` for secret-shaped `VITE_*` names before every release; keep server secrets unprefixed and server-side only; access env only as full literal `import.meta.env.VITE_X` expressions; verify by grepping the `dist/` output for the value.
|
|
16
|
-
|
|
17
|
-
## localforage-store-identity — CRITICAL · rendezvous-string
|
|
18
|
-
**Contract:** The `(driver, name, storeName)` triple in `localforage.config()`/`createInstance()` is the *address* of persisted user data — new code must present the identical triple to find old users' data.
|
|
19
|
-
**Detect:** `localforage.config(`, `localforage.createInstance(`, `name:`, `storeName:`, `setDriver(`, `INDEXEDDB|WEBSQL|LOCALSTORAGE`
|
|
20
|
-
**Ships green, breaks:** Changing `name` (default `"localforage"`) or `storeName` (default `"keyvaluepairs"`) opens a different IndexedDB database/object store — `getItem` resolves `null`, all prior data stranded invisibly; drivers never migrate data between each other. Extra trap: `config()` called after the first data API call **returns** (does not throw) an `Error` object — the reconfiguration silently never applies unless you check the return value. Everything stored is also a serialized-shape: returning users rehydrate last year's object shape into today's code.
|
|
21
|
-
**Safe change:** Treat `name`/`storeName` as frozen identifiers; to move, write read-old-store→write-new-store migration code; call `config()` once at module init before any data call; envelope persisted values (`{v: 2, data}`) and upconvert on read.
|
|
22
|
-
|
|
23
|
-
## redux-store-shape-selectors — HIGH · rendezvous-string
|
|
24
|
-
**Contract:** The keys of `configureStore({reducer: {…}})` ARE the state-tree paths every `useSelector(s => s.key.…)` walks, and `createSlice({name})` is the action-type prefix that string matchers dispatch on.
|
|
25
|
-
**Detect:** `configureStore(`, `combineSlices(`, `useSelector(`, `createSlice({`, `extraReducers`, `addMatcher`, action-type literals `'slice/action'`
|
|
26
|
-
**Ships green, breaks:** Renaming a reducer-map key while string-path or untyped-JS selectors still read the old key → `undefined` propagates and components render empty, no error (typed `RootState` selectors are the only statically-checked subset). Renaming slice `name` changes every dispatched action type (`user/logout` → `account/logout`) — `extraReducers` string cases, listener-middleware predicates, persistence filters, and analytics stop matching with zero feedback.
|
|
27
|
-
**Safe change:** Grep old key and old `name` across the repo including string literals; keep reducer-map key identical to slice `name`; export selectors from the slice file instead of inlining paths in components.
|
|
28
|
-
|
|
29
|
-
## rtk-query-tag-taxonomy — HIGH · rendezvous-string
|
|
30
|
-
**Contract:** `providesTags` on queries and `invalidatesTags` on mutations rendezvous on identical `{type, id}` tuples from `tagTypes` — matched by runtime string equality only.
|
|
31
|
-
**Detect:** `tagTypes:`, `providesTags`, `invalidatesTags`, `injectEndpoints`, `enhanceEndpoints({addTagTypes`
|
|
32
|
-
**Ships green, breaks:** Invalidating a tag that no query provides is a silent no-op — the mutation succeeds and the cached list stays stale until an unrelated refetch masks it. TS checks the `type` half against `tagTypes` but the `id` half is untyped: `{type:'Post', id:'LIST'}` provided vs a forgotten/misspelled `id` on the invalidate side never match. Endpoints injected via `injectEndpoints` that use tag types nobody added via `addTagTypes` never invalidate anything.
|
|
33
|
-
**Safe change:** Define the tag vocabulary once as an exported const and derive `tagTypes` from it; for every mutation, name the exact providing endpoint and diff the full tuple including `id`; verify live via Redux DevTools under `state.<api>.provided` after mutating.
|
|
34
|
-
|
|
35
|
-
## react-router-route-strings — HIGH · rendezvous-string
|
|
36
|
-
**Contract:** The route table (`createBrowserRouter` path strings and `:param` names) and every `<Link to>`, `navigate()`, `useParams().x`, `useSearchParams().get('k')` scattered across components agree on literal strings.
|
|
37
|
-
**Detect:** `createBrowserRouter(`, `path:`, `<Route path=`, `useParams(`, `useSearchParams(`, `navigate(`, `<Link to=`, `generatePath(`
|
|
38
|
-
**Ships green, breaks:** Renaming `:userId` → `:id` leaves `useParams().userId` as `undefined` — the type is already `string | undefined`, so TypeScript stays green. Moving a path 404s every hardcoded `to="/old/path"` with no build error; renaming a search-param key returns `null` silently and breaks inbound deep links (bookmarks, emails) that still carry the old key. Route typegen exists only in **framework mode** (routes.ts + the react-router Vite plugin); plain react-router-dom 7 library mode with `@vitejs/plugin-react-swc` has zero static route checking — do not assume typegen protects you.
|
|
39
|
-
**Safe change:** Centralize paths as builder functions (`routes.user(id)`) and import them for both route table and links; grep the literal path and param name before renaming; treat public search-param keys as a wire contract — support old keys during a deprecation window.
|
|
40
|
-
|
|
41
|
-
## single-react-copy-alias — HIGH · dual-authority
|
|
42
|
-
**Contract:** Exactly one React runtime may own the hooks dispatcher and context registry — `@nurix/*` packages ship a nested `react`, so the app's `vite.config` `resolve.alias` must pin `react`, `react-dom`, `react/jsx-runtime`, and `react/jsx-dev-runtime` to the app's single copy.
|
|
43
|
-
**Detect:** `resolve.alias` in `vite.config.*`, `node_modules/@nurix/*/node_modules/react`, `resolve.dedupe`, `npm ls react`
|
|
44
|
-
**Ships green, breaks:** Two copies bundle with no warning; symptoms are runtime-only: "Invalid hook call", `TypeError: Cannot read properties of null (reading 'useState')`, and the fully silent one — `useContext` inside a library component resolves against the other copy's context object and returns the **default value**; react-redux 9 surfaces this as "could not find react-redux context value" even though the Provider is right there. Dev (esbuild prebundle) and build (Rollup) can dedupe differently, so it can pass `vite dev` and break `vite build`, or the reverse.
|
|
45
|
-
**Safe change:** Alias all four specifiers — the automatic JSX transform imports `react/jsx-runtime`/`jsx-dev-runtime` directly, so aliasing bare `react` alone is insufficient; after any `@nurix/*` or React upgrade, run `vite build && vite preview` and exercise a library component that consumes app context; confirm a single copy with `npm ls react`.
|
|
46
|
-
|
|
47
|
-
## tailwind-theme-token-rename — HIGH · rendezvous-string
|
|
48
|
-
**Contract:** Each `@theme` variable is simultaneously (a) a generated utility family (`--color-brand` → `bg-brand`/`text-brand`/`border-brand`) and (b) a plain CSS variable read via `var(--color-brand)` — one identifier, two independent consumer populations.
|
|
49
|
-
**Detect:** `@theme` blocks in `globals.css`/`personality.css`, `var(--color-`, `var(--radius`, grep utility classes built from the token stem
|
|
50
|
-
**Ships green, breaks:** Renaming a token compiles clean; every class using the old utility silently vanishes from the generated stylesheet (Tailwind emits nothing for unknown classes — elements just lose styling), and every `var(--old-name)` reader (handwritten CSS, inline styles, the component library) falls back to `initial`. Namespaces gate generation: only recognized namespaces (`--color-*`, `--font-*`, `--spacing-*`, `--radius-*`, `--shadow-*`, …) produce utilities — a token moved to an unrecognized namespace keeps the variable but kills the utility with no diagnostic.
|
|
51
|
-
**Safe change:** Grep both consumer forms — utility stems (`bg-old`, `text-old`) and `var(--…-old)` including `node_modules/@nurix/components` dist CSS; keep the old token as an alias for one release; visually diff themed screens after the rename.
|
|
52
|
-
|
|
53
|
-
## tailwind-theme-inline-indirection — HIGH · config-elsewhere
|
|
54
|
-
**Contract:** `@theme inline { --color-primary: var(--primary) }` makes utilities emit the *referenced* variable (`background-color: var(--primary)`), resolved per-element at runtime — the org's theming (personality.css vars swapped under `[data-theme]`/`[data-style]`) only flows through utilities via the `inline` form.
|
|
55
|
-
**Detect:** `@theme inline`, plain `@theme {`, token values that are `var(--…)`, `[data-theme=`/`[data-style=` blocks in `personality.css`
|
|
56
|
-
**Ships green, breaks:** Adding a var-referencing token to a plain (non-`inline`) `@theme` block compiles and looks correct in the default theme; switching `data-theme`/`data-style` then silently fails to restyle those utilities, because the theme variable captured the referenced var's value as resolved at `:root` — subtree/attribute-scoped overrides never reach it. No warning, no error; only the alternate theme is wrong.
|
|
57
|
-
**Safe change:** Any token whose value is `var(--app-var)` goes in the `@theme inline` block, raw literal values in plain `@theme`; after adding tokens, toggle `data-theme` and `data-style` in devtools and confirm the new utilities respond in both states.
|
|
58
|
-
|
|
59
|
-
## tailwind-source-detection — HIGH · generated-artifact
|
|
60
|
-
**Contract:** The generated stylesheet contains only class names that appear as complete, unbroken literal strings in scanned source files — the build-time scanner vs class names assembled at runtime; the default scan set skips `.gitignore`d paths and `node_modules`.
|
|
61
|
-
**Detect:** `` `…${ `` inside `className`, `clsx(`/`cn(` with computed strings, class names arriving from API/CMS/config/DB, `@source` directives in CSS
|
|
62
|
-
**Ships green, breaks:** `bg-${color}-500` builds and renders unstyled — the utility is simply absent from the CSS. Class names stored in data or composed in unscanned code work only as long as some scanned file coincidentally contains the same literal — deleting that last literal usage breaks a "different" feature. Gitignored source directories are silently excluded from scanning.
|
|
63
|
-
**Safe change:** Always write full class literals and select via lookup maps (`{primary: 'bg-blue-600'}[variant]`); safelist genuinely dynamic sets with `@source inline("…")` (v4.1+); add explicit `@source "…"` for out-of-tree or ignored directories.
|
|
64
|
-
|
|
65
|
-
## dark-variant-data-attributes — HIGH · rendezvous-string
|
|
66
|
-
**Contract:** `@custom-variant dark (&:where([data-theme=dark] *))` and the personality selectors `[data-theme="…"]`/`[data-style="…"]` rendezvous on the exact attribute name+value that runtime JS stamps on `<html>` (`document.documentElement.dataset.theme/style`).
|
|
67
|
-
**Detect:** `@custom-variant dark`, `[data-theme=`, `[data-style=`, `dataset.theme`, `setAttribute('data-theme'`, `classList.add('dark')`
|
|
68
|
-
**Ships green, breaks:** Without the `@custom-variant dark` override, Tailwind's default `dark:` variant is the `prefers-color-scheme: dark` media query — every `dark:` class silently follows the OS setting and ignores the app's toggle entirely. A value or mechanism mismatch (JS writes `class="dark"` while CSS expects `[data-theme=dark]`, or a casing typo) leaves the alternate theme unreachable with zero errors. Org default is light: base tokens must stay scoped `:root, :root[data-theme="light"]` or unstamped pages render the wrong theme.
|
|
69
|
-
**Safe change:** Define attribute name and values as one exported constant consumed by the toggle code; add `@custom-variant dark` the moment any `dark:` utility pairs with an attribute toggle; flip the attribute in devtools and verify both directions plus the no-attribute default.
|
|
70
|
-
|
|
71
|
-
## nurix-components-styles-tokens — HIGH · rendezvous-string
|
|
72
|
-
**Contract:** `@nurix/components` is imported by subpath (`@nurix/components/button`) but styled through two separate channels — its own pre-built `styles.css` (imported in `globals.css`) and the app's semantic CSS variables (personality tokens like `--radius-*`, `--color-*`) that its rules read.
|
|
73
|
-
**Detect:** `from '@nurix/components/`, the library `styles.css` `@import` line in `globals.css`, grep `var(--` in `node_modules/@nurix/components/**/styles.css`
|
|
74
|
-
**Ships green, breaks:** Omitting the `styles.css` import renders every component fully functional but unstyled — no JS error, no build warning, no missing-module failure. Renaming or dropping a personality token the library reads silently degrades components to fallback/`initial` values (e.g. a flat archetype's `--radius-*: 0` re-assertion lost in a token refactor). The library is pre-built, so the app's Tailwind scanner never generates utilities for markup inside it — its shipped CSS is the only style source.
|
|
75
|
-
**Safe change:** Keep the `styles.css` import at its mandated position in `globals.css`; before touching any personality token, grep the library's dist CSS for `var(--that-token)`; smoke-render a themed library component after every `@nurix/components` upgrade.
|
|
76
|
-
|
|
77
|
-
## vite-alias-tsconfig-paths — MED · dual-authority
|
|
78
|
-
**Contract:** Path specifiers like `@/lib/x` are resolved by two independent authorities — `tsconfig.json` `compilerOptions.paths` for typecheck/editor and `vite.config` `resolve.alias` for the served bundle.
|
|
79
|
-
**Detect:** `"paths":` in `tsconfig*.json`, `resolve.alias` in `vite.config.*`, `vite-tsconfig-paths` in `package.json`
|
|
80
|
-
**Ships green, breaks:** A missing Vite alias fails loud; the silent case is *divergent targets* — after a folder move or a one-sided edit, `tsc --noEmit` resolves one file while the bundle executes another, and same-named exports mask it completely. Editor auto-imports follow tsconfig and keep generating specifiers Vite resolves elsewhere.
|
|
81
|
-
**Safe change:** Single-source the mapping (`vite-tsconfig-paths`, or generate both from one constant); update both files in the same commit on any directory move; when suspicious, log `import.meta.url` from the module in dev to see which file actually loaded.
|
|
82
|
-
|
|
83
|
-
## css-import-order — MED · lifecycle-protocol
|
|
84
|
-
**Contract:** `globals.css` must import in the fixed sequence `@import "./personality.css"` → `@import "tailwindcss"` → the library's `styles.css` → `@theme inline {…}` — cascade-layer order is fixed at first appearance of each `@layer` name, so file position *is* the styling contract between Tailwind, the component library, and the personality tokens.
|
|
85
|
-
**Detect:** first ~10 lines of `globals.css`, `@import "tailwindcss"`, `@import` of `@nurix/components` styles, `@layer` declarations inside the library CSS
|
|
86
|
-
**Ships green, breaks:** Any reorder compiles and mostly renders — the breakage is visual-only and theme/element-specific: importing the library's `styles.css` before `@import "tailwindcss"` lets preflight's `@layer base` resets land after (and wipe) library button/form styling at equal specificity, and flips which side wins app-utility-vs-library override wars. Nothing logs; a form-heavy screen just looks subtly wrong.
|
|
87
|
-
**Safe change:** Treat the four-line header as frozen; insert new global stylesheets at a deliberate position relative to `tailwindcss` and the library import, never appended blindly; after any reorder, visual-diff a form-heavy screen in every `data-theme`/`data-style` combination.
|
|
88
|
-
|
|
89
|
-
## date-fns-local-tz — HIGH · serialized-shape
|
|
90
|
-
**Contract:** Datetimes cross the wire as UTC ISO strings or date-only `YYYY-MM-DD`, but date-fns `format()` and accessors operate in the runtime's local timezone — serializer and formatter must agree on the zone, and nothing checks that they do.
|
|
91
|
-
**Detect:** `format(`, `parseISO(`, `new Date('20` with date-only literals, `@date-fns/tz`, `TZDate`, `{ in:` option
|
|
92
|
-
**Ships green, breaks:** `new Date("2026-07-03")` parses as UTC midnight; `format(…, 'yyyy-MM-dd')` in any negative-offset zone renders `2026-07-02` — dates shift a day only for users west of UTC, while CI (UTC) and the developer's machine may pass. Writing the formatted local date back corrupts stored data. date-fns 4's fix is opt-in, not default: pass `{ in: tz('UTC') }` or use `TZDate` from `@date-fns/tz` — plain `format` remains local-zone forever.
|
|
93
|
-
**Safe change:** Classify each field: calendar dates stay strings end-to-end (never round-trip through `Date`); instants stay UTC ISO and format with an explicit `in:`/`TZDate`; run tests under a non-UTC zone (`TZ=America/New_York`) to surface the shift.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Generic tail — technologies outside the mandated stack
|
|
2
|
-
|
|
3
|
-
Load when: the touched code exhibits one of these technologies and no dedicated pack covers it. Entries here are deliberately thin — one gotcha and the safe-change essence each; diagnose the rest from the mechanism (see `../mechanisms.md`). When the org adopts one of these seriously, promote its section into a full pack.
|
|
4
|
-
|
|
5
|
-
## API styles
|
|
6
|
-
|
|
7
|
-
- **graphql-schema — CRITICAL · serialized-shape.** The SDL is executed by clients you don't redeploy: removing/renaming a field, tightening output nullability, or removing an in-use enum value breaks live queries at execution. Safe: schema-diff check in CI (GraphQL Inspector / `rover graph check`); `@deprecated` + usage metrics before removal; regenerate client types every change. Federation directives (`@key`, `@external`, `@requires`, `@shareable`) add a cross-*team* facet: composition fails or takes down fields other subgraphs own.
|
|
8
|
-
- **graphql-persisted-queries — HIGH · generated-artifact.** The server executes only registered hashes; editing an operation without republishing the manifest → `PersistedQueryNotFound` for all traffic on that operation.
|
|
9
|
-
- **grpc-protobuf — CRITICAL · serialized-shape.** Field numbers identify fields on the wire and can never be reused — a reused number deserializes garbage with no error, across services on different `.proto` versions. Safe: `reserved` removed numbers; add with new numbers only; `buf breaking` in CI; never change wire type or cardinality in place.
|
|
10
|
-
- **trpc — HIGH · generated-artifact.** The exported `AppRouter` type is the contract; it holds only if client and server build from the same version — deploy skew breaks at runtime despite full type safety. Keep tRPC package versions identical on both sides; version procedures via sub-routers.
|
|
11
|
-
- **openapi-spec — HIGH · generated-artifact.** Spec↔server drift makes generated SDKs/mocks send wrong shapes, visible only at runtime. Regenerate spec-from-code (or code-from-spec) in one change; run a breaking-change linter (`oasdiff`) in CI.
|
|
12
|
-
- **cache-headers/cdn — HIGH · config-elsewhere.** The cache obeys the header you already sent: `immutable`/long `max-age` on non-hashed assets means users never update; caching a personalized response in a shared cache leaks it; any header the response branches on must appear in `Vary` (and never `Vary: Authorization` on a shared cache). Deploys need a purge step.
|
|
13
|
-
|
|
14
|
-
## Event/stream platforms
|
|
15
|
-
|
|
16
|
-
- **kafka-consumer-groups — CRITICAL · rendezvous-string + lifecycle-protocol.** The group id owns the committed offset: renaming a group restarts from `auto.offset.reset` — `earliest` reprocesses everything, `latest` skips the backlog, both silently. Rewind via explicit offset reset, never a group rename.
|
|
17
|
-
- **schema-registry — CRITICAL · serialized-shape.** The compatibility mode (BACKWARD/FORWARD/FULL/TRANSITIVE) defines which producer/consumer upgrade orders are safe; Avro fields without defaults and Protobuf number reuse break consumers fleet-wide. CI-enforce a mode; every new Avro field gets a default.
|
|
18
|
-
- **cdc/debezium — CRITICAL · generated-artifact.** The stream's shape derives from a source schema owned by a team that may not know consumers exist — a column rename silently reshapes the stream; a table rename relocates the topic. Treat CDC-source columns as a published contract; pin topic routing and `REPLICA IDENTITY`.
|
|
19
|
-
- **event-sourcing/audit tables — CRITICAL · serialized-shape + lifecycle-protocol.** Events are immutable and replayed: today's projector must read events written years ago. Version event types + upcast; never mutate history; test a full replay.
|
|
20
|
-
- **outbox/saga — HIGH · lifecycle-protocol.** The outbox table schema is co-owned by the writing transaction and the relay (columns additive; keep the relay mapping synced). Every forward saga step needs its compensation wired into the failure path — a mid-saga failure otherwise leaves money reserved and never released.
|
|
21
|
-
- **workflow-engines (Temporal etc.) — CRITICAL · lifecycle-protocol.** Running workflows replay their history through current code: adding/reordering an `await` on a command-producing call diverges replay and bricks in-flight executions while new-workflow tests pass. Gate logic changes with `patched()`/`getVersion()`; keep I/O in Activities; replay production histories in CI.
|
|
22
|
-
|
|
23
|
-
## Postgres deep end
|
|
24
|
-
|
|
25
|
-
- **rls-policies — CRITICAL · trust-invariant.** A new tenant table without a policy, a missing `FORCE`, an owner/`BYPASSRLS` connection, or a forgotten `SET LOCAL` silently returns all rows. Column + policy + `FORCE` land in one migration; app traffic runs under a non-owner role.
|
|
26
|
-
- **pooled-session-state — HIGH · lifecycle-protocol.** Under PgBouncer transaction pooling, session state (`SET`, prepared statements, advisory locks, RLS GUCs) is shared across clients: use `SET LOCAL` only and disable server-side prepared statements, or tenant context leaks across pooled connections.
|
|
27
|
-
- **collation/extensions — HIGH · config-elsewhere.** A schema object needs its extension installed before the migration that uses it (from-scratch deploys fail); a glibc/ICU collation change silently corrupts B-tree index order → wrong results and "duplicate" unique rows. Reindex after any collation-version change.
|
|
28
|
-
- **matviews/read-models — MED · generated-artifact.** Two contracts: the source schema the refresh reads (a source column change breaks it) and the read-model shape consumers query; `REFRESH … CONCURRENTLY` needs a unique index; staleness serves wrong data with no error.
|
|
29
|
-
|
|
30
|
-
## Frontend long tail
|
|
31
|
-
|
|
32
|
-
- **ssr-hydration — HIGH · serialized-shape.** Server markup must byte-match the client's first render (React 19: mismatch is a hard error discarding the server tree) and transferred state must survive serialization (`Date`/`Map`/`Set`/`undefined` don't, by default). Move non-determinism into effects; use superjson/devalue or explicit rehydration.
|
|
33
|
-
- **module-federation — CRITICAL · config-elsewhere.** Host and remotes deploy independently but must agree at runtime on exposed-module names and `shared` singleton versions — drift loads duplicate React and breaks hooks/context silently. `singleton: true` + `strictVersion` for framework libs; coordinate deploys.
|
|
34
|
-
- **i18n-keys — MED · rendezvous-string.** Key renamed in code but not catalogs shows the raw key; missing interpolation vars render literal `{{name}}`. CI-gate with a parser (`i18next-parser --ci`); generate key types.
|
|
35
|
-
- **analytics-tracking-plan — MED · rendezvous-string.** Renaming an event or property silently breaks dashboards/funnels and fragments history, with no app error. Centralize names in a typed module; add, deprecate, never rename.
|
|
36
|
-
- **feature-flags (hosted) — HIGH · rendezvous-string.** A deleted-but-still-referenced flag returns the SDK default (often `false`), silently hiding a shipped feature; off must always mean previous-safe-behavior so disabling is rollback. Two-step removal in distributed systems: make safe unconditionally → deploy everywhere → delete.
|
|
37
|
-
- **deep-links/oauth-callbacks — HIGH · rendezvous-string.** Links already sent in emails/push are irrecoverable, and OAuth providers allowlist exact callback URLs — a path change breaks both. Keep old paths redirecting; update provider allowlists and AASA/assetlinks in lockstep.
|
|
38
|
-
|
|
39
|
-
## Orchestrated runtime (K8s and friends)
|
|
40
|
-
|
|
41
|
-
- **health-probes — HIGH · rendezvous-string + lifecycle-protocol.** The orchestrator polls a path the app owns: rename `/healthz` and K8s CrashLoopBackOffs a healthy pod. Liveness = cheap process-only check (a DB blip in liveness kills the pod); readiness = critical deps and must fail immediately on SIGTERM or rolling deploys 502.
|
|
42
|
-
- **graceful-shutdown — HIGH · lifecycle-protocol.** Shell-form ENTRYPOINT swallows SIGTERM (signals hit the shell, not the app); a `preStop` sleep covers endpoint-propagation lag. On SIGTERM: fail readiness → stop new connections → drain → exit; grace period ≥ max request time.
|
|
43
|
-
- **resource-limits — HIGH · config-elsewhere.** The kernel enforces limits the app never sees: a bigger cache OOMKills (exit 137) with no app error; CPU limits throttle silently. Make heap settings cgroup-aware; re-evaluate limits when adding caches/deps.
|
|
44
|
-
- **service-discovery/ingress/tls — HIGH · rendezvous-string.** Service names, ingress path rules (order- and specificity-sensitive; rewrites must match mounted routes), and cert SANs each bind by exact string — a rename 404s/NXDOMAINs/fails handshake while the app stays healthy. Add hostnames to the cert SAN list before routing traffic.
|
|
45
|
-
- **mesh-resilience-budgets — HIGH · lifecycle-protocol.** Timeouts must nest (caller > callee × retries) or retries storm a recovering service; these configs live in a different repo/layer than the calling code.
|
|
46
|
-
- **iac-outputs — HIGH · rendezvous-string.** A consuming stack reads producer outputs by name (`terraform_remote_state`, `Fn::ImportValue`): rename/remove one and every consumer fails to plan. Outputs are public API — deprecate, don't rename.
|
|
47
|
-
- **trace/metrics/logs naming — MED · rendezvous-string.** Dashboards, alerts, and cross-service queries pin exact metric names, label keys, and log field names — a rename makes the alert go quiet, not fire. Deprecate, don't rename; never put unbounded cardinality in labels.
|
|
48
|
-
|
|
49
|
-
## Misc
|
|
50
|
-
|
|
51
|
-
- **di-containers — HIGH · rendezvous-string.** String/symbol tokens and registrations match at runtime only; a scope change (singleton↔request) silently shares state across requests/tenants. Typed tokens; treat scope as part of the contract.
|
|
52
|
-
- **worker/ipc boundaries — MED · serialized-shape.** Only structured-cloneable values cross `postMessage`/IPC — class instances lose methods, `instanceof` fails; a renamed IPC channel (`ipcMain.handle` vs `ipcRenderer.invoke`) silently no-ops. Version the message envelope; share channel constants.
|
|
53
|
-
- **object-storage keys — MED · rendezvous-string.** The key template is a contract among uploader, downloader, lifecycle rules, and DB pointers — changing it orphans existing objects (no FK between DB and bucket). Store the full key in the DB; migrate by copy/re-point.
|
|
54
|
-
- **saml — HIGH · config-elsewhere.** IdP signing-cert rotation without the SP re-importing metadata fails every assertion; a `NameID` format change re-keys identity and duplicates accounts. Overlap certs during rotation; pin identity on a stable identifier.
|
|
55
|
-
- **lambda-handler-envelopes — HIGH · serialized-shape.** The platform owns the event shape: API Gateway payload v1→v2 moves `event.httpMethod` to `event.requestContext.http.method`, existing parsing reads `undefined`. Pin the payload format version; validate the event at the handler.
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# Pack: persistence & data model
|
|
2
|
-
|
|
3
|
-
Load when: the touch list exhibits Drizzle/D1/SQLite (`schema.ts`, `init.sql`, `sqliteTable(`, `drizzle-kit`), Postgres (`pgTable`, migrations, enums, pgvector), Redis caching, seeds (`*.seed.sql`, generators), or JSON columns.
|
|
4
|
-
|
|
5
|
-
## init-sql-drizzle-mirror-parity — CRITICAL · dual-authority
|
|
6
|
-
**Contract:** Hand-written canonical DDL (`db/seed/init.sql`) and the Drizzle mirror (`db/schema.ts`) independently declare the same tables/columns/indexes and must match byte-for-byte — the string in `sqliteTable("…")` and every index name equal the DDL.
|
|
7
|
-
**Detect:** `db/seed/init.sql`, `db/schema.ts`, `sqliteTable("`, `uniqueIndex("`, `index("`, `_idx`, `_unique`
|
|
8
|
-
**Ships green, breaks:** Each side validates only against itself — local/test DBs are typically built from `schema.ts` (push) while the real D1 is built from `init.sql`, so tests pass and prod 500s (`no such column`). Subtler drift is fully silent: a column present in DDL but missing from the mirror stays NULL forever (Drizzle never writes it); a DEFAULT that differs between the two gives Drizzle-inserted rows and seed rows different values; a drifted index *name* means a later `drizzle-kit push` DROPs the DDL's index and recreates it under the schema name — hand-tuned index definitions vanish.
|
|
9
|
-
**Safe change:** Edit both files in the same commit; run `npx drizzle-kit export --dialect=sqlite --schema=db/schema.ts` and diff the emitted DDL against `init.sql` (names, defaults, index names); alternatively apply `init.sql` to a scratch DB and confirm `drizzle-kit push` proposes zero statements; grep every `_idx`/`_unique` name on both sides.
|
|
10
|
-
|
|
11
|
-
## drizzle-push-vs-migrate-mixing — CRITICAL · generated-artifact
|
|
12
|
-
**Contract:** One database gets exactly one schema-application mechanism — either `drizzle-kit push` (stateless diff, schema.ts is total authority) or `generate`+`migrate` (journaled in `__drizzle_migrations`); the two do not compose.
|
|
13
|
-
**Detect:** `drizzle-kit push` in `package.json`/CI, `drizzle/` migrations folder, `__drizzle_migrations`, `tablesFilter` in `drizzle.config.ts`, `--force`
|
|
14
|
-
**Ships green, breaks:** `push` diffs schema.ts against the *entire* live DB and proposes `DROP TABLE` for anything not in schema.ts — including tables owned by other tools (Better Auth's `user`/`session`/`account`/`verification`, wrangler's `d1_migrations`); `push --force` in CI auto-approves those data-loss statements with no prompt. `push` records nothing in `__drizzle_migrations`, so a later `migrate` replays the whole folder against the already-pushed DB; `generate` diffs against its own snapshot journal, not the pushed DB, so the generated SQL silently disagrees with reality.
|
|
15
|
-
**Safe change:** Pick one workflow per database and enforce it in CI; set `tablesFilter` in `drizzle.config.ts` to only the tables Drizzle owns; never pass `--force` unattended; when converting push→migrate, generate a baseline migration and mark it applied before the first `migrate`.
|
|
16
|
-
|
|
17
|
-
## sqlite-alter-table-recreate — CRITICAL · lifecycle-protocol
|
|
18
|
-
**Contract:** SQLite `ALTER TABLE` only does RENAME TO / RENAME COLUMN / ADD COLUMN / DROP COLUMN — any type/NOT NULL/DEFAULT/constraint change makes drizzle-kit emit the rebuild dance (`PRAGMA foreign_keys=OFF` → `CREATE __new_<t>` → `INSERT INTO __new_ SELECT …` → `DROP TABLE` → `RENAME`), whose safety depends on FK state during execution.
|
|
19
|
-
**Detect:** `__new_` in `drizzle/*.sql`, `PRAGMA foreign_keys=OFF`, `PRAGMA defer_foreign_keys`, `ON DELETE CASCADE`, `__old_push_`
|
|
20
|
-
**Ships green, breaks:** `PRAGMA foreign_keys` is a no-op inside an open transaction — when the migration runs transaction-wrapped, the rebuild's `DROP TABLE` fires `ON DELETE CASCADE` and silently wipes every child table (a single column-default change has deleted all related rows, no error). On D1, `PRAGMA foreign_keys=OFF` is impossible entirely (every query is an implicit transaction); the sanctioned `PRAGMA defer_foreign_keys=on` defers checks but does NOT suppress `ON DELETE CASCADE`. Column-count mismatches in the copy step have left `__old_push_<t>` holding the data and the new table empty.
|
|
21
|
-
**Safe change:** Hand-review any generated migration containing `__new_`; on D1 add `PRAGMA defer_foreign_keys = on` as the first statement; back up (D1 Time Travel bookmark) before applying; assert child-table rowcounts after the rebuild.
|
|
22
|
-
|
|
23
|
-
## seed-regen-upsert-clobber — CRITICAL · dual-authority
|
|
24
|
-
**Contract:** Generated seed files (`db/seed/*.seed.sql`, UUIDv5 of the natural key, idempotent `ON CONFLICT(slug) DO UPDATE`) and the live DB co-own vocabulary rows — the generator owns `slug/label/description`, the runtime owns `count`, admins own `weight`.
|
|
25
|
-
**Detect:** `gen-seeds.mjs`, `gen-product-domains.mjs`, `*.seed.sql`, `ON CONFLICT(slug) DO UPDATE`, `excluded.count`, `excluded.weight`, `excluded.id`
|
|
26
|
-
**Ships green, breaks:** If the generator's `DO UPDATE SET` list ever includes `count`/`weight`/`id`/`created_at`, the next deploy's re-seed silently resets live drift counters and admin-tuned weights to CSV values — no error, data just reverts. Changing the uuid5 namespace or the natural-key string format re-mints every id: the slug-targeted upsert still matches, but `SET id = excluded.id` dangles every external record of the old UUIDs; if the conflict target were `id` instead of `slug`, re-seed duplicates the whole vocabulary. Mixing UUIDv7 (runtime, time-sortable) and UUIDv5 (seed, hash) ids also means `ORDER BY id` ≠ insertion order.
|
|
27
|
-
**Safe change:** Edit the CSV and rerun the generator, never the `.seed.sql`; diff the regenerated file for id churn before committing; keep `DO UPDATE SET` restricted to label/description/status and always exclude `count`, `weight`, `id`, `created_at`; treat the uuid5 key-format string as frozen API.
|
|
28
|
-
|
|
29
|
-
## pg-on-conflict-arbiter-index — CRITICAL · config-elsewhere
|
|
30
|
-
**Contract:** Every upsert's conflict target is backed by a live unique (or partial-unique) index declared in a migration far from the query; dedupe correctness lives in the index, not the code.
|
|
31
|
-
**Detect:** `onConflictDoUpdate`, `onConflictDoNothing`, `ON CONFLICT`, `INSERT OR IGNORE`, `uniqueIndex(`, `targetWhere`, `_unique`
|
|
32
|
-
**Ships green, breaks:** Targeted `ON CONFLICT (col) DO UPDATE` at least errors loudly if the constraint is dropped — but targetless `ON CONFLICT DO NOTHING` and SQLite `INSERT OR IGNORE` keep returning success and silently insert duplicates once the unique index is gone (e.g. a rebuild migration recreated the table with `_idx` instead of `_unique`). Partial unique indexes need the predicate restated for arbiter inference — in Drizzle, `onConflictDoUpdate({ target, targetWhere })` must repeat the index's `WHERE`; omit it and PG throws `there is no unique or exclusion constraint matching the ON CONFLICT specification` even though "the index exists".
|
|
33
|
-
**Safe change:** Before dropping/renaming any `_unique` index, grep for upserts naming those columns; prefer explicit conflict targets over targetless DO NOTHING; for partial indexes, mirror the predicate in `targetWhere`; after SQLite rebuild migrations, verify uniques survived (`PRAGMA index_list(<table>)`).
|
|
34
|
-
|
|
35
|
-
## d1-batch-not-a-transaction — HIGH · lifecycle-protocol
|
|
36
|
-
**Contract:** Code needing multi-statement atomicity on D1 must use `db.batch([...])` — D1 has no interactive transactions; `BEGIN`/`SAVEPOINT` are rejected and Drizzle's `db.transaction()` fails at runtime on the D1 driver.
|
|
37
|
-
**Detect:** `db.transaction(`, `BEGIN TRANSACTION`, sequential `await db.insert(…).run()` chains, `env.DB.batch`, `db.batch(`
|
|
38
|
-
**Ships green, breaks:** Sequential awaited writes read as transactional in review but each auto-commits — a mid-sequence failure leaves permanent partial writes with no rollback. `batch()` is atomic (a failing statement rolls back the sequence) but non-interactive: a later statement cannot branch on an earlier statement's result, so read-modify-write logic ported into a "batch transaction" carries a TOCTOU race between the pre-read and the batch. Code sharing a service layer with a Node/Postgres path (where `db.transaction` works) compiles and unit-tests green, then throws `D1_ERROR` only on the Workers runtime.
|
|
39
|
-
**Safe change:** Express each multi-write invariant as one `db.batch([...])`; hoist all reads before the batch and encode invariants as SQL conditions (`WHERE`-guarded UPDATE) instead of JS branches; never let `db.transaction` reach a D1 code path.
|
|
40
|
-
|
|
41
|
-
## d1-foreign-keys-always-on — HIGH · config-elsewhere
|
|
42
|
-
**Contract:** D1 enforces `PRAGMA foreign_keys = on` for every query and migration and does not allow turning it off (only `PRAGMA defer_foreign_keys` within a transaction), while local SQLite harnesses (better-sqlite3 et al.) default foreign_keys OFF.
|
|
43
|
-
**Detect:** `better-sqlite3`, `new Database(`, missing `pragma('foreign_keys = ON')` in test setup, `PRAGMA foreign_keys = OFF` in `migrations/*.sql`, `defer_foreign_keys`
|
|
44
|
-
**Ships green, breaks:** Tests that insert children before parents, delete referenced parents, or import fixtures out of order pass locally (orphans silently accepted) and then throw `FOREIGN KEY constraint failed` only on production D1. Conversely, orphan rows accumulated under local FK-off dev later block a table-rebuild migration on D1. A migration file containing `PRAGMA foreign_keys = OFF` is silently ineffective on D1 — and the sanctioned `defer_foreign_keys = on` still lets `ON DELETE CASCADE` fire.
|
|
45
|
-
**Safe change:** Enable `foreign_keys = ON` in every local SQLite test/dev harness; order seed and migration statements parent-first; use `PRAGMA defer_foreign_keys = on` (not `foreign_keys = OFF`) in D1 migrations that transiently violate FKs.
|
|
46
|
-
|
|
47
|
-
## d1-session-bookmark-consistency — HIGH · lifecycle-protocol
|
|
48
|
-
**Contract:** With D1 read replication enabled, cross-request read-your-writes holds only if `session.getBookmark()` is round-tripped (cookie/header) into the next request's `env.DB.withSession(bookmark)`; the constant `"first-unconstrained"` permits arbitrarily stale replica reads.
|
|
49
|
-
**Detect:** `withSession(`, `getBookmark(`, `first-primary`, `first-unconstrained`, `read_replication` in `wrangler.jsonc`/dashboard
|
|
50
|
-
**Ships green, breaks:** Enabling replication and adopting `withSession("first-unconstrained")` makes POST-then-redirect-GET flows intermittently render pre-write state — only for users near a lagging replica, never reproducible in local dev (no replicas exist there). Sessions guarantee *sequential* consistency within one session object; two independent sessions with no bookmark hand-off share no ordering at all. Non-session `env.DB.prepare()` calls still route to the primary — a partial migration to Sessions creates mixed-consistency paths inside one handler.
|
|
51
|
-
**Safe change:** Return the bookmark to the client after every write and feed it into the next request's `withSession()`; default to `"first-primary"` for flows that read after writing; keep a write and its dependent reads on the same session object.
|
|
52
|
-
|
|
53
|
-
## slug-rendezvous-vocabulary-drift — HIGH · rendezvous-string
|
|
54
|
-
**Contract:** `slug` is the org's cross-reference key — enforced refs are real FKs `REFERENCES <table>(slug)`, but drift-tolerant vocabulary columns deliberately have NO FK (paired with a `<thing>_in_taxonomy` boolean), and the vocabulary's source of truth is the taxonomy package, not the D1 snapshot table.
|
|
55
|
-
**Detect:** `REFERENCES sources(slug)`, `_slug` columns without `FOREIGN KEY`, `_in_taxonomy`, `taxonomy/seed/*.csv`
|
|
56
|
-
**Ships green, breaks:** Renaming a slug in the taxonomy CSV regenerates the seed, and the `ON CONFLICT(slug)` upsert *inserts a new row* under the new slug instead of renaming — the old row survives, every existing `_slug` value still points at the now-deprecated slug, joins quietly return empty sets, and `_in_taxonomy` flags go stale. Nothing errors: the columns have no FK by design. FK-enforced references fail loudly instead — but only at write time, long after the rename shipped.
|
|
57
|
-
**Safe change:** Treat slugs as immutable once referenced — change `label`, never `slug`; if a rename is unavoidable, ship an `UPDATE` of every referencing `_slug` column plus old-row retirement in the same migration; recompute `_in_taxonomy` flags afterward.
|
|
58
|
-
|
|
59
|
-
## sqlite-text-timestamp-format — HIGH · serialized-shape
|
|
60
|
-
**Contract:** Every writer of `created_at`/`updated_at` (`TEXT NOT NULL DEFAULT (datetime('now'))`) agrees on the format that default emits: `YYYY-MM-DD HH:MM:SS`, UTC, space separator, no `T`/`Z`/milliseconds.
|
|
61
|
-
**Detect:** `datetime('now')`, `toISOString()`, `$onUpdate`, `text("created_at")`, `new Date(` on timestamp columns
|
|
62
|
-
**Ships green, breaks:** App code writing `new Date().toISOString()` (`2026-07-04T09:00:00.000Z`) into the same column creates mixed formats: TEXT comparison is bytewise, `' '` < `'T'`, so `ORDER BY created_at` and range filters silently drop or misorder rows across the two formats. Reading back, `new Date("2026-07-04 09:00:00")` parses as *local* time in JS while the stored value is UTC — silent per-timezone offsets. And SQLite has no `ON UPDATE`: Drizzle's `$onUpdate` fires only on Drizzle-issued updates, so seed upserts and raw SQL leave `updated_at` stale unless they SET it explicitly.
|
|
63
|
-
**Safe change:** Standardize on the DDL default's format via one shared formatter for app-side writes; when parsing, restore `T`/`Z` (`s.replace(' ','T') + 'Z'`); include `updated_at = datetime('now')` in every raw UPDATE and every seed `DO UPDATE SET`.
|
|
64
|
-
|
|
65
|
-
## sqlite-affinity-boolean-mapping — HIGH · serialized-shape
|
|
66
|
-
**Contract:** All writers store literal `0`/`1` in `is_*`/`has_*` INTEGER columns, because Drizzle's `integer(…, { mode: "boolean" })` deserializes with exactly `Number(value) === 1`.
|
|
67
|
-
**Detect:** `{ mode: "boolean" }`, `is_` / `has_` columns in `init.sql`, quoted booleans `'true'`/`'TRUE'` in seed SQL, `typeof(` audits
|
|
68
|
-
**Ships green, breaks:** SQLite INTEGER affinity rejects nothing — a seed or raw SQL writing `'true'` (quoted) stores the TEXT `'true'` inside the INTEGER column; `Number('true')` is `NaN`, so Drizzle silently reads the row as `false`. Any stored value other than exactly `1` (a `2`, a `-1`, `'yes'`) also reads as `false`. Unquoted `TRUE`/`FALSE` keywords are safe (they are 1/0), which makes the quoted variant an easy copy-paste landmine; D1 tables aren't STRICT, so nothing validates at write time.
|
|
69
|
-
**Safe change:** Write literal `1`/`0` in all hand-written SQL; audit with `SELECT typeof(is_col), is_col, COUNT(*) FROM t GROUP BY 1,2`; ensure every `is_*`/`has_*` column carries `{ mode: "boolean" }` in `schema.ts`.
|
|
70
|
-
|
|
71
|
-
## json-column-shape-versioning — HIGH · serialized-shape
|
|
72
|
-
**Contract:** Readers of a JSON column (`TEXT` holding JSON in SQLite — `design_tokens`, `bbox`, `scorecard`, `meta` — or `jsonb` in PG) must parse every shape ever written; old rows keep their old shape forever, and Drizzle's `{ mode: 'json' }` + `.$type<T>()` is a compile-time assertion never validated at read.
|
|
73
|
-
**Detect:** `mode: 'json'`, `.$type<`, `jsonb(`, `JSON.parse(` on column values
|
|
74
|
-
**Ships green, breaks:** Adding a required field to `T` type-checks green while every pre-existing row lacks it — `undefined` propagates into arithmetic (NaN scores) and skipped branches with no exception; renaming a field silently orphans all historical data under the old key; during a rolling deploy, new-shape writes crash still-running old readers. Tests pass because fixtures are always written by the current code.
|
|
75
|
-
**Safe change:** Make every added field optional with a read-time default, or embed a `v` version field and branch on it; backfill a migration before making a field required; validate at the boundary with a zod schema instead of trusting `$type`.
|
|
76
|
-
|
|
77
|
-
## pg17-enum-lifecycle — HIGH · lifecycle-protocol
|
|
78
|
-
**Contract:** A Postgres enum's value list and declaration order are shared DDL across all writers, readers, and in-flight migrations; PG (through 17) has no `ALTER TYPE … DROP VALUE` — removal requires a full rebuild (columns→text, drop type, recreate, cast back).
|
|
79
|
-
**Detect:** `pgEnum(`, `ALTER TYPE`, `ADD VALUE`, `USING (`…`::text::`, exhaustive `switch` on enum unions
|
|
80
|
-
**Ships green, breaks:** `ADD VALUE` runs fine inside a transaction (since PG12) but *using* the value in the same transaction throws `unsafe use of new value … of enum type` (55P04) — this bites exactly the migration that adds a value and then seeds rows with it, and only at deploy time. Enum `ORDER BY` follows declaration order, so appending without `BEFORE`/`AFTER` silently reorders sorted output. Mixed-version deploys: new pods write the new value while old pods' exhaustive switches receive an unknown variant. The removal rebuild fails mid-migration if any row still holds the removed value.
|
|
81
|
-
**Safe change:** Split ADD VALUE and its first use into separate migrations (separate transactions); use `BEFORE`/`AFTER` when sort order matters; deploy readers tolerant of the new value before writers emit it; before removal, count rows holding the value and review the generated rebuild SQL.
|
|
82
|
-
|
|
83
|
-
## pgvector-opclass-operator-match — HIGH · rendezvous-string
|
|
84
|
-
**Contract:** The query's distance operator, the index's operator class, and the embedding model/dimension must agree: `<->`↔`vector_l2_ops`, `<=>`↔`vector_cosine_ops`, `<#>`↔`vector_ip_ops`, column `vector(N)` fixed to the model's output size.
|
|
85
|
-
**Detect:** `vector_cosine_ops`, `vector_l2_ops`, `vector_ip_ops`, `<=>`, `<->`, `<#>`, `vector({ dimensions:`, `.op('vector_`, `USING hnsw`, `USING ivfflat`
|
|
86
|
-
**Ships green, breaks:** Operator/opclass mismatch produces no error or warning — Postgres just seq-scans (≈5ms indexed → 30s at 1M rows); the only tell is `EXPLAIN` showing Seq Scan. In Drizzle the rendezvous is the literal `.op('vector_cosine_ops')` string on the index vs the `sql`-template operator in a query file that never imports it. Swapping embedding models at the *same* dimension inserts cleanly and returns confidently wrong neighbors — cross-model similarity is meaningless; a different dimension at least errors.
|
|
87
|
-
**Safe change:** Grep every distance operator and match it to an index opclass; `EXPLAIN` the hot query after any index or query change; record the embedding model per row (or per table) and re-embed the entire table on model change — never mix.
|
|
88
|
-
|
|
89
|
-
## redis-key-ttl-serialization-versioning — HIGH · serialized-shape
|
|
90
|
-
**Contract:** Cache writers and readers across deploys rendezvous on literal key strings and an agreed value encoding; Redis outlives every deploy, so pre-deploy keys, shapes, and TTLs keep serving after the code changes.
|
|
91
|
-
**Detect:** key-template literals (`` `user:${id}` ``), `JSON.parse(` on `GET` results, `set(` without `EX`/`KEEPTTL`, `maxmemory-policy`, hardcoded prefixes in more than one module
|
|
92
|
-
**Ships green, breaks:** Renaming a key schema without invalidation leaves old keys serving stale data to any code path still reading the old name — both paths "work". Changing the value shape breaks only on cache *hits*: cold-cache tests and fresh staging pass, production readers throw or mis-parse on surviving pre-deploy entries. Plain `SET key val` (Redis ≥6) silently *removes* an existing TTL unless `KEEPTTL` or `EX` is given — a refresh path without `EX` turns expiring keys immortal. Default `maxmemory-policy noeviction` means a full instance rejects writes (`OOM command not allowed`) instead of evicting.
|
|
93
|
-
**Safe change:** Embed a schema version in the key prefix (`v2:user:{id}`) and bump it with any shape change — old entries expire naturally; always pass `EX`/`PX` (or `KEEPTTL`) on every SET; treat deserialization failure as a cache miss, never an error; set `allkeys-lru` (or similar) for cache workloads.
|