@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,8 @@
1
+ // GOOD: the screen picks the model and composes views; styling lives in them.
2
+ import { observer } from "mobx-react-lite";
3
+ import { rootStore } from "./rootStore";
4
+ import { GalleryGrid } from "./galleryGrid.view";
5
+
6
+ export const GalleryScreen = observer(function GalleryScreen() {
7
+ return <GalleryGrid photos={rootStore.gallery.photos} />;
8
+ });
@@ -0,0 +1,36 @@
1
+ // Every type here MUST be reported by `state-loading-boolean-shape`.
2
+
3
+ // The canonical boolean soup: loading + optional payload + optional error.
4
+ export interface FetchState {
5
+ loading: boolean
6
+ data?: User[]
7
+ error?: string
8
+ }
9
+
10
+ // Multi-flag, no payload — the shape gallerykit's ImageLoadState really has.
11
+ export interface ImageLoadState {
12
+ isLoading: boolean
13
+ isLoaded: boolean
14
+ isError: boolean
15
+ error: Event | null
16
+ }
17
+
18
+ // Null-union rather than optional; same set of impossible states.
19
+ export interface AdapterState {
20
+ loading: boolean
21
+ data: Rows | null
22
+ error: string | null
23
+ }
24
+
25
+ // A type alias rather than an interface.
26
+ export type PlaygroundState = {
27
+ isFetching: boolean
28
+ result?: QueryResult
29
+ error?: string
30
+ }
31
+
32
+ // `pending` is the same axis under another name.
33
+ export interface SaveState {
34
+ pending: boolean
35
+ succeeded: boolean
36
+ }
@@ -0,0 +1,36 @@
1
+ // Every type here MUST be reported by `state-loading-boolean-shape`.
2
+
3
+ // The canonical boolean soup: loading + optional payload + optional error.
4
+ export interface FetchState {
5
+ loading: boolean
6
+ data?: User[]
7
+ error?: string
8
+ }
9
+
10
+ // Multi-flag, no payload — the shape gallerykit's ImageLoadState really has.
11
+ export interface ImageLoadState {
12
+ isLoading: boolean
13
+ isLoaded: boolean
14
+ isError: boolean
15
+ error: Event | null
16
+ }
17
+
18
+ // Null-union rather than optional; same set of impossible states.
19
+ export interface AdapterState {
20
+ loading: boolean
21
+ data: Rows | null
22
+ error: string | null
23
+ }
24
+
25
+ // A type alias rather than an interface.
26
+ export type PlaygroundState = {
27
+ isFetching: boolean
28
+ result?: QueryResult
29
+ error?: string
30
+ }
31
+
32
+ // `pending` is the same axis under another name.
33
+ export interface SaveState {
34
+ pending: boolean
35
+ succeeded: boolean
36
+ }
@@ -0,0 +1,44 @@
1
+ // NOTHING in this file may be reported by `state-loading-boolean-shape`.
2
+
3
+ // The fix: one discriminant, payload only in the branch that has one.
4
+ export type Resource<T> =
5
+ | { status: "idle" }
6
+ | { status: "loading" }
7
+ | { status: "ready"; data: T }
8
+ | { status: "error"; message: string }
9
+
10
+ // Independent axes done RIGHT — this is SWR, not boolean soup: "has data" and
11
+ // "a fetch is in flight" genuinely are orthogonal (report §14).
12
+ export type QueryState<T> =
13
+ | { status: "pending"; fetchActivity: "idle" | "fetching" }
14
+ | { status: "success"; data: T; fetchActivity: "idle" | "fetching" }
15
+ | { status: "error"; error: string; fetchActivity: "idle" | "fetching" }
16
+
17
+ // A LONE loading flag on a presentational component's props. One boolean
18
+ // describing one thing, no payload, no second outcome field — there is no
19
+ // invalid state to make unrepresentable. 71 raw `loading: boolean` sites exist
20
+ // park-wide and most look like this; flagging them would be noise.
21
+ export interface LoadMoreButtonProps {
22
+ loading?: boolean
23
+ onPress: () => void
24
+ label: string
25
+ }
26
+
27
+ export interface InfiniteScrollProps {
28
+ loading?: boolean
29
+ hasMore: boolean
30
+ onLoadMore: () => void
31
+ }
32
+
33
+ // A payload with no loading flag at all.
34
+ export interface UserResponse {
35
+ data?: User
36
+ error?: string
37
+ }
38
+
39
+ // Booleans that are not about async outcome.
40
+ export interface EditorFlags {
41
+ loading: boolean
42
+ readOnly: boolean
43
+ wrapLines: boolean
44
+ }
@@ -0,0 +1,44 @@
1
+ // NOTHING in this file may be reported by `state-loading-boolean-shape`.
2
+
3
+ // The fix: one discriminant, payload only in the branch that has one.
4
+ export type Resource<T> =
5
+ | { status: "idle" }
6
+ | { status: "loading" }
7
+ | { status: "ready"; data: T }
8
+ | { status: "error"; message: string }
9
+
10
+ // Independent axes done RIGHT — this is SWR, not boolean soup: "has data" and
11
+ // "a fetch is in flight" genuinely are orthogonal (report §14).
12
+ export type QueryState<T> =
13
+ | { status: "pending"; fetchActivity: "idle" | "fetching" }
14
+ | { status: "success"; data: T; fetchActivity: "idle" | "fetching" }
15
+ | { status: "error"; error: string; fetchActivity: "idle" | "fetching" }
16
+
17
+ // A LONE loading flag on a presentational component's props. One boolean
18
+ // describing one thing, no payload, no second outcome field — there is no
19
+ // invalid state to make unrepresentable. 71 raw `loading: boolean` sites exist
20
+ // park-wide and most look like this; flagging them would be noise.
21
+ export interface LoadMoreButtonProps {
22
+ loading?: boolean
23
+ onPress: () => void
24
+ label: string
25
+ }
26
+
27
+ export interface InfiniteScrollProps {
28
+ loading?: boolean
29
+ hasMore: boolean
30
+ onLoadMore: () => void
31
+ }
32
+
33
+ // A payload with no loading flag at all.
34
+ export interface UserResponse {
35
+ data?: User
36
+ error?: string
37
+ }
38
+
39
+ // Booleans that are not about async outcome.
40
+ export interface EditorFlags {
41
+ loading: boolean
42
+ readOnly: boolean
43
+ wrapLines: boolean
44
+ }
@@ -0,0 +1,37 @@
1
+ // Every async method here MUST be reported by `store-async-method`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class ProjectStore {
5
+ projects: string[] = []
6
+ current: string | null = null
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ // The action ends at the await; the assignment after it is unbatched.
13
+ async load(): Promise<void> {
14
+ const list = await fetchProjects()
15
+ this.projects = list
16
+ }
17
+
18
+ // Two awaits, two holes.
19
+ async open(id: string): Promise<void> {
20
+ this.current = await resolveId(id)
21
+ this.projects = await fetchProjects()
22
+ }
23
+
24
+ private async refreshQuietly(): Promise<void> {
25
+ this.projects = await fetchProjects()
26
+ }
27
+ }
28
+
29
+ export class SessionStore {
30
+ token = ""
31
+ constructor() {
32
+ makeObservable(this, { token: observable })
33
+ }
34
+ async authenticate(): Promise<void> {
35
+ this.token = await getToken()
36
+ }
37
+ }
@@ -0,0 +1,37 @@
1
+ // Every async method here MUST be reported by `store-async-method`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class ProjectStore {
5
+ projects: string[] = []
6
+ current: string | null = null
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ // The action ends at the await; the assignment after it is unbatched.
13
+ async load(): Promise<void> {
14
+ const list = await fetchProjects()
15
+ this.projects = list
16
+ }
17
+
18
+ // Two awaits, two holes.
19
+ async open(id: string): Promise<void> {
20
+ this.current = await resolveId(id)
21
+ this.projects = await fetchProjects()
22
+ }
23
+
24
+ private async refreshQuietly(): Promise<void> {
25
+ this.projects = await fetchProjects()
26
+ }
27
+ }
28
+
29
+ export class SessionStore {
30
+ token = ""
31
+ constructor() {
32
+ makeObservable(this, { token: observable })
33
+ }
34
+ async authenticate(): Promise<void> {
35
+ this.token = await getToken()
36
+ }
37
+ }
@@ -0,0 +1,73 @@
1
+ // NOTHING in this file may be reported by `store-async-method`.
2
+ import { flow, makeAutoObservable, runInAction } from "mobx"
3
+
4
+ export class ProjectStore {
5
+ projects: string[] = []
6
+
7
+ constructor() {
8
+ makeAutoObservable(this)
9
+ }
10
+
11
+ // The fix: a flow resumes inside an action at every yield.
12
+ load = flow(function* (this: ProjectStore) {
13
+ this.projects = yield fetchProjects()
14
+ })
15
+
16
+ // Already reported by `store-no-runinaction` at error tier, and the fix is
17
+ // the same conversion. Reporting it twice would read as two problems, so this
18
+ // rule stands down when runInAction is present.
19
+ async loadLegacy(): Promise<void> {
20
+ const list = await fetchProjects()
21
+ runInAction(() => {
22
+ this.projects = list
23
+ })
24
+ }
25
+
26
+ // Not async.
27
+ reset(): void {
28
+ this.projects = []
29
+ }
30
+ }
31
+
32
+ // A plain class with no MobX annotation is not a store; async methods on it
33
+ // have no action semantics to lose.
34
+ export class ApiClient {
35
+ async get(url: string): Promise<Response> {
36
+ return fetch(url)
37
+ }
38
+ }
39
+
40
+ // ── the false-positive class this rule was narrowed to exclude ─────────
41
+ // Private async helpers that do pure I/O and never touch an observable field.
42
+ // There is no action coverage to lose, so there is nothing to convert. Real:
43
+ // SiteCraft's walkDir / collectAllFiles / uploadFile — 10 of the rule's 12
44
+ // hits in that repo before the mutation requirement was added.
45
+ export class ContentStore {
46
+ files: string[] = []
47
+
48
+ constructor() {
49
+ makeAutoObservable(this)
50
+ }
51
+
52
+ private async walkDir(fs: FileSystem, dir: string, items: Meta[]): Promise<void> {
53
+ let entries: string[]
54
+ try {
55
+ entries = await fs.readdir(dir)
56
+ } catch {
57
+ return
58
+ }
59
+ for (const e of entries) items.push(await fs.stat(e))
60
+ }
61
+
62
+ private async collectAllFiles(fs: FileSystem, dir: string): Promise<Record<string, string>> {
63
+ const result: Record<string, string> = {}
64
+ await this.walkFs(fs, dir, result)
65
+ return result
66
+ }
67
+
68
+ async uploadFile(targetDir: string, name: string, data: Uint8Array): Promise<string> {
69
+ const path = `${targetDir}/${name}`
70
+ await this.fs.writeFile(path, data)
71
+ return path
72
+ }
73
+ }
@@ -0,0 +1,73 @@
1
+ // NOTHING in this file may be reported by `store-async-method`.
2
+ import { flow, makeAutoObservable, runInAction } from "mobx"
3
+
4
+ export class ProjectStore {
5
+ projects: string[] = []
6
+
7
+ constructor() {
8
+ makeAutoObservable(this)
9
+ }
10
+
11
+ // The fix: a flow resumes inside an action at every yield.
12
+ load = flow(function* (this: ProjectStore) {
13
+ this.projects = yield fetchProjects()
14
+ })
15
+
16
+ // Already reported by `store-no-runinaction` at error tier, and the fix is
17
+ // the same conversion. Reporting it twice would read as two problems, so this
18
+ // rule stands down when runInAction is present.
19
+ async loadLegacy(): Promise<void> {
20
+ const list = await fetchProjects()
21
+ runInAction(() => {
22
+ this.projects = list
23
+ })
24
+ }
25
+
26
+ // Not async.
27
+ reset(): void {
28
+ this.projects = []
29
+ }
30
+ }
31
+
32
+ // A plain class with no MobX annotation is not a store; async methods on it
33
+ // have no action semantics to lose.
34
+ export class ApiClient {
35
+ async get(url: string): Promise<Response> {
36
+ return fetch(url)
37
+ }
38
+ }
39
+
40
+ // ── the false-positive class this rule was narrowed to exclude ─────────
41
+ // Private async helpers that do pure I/O and never touch an observable field.
42
+ // There is no action coverage to lose, so there is nothing to convert. Real:
43
+ // SiteCraft's walkDir / collectAllFiles / uploadFile — 10 of the rule's 12
44
+ // hits in that repo before the mutation requirement was added.
45
+ export class ContentStore {
46
+ files: string[] = []
47
+
48
+ constructor() {
49
+ makeAutoObservable(this)
50
+ }
51
+
52
+ private async walkDir(fs: FileSystem, dir: string, items: Meta[]): Promise<void> {
53
+ let entries: string[]
54
+ try {
55
+ entries = await fs.readdir(dir)
56
+ } catch {
57
+ return
58
+ }
59
+ for (const e of entries) items.push(await fs.stat(e))
60
+ }
61
+
62
+ private async collectAllFiles(fs: FileSystem, dir: string): Promise<Record<string, string>> {
63
+ const result: Record<string, string> = {}
64
+ await this.walkFs(fs, dir, result)
65
+ return result
66
+ }
67
+
68
+ async uploadFile(targetDir: string, name: string, data: Uint8Array): Promise<string> {
69
+ const path = `${targetDir}/${name}`
70
+ await this.fs.writeFile(path, data)
71
+ return path
72
+ }
73
+ }
@@ -0,0 +1,16 @@
1
+ // BAD: the whole context channel D1 removes — provider, hook, null-check.
2
+ import { createContext, useContext } from "react";
3
+
4
+ type RootStore = { readonly ready: boolean };
5
+
6
+ export const StoreContext = createContext<RootStore | null>(null);
7
+
8
+ export const useStore = (): RootStore => {
9
+ const store = useContext(StoreContext);
10
+ if (store === null) throw new Error("StoreProvider missing");
11
+ return store;
12
+ };
13
+
14
+ export function readReady(): boolean {
15
+ return useStore().ready;
16
+ }
@@ -0,0 +1,24 @@
1
+ // BAD: provider + hook, the shape every store-bearing app in the park still has.
2
+ import { createContext, useContext } from "react";
3
+
4
+ type RootStore = { readonly ready: boolean };
5
+
6
+ const StoreContext = createContext<RootStore | null>(null);
7
+
8
+ export function useStore(): RootStore {
9
+ const store = useContext(StoreContext);
10
+ if (store === null) throw new Error("StoreProvider missing");
11
+ return store;
12
+ }
13
+
14
+ export function App(props: { readonly store: RootStore }) {
15
+ return (
16
+ <StoreContext.Provider value={props.store}>
17
+ <ReadyBadge />
18
+ </StoreContext.Provider>
19
+ );
20
+ }
21
+
22
+ function ReadyBadge() {
23
+ return <span>{String(useStore().ready)}</span>;
24
+ }
@@ -0,0 +1,13 @@
1
+ // GOOD: one module-level store, dependencies injected once from the entry file.
2
+ type RootStore = { readonly ready: boolean };
3
+
4
+ let instance: RootStore | undefined;
5
+
6
+ export function initRootStore(deps: RootStore): void {
7
+ instance = deps;
8
+ }
9
+
10
+ export function getRootStore(): RootStore {
11
+ if (instance === undefined) throw new Error("initRootStore() has not run yet");
12
+ return instance;
13
+ }
@@ -0,0 +1,11 @@
1
+ // GOOD: the screen imports the singleton directly, the row gets a prop.
2
+ import { observer } from "mobx-react-lite";
3
+ import { rootStore } from "./rootStore";
4
+
5
+ export const AppScreen = observer(function AppScreen() {
6
+ return <ReadyBadge ready={rootStore.ready} />;
7
+ });
8
+
9
+ function ReadyBadge(props: { readonly ready: boolean }) {
10
+ return <span>{String(props.ready)}</span>;
11
+ }
@@ -0,0 +1,30 @@
1
+ // Every getter here MUST be reported by `store-delegation-getter`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class AppStore {
5
+ constructor(
6
+ public userStore: UserStore,
7
+ public connectionStore: ConnectionStore,
8
+ ) {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ // A second name for someone else's field.
13
+ get userName(): string {
14
+ return this.userStore.name
15
+ }
16
+
17
+ get userId(): string {
18
+ return this.userStore.id
19
+ }
20
+
21
+ // The real shape found in the park (2 sites, metro/byok survey).
22
+ get isConnected(): boolean {
23
+ return this.connectionStore.isConnected
24
+ }
25
+
26
+ // Single-expression body, same defect.
27
+ get retryCount(): number {
28
+ return this.connectionStore.retries
29
+ }
30
+ }
@@ -0,0 +1,30 @@
1
+ // Every getter here MUST be reported by `store-delegation-getter`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class AppStore {
5
+ constructor(
6
+ public userStore: UserStore,
7
+ public connectionStore: ConnectionStore,
8
+ ) {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ // A second name for someone else's field.
13
+ get userName(): string {
14
+ return this.userStore.name
15
+ }
16
+
17
+ get userId(): string {
18
+ return this.userStore.id
19
+ }
20
+
21
+ // The real shape found in the park (2 sites, metro/byok survey).
22
+ get isConnected(): boolean {
23
+ return this.connectionStore.isConnected
24
+ }
25
+
26
+ // Single-expression body, same defect.
27
+ get retryCount(): number {
28
+ return this.connectionStore.retries
29
+ }
30
+ }
@@ -0,0 +1,65 @@
1
+ // NOTHING in this file may be reported by `store-delegation-getter`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ // ── The data-wrapper pattern: 89 of the 91 single-line `return this.X.y`
5
+ // getters measured across byok, metro, SiteCraft and observatory look exactly
6
+ // like this. A model holding a plain record and republishing its fields as
7
+ // observable getters is the sanctioned shape (byok packages/shared/src/models/
8
+ // model.ts, metro apps/web-pmetro/src/stores/domain/station.store.ts).
9
+ export class Model {
10
+ readonly data: ModelRecordData
11
+
12
+ constructor(data: ModelRecordData) {
13
+ this.data = data
14
+ makeAutoObservable(this, { data: false })
15
+ }
16
+
17
+ get id(): string {
18
+ return this.data.id
19
+ }
20
+ get slug(): string {
21
+ return this.data.slug
22
+ }
23
+ get name(): string {
24
+ return this.data.name
25
+ }
26
+ get backend(): ModelBackend {
27
+ return this.data.backend
28
+ }
29
+ }
30
+
31
+ export class AppStore {
32
+ constructor(public userStore: UserStore) {
33
+ makeAutoObservable(this)
34
+ }
35
+
36
+ // A real derivation — combines, formats, decides. This is what computeds are
37
+ // for and it is not an alias.
38
+ get greeting(): string {
39
+ return `Hello, ${this.userStore.name}!`
40
+ }
41
+
42
+ get hasName(): boolean {
43
+ return this.userStore.name.length > 0
44
+ }
45
+
46
+ // Computed on the way out, so not a bare forward.
47
+ get displayName(): string {
48
+ const raw = this.userStore.name
49
+ return raw.trim() === "" ? "anonymous" : raw
50
+ }
51
+
52
+ // Reads its OWN field, not another store's.
53
+ get ready(): boolean {
54
+ return this.loaded
55
+ }
56
+
57
+ // KNOWN, DELIBERATE MISS. A rootStore reference conventionally named `root`
58
+ // (WireDrill spells it that way, annotated `{ root: false }`) is a genuine
59
+ // store-to-store alias that this rule does not catch — the name test keys on
60
+ // a `Store` suffix, and widening it to `root` starts pulling `this.data`-shaped
61
+ // names back into scope. Pinned here so the gap stays a decision.
62
+ get rootUser(): User {
63
+ return this.root.user
64
+ }
65
+ }
@@ -0,0 +1,65 @@
1
+ // NOTHING in this file may be reported by `store-delegation-getter`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ // ── The data-wrapper pattern: 89 of the 91 single-line `return this.X.y`
5
+ // getters measured across byok, metro, SiteCraft and observatory look exactly
6
+ // like this. A model holding a plain record and republishing its fields as
7
+ // observable getters is the sanctioned shape (byok packages/shared/src/models/
8
+ // model.ts, metro apps/web-pmetro/src/stores/domain/station.store.ts).
9
+ export class Model {
10
+ readonly data: ModelRecordData
11
+
12
+ constructor(data: ModelRecordData) {
13
+ this.data = data
14
+ makeAutoObservable(this, { data: false })
15
+ }
16
+
17
+ get id(): string {
18
+ return this.data.id
19
+ }
20
+ get slug(): string {
21
+ return this.data.slug
22
+ }
23
+ get name(): string {
24
+ return this.data.name
25
+ }
26
+ get backend(): ModelBackend {
27
+ return this.data.backend
28
+ }
29
+ }
30
+
31
+ export class AppStore {
32
+ constructor(public userStore: UserStore) {
33
+ makeAutoObservable(this)
34
+ }
35
+
36
+ // A real derivation — combines, formats, decides. This is what computeds are
37
+ // for and it is not an alias.
38
+ get greeting(): string {
39
+ return `Hello, ${this.userStore.name}!`
40
+ }
41
+
42
+ get hasName(): boolean {
43
+ return this.userStore.name.length > 0
44
+ }
45
+
46
+ // Computed on the way out, so not a bare forward.
47
+ get displayName(): string {
48
+ const raw = this.userStore.name
49
+ return raw.trim() === "" ? "anonymous" : raw
50
+ }
51
+
52
+ // Reads its OWN field, not another store's.
53
+ get ready(): boolean {
54
+ return this.loaded
55
+ }
56
+
57
+ // KNOWN, DELIBERATE MISS. A rootStore reference conventionally named `root`
58
+ // (WireDrill spells it that way, annotated `{ root: false }`) is a genuine
59
+ // store-to-store alias that this rule does not catch — the name test keys on
60
+ // a `Store` suffix, and widening it to `root` starts pulling `this.data`-shaped
61
+ // names back into scope. Pinned here so the gap stays a decision.
62
+ get rootUser(): User {
63
+ return this.root.user
64
+ }
65
+ }
@@ -0,0 +1,22 @@
1
+ // Every field here MUST be reported by `store-new-map`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class GalleryStore {
5
+ // Observable field, non-observable contents: .set() notifies nobody.
6
+ entries = new Map<string, Row>()
7
+ byId = new Map()
8
+
9
+ constructor() {
10
+ makeAutoObservable(this)
11
+ }
12
+ }
13
+
14
+ export class CalendarStore {
15
+ // Annotated, but this field is not the one exempted.
16
+ monthCache = new Map<string, Day[]>()
17
+ private _other = new Map<string, number>()
18
+
19
+ constructor() {
20
+ makeAutoObservable(this, { somethingElse: false })
21
+ }
22
+ }