@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,98 @@
1
+ # ── wave 1, store rule 2 of 7 ──────────────────────────────────────────
2
+ #
3
+ # The other half of `store-no-runinaction`, and the reason that rule has so much
4
+ # to find: an `async` method's continuation after each `await` is NOT covered by
5
+ # the action `makeAutoObservable` created. Every assignment past the first await
6
+ # is an unbatched mutation outside an action.
7
+ #
8
+ # Deliberately does NOT fire when the method already contains `runInAction` —
9
+ # that method is reported by `store-no-runinaction` at `error`, and the fix is
10
+ # the same conversion. Two findings prescribing one change read as two problems,
11
+ # and the error-tier one is the one worth showing.
12
+ #
13
+ # So this rule catches the quieter half: the async store method that mutates
14
+ # without even the hand-patched `runInAction`, where nothing marks the hole.
15
+ id: store-async-method
16
+ language: typescript
17
+ severity: warning
18
+ ignores:
19
+ - '**/*.test.ts'
20
+ - '**/*.spec.ts'
21
+ - '**/__tests__/**'
22
+ utils:
23
+ store-class:
24
+ kind: class_declaration
25
+ has:
26
+ stopBy: end
27
+ kind: call_expression
28
+ has:
29
+ field: function
30
+ kind: identifier
31
+ regex: '^make(Auto)?Observable$'
32
+ contains-runinaction:
33
+ has:
34
+ stopBy: end
35
+ kind: call_expression
36
+ has:
37
+ field: function
38
+ kind: identifier
39
+ regex: '^runInAction$'
40
+ # An observable MUTATION — which is the whole defect. Without this clause the
41
+ # rule fires on any async method, and in SiteCraft 10 of its 12 hits were
42
+ # private I/O helpers (`walkDir`, `collectAllFiles`, `uploadFile`) that await
43
+ # a filesystem and never touch a field. Those have no action coverage to lose,
44
+ # so at warning tier they were pure noise.
45
+ mutates-this:
46
+ has:
47
+ stopBy: end
48
+ any:
49
+ # this.field = … / this.field += …
50
+ - all:
51
+ - any:
52
+ - kind: assignment_expression
53
+ - kind: augmented_assignment_expression
54
+ - has:
55
+ field: left
56
+ kind: member_expression
57
+ has:
58
+ field: object
59
+ kind: this
60
+ # this.list.push(…), this.map.set(…), … — mutation through a method.
61
+ - all:
62
+ - kind: call_expression
63
+ - has:
64
+ field: function
65
+ kind: member_expression
66
+ all:
67
+ - has:
68
+ field: property
69
+ regex: '^(push|pop|shift|unshift|splice|sort|reverse|set|delete|clear|add)$'
70
+ - has:
71
+ field: object
72
+ kind: member_expression
73
+ has:
74
+ field: object
75
+ kind: this
76
+ message: |
77
+ An `async` method on a store class. The action that `makeAutoObservable`
78
+ wrapped around it ends at the first `await` — everything after resumes
79
+ OUTSIDE it, so each assignment there is an unbatched mutation and observers
80
+ see the half-updated store between them.
81
+ Use a `flow`, which resumes inside an action at every yield:
82
+ async load() { this.x = await f(); this.y = await g() }
83
+ → load = flow(function* (this: Store) { this.x = yield f(); this.y = yield g() })
84
+ Scope: this fires only when the method actually MUTATES an observable —
85
+ assigns `this.field` or calls `push`/`set`/`delete` on one. A private async
86
+ helper that only does I/O and returns has no action coverage to lose and stays
87
+ silent.
88
+ See the mobx-models skill.
89
+ rule:
90
+ all:
91
+ - kind: method_definition
92
+ - regex: '^((private|public|protected|static|override|abstract)\s+)*async\s'
93
+ - inside:
94
+ stopBy: end
95
+ matches: store-class
96
+ - matches: mutates-this
97
+ - not:
98
+ matches: contains-runinaction
@@ -0,0 +1,68 @@
1
+ # ADOPTION NOTE (canon promotion): brownfield-heavy — ~48-105 whole-tree hits
2
+ # per useStore-era repo (tg-gallery/observatory/ppa). Run it DIFF-SCOPED
3
+ # (verify-diff / hook) until a repo's useStore migration campaign lands;
4
+ # whole-tree `verify` output will be loud there by design, not by accident.
5
+ # ── simple-components, rule 1 of 4 — the .tsx half ─────────────────────
6
+ #
7
+ # ast-grep's `typescript` and `tsx` languages are disjoint, not nested: a rule at
8
+ # `language: typescript` never sees a `.tsx` file. So this is the same rule
9
+ # wearing a second id, plus one arm that only exists in JSX (`<StoreContext.Provider>`).
10
+ # Suppress with the id the finding PRINTS — `-tsx` in a `.tsx` file.
11
+ # Keep the two `rule:` blocks in sync.
12
+ id: store-context-provider-tsx
13
+ language: tsx
14
+ severity: error
15
+ ignores:
16
+ - '**/*.test.tsx'
17
+ - '**/*.spec.tsx'
18
+ - '**/__tests__/**'
19
+ message: |
20
+ A store reached through React context. Decision D1: MobX already gives every
21
+ component a live channel to the data — a second one buys nothing and costs a
22
+ provider, a null-check and a hook on every file that reads state.
23
+ Do instead:
24
+ - one module-level `rootStore` in the canonical store module, imported
25
+ DIRECTLY and only by screen-level files;
26
+ - platform/DI dependencies through `initRootStore(deps)`, called once from
27
+ the entry file before the first render;
28
+ - page-scoped state through `usePageStore(() => new XxxPageStore(...), [k])`;
29
+ - everything below a screen receives `model={...}` as a prop.
30
+ Tests stop needing a provider at all: `new XxxStore(fakeDeps)` directly, and
31
+ `*.view.tsx` files mount with nothing around them.
32
+ See the simple-components skill (§store access) and report §8.
33
+ rule:
34
+ any:
35
+ - all:
36
+ - kind: call_expression
37
+ - has:
38
+ field: function
39
+ kind: identifier
40
+ regex: '^useStore$'
41
+ - all:
42
+ - any:
43
+ - kind: function_declaration
44
+ - kind: variable_declarator
45
+ - has:
46
+ field: name
47
+ regex: '^useStore$'
48
+ - all:
49
+ - kind: call_expression
50
+ - has:
51
+ field: function
52
+ kind: identifier
53
+ regex: '^createContext$'
54
+ - inside:
55
+ stopBy: end
56
+ kind: variable_declarator
57
+ regex: '[Ss]tore'
58
+ - all:
59
+ - kind: call_expression
60
+ - has:
61
+ field: function
62
+ kind: identifier
63
+ regex: '^useContext$'
64
+ - regex: '[Ss]tore'
65
+ # JSX-only arm: `<XxxStoreContext.Provider value={…}>`.
66
+ - all:
67
+ - kind: jsx_opening_element
68
+ - regex: '[Ss]tore[A-Za-z]*\.Provider'
@@ -0,0 +1,78 @@
1
+ # ADOPTION NOTE (canon promotion): brownfield-heavy — ~48-105 whole-tree hits
2
+ # per useStore-era repo (tg-gallery/observatory/ppa). Run it DIFF-SCOPED
3
+ # (verify-diff / hook) until a repo's useStore migration campaign lands;
4
+ # whole-tree `verify` output will be loud there by design, not by accident.
5
+ # ── simple-components, rule 1 of 4 ─────────────────────────────────────
6
+ #
7
+ # Decision D1: a MobX project has NO store context. One module-level `rootStore`,
8
+ # imported at screen level only; everything below gets models through props.
9
+ #
10
+ # The canon rule `direct-store-import` covers the other half — where a store
11
+ # import is legal. Nothing in the canon bans the context CHANNEL itself, which is
12
+ # what this rule is for. Audit basis (report §8): all four store-bearing apps
13
+ # route through a `useStore()` context hook (215/97/69/46 call sites) and not one
14
+ # of them uses it for anything context is needed for: in one of them the provider
15
+ # hands out a module-level singleton unconditionally, and its own tests bypass
16
+ # the provider entirely.
17
+ #
18
+ # NOT a candidate for blanket adoption in a repo that has not migrated: on
19
+ # today's park this fires hundreds of times. Run it diff-scoped (the skill's
20
+ # check-simple.sh --changed, or `guardrails verify-diff`) until the migration
21
+ # lands.
22
+ id: store-context-provider
23
+ language: typescript
24
+ severity: error
25
+ ignores:
26
+ - '**/*.test.ts'
27
+ - '**/*.spec.ts'
28
+ - '**/__tests__/**'
29
+ message: |
30
+ A store reached through React context. Decision D1: MobX already gives every
31
+ component a live channel to the data — a second one buys nothing and costs a
32
+ provider, a null-check and a hook on every file that reads state.
33
+ Do instead:
34
+ - one module-level `rootStore` in the canonical store module, imported
35
+ DIRECTLY and only by screen-level files;
36
+ - platform/DI dependencies through `initRootStore(deps)`, called once from
37
+ the entry file before the first render;
38
+ - page-scoped state through `usePageStore(() => new XxxPageStore(...), [k])`;
39
+ - everything below a screen receives `model={...}` as a prop.
40
+ Tests stop needing a provider at all: `new XxxStore(fakeDeps)` directly, and
41
+ `*.view.tsx` files mount with nothing around them.
42
+ See the simple-components skill (§store access) and report §8.
43
+ rule:
44
+ any:
45
+ # `useStore()` — the call...
46
+ - all:
47
+ - kind: call_expression
48
+ - has:
49
+ field: function
50
+ kind: identifier
51
+ regex: '^useStore$'
52
+ # ...and its declaration, which is the thing that enables every call site.
53
+ - all:
54
+ - any:
55
+ - kind: function_declaration
56
+ - kind: variable_declarator
57
+ - has:
58
+ field: name
59
+ regex: '^useStore$'
60
+ # `createContext` whose declarator mentions a store.
61
+ - all:
62
+ - kind: call_expression
63
+ - has:
64
+ field: function
65
+ kind: identifier
66
+ regex: '^createContext$'
67
+ - inside:
68
+ stopBy: end
69
+ kind: variable_declarator
70
+ regex: '[Ss]tore'
71
+ # `useContext(SomethingStoreContext)`.
72
+ - all:
73
+ - kind: call_expression
74
+ - has:
75
+ field: function
76
+ kind: identifier
77
+ regex: '^useContext$'
78
+ - regex: '[Ss]tore'
@@ -0,0 +1,104 @@
1
+ # ── the .tsx half of `store-delegation-getter` ──
2
+ #
3
+ # ast-grep's `typescript` and `tsx` languages are DISJOINT: a `language:
4
+ # typescript` rule reads .ts and never .tsx. `demo-mode-by-default-ts.yml`
5
+ # records this repo learning that the hard way, so it is measured here rather
6
+ # than assumed — 30 .tsx files in the park call make(Auto)Observable, and the
7
+ # five twins with hits add 29 findings the .ts halves cannot see.
8
+ #
9
+ # Keep this file's `rule:` block IDENTICAL to `store-delegation-getter.yml`.
10
+ # Suppressing needs THIS id: `ast-grep-ignore: store-delegation-getter-tsx`.
11
+ #
12
+ # ── wave 1, store rule 5 of 7 ──────────────────────────────────────────
13
+ #
14
+ # A getter whose entire body is `return this.someStore.prop` — a store aliasing
15
+ # another store's field. It adds a second name for one piece of state, so call
16
+ # sites disagree about which name is canonical, and the alias silently outlives
17
+ # the field it forwards to.
18
+ #
19
+ # THE NEGATIVE IS THE WHOLE DESIGN OF THIS RULE. The identical shape,
20
+ # `return this.data.x`, is the house data-wrapper pattern and is completely
21
+ # legal: a `Model` class holding a plain record and exposing its fields as
22
+ # observable getters. Measured across SiteCraft, observatory, byok and metro,
23
+ # single-line `return this.X.y` getters break down as:
24
+ #
25
+ # 89 × this.data.* byok Model, metro domain stores — LEGAL
26
+ # 2 × this.connectionStore.* — the real store→store alias
27
+ #
28
+ # So the discriminator is the intermediate NAME, and the rule matches only
29
+ # `Store`-suffixed ones — the same `[Ss]tore` convention mobx-effect-observable-dep
30
+ # and mobx-usestate-from-store already key on.
31
+ #
32
+ # KNOWN AND DELIBERATE MISS: a rootStore reference conventionally named `root`
33
+ # (WireDrill annotates `{ root: false }`), so `return this.root.user` is a real
34
+ # store→store alias this rule does not catch. Widening to `root` would put
35
+ # `this.data`-shaped names back in scope by degrees; the good fixture pins the
36
+ # miss so it stays a decision rather than becoming a surprise.
37
+ id: store-delegation-getter-tsx
38
+ language: tsx
39
+ severity: warning
40
+ ignores:
41
+ - '**/*.test.tsx'
42
+ - '**/*.spec.tsx'
43
+ - '**/__tests__/**'
44
+ utils:
45
+ store-class:
46
+ kind: class_declaration
47
+ has:
48
+ stopBy: end
49
+ kind: call_expression
50
+ has:
51
+ field: function
52
+ kind: identifier
53
+ regex: '^make(Auto)?Observable$'
54
+ message: |
55
+ A getter that only forwards another store's field. Now the value has two
56
+ names, and every call site has to know which one is current — the alias keeps
57
+ compiling long after the field behind it moves or changes meaning.
58
+ Read it where it lives (`rootStore.userStore.name`), or, if this store really
59
+ owns the concept, hold the value rather than a pointer to someone else's.
60
+ A computed that DERIVES something (combines, filters, formats) is the point of
61
+ computeds and is not this — this fires only on a body that is a bare
62
+ `return this.otherStore.field`.
63
+ Not this rule: `return this.data.x` on a model wrapping a plain record. That
64
+ is the sanctioned data-wrapper pattern and stays silent.
65
+ See the mobx-models skill.
66
+ rule:
67
+ all:
68
+ - kind: method_definition
69
+ - regex: '^((private|public|protected|static|override)\s+)*get\s'
70
+ - inside:
71
+ stopBy: end
72
+ matches: store-class
73
+ - has:
74
+ field: body
75
+ all:
76
+ - kind: statement_block
77
+ - has:
78
+ kind: return_statement
79
+ has:
80
+ kind: member_expression
81
+ has:
82
+ field: object
83
+ all:
84
+ - kind: member_expression
85
+ - has:
86
+ field: object
87
+ kind: this
88
+ - has:
89
+ field: property
90
+ regex: '[Ss]tore$'
91
+ # Body is ONLY that return — anything computed on the way makes it a
92
+ # real derivation rather than an alias.
93
+ - not:
94
+ has:
95
+ any:
96
+ - kind: if_statement
97
+ - kind: lexical_declaration
98
+ - kind: variable_declaration
99
+ - kind: expression_statement
100
+ - kind: for_statement
101
+ - kind: for_in_statement
102
+ - kind: while_statement
103
+ - kind: switch_statement
104
+ - kind: try_statement
@@ -0,0 +1,93 @@
1
+ # ── wave 1, store rule 5 of 7 ──────────────────────────────────────────
2
+ #
3
+ # A getter whose entire body is `return this.someStore.prop` — a store aliasing
4
+ # another store's field. It adds a second name for one piece of state, so call
5
+ # sites disagree about which name is canonical, and the alias silently outlives
6
+ # the field it forwards to.
7
+ #
8
+ # THE NEGATIVE IS THE WHOLE DESIGN OF THIS RULE. The identical shape,
9
+ # `return this.data.x`, is the house data-wrapper pattern and is completely
10
+ # legal: a `Model` class holding a plain record and exposing its fields as
11
+ # observable getters. Measured across SiteCraft, observatory, byok and metro,
12
+ # single-line `return this.X.y` getters break down as:
13
+ #
14
+ # 89 × this.data.* byok Model, metro domain stores — LEGAL
15
+ # 2 × this.connectionStore.* — the real store→store alias
16
+ #
17
+ # So the discriminator is the intermediate NAME, and the rule matches only
18
+ # `Store`-suffixed ones — the same `[Ss]tore` convention mobx-effect-observable-dep
19
+ # and mobx-usestate-from-store already key on.
20
+ #
21
+ # KNOWN AND DELIBERATE MISS: a rootStore reference conventionally named `root`
22
+ # (WireDrill annotates `{ root: false }`), so `return this.root.user` is a real
23
+ # store→store alias this rule does not catch. Widening to `root` would put
24
+ # `this.data`-shaped names back in scope by degrees; the good fixture pins the
25
+ # miss so it stays a decision rather than becoming a surprise.
26
+ id: store-delegation-getter
27
+ language: typescript
28
+ severity: warning
29
+ ignores:
30
+ - '**/*.test.ts'
31
+ - '**/*.spec.ts'
32
+ - '**/__tests__/**'
33
+ utils:
34
+ store-class:
35
+ kind: class_declaration
36
+ has:
37
+ stopBy: end
38
+ kind: call_expression
39
+ has:
40
+ field: function
41
+ kind: identifier
42
+ regex: '^make(Auto)?Observable$'
43
+ message: |
44
+ A getter that only forwards another store's field. Now the value has two
45
+ names, and every call site has to know which one is current — the alias keeps
46
+ compiling long after the field behind it moves or changes meaning.
47
+ Read it where it lives (`rootStore.userStore.name`), or, if this store really
48
+ owns the concept, hold the value rather than a pointer to someone else's.
49
+ A computed that DERIVES something (combines, filters, formats) is the point of
50
+ computeds and is not this — this fires only on a body that is a bare
51
+ `return this.otherStore.field`.
52
+ Not this rule: `return this.data.x` on a model wrapping a plain record. That
53
+ is the sanctioned data-wrapper pattern and stays silent.
54
+ See the mobx-models skill.
55
+ rule:
56
+ all:
57
+ - kind: method_definition
58
+ - regex: '^((private|public|protected|static|override)\s+)*get\s'
59
+ - inside:
60
+ stopBy: end
61
+ matches: store-class
62
+ - has:
63
+ field: body
64
+ all:
65
+ - kind: statement_block
66
+ - has:
67
+ kind: return_statement
68
+ has:
69
+ kind: member_expression
70
+ has:
71
+ field: object
72
+ all:
73
+ - kind: member_expression
74
+ - has:
75
+ field: object
76
+ kind: this
77
+ - has:
78
+ field: property
79
+ regex: '[Ss]tore$'
80
+ # Body is ONLY that return — anything computed on the way makes it a
81
+ # real derivation rather than an alias.
82
+ - not:
83
+ has:
84
+ any:
85
+ - kind: if_statement
86
+ - kind: lexical_declaration
87
+ - kind: variable_declaration
88
+ - kind: expression_statement
89
+ - kind: for_statement
90
+ - kind: for_in_statement
91
+ - kind: while_statement
92
+ - kind: switch_statement
93
+ - kind: try_statement
@@ -0,0 +1,84 @@
1
+ # ── the .tsx half of `store-new-map` ──
2
+ #
3
+ # ast-grep's `typescript` and `tsx` languages are DISJOINT: a `language:
4
+ # typescript` rule reads .ts and never .tsx. `demo-mode-by-default-ts.yml`
5
+ # records this repo learning that the hard way, so it is measured here rather
6
+ # than assumed — 30 .tsx files in the park call make(Auto)Observable, and the
7
+ # five twins with hits add 29 findings the .ts halves cannot see.
8
+ #
9
+ # Keep this file's `rule:` block IDENTICAL to `store-new-map.yml`.
10
+ # Suppressing needs THIS id: `ast-grep-ignore: store-new-map-tsx`.
11
+ #
12
+ # ── wave 1, store rule 3 of 7 ──────────────────────────────────────────
13
+ #
14
+ # A plain `new Map()` held as a store field. `makeAutoObservable` makes the FIELD
15
+ # observable — reassigning it notifies — but not the map's contents: `.set()`
16
+ # and `.delete()` mutate in place and no observer wakes up. The usual symptom is
17
+ # a cache that updates and a UI that does not, "fixed" by reassigning a fresh
18
+ # Map on every write.
19
+ #
20
+ # The negative that matters is the legal one, and it is why this rule reads the
21
+ # annotation object rather than just the initializer: a field explicitly
22
+ # annotated `{ _cache: false }` is deliberately NOT observable — a private
23
+ # memo/dedup cache nothing renders from. That is a correct pattern and must stay
24
+ # silent. A shared `$FIELD` metavariable ties the field's name to the annotation
25
+ # entry, so the check is per-field rather than per-class.
26
+ id: store-new-map-tsx
27
+ language: tsx
28
+ severity: warning
29
+ ignores:
30
+ - '**/*.test.tsx'
31
+ - '**/*.spec.tsx'
32
+ - '**/__tests__/**'
33
+ utils:
34
+ store-class:
35
+ kind: class_declaration
36
+ has:
37
+ stopBy: end
38
+ kind: call_expression
39
+ has:
40
+ field: function
41
+ kind: identifier
42
+ regex: '^make(Auto)?Observable$'
43
+ message: |
44
+ A plain `Map` as an observable store field. The field reference is
45
+ observable; the map's CONTENTS are not — `.set()` and `.delete()` mutate in
46
+ place and notify nobody, so anything rendering from this map silently stops
47
+ updating.
48
+ Use `observable.map()`, whose mutations are tracked:
49
+ entries = new Map<string, Row>() → entries = observable.map<string, Row>()
50
+ If this is a private cache nothing renders from, say so in the annotation and
51
+ the rule steps aside:
52
+ makeAutoObservable(this, { _cache: false })
53
+ See the mobx-models skill.
54
+ rule:
55
+ all:
56
+ - kind: public_field_definition
57
+ - has:
58
+ field: name
59
+ pattern: $FIELD
60
+ - has:
61
+ field: value
62
+ kind: new_expression
63
+ has:
64
+ field: constructor
65
+ kind: identifier
66
+ regex: '^Map$'
67
+ - inside:
68
+ stopBy: end
69
+ matches: store-class
70
+ # Annotated non-observable for this exact field name — the legal escape.
71
+ - not:
72
+ inside:
73
+ stopBy: end
74
+ kind: class_declaration
75
+ has:
76
+ stopBy: end
77
+ all:
78
+ - kind: pair
79
+ - has:
80
+ field: key
81
+ pattern: $FIELD
82
+ - has:
83
+ field: value
84
+ regex: '^false$'
@@ -0,0 +1,73 @@
1
+ # ── wave 1, store rule 3 of 7 ──────────────────────────────────────────
2
+ #
3
+ # A plain `new Map()` held as a store field. `makeAutoObservable` makes the FIELD
4
+ # observable — reassigning it notifies — but not the map's contents: `.set()`
5
+ # and `.delete()` mutate in place and no observer wakes up. The usual symptom is
6
+ # a cache that updates and a UI that does not, "fixed" by reassigning a fresh
7
+ # Map on every write.
8
+ #
9
+ # The negative that matters is the legal one, and it is why this rule reads the
10
+ # annotation object rather than just the initializer: a field explicitly
11
+ # annotated `{ _cache: false }` is deliberately NOT observable — a private
12
+ # memo/dedup cache nothing renders from. That is a correct pattern and must stay
13
+ # silent. A shared `$FIELD` metavariable ties the field's name to the annotation
14
+ # entry, so the check is per-field rather than per-class.
15
+ id: store-new-map
16
+ language: typescript
17
+ severity: warning
18
+ ignores:
19
+ - '**/*.test.ts'
20
+ - '**/*.spec.ts'
21
+ - '**/__tests__/**'
22
+ utils:
23
+ store-class:
24
+ kind: class_declaration
25
+ has:
26
+ stopBy: end
27
+ kind: call_expression
28
+ has:
29
+ field: function
30
+ kind: identifier
31
+ regex: '^make(Auto)?Observable$'
32
+ message: |
33
+ A plain `Map` as an observable store field. The field reference is
34
+ observable; the map's CONTENTS are not — `.set()` and `.delete()` mutate in
35
+ place and notify nobody, so anything rendering from this map silently stops
36
+ updating.
37
+ Use `observable.map()`, whose mutations are tracked:
38
+ entries = new Map<string, Row>() → entries = observable.map<string, Row>()
39
+ If this is a private cache nothing renders from, say so in the annotation and
40
+ the rule steps aside:
41
+ makeAutoObservable(this, { _cache: false })
42
+ See the mobx-models skill.
43
+ rule:
44
+ all:
45
+ - kind: public_field_definition
46
+ - has:
47
+ field: name
48
+ pattern: $FIELD
49
+ - has:
50
+ field: value
51
+ kind: new_expression
52
+ has:
53
+ field: constructor
54
+ kind: identifier
55
+ regex: '^Map$'
56
+ - inside:
57
+ stopBy: end
58
+ matches: store-class
59
+ # Annotated non-observable for this exact field name — the legal escape.
60
+ - not:
61
+ inside:
62
+ stopBy: end
63
+ kind: class_declaration
64
+ has:
65
+ stopBy: end
66
+ all:
67
+ - kind: pair
68
+ - has:
69
+ field: key
70
+ pattern: $FIELD
71
+ - has:
72
+ field: value
73
+ regex: '^false$'
@@ -0,0 +1,71 @@
1
+ # ── the .tsx half of `store-no-runinaction` ──
2
+ #
3
+ # ast-grep's `typescript` and `tsx` languages are DISJOINT: a `language:
4
+ # typescript` rule reads .ts and never .tsx. `demo-mode-by-default-ts.yml`
5
+ # records this repo learning that the hard way, so it is measured here rather
6
+ # than assumed — 30 .tsx files in the park call make(Auto)Observable, and the
7
+ # five twins with hits add 29 findings the .ts halves cannot see.
8
+ #
9
+ # Keep this file's `rule:` block IDENTICAL to `store-no-runinaction.yml`.
10
+ # Suppressing needs THIS id: `ast-grep-ignore: store-no-runinaction-tsx`.
11
+ #
12
+ # ── wave 1, store rule 1 of 7 ──────────────────────────────────────────
13
+ #
14
+ # The report's gap number 1: the most-repeated store rule in the corpus had no
15
+ # mechanical check at all, and the one script that did grep for it
16
+ # (mobx-models/scripts/lint-mobx.sh) was wired to nothing and has since been
17
+ # deleted.
18
+ #
19
+ # Scoped by the CLASS, not by the path. `**/stores/**` would have been the
20
+ # obvious glob and it is the wrong one twice over: it misses a store that lives
21
+ # beside its feature, and it catches plain helper classes that happen to sit in
22
+ # the folder. "Has a make(Auto)Observable call in it" is the actual definition
23
+ # of the thing the rule is about.
24
+ #
25
+ # Note the callee is matched as an identifier rather than with
26
+ # `pattern: makeAutoObservable($$$)`. A pattern does not match a call carrying
27
+ # explicit type arguments, and SiteCraft writes
28
+ # `makeAutoObservable<this, '_buildTimer' | 'buildManager'>(this, {…})` — which
29
+ # silently cost the whole of project.store.ts, 7 real call sites, until the
30
+ # count came up short against the grep.
31
+ id: store-no-runinaction-tsx
32
+ language: tsx
33
+ severity: error
34
+ ignores:
35
+ # A test may drive a store's state directly to set up a scenario; that is the
36
+ # sanctioned way to test a store without a render. All 6 of tg-gallery's
37
+ # runInAction sites are tests, which is why the report counts it as 0.
38
+ - '**/*.test.tsx'
39
+ - '**/*.spec.tsx'
40
+ - '**/__tests__/**'
41
+ utils:
42
+ store-class:
43
+ kind: class_declaration
44
+ has:
45
+ stopBy: end
46
+ kind: call_expression
47
+ has:
48
+ field: function
49
+ kind: identifier
50
+ regex: '^make(Auto)?Observable$'
51
+ message: |
52
+ `runInAction` inside a store class. Every method is already an action —
53
+ `makeAutoObservable` made it one — so this wraps an action in an action and
54
+ buys nothing. Where it appears is the tell: after an `await`, because the
55
+ continuation of an async method is NOT covered by the method's action, and
56
+ each `runInAction` is a hand-patched hole in that coverage.
57
+ Convert the method to a `flow`, which resumes inside an action at every yield
58
+ and needs no patching:
59
+ async load() { const x = await f(); runInAction(() => { this.x = x }) }
60
+ → load = flow(function* (this: Store) { this.x = yield f() })
61
+ See the mobx-models skill.
62
+ rule:
63
+ all:
64
+ - kind: call_expression
65
+ - has:
66
+ field: function
67
+ kind: identifier
68
+ regex: '^runInAction$'
69
+ - inside:
70
+ stopBy: end
71
+ matches: store-class