@agentvibes/guardrails 0.1.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 (260) hide show
  1. package/README.md +239 -0
  2. package/candidates/README.md +30 -0
  3. package/candidates/r10-as-cast.yml +24 -0
  4. package/candidates/r1d-scattered-numeric-const-in-component.yml +13 -0
  5. package/candidates/r2-helper-fn-in-tsx.yml +22 -0
  6. package/candidates/r3-hooks-outside-layout.yml +21 -0
  7. package/candidates/r6-jsx-comment-smell.yml +14 -0
  8. package/candidates/r7-raw-hsl-outside-tokens.yml +17 -0
  9. package/candidates/r8-inline-style-prop.yml +13 -0
  10. package/candidates/r9-optional-store-arg.yml +24 -0
  11. package/configs/biome.json +96 -0
  12. package/configs/tsconfig.base.json +21 -0
  13. package/dist/astGrep.d.ts +16 -0
  14. package/dist/astGrep.js +48 -0
  15. package/dist/astGrep.js.map +1 -0
  16. package/dist/cli.d.ts +2 -0
  17. package/dist/cli.js +138 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/deploy.d.ts +1 -0
  20. package/dist/deploy.js +34 -0
  21. package/dist/deploy.js.map +1 -0
  22. package/dist/doctor.d.ts +6 -0
  23. package/dist/doctor.js +102 -0
  24. package/dist/doctor.js.map +1 -0
  25. package/dist/fileWalk.d.ts +2 -0
  26. package/dist/fileWalk.js +45 -0
  27. package/dist/fileWalk.js.map +1 -0
  28. package/dist/findings.d.ts +12 -0
  29. package/dist/findings.js +8 -0
  30. package/dist/findings.js.map +1 -0
  31. package/dist/gateRed.d.ts +1 -0
  32. package/dist/gateRed.js +93 -0
  33. package/dist/gateRed.js.map +1 -0
  34. package/dist/gitDiff.d.ts +18 -0
  35. package/dist/gitDiff.js +90 -0
  36. package/dist/gitDiff.js.map +1 -0
  37. package/dist/hookPostedit.d.ts +1 -0
  38. package/dist/hookPostedit.js +117 -0
  39. package/dist/hookPostedit.js.map +1 -0
  40. package/dist/hookStop.d.ts +1 -0
  41. package/dist/hookStop.js +56 -0
  42. package/dist/hookStop.js.map +1 -0
  43. package/dist/hookStopTest.d.ts +1 -0
  44. package/dist/hookStopTest.js +71 -0
  45. package/dist/hookStopTest.js.map +1 -0
  46. package/dist/init.d.ts +24 -0
  47. package/dist/init.js +172 -0
  48. package/dist/init.js.map +1 -0
  49. package/dist/initTest.d.ts +1 -0
  50. package/dist/initTest.js +88 -0
  51. package/dist/initTest.js.map +1 -0
  52. package/dist/iterate.d.ts +9 -0
  53. package/dist/iterate.js +79 -0
  54. package/dist/iterate.js.map +1 -0
  55. package/dist/iterateTest.d.ts +1 -0
  56. package/dist/iterateTest.js +91 -0
  57. package/dist/iterateTest.js.map +1 -0
  58. package/dist/leakPatterns.d.ts +5 -0
  59. package/dist/leakPatterns.js +22 -0
  60. package/dist/leakPatterns.js.map +1 -0
  61. package/dist/leaks.d.ts +8 -0
  62. package/dist/leaks.js +211 -0
  63. package/dist/leaks.js.map +1 -0
  64. package/dist/leaksRed.d.ts +1 -0
  65. package/dist/leaksRed.js +62 -0
  66. package/dist/leaksRed.js.map +1 -0
  67. package/dist/metrics.d.ts +10 -0
  68. package/dist/metrics.js +113 -0
  69. package/dist/metrics.js.map +1 -0
  70. package/dist/metricsAggregate.d.ts +5 -0
  71. package/dist/metricsAggregate.js +78 -0
  72. package/dist/metricsAggregate.js.map +1 -0
  73. package/dist/metricsBaseline.d.ts +21 -0
  74. package/dist/metricsBaseline.js +88 -0
  75. package/dist/metricsBaseline.js.map +1 -0
  76. package/dist/metricsCollect.d.ts +8 -0
  77. package/dist/metricsCollect.js +295 -0
  78. package/dist/metricsCollect.js.map +1 -0
  79. package/dist/metricsCycle.d.ts +1 -0
  80. package/dist/metricsCycle.js +79 -0
  81. package/dist/metricsCycle.js.map +1 -0
  82. package/dist/metricsTypes.d.ts +102 -0
  83. package/dist/metricsTypes.js +2 -0
  84. package/dist/metricsTypes.js.map +1 -0
  85. package/dist/packagePaths.d.ts +7 -0
  86. package/dist/packagePaths.js +11 -0
  87. package/dist/packagePaths.js.map +1 -0
  88. package/dist/pluginApi.d.ts +24 -0
  89. package/dist/pluginApi.js +12 -0
  90. package/dist/pluginApi.js.map +1 -0
  91. package/dist/pluginResolve.d.ts +6 -0
  92. package/dist/pluginResolve.js +48 -0
  93. package/dist/pluginResolve.js.map +1 -0
  94. package/dist/screenScope.d.ts +21 -0
  95. package/dist/screenScope.js +81 -0
  96. package/dist/screenScope.js.map +1 -0
  97. package/dist/screenScopeTest.d.ts +1 -0
  98. package/dist/screenScopeTest.js +116 -0
  99. package/dist/screenScopeTest.js.map +1 -0
  100. package/dist/severity.d.ts +9 -0
  101. package/dist/severity.js +62 -0
  102. package/dist/severity.js.map +1 -0
  103. package/dist/severityTest.d.ts +1 -0
  104. package/dist/severityTest.js +63 -0
  105. package/dist/severityTest.js.map +1 -0
  106. package/dist/sourceLines.d.ts +9 -0
  107. package/dist/sourceLines.js +52 -0
  108. package/dist/sourceLines.js.map +1 -0
  109. package/dist/structure.d.ts +2 -0
  110. package/dist/structure.js +98 -0
  111. package/dist/structure.js.map +1 -0
  112. package/dist/testRules.d.ts +1 -0
  113. package/dist/testRules.js +285 -0
  114. package/dist/testRules.js.map +1 -0
  115. package/dist/textGrep.d.ts +2 -0
  116. package/dist/textGrep.js +52 -0
  117. package/dist/textGrep.js.map +1 -0
  118. package/dist/tomlTable.d.ts +2 -0
  119. package/dist/tomlTable.js +26 -0
  120. package/dist/tomlTable.js.map +1 -0
  121. package/dist/toolResolve.d.ts +18 -0
  122. package/dist/toolResolve.js +50 -0
  123. package/dist/toolResolve.js.map +1 -0
  124. package/dist/verify.d.ts +3 -0
  125. package/dist/verify.js +49 -0
  126. package/dist/verify.js.map +1 -0
  127. package/dist/verifyDiff.d.ts +18 -0
  128. package/dist/verifyDiff.js +126 -0
  129. package/dist/verifyDiff.js.map +1 -0
  130. package/dist/workspaces.d.ts +6 -0
  131. package/dist/workspaces.js +84 -0
  132. package/dist/workspaces.js.map +1 -0
  133. package/mise.toml +9 -0
  134. package/package.json +62 -0
  135. package/rules/__fixtures__/as-any-escape/bad.ts +8 -0
  136. package/rules/__fixtures__/as-any-escape/good.ts +14 -0
  137. package/rules/__fixtures__/catch-empty/bad.ts +7 -0
  138. package/rules/__fixtures__/catch-empty/good.ts +14 -0
  139. package/rules/__fixtures__/direct-store-import/bad.tsx +23 -0
  140. package/rules/__fixtures__/direct-store-import/good.tsx +34 -0
  141. package/rules/__fixtures__/direct-store-import/showcase/ignored.tsx +9 -0
  142. package/rules/__fixtures__/discriminator-ternary/bad.tsx +10 -0
  143. package/rules/__fixtures__/discriminator-ternary/good.tsx +23 -0
  144. package/rules/__fixtures__/hardcoded-url-in-component/components/bad.tsx +8 -0
  145. package/rules/__fixtures__/hardcoded-url-in-component/components/good.tsx +14 -0
  146. package/rules/__fixtures__/inline-map-row/bad.tsx +72 -0
  147. package/rules/__fixtures__/inline-map-row/good.tsx +51 -0
  148. package/rules/__fixtures__/instanceof-map-set/bad.ts +7 -0
  149. package/rules/__fixtures__/instanceof-map-set/good.ts +8 -0
  150. package/rules/__fixtures__/literal-union-in-component/components/bad.ts +8 -0
  151. package/rules/__fixtures__/literal-union-in-component/components/good.ts +23 -0
  152. package/rules/__fixtures__/missing-observer/bad.tsx +66 -0
  153. package/rules/__fixtures__/missing-observer/good.tsx +69 -0
  154. package/rules/__fixtures__/no-local-kit-clone/bad.ts +30 -0
  155. package/rules/__fixtures__/no-local-kit-clone/bad.tsx +30 -0
  156. package/rules/__fixtures__/no-local-kit-clone/good.ts +31 -0
  157. package/rules/__fixtures__/no-local-kit-clone/good.tsx +31 -0
  158. package/rules/__fixtures__/non-exhaustive-match/bad.ts +8 -0
  159. package/rules/__fixtures__/non-exhaustive-match/good.ts +14 -0
  160. package/rules/__fixtures__/page-store-bare-usememo/bad.ts +15 -0
  161. package/rules/__fixtures__/page-store-bare-usememo/bad.tsx +13 -0
  162. package/rules/__fixtures__/page-store-bare-usememo/good.ts +17 -0
  163. package/rules/__fixtures__/page-store-bare-usememo/good.tsx +14 -0
  164. package/rules/__fixtures__/screen-file-styling/bad.screen.tsx +10 -0
  165. package/rules/__fixtures__/screen-file-styling/good.screen.tsx +8 -0
  166. package/rules/__fixtures__/state-loading-boolean-shape/bad.ts +36 -0
  167. package/rules/__fixtures__/state-loading-boolean-shape/bad.tsx +36 -0
  168. package/rules/__fixtures__/state-loading-boolean-shape/good.ts +44 -0
  169. package/rules/__fixtures__/state-loading-boolean-shape/good.tsx +44 -0
  170. package/rules/__fixtures__/store-async-method/bad.ts +37 -0
  171. package/rules/__fixtures__/store-async-method/bad.tsx +37 -0
  172. package/rules/__fixtures__/store-async-method/good.ts +73 -0
  173. package/rules/__fixtures__/store-async-method/good.tsx +73 -0
  174. package/rules/__fixtures__/store-context-provider/bad.ts +16 -0
  175. package/rules/__fixtures__/store-context-provider/bad.tsx +24 -0
  176. package/rules/__fixtures__/store-context-provider/good.ts +13 -0
  177. package/rules/__fixtures__/store-context-provider/good.tsx +11 -0
  178. package/rules/__fixtures__/store-delegation-getter/bad.ts +30 -0
  179. package/rules/__fixtures__/store-delegation-getter/bad.tsx +30 -0
  180. package/rules/__fixtures__/store-delegation-getter/good.ts +65 -0
  181. package/rules/__fixtures__/store-delegation-getter/good.tsx +65 -0
  182. package/rules/__fixtures__/store-new-map/bad.ts +22 -0
  183. package/rules/__fixtures__/store-new-map/bad.tsx +22 -0
  184. package/rules/__fixtures__/store-new-map/good.ts +41 -0
  185. package/rules/__fixtures__/store-new-map/good.tsx +41 -0
  186. package/rules/__fixtures__/store-no-runinaction/bad.ts +67 -0
  187. package/rules/__fixtures__/store-no-runinaction/bad.tsx +67 -0
  188. package/rules/__fixtures__/store-no-runinaction/good.ts +39 -0
  189. package/rules/__fixtures__/store-no-runinaction/good.tsx +39 -0
  190. package/rules/__fixtures__/store-no-setinterval-poll/bad.ts +19 -0
  191. package/rules/__fixtures__/store-no-setinterval-poll/bad.tsx +19 -0
  192. package/rules/__fixtures__/store-no-setinterval-poll/good.ts +29 -0
  193. package/rules/__fixtures__/store-no-setinterval-poll/good.tsx +29 -0
  194. package/rules/__fixtures__/ts-pattern-dangling-match/bad.ts +20 -0
  195. package/rules/__fixtures__/ts-pattern-dangling-match/good.ts +35 -0
  196. package/rules/__fixtures__/ui-imports-app-store/src/ui/bad.tsx +13 -0
  197. package/rules/__fixtures__/ui-imports-app-store/src/ui/good.tsx +23 -0
  198. package/rules/__fixtures__/view-file-logic/bad.view.tsx +16 -0
  199. package/rules/__fixtures__/view-file-logic/good.view.tsx +7 -0
  200. package/rules/__fixtures__/view-imports-store/bad.view.tsx +13 -0
  201. package/rules/__fixtures__/view-imports-store/good.view.tsx +23 -0
  202. package/rules/__fixtures__/zod-optional-nullable/bad.ts +10 -0
  203. package/rules/__fixtures__/zod-optional-nullable/bad.tsx +17 -0
  204. package/rules/__fixtures__/zod-optional-nullable/good.ts +16 -0
  205. package/rules/__fixtures__/zod-optional-nullable/good.tsx +20 -0
  206. package/rules/as-any-escape-tsx.yml +53 -0
  207. package/rules/as-any-escape.yml +29 -0
  208. package/rules/catch-empty-tsx.yml +55 -0
  209. package/rules/catch-empty.yml +31 -0
  210. package/rules/classname-not-composed.yml +28 -0
  211. package/rules/classname-visual-identity.yml +34 -0
  212. package/rules/demo-mode-by-default-ts.yml +76 -0
  213. package/rules/demo-mode-by-default.yml +54 -0
  214. package/rules/direct-store-import.yml +118 -0
  215. package/rules/discriminator-ternary.yml +70 -0
  216. package/rules/hardcoded-url-in-component.yml +44 -0
  217. package/rules/inline-map-row.yml +107 -0
  218. package/rules/instanceof-map-set-tsx.yml +46 -0
  219. package/rules/instanceof-map-set.yml +22 -0
  220. package/rules/json-roundtrip.yml +9 -0
  221. package/rules/jsx-cond-and.yml +16 -0
  222. package/rules/jsx-ternary.yml +35 -0
  223. package/rules/kind-if-without-match.yml +13 -0
  224. package/rules/literal-union-in-component.yml +71 -0
  225. package/rules/match-bool-to-null.yml +15 -0
  226. package/rules/missing-observer.yml +189 -0
  227. package/rules/mobx-effect-observable-dep.yml +23 -0
  228. package/rules/mobx-effect-store-write.yml +21 -0
  229. package/rules/mobx-usestate-from-store.yml +20 -0
  230. package/rules/no-local-kit-clone-tsx.yml +104 -0
  231. package/rules/no-local-kit-clone.yml +93 -0
  232. package/rules/non-exhaustive-match.yml +34 -0
  233. package/rules/page-store-bare-usememo-tsx.yml +47 -0
  234. package/rules/page-store-bare-usememo.yml +43 -0
  235. package/rules/screen-file-styling.yml +29 -0
  236. package/rules/silent-default-return.yml +17 -0
  237. package/rules/state-loading-boolean-shape-tsx.yml +102 -0
  238. package/rules/state-loading-boolean-shape.yml +91 -0
  239. package/rules/store-async-method-tsx.yml +108 -0
  240. package/rules/store-async-method.yml +98 -0
  241. package/rules/store-context-provider-tsx.yml +68 -0
  242. package/rules/store-context-provider.yml +78 -0
  243. package/rules/store-delegation-getter-tsx.yml +104 -0
  244. package/rules/store-delegation-getter.yml +93 -0
  245. package/rules/store-new-map-tsx.yml +84 -0
  246. package/rules/store-new-map.yml +73 -0
  247. package/rules/store-no-runinaction-tsx.yml +71 -0
  248. package/rules/store-no-runinaction.yml +60 -0
  249. package/rules/store-no-setinterval-poll-tsx.yml +62 -0
  250. package/rules/store-no-setinterval-poll.yml +51 -0
  251. package/rules/ts-pattern-dangling-match-tsx.yml +41 -0
  252. package/rules/ts-pattern-dangling-match.yml +63 -0
  253. package/rules/ui-imports-app-store.yml +49 -0
  254. package/rules/view-file-logic.yml +53 -0
  255. package/rules/view-imports-store.yml +64 -0
  256. package/rules/zod-optional-nullable-tsx.yml +56 -0
  257. package/rules/zod-optional-nullable.yml +29 -0
  258. package/sgconfig.yml +8 -0
  259. package/structure/rules/component-decl.yml +48 -0
  260. package/structure/sgconfig.yml +7 -0
@@ -0,0 +1,34 @@
1
+ // NOTHING in this file may be reported by `direct-store-import`.
2
+
3
+ // D1: the global rootStore imported at a screen is legal.
4
+ import { rootStore } from "../../stores/rootStore"
5
+
6
+ // Type-only imports are the view-model contract (C40), not a store dependency.
7
+ import type { IGalleryModel } from "../../stores/galleryStore"
8
+ import type { RootStore } from "../../stores/rootStore"
9
+ import type Photo from "../../stores/photo"
10
+ import type * as StoreTypes from "../../stores/types"
11
+ import { type IUiModel, type ITabModel } from "../../stores/uiStore"
12
+
13
+ // A mixed import: a real value constant alongside an inline-typed store name.
14
+ // The statement is not type-only, but the store binding in it still is.
15
+ import { NOTIFICATION_KINDS, type NotificationSettingsStore } from "../../stores/NotificationSettingsStore"
16
+
17
+ // The sanctioned access hooks are not store instances.
18
+ import { useStore } from "../../stores/StoreContext"
19
+ import { usePageStore } from "../../stores/usePageStore"
20
+
21
+ // Non-store bindings from a stores path are untouched — the rule keys on the
22
+ // binding name, not on the directory alone.
23
+ import { StoreProvider } from "../../stores/StoreContext"
24
+ import { SESSION_KEY } from "../../stores/keys"
25
+
26
+ // Imports from paths that are not a stores directory.
27
+ import { Restore } from "../../components/Restore/Restore"
28
+ import { formatBytes } from "../../lib/format"
29
+
30
+ export const X = ({ model }: { model: IGalleryModel }) => (
31
+ <StoreProvider value={rootStore}>
32
+ <Restore label={formatBytes(model.size)} k={SESSION_KEY} />
33
+ </StoreProvider>
34
+ )
@@ -0,0 +1,9 @@
1
+ // Pins the `ignores:` globs. These are the SAME imports bad.tsx is flagged for,
2
+ // under `showcase/` — a preview harness legitimately builds stores over
3
+ // fixtures (fake-data.md). If someone removes the ignore globs, this file
4
+ // starts reporting and the assertion below it fails, instead of the 21-hit
5
+ // false-positive class quietly coming back.
6
+ import { GalleryStore } from "../../stores/galleryStore"
7
+ import { galleryStore } from "../../stores/galleryStore"
8
+
9
+ export const Preview = () => <div>{GalleryStore.name}{galleryStore.count}</div>
@@ -0,0 +1,10 @@
1
+ // Every ternary here MUST be reported by `discriminator-ternary`.
2
+
3
+ // The terse option-unwrap the PPA audit found 18 of.
4
+ export const first = (m: Maybe<string[]>) => (m.kind === "some" ? m.value : [])
5
+ export const adapter = (s: S) => (s.adapter.kind === "some" ? s.adapter.value : "—")
6
+ // Other discriminator names the rule covers.
7
+ export const label = (r: R) => (r.status === "error" ? "Bad" : "Ok")
8
+ export const port = (c: C) => (c.type === "tcp" ? c.port : 0)
9
+ export const key = (p: P) => (p.provider === "openai" ? p.apiKey : p.token)
10
+ export const tone = (n: N) => (n.mode !== "dark" ? "light" : "dark")
@@ -0,0 +1,23 @@
1
+ // NOTHING in this file may be reported by `discriminator-ternary`.
2
+ import { match } from "ts-pattern"
3
+
4
+ // The fix: match the value, so a new variant is a build error.
5
+ export const first = (m: Maybe<string[]>) =>
6
+ match(m)
7
+ .with({ kind: "some" }, ({ value }) => value)
8
+ .with({ kind: "none" }, () => [])
9
+ .exhaustive()
10
+
11
+ // A ternary whose branches are JSX belongs to `jsx-ternary`, which owns that
12
+ // case with the same fix. This rule stands down so the two can never report the
13
+ // same line twice — measured overlap on tg-gallery + observatory: 0.
14
+ export const View = (r: R) => (r.status === "error" ? <ErrorState /> : <Ready />)
15
+ export const View2 = (r: R) => (r.kind === "idle" ? null : <Spinner />)
16
+
17
+ // Not a discriminator field.
18
+ export const n = (u: U) => (u.count === 0 ? "none" : "some")
19
+ export const s = (u: U) => (u.name === "root" ? 1 : 2)
20
+
21
+ // Not an equality test on a discriminator.
22
+ export const t = (u: U) => (u.ready ? "y" : "n")
23
+ export const v = (u: U) => (u.items.length > 3 ? "many" : "few")
@@ -0,0 +1,8 @@
1
+ // Every URL here MUST be reported by `hardcoded-url-in-component`.
2
+ const API = "https://api.example.com/v1"
3
+
4
+ export const Panel = () => {
5
+ const upload = "http://uploads.example.com/put"
6
+ const cdn = "https://cdn.example.com/assets"
7
+ return <a href="https://gallery.internal.example/g/abc">{API}{upload}{cdn}</a>
8
+ }
@@ -0,0 +1,14 @@
1
+ // NOTHING in this file may be reported by `hardcoded-url-in-component`.
2
+
3
+ // The fix: the URL comes from a constants module (or a store computed), so the
4
+ // component does not know which environment it is in.
5
+ import { API_BASE, GALLERY_URL } from "../../lib/constants/urls"
6
+
7
+ export const Panel = () => <a href={GALLERY_URL}>{API_BASE}</a>
8
+
9
+ // Relative paths carry no environment.
10
+ export const Link = () => <a href="/g/abc">gallery</a>
11
+
12
+ // Schema/namespace URLs are identifiers, not endpoints — the honest exception.
13
+ export const Icon = () => <svg xmlns="http://www.w3.org/2000/svg" />
14
+ export const SCHEMA = "https://json-schema.org/draft/2020-12/schema"
@@ -0,0 +1,72 @@
1
+ // Every `.map` in this file MUST be reported by `inline-map-row`.
2
+ //
3
+ // All of them sit inside `observer(...)`: that is the whole defect. The
4
+ // parent's reactive scope tracks every field every row body reads, so one
5
+ // item changing one field re-renders the entire list.
6
+
7
+ import { observer } from "mobx-react-lite"
8
+
9
+ // Arrow with an expression body.
10
+ export const A = observer(() => <ul>{xs.map((x) => <li key={x.id}><img src={x.src} /><span>{x.title}</span></li>)}</ul>)
11
+
12
+ // Arrow with a block body and an explicit return.
13
+ export const B = observer(() => (
14
+ <ul>
15
+ {xs.map((x) => {
16
+ return (
17
+ <li key={x.id}>
18
+ <img src={x.src} />
19
+ <span>{x.title}</span>
20
+ </li>
21
+ )
22
+ })}
23
+ </ul>
24
+ ))
25
+
26
+ // `function` expression callback.
27
+ export const C = observer(() => (
28
+ <ul>
29
+ {xs.map(function (x) {
30
+ return <li key={x.id}><img src={x.src} /><span>{x.title}</span></li>
31
+ })}
32
+ </ul>
33
+ ))
34
+
35
+ // Chained before `.map`, parenthesized body, index parameter.
36
+ export const D = observer(() => (
37
+ <ul>{xs.filter(Boolean).map((x, i) => (<li key={i}><b>{x.a}</b><i>{x.b}</i></li>))}</ul>
38
+ ))
39
+
40
+ // Deeply nested rather than sibling-nested — three levels of inline row body.
41
+ export const E = observer(() => (
42
+ <div>
43
+ {rows.map((r) => (
44
+ <div key={r.id}>
45
+ <header>
46
+ <h3>{r.title}</h3>
47
+ <time>{r.at}</time>
48
+ </header>
49
+ </div>
50
+ ))}
51
+ </div>
52
+ ))
53
+
54
+ // The real shape, with design-system tags rather than host elements:
55
+ // observatory's SessionsTreeSection with its extracted <StatusGroup/> inlined.
56
+ export const SessionsTreeSection = observer(() => {
57
+ const { data, ui } = useStore()
58
+ const byStatus = (s: SessionStatus) => data.sessions.filter((x) => x.status === s)
59
+ return (
60
+ <TreeSection sectionKey="section:sessions" onActivate={() => ui.openSessions("all")}>
61
+ {GROUP_ORDER.map((status) => (
62
+ <VStack key={status} gap={0}>
63
+ <HStack align="center">
64
+ <Text weight="bold">{sessionStatusLabel(status)}</Text>
65
+ <Badge count={byStatus(status).length} />
66
+ </HStack>
67
+ <Divider />
68
+ </VStack>
69
+ ))}
70
+ </TreeSection>
71
+ )
72
+ })
@@ -0,0 +1,51 @@
1
+ // NOTHING in this file may be reported by `inline-map-row`.
2
+
3
+ import { observer } from "mobx-react-lite"
4
+ import { SessionRow } from "./SessionRow/SessionRow"
5
+
6
+ // The target pattern: one extracted, self-closed row component per item.
7
+ export const A = observer(() => <ul>{xs.map((x) => <SessionRow key={x.id} item={x} />)}</ul>)
8
+
9
+ // An extracted row that takes children is still one component reference.
10
+ export const B = observer(() => <ul>{xs.map((x) => <SessionRow key={x.id}>{x.title}</SessionRow>)}</ul>)
11
+
12
+ // A single host element with only an expression inside is not a row body.
13
+ export const C = observer(() => <ul>{xs.map((x) => <li key={x.id}>{x.title}</li>)}</ul>)
14
+
15
+ // A thin wrapper around ONE extracted component. Deliberately not flagged: the
16
+ // rule needs two or more nested elements before it calls something a row body,
17
+ // so the wrapper costs a false positive at `error` severity rather than a fix.
18
+ export const D = observer(() => <ul>{xs.map((x) => <li key={x.id}><SessionRow item={x} /></li>)}</ul>)
19
+
20
+ // One nested element deep. Same reasoning as D.
21
+ export const E = observer(() => <ul>{xs.map((x) => <li key={x.id}><a href={x.href}>{x.title}</a></li>)}</ul>)
22
+
23
+ // Mapping to non-JSX values is untouched.
24
+ export const F = observer(() => <ul>{xs.map((x) => x.title).join(", ")}</ul>)
25
+
26
+ // A literal array — CLAUDE.md's "pure data" carve-out. A skeleton drawing four
27
+ // placeholder cards has no observable to subscribe to and no row to extract.
28
+ export const G = observer(() => (
29
+ <div>
30
+ {[1, 2, 3, 4].map((i) => (
31
+ <div key={i}>
32
+ <Skeleton width={80} />
33
+ <Skeleton width="60%" />
34
+ </div>
35
+ ))}
36
+ </div>
37
+ ))
38
+
39
+ // The same inline row body, in a component that is NOT an observer. There is no
40
+ // reactive scope to over-subscribe, so there is no defect of this kind — a
41
+ // plain UI library renders lists exactly like this and must stay silent.
42
+ export const H = () => (
43
+ <ul>
44
+ {items.map((it) => (
45
+ <li key={it.id}>
46
+ <img src={it.src} />
47
+ <span>{it.title}</span>
48
+ </li>
49
+ ))}
50
+ </ul>
51
+ )
@@ -0,0 +1,7 @@
1
+ // Every check here MUST be reported by `instanceof-map-set`.
2
+ export const a = (v: unknown) => v instanceof Map
3
+ export const b = (v: unknown) => v instanceof Set
4
+ // ADDED BY THE WAVE-2 UPGRADE: the old body named only Map and Set, and these
5
+ // two have exactly the same cross-realm failure.
6
+ export const c = (v: unknown) => v instanceof WeakMap
7
+ export const d = (v: unknown) => v instanceof WeakSet
@@ -0,0 +1,8 @@
1
+ // NOTHING in this file may be reported by `instanceof-map-set`.
2
+ // The fix: a structural check that survives a realm boundary (iframe, worker,
3
+ // vm context), where `instanceof` silently returns false.
4
+ export const isMapLike = (v: unknown): v is Map<unknown, unknown> =>
5
+ typeof v === "object" && v !== null && typeof (v as Map<unknown, unknown>).get === "function"
6
+
7
+ export const a = (v: unknown) => v instanceof Date
8
+ export const b = (v: unknown) => v instanceof Error
@@ -0,0 +1,8 @@
1
+ // Every type here MUST be reported by `literal-union-in-component`.
2
+ // Path matters: the rule is scoped to component/screen/app directories.
3
+
4
+ // Enum-style UI state redeclared in the file that renders it.
5
+ export type Tab = "tasks" | "habits" | "stream"
6
+ export type LoadPhase = "idle" | "loading" | "ready" | "error"
7
+ type Align = "start" | "center" | "end"
8
+ export type Tone = "neutral" | "danger" | "success" | "warning"
@@ -0,0 +1,23 @@
1
+ // NOTHING in this file may be reported by `literal-union-in-component`.
2
+
3
+ // The fix: consume the shared type, do not restate it.
4
+ import type { Tab } from "../../stores/uiStore"
5
+ import type { Tone } from "../../lib/tone"
6
+
7
+ export interface TabBarProps {
8
+ tab: Tab
9
+ tone: Tone
10
+ }
11
+
12
+ // A single-member alias is not an enum-style union.
13
+ export type Only = "one"
14
+
15
+ // A union of non-literals is a different thing entirely.
16
+ export type Id = string | number
17
+ export type Handler = (() => void) | undefined
18
+
19
+ // A discriminated union of OBJECT shapes is the pattern this codebase wants
20
+ // everywhere — it is not a bare string enum and must not be flagged.
21
+ export type Resource<T> =
22
+ | { status: "idle" }
23
+ | { status: "ready"; data: T }
@@ -0,0 +1,66 @@
1
+ // Every declaration in this file MUST be reported by `missing-observer`.
2
+
3
+ import { useStore } from "../../stores/StoreContext"
4
+
5
+ // Arrow component reading the store hook, no observer wrap — the canonical shape
6
+ // the previously-documented (never-implemented) rule could not match.
7
+ export const SessionsList = () => {
8
+ const { data, ui } = useStore()
9
+ return <div>{data.sessions.length} sessions, tab {ui.tab}</div>
10
+ }
11
+
12
+ // Bare call, no destructuring.
13
+ export const Header = () => {
14
+ const store = useStore()
15
+ return <header>{store.auth.userName}</header>
16
+ }
17
+
18
+ // function_declaration form.
19
+ export function Footer() {
20
+ const { ui } = useStore()
21
+ return <footer>{ui.statusText}</footer>
22
+ }
23
+
24
+ // A project-specific store hook (`use<Name>Store`), not just `useStore`.
25
+ export const GalleryGrid = () => {
26
+ const { photos } = useGalleryStore()
27
+ return <div>{photos.length}</div>
28
+ }
29
+
30
+ // Page store created on a screen — still needs the observer wrap.
31
+ export const GalleryScreen = () => {
32
+ const page = usePageStore(() => new GalleryPageStore(rootStore, "slug"), [])
33
+ return <section>{page.title}</section>
34
+ }
35
+
36
+ // The model-prop arm: takes `model`, dereferences it, never wrapped. This is
37
+ // the per-row extraction done right and then de-observed — the row re-renders
38
+ // only when the parent does.
39
+ export const SessionRow = ({ model }: { model: ISessionModel }) => {
40
+ return <li>{model.title}</li>
41
+ }
42
+
43
+ // Same for a prop named `store`.
44
+ export const TabBar = ({ store }: { store: IUiModel }) => {
45
+ return <nav>{store.activeTab}</nav>
46
+ }
47
+
48
+ // ── shapes taken from real components, not invented ────────────────────
49
+ // The store read happens at body level and never appears inside the JSX.
50
+ // tg-gallery's GalleryView does exactly this; an earlier "deref must be inside
51
+ // a jsx_expression" version of this rule missed it completely.
52
+ export const GalleryView = function GalleryView() {
53
+ const { galleryStore, themeStore } = useStore()
54
+ const layoutType = themeStore.layout.type
55
+ return match(galleryStore.detailState)
56
+ .with({ status: "loading" }, () => <GalleryViewSkeleton />)
57
+ .with({ status: "loaded" }, ({ gallery }) => <GalleryLoadedView gallery={gallery} layoutType={layoutType} />)
58
+ .exhaustive()
59
+ }
60
+
61
+ // Read straight into a local derivation — observatory's SessionsTreeSection.
62
+ export const SessionsTreeSection = () => {
63
+ const { data, ui } = useStore()
64
+ const matched = data.sessions.filter((s) => sessionMatches(s, ui.searchQuery))
65
+ return <TreeSection count={matched.length}>{null}</TreeSection>
66
+ }
@@ -0,0 +1,69 @@
1
+ // NOTHING in this file may be reported by `missing-observer`.
2
+
3
+ import { observer } from "mobx-react-lite"
4
+ import { useStore } from "../../stores/StoreContext"
5
+
6
+ // The canonical wrapped shape.
7
+ export const SessionsList = observer(() => {
8
+ const { data, ui } = useStore()
9
+ return <div>{data.sessions.length} sessions, tab {ui.tab}</div>
10
+ })
11
+
12
+ // Wrapped named function expression — the shape tg-gallery uses.
13
+ export const GalleryEmptyStateView = observer(function GalleryEmptyStateView({
14
+ title,
15
+ }: {
16
+ title: string
17
+ }) {
18
+ const { platform } = useStore()
19
+ return <p>{title} on {platform.name}</p>
20
+ })
21
+
22
+ // Destructured props, wrapped.
23
+ export const SessionRow = observer(({ model }: { model: ISessionModel }) => {
24
+ return <li>{model.title}</li>
25
+ })
26
+
27
+ // A custom hook that reads the store: hooks are not components, the CONSUMER
28
+ // carries the observer wrap. camelCase name keeps it out of the rule.
29
+ export const useSessionTitle = (id: string) => {
30
+ const { data } = useStore()
31
+ return data.sessionById(id)?.title ?? ""
32
+ }
33
+
34
+ // Same, as a function declaration.
35
+ export function useTabLabel() {
36
+ const { ui } = useStore()
37
+ return ui.tab
38
+ }
39
+
40
+ // A presentational component with no store contact at all — observer is not
41
+ // required and adding it would be noise.
42
+ export const Badge = ({ label }: { label: string }) => <span>{label}</span>
43
+
44
+ // A non-component helper that happens to be PascalCase but renders no JSX.
45
+ export const StoreKeys = () => {
46
+ const { data } = useStore()
47
+ return Object.keys(data)
48
+ }
49
+
50
+ // A `model` prop that is never dereferenced as `model.x` — no observable read.
51
+ export const ModelDebugLabel = ({ model }: { model: unknown }) => (
52
+ <code>{JSON.stringify(model)}</code>
53
+ )
54
+
55
+ // Pulls the store ONLY to call a stable action from a handler. Nothing is read
56
+ // during render, so nothing needs subscribing and the parent observer owns the
57
+ // page. Observatory writes two of these on purpose (`RichEntityListView`,
58
+ // `KanbanView`) with a comment saying exactly this.
59
+ export const OpenEntityButton = ({ id, label }: { id: string; label: string }) => {
60
+ const { ui } = useStore()
61
+ return <button onClick={() => ui.openPluginEntity(id)}>{label}</button>
62
+ }
63
+
64
+ // Same shape via a component-level handler rather than an inline arrow.
65
+ export const CloseButton = ({ id }: { id: string }) => {
66
+ const { ui } = useStore()
67
+ const onPress = () => ui.close(id)
68
+ return <button onClick={onPress}>close</button>
69
+ }
@@ -0,0 +1,30 @@
1
+ // Every declaration here MUST be reported by `no-local-kit-clone`.
2
+
3
+ // 7 of these exist across the park, discriminated by `kind` in three repos,
4
+ // `status` in two and `state` in one.
5
+ export type Resource<T> =
6
+ | { kind: "loading" }
7
+ | { kind: "ready"; value: T }
8
+ | { kind: "error"; message: string }
9
+
10
+ export type QueryState<T> =
11
+ | { status: "pending" }
12
+ | { status: "success"; data: T }
13
+
14
+ export class ObservableQuery<T> {
15
+ constructor(private readonly fetcher: () => Promise<T>) {}
16
+ }
17
+
18
+ // gramforge carries six byte-identical copies of these two inside one monorepo.
19
+ export function warnDegraded(context: string, detail: string): void {
20
+ console.warn(`[degraded] ${context}: ${detail}`)
21
+ }
22
+
23
+ export function warnNotImplemented(category: string, detail: string): void {
24
+ console.warn(`[not-implemented] ${category}: ${detail}`)
25
+ }
26
+
27
+ // The useMemo improvisation with no dispose that report §8 found everywhere.
28
+ export const usePageStore = <T>(factory: () => T, deps: unknown[]): T => {
29
+ return useMemo(factory, deps)
30
+ }
@@ -0,0 +1,30 @@
1
+ // Every declaration here MUST be reported by `no-local-kit-clone`.
2
+
3
+ // 7 of these exist across the park, discriminated by `kind` in three repos,
4
+ // `status` in two and `state` in one.
5
+ export type Resource<T> =
6
+ | { kind: "loading" }
7
+ | { kind: "ready"; value: T }
8
+ | { kind: "error"; message: string }
9
+
10
+ export type QueryState<T> =
11
+ | { status: "pending" }
12
+ | { status: "success"; data: T }
13
+
14
+ export class ObservableQuery<T> {
15
+ constructor(private readonly fetcher: () => Promise<T>) {}
16
+ }
17
+
18
+ // gramforge carries six byte-identical copies of these two inside one monorepo.
19
+ export function warnDegraded(context: string, detail: string): void {
20
+ console.warn(`[degraded] ${context}: ${detail}`)
21
+ }
22
+
23
+ export function warnNotImplemented(category: string, detail: string): void {
24
+ console.warn(`[not-implemented] ${category}: ${detail}`)
25
+ }
26
+
27
+ // The useMemo improvisation with no dispose that report §8 found everywhere.
28
+ export const usePageStore = <T,>(factory: () => T, deps: unknown[]): T => {
29
+ return useMemo(factory, deps)
30
+ }
@@ -0,0 +1,31 @@
1
+ // NOTHING in this file may be reported by `no-local-kit-clone`.
2
+
3
+ // The fix: import, do not redeclare.
4
+ import { warnDegraded, warnNotImplemented } from "@agentvibes/kit/diagnostics"
5
+ import { usePageStore } from "@agentvibes/kit/react"
6
+ import type { QueryState, Resource } from "@agentvibes/kit/resource"
7
+
8
+ // An unrelated domain type that happens to be called Resource. Real: the
9
+ // VolumePhotography conferences page. No type parameters, no async state — the
10
+ // type-parameter requirement is what keeps this out.
11
+ interface Resource {
12
+ name: string
13
+ url: string
14
+ type: string
15
+ description: string
16
+ }
17
+
18
+ const conferences: Resource[] = []
19
+
20
+ // Re-exporting the kit's types is not redeclaring them.
21
+ export type { QueryState, Resource }
22
+ export { usePageStore, warnDegraded, warnNotImplemented }
23
+
24
+ // A local helper with a name of its own.
25
+ export function warnStale(context: string): void {
26
+ warnDegraded(context, "cache is stale")
27
+ }
28
+
29
+ // A differently-named local async union is out of this rule's scope; it is
30
+ // `state-loading-boolean-shape` and the Resource migration that speak to it.
31
+ export type LoadPhase = { status: "idle" } | { status: "busy" }
@@ -0,0 +1,31 @@
1
+ // NOTHING in this file may be reported by `no-local-kit-clone`.
2
+
3
+ // The fix: import, do not redeclare.
4
+ import { warnDegraded, warnNotImplemented } from "@agentvibes/kit/diagnostics"
5
+ import { usePageStore } from "@agentvibes/kit/react"
6
+ import type { QueryState, Resource } from "@agentvibes/kit/resource"
7
+
8
+ // An unrelated domain type that happens to be called Resource. Real: the
9
+ // VolumePhotography conferences page. No type parameters, no async state — the
10
+ // type-parameter requirement is what keeps this out.
11
+ interface Resource {
12
+ name: string
13
+ url: string
14
+ type: string
15
+ description: string
16
+ }
17
+
18
+ const conferences: Resource[] = []
19
+
20
+ // Re-exporting the kit's types is not redeclaring them.
21
+ export type { QueryState, Resource }
22
+ export { usePageStore, warnDegraded, warnNotImplemented }
23
+
24
+ // A local helper with a name of its own.
25
+ export function warnStale(context: string): void {
26
+ warnDegraded(context, "cache is stale")
27
+ }
28
+
29
+ // A differently-named local async union is out of this rule's scope; it is
30
+ // `state-loading-boolean-shape` and the Resource migration that speak to it.
31
+ export type LoadPhase = { status: "idle" } | { status: "busy" }
@@ -0,0 +1,8 @@
1
+ // Every chain here MUST be reported by `non-exhaustive-match`.
2
+ import { match } from "ts-pattern"
3
+ export const a = (r: R) => match(r).with({ kind: "idle" }, () => 0).otherwise(() => -1)
4
+ export const b = (r: R) =>
5
+ match(r)
6
+ .with({ kind: "idle" }, () => 0)
7
+ .with({ kind: "ready" }, () => 1)
8
+ .otherwise(() => -1)
@@ -0,0 +1,14 @@
1
+ // NOTHING in this file may be reported by `non-exhaustive-match`.
2
+ import { match } from "ts-pattern"
3
+
4
+ export const a = (r: R) =>
5
+ match(r)
6
+ .with({ kind: "idle" }, () => 0)
7
+ .with({ kind: "ready" }, () => 1)
8
+ .exhaustive()
9
+
10
+ // REMOVED BY THE WAVE-2 UPGRADE: the old body was a bare `$X.otherwise($$$)`
11
+ // pattern, so any method called `otherwise` on any object was a finding. The
12
+ // rule now anchors to a ts-pattern `match()` chain.
13
+ export const q = someBuilder.otherwise(() => 1)
14
+ export const r = config.fallback.otherwise
@@ -0,0 +1,15 @@
1
+ // BAD: a page store on a bare useMemo — no dispose, two of them under StrictMode.
2
+ import { useMemo } from "react";
3
+
4
+ class GalleryPageStore {
5
+ constructor(readonly slug: string) {}
6
+ }
7
+ class SettingsPageStore {}
8
+
9
+ export function useGallery(slug: string) {
10
+ return useMemo(() => new GalleryPageStore(slug), [slug]);
11
+ }
12
+
13
+ export function useSettings() {
14
+ return useMemo(() => new SettingsPageStore(), []);
15
+ }
@@ -0,0 +1,13 @@
1
+ // BAD: same leak, in the file where page stores actually get created.
2
+ import { useMemo } from "react";
3
+
4
+ class GalleryPageStore {
5
+ constructor(readonly slug: string) {}
6
+ }
7
+ class FiltersPageStore {}
8
+
9
+ export function GalleryScreenBody(props: { readonly slug: string }) {
10
+ const store = useMemo(() => new GalleryPageStore(props.slug), [props.slug]);
11
+ const filters = useMemo(() => new FiltersPageStore(), []);
12
+ return <span>{String(store.slug) + String(filters)}</span>;
13
+ }
@@ -0,0 +1,17 @@
1
+ // GOOD: the sanctioned hook owns creation, ref-counting and disposal.
2
+ // A plain useMemo over a non-store value stays legal.
3
+ import { useMemo } from "react";
4
+ import { usePageStore } from "@agentvibes/kit/react";
5
+
6
+ class GalleryPageStore {
7
+ constructor(readonly slug: string) {}
8
+ dispose(): void {}
9
+ }
10
+
11
+ export function useGallery(slug: string) {
12
+ return usePageStore(() => new GalleryPageStore(slug), [slug]);
13
+ }
14
+
15
+ export function useIndex() {
16
+ return useMemo(() => new Map<string, number>(), []);
17
+ }
@@ -0,0 +1,14 @@
1
+ // GOOD: usePageStore, plus a plain useMemo over a non-store value.
2
+ import { useMemo } from "react";
3
+ import { usePageStore } from "@agentvibes/kit/react";
4
+
5
+ class GalleryPageStore {
6
+ constructor(readonly slug: string) {}
7
+ dispose(): void {}
8
+ }
9
+
10
+ export function GalleryScreenBody(props: { readonly slug: string }) {
11
+ const store = usePageStore(() => new GalleryPageStore(props.slug), [props.slug]);
12
+ const seen = useMemo(() => new Set<string>(), []);
13
+ return <span>{store.slug + String(seen.size)}</span>;
14
+ }
@@ -0,0 +1,10 @@
1
+ // BAD: a screen growing its own layout instead of delegating to a view.
2
+ export function GalleryScreen() {
3
+ return (
4
+ <section className="p-4">
5
+ <header style={{ gap: 8 }}>
6
+ <h1 className="title">Gallery</h1>
7
+ </header>
8
+ </section>
9
+ );
10
+ }