@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,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
+ }
@@ -0,0 +1,41 @@
1
+ // NOTHING in this file may be reported by `store-new-map`.
2
+ import { makeAutoObservable, observable } from "mobx"
3
+
4
+ export class GalleryStore {
5
+ // The fix: mutations are tracked.
6
+ entries = observable.map<string, Row>()
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+ }
12
+
13
+ // The LEGAL pattern the rule must not break: a private cache explicitly
14
+ // annotated non-observable. Nothing renders from it, so a plain Map is right.
15
+ export class MainPageStore {
16
+ private _jobInfoCache = new Map<string, JobInfo>()
17
+ jobs: Job[] = []
18
+
19
+ constructor() {
20
+ makeAutoObservable(this, { _jobInfoCache: false })
21
+ }
22
+ }
23
+
24
+ // Several exempted fields at once.
25
+ export class MetricsStore {
26
+ private counters = new Map<string, number>()
27
+ private gauges = new Map<string, number>()
28
+ total = 0
29
+
30
+ constructor() {
31
+ makeAutoObservable(this, { counters: false, gauges: false })
32
+ }
33
+ }
34
+
35
+ // Not a store: a plain service class.
36
+ export class MetricsService {
37
+ private counters = new Map<string, number>()
38
+ }
39
+
40
+ // Module-level constant, not a store field.
41
+ export const REGISTRY = new Map<string, string>()
@@ -0,0 +1,41 @@
1
+ // NOTHING in this file may be reported by `store-new-map`.
2
+ import { makeAutoObservable, observable } from "mobx"
3
+
4
+ export class GalleryStore {
5
+ // The fix: mutations are tracked.
6
+ entries = observable.map<string, Row>()
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+ }
12
+
13
+ // The LEGAL pattern the rule must not break: a private cache explicitly
14
+ // annotated non-observable. Nothing renders from it, so a plain Map is right.
15
+ export class MainPageStore {
16
+ private _jobInfoCache = new Map<string, JobInfo>()
17
+ jobs: Job[] = []
18
+
19
+ constructor() {
20
+ makeAutoObservable(this, { _jobInfoCache: false })
21
+ }
22
+ }
23
+
24
+ // Several exempted fields at once.
25
+ export class MetricsStore {
26
+ private counters = new Map<string, number>()
27
+ private gauges = new Map<string, number>()
28
+ total = 0
29
+
30
+ constructor() {
31
+ makeAutoObservable(this, { counters: false, gauges: false })
32
+ }
33
+ }
34
+
35
+ // Not a store: a plain service class.
36
+ export class MetricsService {
37
+ private counters = new Map<string, number>()
38
+ }
39
+
40
+ // Module-level constant, not a store field.
41
+ export const REGISTRY = new Map<string, string>()
@@ -0,0 +1,67 @@
1
+ // Every runInAction call in this file MUST be reported by `store-no-runinaction`.
2
+ import { makeAutoObservable, runInAction } from "mobx"
3
+
4
+ export class GitStore {
5
+ loading = false
6
+ branch: string | null = null
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ async refresh(dir: string): Promise<void> {
13
+ runInAction(() => {
14
+ this.loading = true
15
+ })
16
+ const branch = await readBranch(dir)
17
+ runInAction(() => {
18
+ this.branch = branch
19
+ this.loading = false
20
+ })
21
+ }
22
+
23
+ // Nested inside a callback — still inside the store class.
24
+ subscribe(): void {
25
+ onChange(() => {
26
+ runInAction(() => {
27
+ this.branch = null
28
+ })
29
+ })
30
+ }
31
+ }
32
+
33
+ // The generic-argument spelling of the annotation. A `pattern:
34
+ // makeAutoObservable($$$)` does not match a call carrying explicit type
35
+ // arguments, which is how SiteCraft's project.store.ts — 7 real call sites —
36
+ // went missing from the first version of this rule.
37
+ export class ProjectStore {
38
+ private _buildTimer: number | null = null
39
+ projects: string[] = []
40
+
41
+ constructor() {
42
+ makeAutoObservable<this, "_buildTimer">(this, { _buildTimer: false })
43
+ }
44
+
45
+ async load(): Promise<void> {
46
+ const list = await fetchProjects()
47
+ runInAction(() => {
48
+ this.projects = list
49
+ })
50
+ }
51
+ }
52
+
53
+ // `makeObservable` is the other spelling of the same thing.
54
+ export class SessionStore {
55
+ items: string[] = []
56
+
57
+ constructor() {
58
+ makeObservable(this, { items: observable })
59
+ }
60
+
61
+ async pull(): Promise<void> {
62
+ const items = await fetchItems()
63
+ runInAction(() => {
64
+ this.items = items
65
+ })
66
+ }
67
+ }
@@ -0,0 +1,67 @@
1
+ // Every runInAction call in this file MUST be reported by `store-no-runinaction`.
2
+ import { makeAutoObservable, runInAction } from "mobx"
3
+
4
+ export class GitStore {
5
+ loading = false
6
+ branch: string | null = null
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ async refresh(dir: string): Promise<void> {
13
+ runInAction(() => {
14
+ this.loading = true
15
+ })
16
+ const branch = await readBranch(dir)
17
+ runInAction(() => {
18
+ this.branch = branch
19
+ this.loading = false
20
+ })
21
+ }
22
+
23
+ // Nested inside a callback — still inside the store class.
24
+ subscribe(): void {
25
+ onChange(() => {
26
+ runInAction(() => {
27
+ this.branch = null
28
+ })
29
+ })
30
+ }
31
+ }
32
+
33
+ // The generic-argument spelling of the annotation. A `pattern:
34
+ // makeAutoObservable($$$)` does not match a call carrying explicit type
35
+ // arguments, which is how SiteCraft's project.store.ts — 7 real call sites —
36
+ // went missing from the first version of this rule.
37
+ export class ProjectStore {
38
+ private _buildTimer: number | null = null
39
+ projects: string[] = []
40
+
41
+ constructor() {
42
+ makeAutoObservable<this, "_buildTimer">(this, { _buildTimer: false })
43
+ }
44
+
45
+ async load(): Promise<void> {
46
+ const list = await fetchProjects()
47
+ runInAction(() => {
48
+ this.projects = list
49
+ })
50
+ }
51
+ }
52
+
53
+ // `makeObservable` is the other spelling of the same thing.
54
+ export class SessionStore {
55
+ items: string[] = []
56
+
57
+ constructor() {
58
+ makeObservable(this, { items: observable })
59
+ }
60
+
61
+ async pull(): Promise<void> {
62
+ const items = await fetchItems()
63
+ runInAction(() => {
64
+ this.items = items
65
+ })
66
+ }
67
+ }
@@ -0,0 +1,39 @@
1
+ // NOTHING in this file may be reported by `store-no-runinaction`.
2
+ import { flow, makeAutoObservable } from "mobx"
3
+
4
+ // The fix: a flow. Every yield point resumes inside an action, so there is
5
+ // nothing left for runInAction to wrap.
6
+ export class GitStore {
7
+ loading = false
8
+ branch: string | null = null
9
+
10
+ constructor() {
11
+ makeAutoObservable(this)
12
+ }
13
+
14
+ refresh = flow(function* (this: GitStore, dir: string) {
15
+ this.loading = true
16
+ this.branch = yield readBranch(dir)
17
+ this.loading = false
18
+ })
19
+ }
20
+
21
+ // A plain class with no MobX annotation is not a store. `runInAction` here
22
+ // would be pointless rather than banned, and this rule is about stores.
23
+ export class PlainCache {
24
+ entries: string[] = []
25
+
26
+ add(entry: string): void {
27
+ this.entries.push(entry)
28
+ }
29
+ }
30
+
31
+ // A module-level helper that is not inside any store class.
32
+ export function describeBranch(branch: string): string {
33
+ return `on ${branch}`
34
+ }
35
+
36
+ // Importing the symbol is not calling it. The report's "66 runInAction in
37
+ // SiteCraft" counted 7 import lines exactly like this one; the real call-site
38
+ // count is 59.
39
+ export { runInAction } from "mobx"
@@ -0,0 +1,39 @@
1
+ // NOTHING in this file may be reported by `store-no-runinaction`.
2
+ import { flow, makeAutoObservable } from "mobx"
3
+
4
+ // The fix: a flow. Every yield point resumes inside an action, so there is
5
+ // nothing left for runInAction to wrap.
6
+ export class GitStore {
7
+ loading = false
8
+ branch: string | null = null
9
+
10
+ constructor() {
11
+ makeAutoObservable(this)
12
+ }
13
+
14
+ refresh = flow(function* (this: GitStore, dir: string) {
15
+ this.loading = true
16
+ this.branch = yield readBranch(dir)
17
+ this.loading = false
18
+ })
19
+ }
20
+
21
+ // A plain class with no MobX annotation is not a store. `runInAction` here
22
+ // would be pointless rather than banned, and this rule is about stores.
23
+ export class PlainCache {
24
+ entries: string[] = []
25
+
26
+ add(entry: string): void {
27
+ this.entries.push(entry)
28
+ }
29
+ }
30
+
31
+ // A module-level helper that is not inside any store class.
32
+ export function describeBranch(branch: string): string {
33
+ return `on ${branch}`
34
+ }
35
+
36
+ // Importing the symbol is not calling it. The report's "66 runInAction in
37
+ // SiteCraft" counted 7 import lines exactly like this one; the real call-site
38
+ // count is 59.
39
+ export { runInAction } from "mobx"
@@ -0,0 +1,19 @@
1
+ // Every setInterval call here MUST be reported by `store-no-setinterval-poll`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class LiveDataStore {
5
+ rows: Row[] = []
6
+ private timer: ReturnType<typeof setInterval> | undefined
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ start(): void {
13
+ this.timer = setInterval(() => void this.refresh(), POLL_MS)
14
+ }
15
+
16
+ startSynthetic(sink: Sink): void {
17
+ setInterval(() => sink.onRow(this.emitSyntheticRow()), SSE_TICK_MS)
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ // Every setInterval call here MUST be reported by `store-no-setinterval-poll`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class LiveDataStore {
5
+ rows: Row[] = []
6
+ private timer: ReturnType<typeof setInterval> | undefined
7
+
8
+ constructor() {
9
+ makeAutoObservable(this)
10
+ }
11
+
12
+ start(): void {
13
+ this.timer = setInterval(() => void this.refresh(), POLL_MS)
14
+ }
15
+
16
+ startSynthetic(sink: Sink): void {
17
+ setInterval(() => sink.onRow(this.emitSyntheticRow()), SSE_TICK_MS)
18
+ }
19
+ }
@@ -0,0 +1,29 @@
1
+ // NOTHING in this file may be reported by `store-no-setinterval-poll`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class LiveDataStore {
5
+ rows: Row[] = []
6
+
7
+ // A TYPE position, not a call. The commonest way `setInterval` appears in a
8
+ // store field declaration, and it must stay silent.
9
+ private timer: ReturnType<typeof setInterval> | undefined
10
+
11
+ constructor() {
12
+ makeAutoObservable(this)
13
+ }
14
+
15
+ // The fix: the Query object owns the disposer, the in-flight dedup and the
16
+ // visibility handling.
17
+ readonly rowsQuery = new Query(() => fetchRows(), { refetchInterval: POLL_MS })
18
+
19
+ stop(): void {
20
+ clearInterval(this.timer)
21
+ }
22
+ }
23
+
24
+ // Not a store: a plain scheduler class.
25
+ export class Ticker {
26
+ start(): void {
27
+ setInterval(() => this.tick(), 1000)
28
+ }
29
+ }
@@ -0,0 +1,29 @@
1
+ // NOTHING in this file may be reported by `store-no-setinterval-poll`.
2
+ import { makeAutoObservable } from "mobx"
3
+
4
+ export class LiveDataStore {
5
+ rows: Row[] = []
6
+
7
+ // A TYPE position, not a call. The commonest way `setInterval` appears in a
8
+ // store field declaration, and it must stay silent.
9
+ private timer: ReturnType<typeof setInterval> | undefined
10
+
11
+ constructor() {
12
+ makeAutoObservable(this)
13
+ }
14
+
15
+ // The fix: the Query object owns the disposer, the in-flight dedup and the
16
+ // visibility handling.
17
+ readonly rowsQuery = new Query(() => fetchRows(), { refetchInterval: POLL_MS })
18
+
19
+ stop(): void {
20
+ clearInterval(this.timer)
21
+ }
22
+ }
23
+
24
+ // Not a store: a plain scheduler class.
25
+ export class Ticker {
26
+ start(): void {
27
+ setInterval(() => this.tick(), 1000)
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ // Every match chain here MUST be reported by `ts-pattern-dangling-match`.
2
+ import { match } from "ts-pattern"
3
+
4
+ // A real observatory chain with its terminator deleted: evaluates to a builder
5
+ // object, which is truthy, so the caller keeps compiling and every arm is dead.
6
+ export function label(r: Resource<string>): string {
7
+ return match(r)
8
+ .with({ kind: "idle" }, () => "idle")
9
+ .with({ kind: "ready" }, ({ value }) => value)
10
+ }
11
+
12
+ // The hard case: a dangling chain nested inside a callback of an OUTER
13
+ // terminated chain. The spine-walk must not let the outer terminator mask it.
14
+ export function label3(r: Resource<Item[]>): string {
15
+ return match(r)
16
+ .with({ kind: "ready" }, ({ value }) =>
17
+ value.map((it) => match(it).with({ t: "a" }, () => "A")).join(","),
18
+ )
19
+ .otherwise(() => "")
20
+ }
@@ -0,0 +1,35 @@
1
+ // NOTHING in this file may be reported by `ts-pattern-dangling-match`.
2
+ import { match } from "ts-pattern"
3
+
4
+ export function label(r: Resource<string>): string {
5
+ return match(r)
6
+ .with({ kind: "idle" }, () => "idle")
7
+ .with({ kind: "ready" }, ({ value }) => value)
8
+ .exhaustive()
9
+ }
10
+
11
+ // `.otherwise()` and `.run()` terminate a chain too. Whether `.exhaustive()`
12
+ // should be PREFERRED over `.otherwise()` is a separate question the owner has
13
+ // deferred (decision #4); this rule is only about a chain that ends nowhere.
14
+ export function label2(r: Resource<string>): string {
15
+ return match(r).with({ kind: "idle" }, () => "idle").otherwise(() => "?")
16
+ }
17
+
18
+ export function label4(r: Resource<string>): string {
19
+ return match(r).with({ kind: "idle" }, () => "idle").run()
20
+ }
21
+
22
+ // Per-arm comments are why the terminator is matched structurally rather than
23
+ // with a surface pattern: `smart` strictness compares comment nodes too.
24
+ export function label5(r: Resource<string>): string {
25
+ return match(r)
26
+ // the boring case
27
+ .with({ kind: "idle" }, () => "idle")
28
+ // the interesting one
29
+ .with({ kind: "ready" }, ({ value }) => value)
30
+ .exhaustive()
31
+ }
32
+
33
+ // String.prototype.match is not ts-pattern and must never be confused for it.
34
+ export const m = "abc".match(/b/)
35
+ export const m2 = someText.match(pattern)?.[0]
@@ -0,0 +1,13 @@
1
+ // Every import in this file MUST be reported by `ui-imports-app-store`.
2
+ // Path matters: the rule is scoped to `**/src/ui/**`, so this fixture has to
3
+ // live under `src/ui/` for the rule to see it at all.
4
+
5
+ import { galleryStore } from "../../stores/galleryStore"
6
+ import { GalleryStore } from "../../stores/galleryStore"
7
+ import { rootStore } from "../../store/rootStore"
8
+ import { useStore } from "../../stores/StoreContext"
9
+ import photoStore from "../../stores/photoStore"
10
+
11
+ export const Panel = ({ label }: { label: string }) => (
12
+ <div>{label}{galleryStore.n}{GalleryStore.name}{rootStore.n}{useStore.name}{photoStore.n}</div>
13
+ )
@@ -0,0 +1,23 @@
1
+ // NOTHING in this file may be reported by `ui-imports-app-store`.
2
+
3
+ // X7 (report §2): a reusable UI component typed against the view-model
4
+ // interface the store implements is exactly what the mobx-models view-model
5
+ // pattern prescribes. `import type` erases at build time — the component
6
+ // depends on a shape, not on an app store — so it must not trip the rule that
7
+ // bans importing the store itself.
8
+ import type { IGalleryModel } from "../../stores/galleryStore"
9
+ import type { RootStore } from "../../store/rootStore"
10
+ import type Photo from "../../stores/photo"
11
+ import type * as StoreTypes from "../../stores/types"
12
+ import { type IUiModel, type ITabModel } from "../../stores/uiStore"
13
+
14
+ // Ordinary UI-package dependencies.
15
+ import { cn } from "../../lib/cn"
16
+
17
+ export const Panel = ({
18
+ model,
19
+ ui,
20
+ }: {
21
+ model: IGalleryModel
22
+ ui: IUiModel
23
+ }) => <div className={cn("panel")}>{model.title} {ui.tab}</div>
@@ -0,0 +1,16 @@
1
+ // BAD: a view that decides. Hook, match, ternary — all three belong upstairs.
2
+ import { useState } from "react";
3
+ import { match } from "ts-pattern";
4
+
5
+ export function StatusBadge(props: { readonly status: "ok" | "bad"; readonly label: string }) {
6
+ const [open, setOpen] = useState(false);
7
+ const tone = match(props.status)
8
+ .with("ok", () => "green")
9
+ .with("bad", () => "red")
10
+ .exhaustive();
11
+ return (
12
+ <span onClick={() => setOpen(true)} style={{ color: tone }}>
13
+ {open ? props.label : ""}
14
+ </span>
15
+ );
16
+ }
@@ -0,0 +1,7 @@
1
+ // GOOD: props in, JSX out. The tone was decided upstairs; useTheme is exempt.
2
+ import { useTheme } from "./theme";
3
+
4
+ export function StatusBadge(props: { readonly tone: string; readonly label: string }) {
5
+ const theme = useTheme();
6
+ return <span style={{ color: props.tone, fontFamily: theme.font }}>{props.label}</span>;
7
+ }
@@ -0,0 +1,13 @@
1
+ // Every import in this file MUST be reported by `view-imports-store`.
2
+ // A `*.view.tsx` is props in, JSX out — it may not reach a store by any route,
3
+ // not even the global rootStore that a screen is allowed to import.
4
+
5
+ import { rootStore } from "../../stores/rootStore"
6
+ import { galleryStore } from "../../stores/galleryStore"
7
+ import { GalleryStore } from "../../stores/galleryStore"
8
+ import { useStore } from "../../stores/StoreContext"
9
+ import { SESSION_KEY } from "../../stores/keys"
10
+
11
+ export const GalleryHeaderView = ({ title }: { title: string }) => (
12
+ <h1 data-k={SESSION_KEY}>{title}{rootStore.n}{galleryStore.n}{GalleryStore.name}{useStore.name}</h1>
13
+ )
@@ -0,0 +1,23 @@
1
+ // NOTHING in this file may be reported by `view-imports-store`.
2
+
3
+ // A view is typed against the model interface it renders (C40). The type
4
+ // vanishes at build time, so it is a contract, not a store dependency.
5
+ import type { IGalleryModel } from "../../stores/galleryStore"
6
+ import { type IUiModel } from "../../stores/uiStore"
7
+
8
+ // Everything else a view is allowed to touch: shared UI primitives and pure
9
+ // formatting helpers.
10
+ import { Badge } from "../../components/Badge/Badge"
11
+ import { formatBytes } from "../../lib/format"
12
+
13
+ export const GalleryHeaderView = ({
14
+ model,
15
+ ui,
16
+ }: {
17
+ model: IGalleryModel
18
+ ui: IUiModel
19
+ }) => (
20
+ <h1>
21
+ {model.title} <Badge label={formatBytes(model.size)} /> {ui.tab}
22
+ </h1>
23
+ )
@@ -0,0 +1,10 @@
1
+ // Every call here MUST be reported by `zod-optional-nullable`.
2
+ import { z } from "zod"
3
+ export const A = z.object({ name: z.string().optional() })
4
+ export const B = z.object({ age: z.number().nullable() })
5
+ export const C = z.object({ bio: z.string().nullish() })
6
+ // ADDED BY THE WAVE-2 UPGRADE: the old `z.$T().optional()` matched only a
7
+ // DIRECT call on `z`, so a chained builder and a named schema both escaped.
8
+ export const D = z.object({ name: z.string().min(1).optional() })
9
+ export const E = UserSchema.optional()
10
+ export const F = z.object({ tag: z.enum(["a", "b"]).trim().nullable() })
@@ -0,0 +1,17 @@
1
+ // Every call here MUST be reported by `zod-optional-nullable-tsx`.
2
+ // The .tsx twin of bad.ts, placed where this rule actually needed to see:
3
+ // a validation schema sitting right next to the component that uses it.
4
+ import { z } from "zod"
5
+
6
+ export const A = z.object({ name: z.string().optional() })
7
+ export const B = z.object({ age: z.number().nullable() })
8
+ export const C = z.object({ bio: z.string().nullish() })
9
+ // ADDED BY THE WAVE-2 UPGRADE: the old `z.$T().optional()` matched only a
10
+ // DIRECT call on `z`, so a chained builder and a named schema both escaped.
11
+ export const D = z.object({ name: z.string().min(1).optional() })
12
+ export const E = UserSchema.optional()
13
+ export const F = z.object({ tag: z.enum(["a", "b"]).trim().nullable() })
14
+
15
+ export const ProfileForm = () => {
16
+ return <form aria-label="profile">{A.shape.name.description}</form>
17
+ }
@@ -0,0 +1,16 @@
1
+ // NOTHING in this file may be reported by `zod-optional-nullable`.
2
+ import { z } from "zod"
3
+
4
+ // The fix: model the absence as a variant, so "missing" and "present" are
5
+ // distinguishable states rather than one field that might be undefined.
6
+ export const User = z.discriminatedUnion("status", [
7
+ z.object({ status: z.literal("anonymous") }),
8
+ z.object({ status: z.literal("named"), name: z.string() }),
9
+ ])
10
+
11
+ export const Plain = z.object({ name: z.string(), age: z.number() })
12
+
13
+ // `.optional()` on something that is not a zod schema — the receiver anchor is
14
+ // what keeps an unrelated builder with the same method name out of this rule.
15
+ export const q = queryBuilder.optional()
16
+ export const p = protoField.nullable()
@@ -0,0 +1,20 @@
1
+ // NOTHING in this file may be reported by `zod-optional-nullable-tsx`.
2
+ import { z } from "zod"
3
+
4
+ // The fix: model the absence as a variant, so "missing" and "present" are
5
+ // distinguishable states rather than one field that might be undefined.
6
+ export const User = z.discriminatedUnion("status", [
7
+ z.object({ status: z.literal("anonymous") }),
8
+ z.object({ status: z.literal("named"), name: z.string() }),
9
+ ])
10
+
11
+ export const Plain = z.object({ name: z.string(), age: z.number() })
12
+
13
+ // `.optional()` on something that is not a zod schema — the receiver anchor is
14
+ // what keeps an unrelated builder with the same method name out of this rule.
15
+ export const q = queryBuilder.optional()
16
+ export const p = protoField.nullable()
17
+
18
+ export const ProfileForm = () => {
19
+ return <form aria-label="profile">{Plain.shape.name.description}</form>
20
+ }