acuvo-code 0.2.0

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.
Files changed (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: performance
3
+ description: Measure before optimising — images, bundle size, the render loop, and the costs that dominate
4
+ when: When a page feels slow, before optimising anything, or when adding a dependency or an image
5
+ ---
6
+
7
+ # Performance
8
+
9
+ ## ⚠️⚠️ Measure first. Optimising by intuition is how a day disappears.
10
+
11
+ Intuition about what is slow is wrong often enough that acting on it unmeasured
12
+ is gambling. Open the profiler, or time it:
13
+
14
+ ```js
15
+ console.time('render'); doTheThing(); console.timeEnd('render');
16
+ ```
17
+
18
+ ⭐ And know what "slow" means here — a 400ms function called once on load is
19
+ irrelevant; a 4ms function called 3,000 times in a scroll handler is the bug.
20
+
21
+ ## The costs that actually dominate a web page
22
+
23
+ In rough order of how often they matter:
24
+
25
+ 1. **Images.** Usually most of the bytes. Wrong-sized hero images are the single
26
+ most common cause of a slow page.
27
+ 2. **Blocking requests** in the head — a font, a stylesheet, a synchronous
28
+ script — each one delays first paint.
29
+ 3. **JavaScript bundle size** — download, then parse, then execute.
30
+ 4. **Layout thrash** — reading a layout property after writing one, in a loop.
31
+ 5. Everything else.
32
+
33
+ ## Images
34
+
35
+ - Size them for the box they land in. A 4000px photo in a 400px card wastes 99%
36
+ of its bytes.
37
+ - Modern format (WebP/AVIF) with a fallback.
38
+ - `loading="lazy"` for anything below the fold — never for the hero, which is
39
+ the one thing you want early.
40
+ - **Always set `width` and `height`** (or `aspect-ratio`). Without them the page
41
+ reflows when each image arrives, which is both ugly and a Core Web Vital.
42
+
43
+ ## The render loop
44
+
45
+ ```js
46
+ ✗ items.forEach(i => list.appendChild(render(i))); // layout per item
47
+ ✓ const frag = document.createDocumentFragment();
48
+ items.forEach(i => frag.appendChild(render(i)));
49
+ list.appendChild(frag); // one layout
50
+ ```
51
+
52
+ ⚠️ **Reading forces a recalculation.** `offsetHeight`, `getBoundingClientRect`
53
+ and friends flush pending layout. Reading one in a loop that also writes makes
54
+ the browser recompute every iteration. Batch: read everything, then write
55
+ everything.
56
+
57
+ Debounce input handlers; throttle scroll and resize.
58
+
59
+ ## ⚠️ Every dependency is bytes the user downloads
60
+
61
+ Before adding one, check what it costs and whether a few lines would do. A date
62
+ library for one `toLocaleDateString` is a bad trade. Import only what you use —
63
+ `import { debounce } from 'lodash-es'`, never the whole namespace.
64
+
65
+ ## Do less work
66
+
67
+ The fastest request is the one not made, and the fastest render is the one
68
+ skipped. Cache what does not change; paginate instead of loading everything
69
+ (see `api-design`); do not recompute a derived value on every render when it
70
+ only changes when its inputs do.
71
+
72
+ ## ⚠️ Perceived speed is real speed
73
+
74
+ A skeleton that appears in 100ms feels faster than a blank screen for 400ms and
75
+ then everything at once — same total time. Show structure early, respond to
76
+ input immediately even if the result takes a moment, and never leave a click
77
+ with no visible acknowledgement.
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: plan-before-building
3
+ description: Use the planning verbs and the whiteboard to think before writing code
4
+ when: Any task with more than about three steps, or any ambiguous request
5
+ ---
6
+
7
+ # Think first, then build
8
+
9
+ ## Why this pays
10
+
11
+ An agent that starts typing immediately rewrites files it already wrote, loses
12
+ track of what is done, and produces the same file three times. Planning is not
13
+ ceremony — it is what keeps a long build coherent.
14
+
15
+ Use `plan_start` / `plan_step` / `plan_status`. They exist so the plan survives
16
+ the context, not just your intention.
17
+
18
+ ## The shape of a good plan
19
+
20
+ 1. **Restate the goal in one sentence.** If you cannot, you do not have one yet.
21
+ 2. **List what you do not know** and how you will find out — read a file, run a
22
+ command, check a schema. Do this BEFORE writing code that assumes an answer.
23
+ 3. **Order by risk, not by ease.** The step most likely to invalidate the others
24
+ goes first. Discovering the API shape after building the UI around it is the
25
+ expensive order.
26
+ 4. **Name what "done" looks like** in terms someone else could check.
27
+
28
+ ## Read before you write
29
+
30
+ Before editing a file, read it. Before adding a helper, search for one that
31
+ exists. Before designing a schema, look at the tables already there.
32
+
33
+ Most duplicated code is not a naming failure — it is someone writing before
34
+ looking. `list_files` and `search_text` are cheap; a second implementation of an
35
+ existing thing is not.
36
+
37
+ ## Use the whiteboard to think, not just to output
38
+
39
+ A whiteboard or diagram is most valuable BEFORE the code: laying out the screens,
40
+ the data flow, the states. Using it only to render a finished result wastes the
41
+ half where it actually helps.
42
+
43
+ Sketch: what are the screens, what data does each need, where does that data come
44
+ from, what happens when it is missing.
45
+
46
+ ## When to stop and ask
47
+
48
+ Ask when two readings of the request produce materially different work and you
49
+ cannot tell which is meant. Do not ask about things you can determine yourself by
50
+ reading the code — that is slower for everyone and it is your job.
51
+
52
+ State the assumption and keep moving when the cost of being wrong is low.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: planning-and-delegating
3
+ description: Multi-deliverable work — declare the plan, track it honestly, delegate to save context
4
+ when: When a task has more than one deliverable, when the user names commands that must pass, or when a subtask would flood your context
5
+ ---
6
+
7
+ # Planning And Delegating
8
+
9
+ ## ⭐⭐ Declare the deliverables BEFORE starting
10
+
11
+ `plan_start` records what a task's deliverables are. Every later tool result
12
+ then carries a line telling you how many remain.
13
+
14
+ **Why it matters more than it looks:** the common failure on a five-part request
15
+ is not doing a part badly, it is **finishing three and reporting done.** A plan
16
+ you cannot see is a plan you will drift off. The reminder rides along with work
17
+ you were doing anyway.
18
+
19
+ Use it whenever the request has more than one deliverable — *"add the endpoint,
20
+ write the test, and update the docs"* is three, not one.
21
+
22
+ ## ⚠️⚠️ `plan_step` done means the deliverable EXISTS and you CHECKED
23
+
24
+ Marking a step done because you wrote the code that should produce it is how a
25
+ plan becomes fiction. **Nothing else marks a step done** — so if the plan says
26
+ done, that is a claim you made, and the user will read it as verified.
27
+
28
+ `blocked` is a real state and an honest one. Use it. A blocked step that says so
29
+ is worth far more than a done step that is not.
30
+
31
+ ⭐ **`plan_status` when you have lost the thread** — after a long detour, or
32
+ resuming. Cheaper than re-reading the conversation, and it is authoritative.
33
+
34
+ ## ⭐⭐⭐ Acceptance criteria are the USER'S words, recorded early
35
+
36
+ When the user says *"it's done when `npm test` passes"*, call
37
+ **`declare_acceptance` with their command, verbatim, ONCE, before doing the
38
+ work.**
39
+
40
+ ⚠️ **A criterion chosen AFTER the work is a criterion chosen to pass.** That is
41
+ the whole reason it is recorded first — a test you picked because it goes green
42
+ proves nothing about what was asked.
43
+
44
+ **`check_acceptance` is the only thing that clears them.** Running the command
45
+ yourself and seeing it pass does not; `evaluate` returning true does not. If the
46
+ criteria are outstanding, the work is outstanding, however good the code looks.
47
+
48
+ ## ⭐⭐ `delegate` protects the thing you cannot get back: context
49
+
50
+ Hand off a self-contained piece of work and you get back **a short summary, not
51
+ everything the helper read.** The helper burns its own context; yours stays for
52
+ the work only you can do.
53
+
54
+ **Good delegation:** *"find every call site of `readRows` and report which ones
55
+ ignore the status field"* — a big search, a small answer.
56
+
57
+ ⚠️ **Bad delegation:** anything where you need the intermediate detail, or where
58
+ the task is not self-contained. The helper cannot see your conversation. If the
59
+ brief needs three paragraphs of background, you are better off doing it.
60
+
61
+ ⭐ **It only READS by default.** That is the safe setting and it should stay the
62
+ default in your head: send it to find out, come back to decide.
63
+
64
+ ## ⭐ `remember` for what a future session would otherwise get WRONG
65
+
66
+ Not a diary. One durable fact, with **why**, and only if a future session would
67
+ otherwise repeat a mistake or rediscover something expensive.
68
+
69
+ *"The dev server needs `npm install` first — `node_modules` ships incomplete"* is
70
+ worth remembering. *"Fixed the header"* is not.
71
+
72
+ Related skills: `plan-before-building`, `verify-your-own-work`, `debugging`.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: refactoring
3
+ description: Changing code safely without a rewrite — small steps, a green check between each, no behaviour change
4
+ when: Before restructuring code, when tempted to rewrite, or when a change touches many files
5
+ ---
6
+
7
+ # Refactoring
8
+
9
+ ## ⚠️⚠️ Refactoring means behaviour does NOT change
10
+
11
+ If the output changes, it is not a refactor — it is a rewrite with a reassuring
12
+ name, and it will be reviewed as if nothing could have broken.
13
+
14
+ Do them one at a time:
15
+
16
+ - **Refactor**, verify it still behaves identically, commit.
17
+ - **Then** change the behaviour, in its own commit.
18
+
19
+ Mixing the two produces a diff where nobody — including you next month — can
20
+ tell which lines were meant to change anything.
21
+
22
+ ## Small steps with a green check between each
23
+
24
+ The safety comes from the size of the step, not from care. Rename, run. Extract,
25
+ run. Move, run. If something breaks you know exactly what did it, because you
26
+ did exactly one thing.
27
+
28
+ ⚠️ A refactor that cannot be verified between steps is a rewrite. **Get a check
29
+ in place FIRST** — if the code has no test, the first commit is a test that
30
+ pins current behaviour, before you touch anything.
31
+
32
+ ⭐ And confirm that check can fail: break the code deliberately, see it go red,
33
+ restore. A green suite that would stay green through a mistake gives you
34
+ confidence you have not earned (`verify-your-own-work`).
35
+
36
+ ## ⚠️ The rewrite trap
37
+
38
+ "This is a mess, I will just rewrite it" loses everything the mess encodes: the
39
+ edge cases, the bug fixes, the reason for the strange branch on line 40 that
40
+ turns out to be a customer's data. The ugly code has been in production; the
41
+ clean replacement has not.
42
+
43
+ Rewrite when the requirements genuinely changed. Refactor when the code is hard
44
+ to work with. They are different problems and only one is solved by starting
45
+ over.
46
+
47
+ ## What to do first
48
+
49
+ 1. **Rename** to what things actually are. Free, reversible, and often the
50
+ whole problem — a lot of "confusing code" is code with lying names.
51
+ 2. **Extract** a well-named function from a comment that says what the next ten
52
+ lines do.
53
+ 3. **Delete** what nothing calls. Dead code is read, maintained and believed by
54
+ everyone who comes after.
55
+ 4. **Split** only once the seams are obvious. Splitting early puts boundaries in
56
+ the wrong places, and a wrong boundary is harder to remove than no boundary.
57
+
58
+ ## Duplication is cheaper than the wrong abstraction
59
+
60
+ Two similar blocks that evolve differently are fine. One "shared" helper with
61
+ four boolean flags to serve four callers is worse than the duplication it
62
+ replaced. **Wait for the third occurrence** before generalising — by then you
63
+ can see what is genuinely common.
64
+
65
+ ## Leave the campsite tidy, not rebuilt
66
+
67
+ Touching a file is a good moment to fix the name you had to squint at. It is not
68
+ a good moment to restructure the module — that lands in someone's review of an
69
+ unrelated change, and it is where "small fix" becomes a 400-line diff nobody can
70
+ approve with confidence.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: security-basics
3
+ description: The handful of vulnerabilities that actually occur — XSS, injection, secrets, and safe defaults
4
+ when: When handling user input, rendering it back, calling a database, or touching a credential
5
+ ---
6
+
7
+ # Security basics
8
+
9
+ Not the whole OWASP list — the few that actually turn up in application code.
10
+
11
+ ## ⚠️⚠️ XSS: never build HTML by concatenating input
12
+
13
+ ```js
14
+ ✗ el.innerHTML = `<p>Hello ${name}</p>`; // name = "<img src=x onerror=alert(1)>"
15
+ ✓ el.textContent = `Hello ${name}`;
16
+ ✓ el.append(document.createTextNode(name));
17
+ ```
18
+
19
+ `textContent` cannot execute anything. Reach for `innerHTML` only with a string
20
+ you built entirely yourself, and treat every alternative as better.
21
+
22
+ Also XSS, and easier to miss:
23
+
24
+ ```js
25
+ ✗ <a href={userSupplied}> // "javascript:…" runs on click
26
+ ✗ el.setAttribute('onclick', …)
27
+ ✗ eval(…) / new Function(userInput) / setTimeout("string")
28
+ ```
29
+
30
+ For a URL, check the scheme against an allowlist (`https:`, `http:`, `mailto:`)
31
+ rather than looking for bad ones.
32
+
33
+ ## Injection: the query is code, the input is data
34
+
35
+ ```js
36
+ ✗ db.query(`SELECT * FROM users WHERE email = '${email}'`)
37
+ ✓ db.query('SELECT * FROM users WHERE email = $1', [email])
38
+ ```
39
+
40
+ Parameterise. Escaping by hand fails on the case you did not think of. The same
41
+ rule applies to shell commands: pass an argument array, never build a string.
42
+
43
+ ## ⚠️⚠️ Secrets
44
+
45
+ - Never in client code. Anything shipped to a browser is public — a "hidden"
46
+ API key in a bundle is a published API key.
47
+ - Never in the repository. `.env` is gitignored; `.env.example` holds the names
48
+ with no values.
49
+ - Never in a log line, an error message, or a URL (URLs land in history and in
50
+ access logs).
51
+ - A leaked key is rotated, not deleted from the history — assume it was read.
52
+
53
+ ## CSRF
54
+
55
+ A cookie-authenticated state-changing request needs more than the cookie, since
56
+ the browser attaches it automatically from any origin. `SameSite=Lax` covers
57
+ most of it; add a token for the rest. `GET` must never change anything.
58
+
59
+ ## Validate on the server, always
60
+
61
+ The client's validation is for the user's benefit. Re-check types, ranges,
62
+ lengths and permissions on arrival — every value, every time, including ones a
63
+ dropdown "could only" have produced.
64
+
65
+ ## Safe defaults
66
+
67
+ - **Deny by default.** New endpoint, no explicit permission check → it should
68
+ fail closed, not open.
69
+ - **Least privilege.** The database user for the app does not need `DROP`.
70
+ - **Do not tell an attacker why.** "Email or password is incorrect" — not
71
+ "no such user", which turns your login form into a list of who has an account.
72
+
73
+ ## ⚠️ Dependencies are your code
74
+
75
+ A package you added runs with your permissions. Prefer fewer, prefer maintained,
76
+ and read what a postinstall script does before it runs on your machine.
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: state-management
3
+ description: One source of truth, derive the rest, and knowing when local state stops being enough
4
+ when: When adding state, when two components disagree, or before reaching for a state library
5
+ ---
6
+
7
+ # State
8
+
9
+ ## ⚠️⚠️ One source of truth. Derive everything else.
10
+
11
+ The bug where two parts of the screen disagree is almost always two copies of
12
+ one fact.
13
+
14
+ ```js
15
+ ✗ let items = [...]; let itemCount = 0; // now they can differ, and they will
16
+ ✓ let items = [...]; const count = items.length; // derived, cannot drift
17
+ ```
18
+
19
+ If a value can be computed from another value, **compute it**. Store only what
20
+ cannot be derived. A cached derived value is a second source of truth wearing a
21
+ disguise.
22
+
23
+ ## Where state should live
24
+
25
+ In order — take the first that works:
26
+
27
+ 1. **In the component that uses it.** Most state is local and should stay there.
28
+ 2. **In the nearest common parent**, when two siblings need it.
29
+ 3. **In the URL**, when it should survive a refresh or be shareable — filters,
30
+ the current tab, a search query, pagination. This is the most under-used
31
+ option and it is free.
32
+ 4. **On the server**, when it is data rather than interface state.
33
+ 5. **In a global store**, only when many distant components genuinely need it.
34
+
35
+ ⚠️ Reaching for a global store first makes every piece of state everyone's
36
+ business, and nothing can then be changed locally with confidence.
37
+
38
+ ## Server data is not UI state
39
+
40
+ Data fetched from an API has its own concerns — loading, error, stale, refetch,
41
+ cache — and modelling it as plain state means re-implementing all of them badly.
42
+ Keep it separate from interface state (which panel is open, what is typed).
43
+
44
+ ⭐ And remember the third outcome: a request is **loading**, **failed**, or
45
+ **succeeded**. A component modelling only "have data / no data" shows an empty
46
+ state during loading and after a failure, which are three different things
47
+ flattened into one. See `error-handling`.
48
+
49
+ ## Never mutate what you are about to compare
50
+
51
+ ```js
52
+ ✗ state.items.push(x); // same reference — a change detector sees nothing
53
+ ✓ state = { ...state, items: [...state.items, x] };
54
+ ```
55
+
56
+ This is the cause of "the data is right but the screen did not update".
57
+
58
+ ## ⚠️ Do not put derived data in the URL or in storage
59
+
60
+ Persist the minimum: an id, not the whole object it points at; a filter, not the
61
+ filtered result. Anything stored is a copy that can go stale, and stored copies
62
+ outlive the code that wrote them — `localStorage` from a version you shipped
63
+ last year will be handed to today's code.
64
+
65
+ Validate anything read back from storage. It is untrusted input: the user can
66
+ edit it, and an old version of your app may have written it.
67
+
68
+ ## Keep updates close to the event
69
+
70
+ State that changes in five places for one user action is state nobody can
71
+ follow. One action → one update → the screen follows from the new state. If you
72
+ cannot say what a click changes in a sentence, the shape is wrong, not the
73
+ library.
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: supabase-multitenant
3
+ description: Supabase with real multi-tenant safety — RLS, service role, and the leaks that look like working code
4
+ when: Reading or writing any database table, or adding a migration
5
+ ---
6
+
7
+ # Supabase, multi-tenant
8
+
9
+ ## The two clients, and why mixing them leaks data
10
+
11
+ - **Anon / user client** — respects Row Level Security. Use for anything a user
12
+ triggers about their own data.
13
+ - **Service role** — BYPASSES RLS entirely. Use only in operator/admin paths,
14
+ and never where a tenant id came from the request.
15
+
16
+ ⚠️⚠️ **The classic leak looks like working code:**
17
+ ```ts
18
+ // WRONG — the client chose whose data to read
19
+ const { data } = await admin.from('invoices').select('*').eq('tenant_id', body.tenantId);
20
+ ```
21
+ Derive the tenant from the session, never from the payload.
22
+
23
+ ## RLS: a GRANT and a POLICY are different things
24
+
25
+ - `GRANT` missing → `42501 permission denied`
26
+ - `POLICY` missing → **zero rows, no error**
27
+
28
+ ⚠️ The second is the dangerous one: it renders as an innocent empty state. If a
29
+ table "has no rows" and you are sure it should, check the policy before the code.
30
+
31
+ ```sql
32
+ alter table app.invoices enable row level security;
33
+ grant select on app.invoices to authenticated;
34
+ create policy invoices_own on app.invoices
35
+ for select to authenticated
36
+ using (tenant_id in (select tenant_id from app.tenant_users where user_id = auth.uid()));
37
+ ```
38
+
39
+ ⚠️ **A VIEW does not inherit the underlying table's RLS** — it runs with the
40
+ definer's rights. A view over a tenant table is a leak unless it filters
41
+ explicitly or is operator-only.
42
+
43
+ ## Reading honestly
44
+
45
+ A failed read and an empty result are DIFFERENT FACTS and must render
46
+ differently. Never `?? []`:
47
+
48
+ ```ts
49
+ const { data, error } = await supabase.from('x').select('*');
50
+ if (error) return { rows: [], unreadable: error.message }; // "we could not read"
51
+ return { rows: data ?? [], unreadable: null }; // "there is nothing"
52
+ ```
53
+
54
+ ## Counting and caps
55
+
56
+ `.limit(n)` without a count silently turns a partial sample into a confident
57
+ total. Ask for the count and say when you hit the cap:
58
+ ```ts
59
+ const { data, count } = await supabase.from('x').select('*', { count: 'exact' }).limit(5000);
60
+ const truncated = (count ?? 0) > (data?.length ?? 0);
61
+ ```
62
+ ⭐ The real fix at scale is aggregating in SQL (a view with `sum()`/`group by`)
63
+ so the row count stops mattering. Raising the limit only moves the cliff.
64
+
65
+ ## Migrations
66
+
67
+ - Number them sequentially and **check the highest number on disk first** — two
68
+ files claiming one number breaks whoever applies them next.
69
+ - Idempotent always: `add column if not exists`, `create index if not exists`.
70
+ - A `GENERATED` column cannot be written to. If code needs to insert it, it must
71
+ be a plain column.
72
+ - Add the `GRANT` and the `POLICY` in the same migration as the table.
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: typescript-strict
3
+ description: The types that catch real bugs versus the ones that are ceremony — and why `any` costs more than it saves
4
+ when: When adding types, when tempted to write `any` or a cast, or when configuring tsconfig
5
+ ---
6
+
7
+ # TypeScript
8
+
9
+ ## Turn `strict` on. It is where the value is.
10
+
11
+ ```json
12
+ { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } }
13
+ ```
14
+
15
+ Without `strict`, `null` and `undefined` are assignable to everything and the
16
+ compiler cannot catch the single most common runtime error there is. Without
17
+ `noUncheckedIndexedAccess`, `arr[10]` is typed as present when it is not.
18
+
19
+ ## ⚠️⚠️ `any` does not silence one error, it disables checking downstream
20
+
21
+ ```ts
22
+ ✗ const data: any = await res.json();
23
+ data.user.name // no error here, and no error anywhere it flows
24
+ ✓ const data: unknown = await res.json();
25
+ // now you MUST narrow it — which is the check you actually wanted
26
+ ```
27
+
28
+ `unknown` is the honest version of `any`. It says "we do not know yet" and
29
+ forces exactly one narrowing at the boundary, instead of letting a wrong shape
30
+ travel silently through ten functions.
31
+
32
+ The same applies to `as`. A cast is you telling the compiler to stop checking:
33
+
34
+ ```ts
35
+ ✗ const user = json as User; // asserted, never verified
36
+ ✓ if (!isUser(json)) throw new Error('unexpected response shape');
37
+ ```
38
+
39
+ ## Types that catch bugs
40
+
41
+ **Make illegal states unrepresentable.** This is the highest-value thing types
42
+ do:
43
+
44
+ ```ts
45
+ ✗ { loading: boolean; error?: string; data?: Invoice[] }
46
+ // loading AND error AND data — eight states, five of them nonsense
47
+ ✓ | { status: 'loading' }
48
+ | { status: 'error'; error: string }
49
+ | { status: 'ready'; data: Invoice[] }
50
+ ```
51
+
52
+ Now "loading with an error" cannot be written, and every consumer is forced to
53
+ handle all three — which is the `error-handling` rule (empty ≠ failed ≠ never
54
+ ran) enforced by the compiler.
55
+
56
+ **Distinguish things that are both strings:**
57
+
58
+ ```ts
59
+ type TenantId = string & { readonly __brand: 'TenantId' };
60
+ ```
61
+
62
+ Passing a `UserId` where a `TenantId` belongs is a multi-tenant data leak, and
63
+ it is a compile error rather than an incident.
64
+
65
+ ## Ceremony to skip
66
+
67
+ - Annotating what is obvious: `const n: number = 5`. Inference is not weaker
68
+ typing — it is the same type, without the noise.
69
+ - An `interface` per function argument list used once.
70
+ - `Promise<void>` return annotations on every async function.
71
+ - Enums where a union of string literals reads better and needs no runtime.
72
+
73
+ ⭐ Type the BOUNDARIES — function signatures, exported values, anything crossing
74
+ the network — and let inference handle the inside.
75
+
76
+ ## ⚠️ Types are erased, so they are not validation
77
+
78
+ A `User` type proves nothing about what the API actually sent; it is a comment
79
+ the compiler checks *your* code against. Data arriving from outside — a
80
+ response, `localStorage`, a query param — must be checked at runtime, and only
81
+ then does its type mean anything (`security-basics`).
82
+
83
+ ## `satisfies`, when you want both
84
+
85
+ ```ts
86
+ const config = { port: 3000 } satisfies Config; // checked AND still literal
87
+ ```
88
+
89
+ Checks the shape without widening the type, which is what `as Config` would
90
+ have done while also disabling the check.