@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,104 @@
1
+ # ── the .tsx half of `no-local-kit-clone` ──
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 `no-local-kit-clone.yml`.
10
+ # Suppressing needs THIS id: `ast-grep-ignore: no-local-kit-clone-tsx`.
11
+ #
12
+ # ── wave 1, store rule 6 of 7 ──────────────────────────────────────────
13
+ #
14
+ # Report §13 step 1: the same handful of primitives is redeclared in repo after
15
+ # repo — 7 local `Resource<T>` unions, 6 copies of `diagnostics.ts` inside a
16
+ # single monorepo, a `usePageStore` improvised on `useMemo` with no dispose.
17
+ # They drift: the discriminator alone is spelled `kind` in three repos, `status`
18
+ # in two and `state` in one, so the "same" type does not typecheck against
19
+ # itself across a boundary.
20
+ #
21
+ # TWO CARVE-OUTS, both deliberate, because an error-tier rule that fires on the
22
+ # canonical definition is worse than no rule:
23
+ #
24
+ # 1. The kit package itself. `@agentvibes/kit` DOES NOT EXIST YET — the glob
25
+ # below currently guards a path with nothing behind it, and is here so the
26
+ # package can be created without immediately tripping its own rule. Until it
27
+ # is published, this rule names a destination that cannot yet be imported;
28
+ # that is the honest state of it, and the reason it is worth having anyway
29
+ # is to stop the count of copies growing while the package is built.
30
+ # 2. `packages/observable-query/` in Retouch4Me — the reference implementation
31
+ # being promoted INTO the kit (§14). Telling it to import itself would be
32
+ # nonsense.
33
+ #
34
+ # `Resource` must carry type parameters to match. Without that the rule hits
35
+ # `interface Resource { name; url; type; description }` in VolumePhotography — a
36
+ # page listing conference links, nothing to do with async state.
37
+ id: no-local-kit-clone-tsx
38
+ language: tsx
39
+ severity: error
40
+ ignores:
41
+ - '**/node_modules/**'
42
+ - '**/packages/kit/**'
43
+ - '**/agentvibes-kit/**'
44
+ - '**/packages/observable-query/**'
45
+ - '**/*.test.tsx'
46
+ - '**/*.spec.tsx'
47
+ - '**/__tests__/**'
48
+ message: |
49
+ A local redeclaration of a primitive that belongs to the shared kit. Every
50
+ copy drifts — across this codebase the same async union is discriminated by
51
+ `kind` in three repos, `status` in two and `state` in one, so the "same" type
52
+ will not typecheck against itself across a package boundary, and a fix to one
53
+ copy reaches none of the others.
54
+ Import it instead:
55
+ Resource<T> / QueryState<T> → @agentvibes/kit/resource
56
+ usePageStore, QueryView, ShowWhen → @agentvibes/kit/react
57
+ warnDegraded, warnNotImplemented → @agentvibes/kit/diagnostics
58
+ If this IS the canonical definition — the kit package, or the implementation
59
+ being promoted into it — it belongs behind the path carve-outs at the top of
60
+ this rule rather than behind a suppression comment.
61
+ NOTE: `@agentvibes/kit` is not published yet, so today this rule freezes the
62
+ number of copies rather than offering an import you can actually write. Until
63
+ it ships, editing the declaration line of an existing copy needs a suppression
64
+ with a reason — the rule is here to stop copy number eight, not to claim the
65
+ migration is available.
66
+ rule:
67
+ any:
68
+ # type Resource<T> = … / type QueryState<T> = …
69
+ - all:
70
+ - kind: type_alias_declaration
71
+ - has:
72
+ field: name
73
+ regex: '^(Resource|QueryResource|QueryState)$'
74
+ # Type parameters required: an unrelated domain `interface Resource`
75
+ # naming a link or a document is not this.
76
+ - has:
77
+ field: type_parameters
78
+ kind: type_parameters
79
+ # class Resource<T> / class ObservableQuery<T>
80
+ - all:
81
+ - kind: class_declaration
82
+ - has:
83
+ field: name
84
+ regex: '^(Resource|ObservableQuery|QueryResource)$'
85
+ - has:
86
+ field: type_parameters
87
+ kind: type_parameters
88
+ # function warnDegraded(…) / function usePageStore(…)
89
+ - all:
90
+ - kind: function_declaration
91
+ - has:
92
+ field: name
93
+ regex: '^(warnDegraded|warnNotImplemented|usePageStore|resetDiagnostics)$'
94
+ # const usePageStore = (…) => … / const warnDegraded = function (…) …
95
+ - all:
96
+ - kind: variable_declarator
97
+ - has:
98
+ field: name
99
+ regex: '^(warnDegraded|warnNotImplemented|usePageStore|resetDiagnostics)$'
100
+ - has:
101
+ field: value
102
+ any:
103
+ - kind: arrow_function
104
+ - kind: function_expression
@@ -0,0 +1,93 @@
1
+ # ── wave 1, store rule 6 of 7 ──────────────────────────────────────────
2
+ #
3
+ # Report §13 step 1: the same handful of primitives is redeclared in repo after
4
+ # repo — 7 local `Resource<T>` unions, 6 copies of `diagnostics.ts` inside a
5
+ # single monorepo, a `usePageStore` improvised on `useMemo` with no dispose.
6
+ # They drift: the discriminator alone is spelled `kind` in three repos, `status`
7
+ # in two and `state` in one, so the "same" type does not typecheck against
8
+ # itself across a boundary.
9
+ #
10
+ # TWO CARVE-OUTS, both deliberate, because an error-tier rule that fires on the
11
+ # canonical definition is worse than no rule:
12
+ #
13
+ # 1. The kit package itself. `@agentvibes/kit` DOES NOT EXIST YET — the glob
14
+ # below currently guards a path with nothing behind it, and is here so the
15
+ # package can be created without immediately tripping its own rule. Until it
16
+ # is published, this rule names a destination that cannot yet be imported;
17
+ # that is the honest state of it, and the reason it is worth having anyway
18
+ # is to stop the count of copies growing while the package is built.
19
+ # 2. `packages/observable-query/` in Retouch4Me — the reference implementation
20
+ # being promoted INTO the kit (§14). Telling it to import itself would be
21
+ # nonsense.
22
+ #
23
+ # `Resource` must carry type parameters to match. Without that the rule hits
24
+ # `interface Resource { name; url; type; description }` in VolumePhotography — a
25
+ # page listing conference links, nothing to do with async state.
26
+ id: no-local-kit-clone
27
+ language: typescript
28
+ severity: error
29
+ ignores:
30
+ - '**/node_modules/**'
31
+ - '**/packages/kit/**'
32
+ - '**/agentvibes-kit/**'
33
+ - '**/packages/observable-query/**'
34
+ - '**/*.test.ts'
35
+ - '**/*.spec.ts'
36
+ - '**/__tests__/**'
37
+ message: |
38
+ A local redeclaration of a primitive that belongs to the shared kit. Every
39
+ copy drifts — across this codebase the same async union is discriminated by
40
+ `kind` in three repos, `status` in two and `state` in one, so the "same" type
41
+ will not typecheck against itself across a package boundary, and a fix to one
42
+ copy reaches none of the others.
43
+ Import it instead:
44
+ Resource<T> / QueryState<T> → @agentvibes/kit/resource
45
+ usePageStore, QueryView, ShowWhen → @agentvibes/kit/react
46
+ warnDegraded, warnNotImplemented → @agentvibes/kit/diagnostics
47
+ If this IS the canonical definition — the kit package, or the implementation
48
+ being promoted into it — it belongs behind the path carve-outs at the top of
49
+ this rule rather than behind a suppression comment.
50
+ NOTE: `@agentvibes/kit` is not published yet, so today this rule freezes the
51
+ number of copies rather than offering an import you can actually write. Until
52
+ it ships, editing the declaration line of an existing copy needs a suppression
53
+ with a reason — the rule is here to stop copy number eight, not to claim the
54
+ migration is available.
55
+ rule:
56
+ any:
57
+ # type Resource<T> = … / type QueryState<T> = …
58
+ - all:
59
+ - kind: type_alias_declaration
60
+ - has:
61
+ field: name
62
+ regex: '^(Resource|QueryResource|QueryState)$'
63
+ # Type parameters required: an unrelated domain `interface Resource`
64
+ # naming a link or a document is not this.
65
+ - has:
66
+ field: type_parameters
67
+ kind: type_parameters
68
+ # class Resource<T> / class ObservableQuery<T>
69
+ - all:
70
+ - kind: class_declaration
71
+ - has:
72
+ field: name
73
+ regex: '^(Resource|ObservableQuery|QueryResource)$'
74
+ - has:
75
+ field: type_parameters
76
+ kind: type_parameters
77
+ # function warnDegraded(…) / function usePageStore(…)
78
+ - all:
79
+ - kind: function_declaration
80
+ - has:
81
+ field: name
82
+ regex: '^(warnDegraded|warnNotImplemented|usePageStore|resetDiagnostics)$'
83
+ # const usePageStore = (…) => … / const warnDegraded = function (…) …
84
+ - all:
85
+ - kind: variable_declarator
86
+ - has:
87
+ field: name
88
+ regex: '^(warnDegraded|warnNotImplemented|usePageStore|resetDiagnostics)$'
89
+ - has:
90
+ field: value
91
+ any:
92
+ - kind: arrow_function
93
+ - kind: function_expression
@@ -0,0 +1,34 @@
1
+ # ── rule body adopted from faceless-photo-lib `ts-pattern-prefer-exhaustive` (wave 2 triage) ──
2
+ # The two rules were duplicates; the faceless implementation is strictly better,
3
+ # so the canon id keeps its message and takes that body. Why it is better here:
4
+ # the old `pattern: $X.otherwise($$$)` fired on ANY `.otherwise()` method, not just
5
+ # a ts-pattern chain — a false-positive class. Anchoring to a `match()` chain
6
+ # removes it. Measured: +1 finding, 0 lost.
7
+ #
8
+ id: non-exhaustive-match
9
+ language: typescript
10
+ severity: warning
11
+ message: |
12
+ `.otherwise()` catches unmatched variants silently. Prefer `.exhaustive()` so adding a
13
+ new union variant becomes a compile error everywhere it's branched on.
14
+ See defensive-errors Rule 2 and pattern-matching skill.
15
+ rule:
16
+ all:
17
+ - kind: call_expression
18
+ has:
19
+ field: function
20
+ kind: identifier
21
+ regex: "^match$"
22
+ - inside:
23
+ stopBy:
24
+ not:
25
+ any:
26
+ - kind: member_expression
27
+ - kind: call_expression
28
+ kind: call_expression
29
+ has:
30
+ field: function
31
+ kind: member_expression
32
+ has:
33
+ field: property
34
+ regex: "^otherwise$"
@@ -0,0 +1,47 @@
1
+ # ── simple-components, rule 2 of 4 ─────────────────────────────────────
2
+ #
3
+ # A page store built by a bare `useMemo` has no `dispose()` and no protection
4
+ # against StrictMode's double-mount, so its reactions and subscriptions outlive
5
+ # the page that opened it. Report §8: the `PageStoreManager` the mobx-models
6
+ # skill prescribes exists in exactly one repo of the park, and everywhere else
7
+ # page stores are hand-rolled on `useMemo` — one of them carrying the comment
8
+ # "PageStoreManager is not yet wired — use direct useMemo".
9
+ #
10
+ # The canon rule `no-local-kit-clone` bans DECLARING your own `usePageStore`;
11
+ # this bans skipping it. Different sites, no overlap.
12
+ #
13
+ # The .tsx half: ast-grep's `typescript` and `tsx` languages are disjoint, so the
14
+ # same rule ships twice under two ids. This is the half that matters most — page
15
+ # stores are created in components. Suppress with the id the finding PRINTS.
16
+ id: page-store-bare-usememo-tsx
17
+ language: tsx
18
+ severity: error
19
+ ignores:
20
+ - '**/*.test.tsx'
21
+ - '**/*.spec.tsx'
22
+ - '**/__tests__/**'
23
+ message: |
24
+ A store constructed inside a bare `useMemo`. `useMemo` is a cache, not a
25
+ lifecycle: React may drop and rebuild the value, and it never tells anyone the
26
+ old one is dead — so the store's reactions, intervals and subscriptions keep
27
+ running, and StrictMode's double-mount builds two of them.
28
+ Use the sanctioned hook, which owns creation, ref-counting and disposal:
29
+ const store = usePageStore(() => new GalleryPageStore(rootStore, slug), [slug])
30
+ It ships in `@agentvibes/kit/react`. Pass dependencies to the constructor
31
+ explicitly so the store stays testable with `new GalleryPageStore(fake, "x")`
32
+ and no render at all.
33
+ See the simple-components skill (§store access) and report §8.
34
+ rule:
35
+ all:
36
+ - kind: call_expression
37
+ - has:
38
+ field: function
39
+ kind: identifier
40
+ regex: '^useMemo$'
41
+ - has:
42
+ stopBy: end
43
+ kind: new_expression
44
+ has:
45
+ field: constructor
46
+ kind: identifier
47
+ regex: 'Store$'
@@ -0,0 +1,43 @@
1
+ # ── simple-components, rule 2 of 4 ─────────────────────────────────────
2
+ #
3
+ # A page store built by a bare `useMemo` has no `dispose()` and no protection
4
+ # against StrictMode's double-mount, so its reactions and subscriptions outlive
5
+ # the page that opened it. Report §8: the `PageStoreManager` the mobx-models
6
+ # skill prescribes exists in exactly one repo of the park, and everywhere else
7
+ # page stores are hand-rolled on `useMemo` — one of them carrying the comment
8
+ # "PageStoreManager is not yet wired — use direct useMemo".
9
+ #
10
+ # The canon rule `no-local-kit-clone` bans DECLARING your own `usePageStore`;
11
+ # this bans skipping it. Different sites, no overlap.
12
+ id: page-store-bare-usememo
13
+ language: typescript
14
+ severity: error
15
+ ignores:
16
+ - '**/*.test.ts'
17
+ - '**/*.spec.ts'
18
+ - '**/__tests__/**'
19
+ message: |
20
+ A store constructed inside a bare `useMemo`. `useMemo` is a cache, not a
21
+ lifecycle: React may drop and rebuild the value, and it never tells anyone the
22
+ old one is dead — so the store's reactions, intervals and subscriptions keep
23
+ running, and StrictMode's double-mount builds two of them.
24
+ Use the sanctioned hook, which owns creation, ref-counting and disposal:
25
+ const store = usePageStore(() => new GalleryPageStore(rootStore, slug), [slug])
26
+ It ships in `@agentvibes/kit/react`. Pass dependencies to the constructor
27
+ explicitly so the store stays testable with `new GalleryPageStore(fake, "x")`
28
+ and no render at all.
29
+ See the simple-components skill (§store access) and report §8.
30
+ rule:
31
+ all:
32
+ - kind: call_expression
33
+ - has:
34
+ field: function
35
+ kind: identifier
36
+ regex: '^useMemo$'
37
+ - has:
38
+ stopBy: end
39
+ kind: new_expression
40
+ has:
41
+ field: constructor
42
+ kind: identifier
43
+ regex: 'Store$'
@@ -0,0 +1,29 @@
1
+ # ── simple-components, rule 4 of 4 ─────────────────────────────────────
2
+ #
3
+ # The logic half of the two-tier split (decision D8, report §9): a screen picks
4
+ # the models, matches the state and composes views. It carries no styling.
5
+ #
6
+ # Scoped to `**/*.screen.tsx` and nothing else, deliberately. The contract says
7
+ # "logic components carry no styles", but there is no mechanical way to tell an
8
+ # unmarked `.tsx` logic component from a plain one, and a blanket ban on
9
+ # `className` would fire on every file of a Tailwind app. `*.screen.tsx` is the
10
+ # one filename the convention makes explicit, so it is the one place the rule can
11
+ # be right without a manifest.
12
+ #
13
+ # WARNING TIER: same reason as `view-file-logic` — D8 is recommendation-tier.
14
+ id: screen-file-styling
15
+ language: tsx
16
+ severity: warning
17
+ files:
18
+ - '**/*.screen.tsx'
19
+ message: |
20
+ Styling on a screen. A screen's whole job is choosing models, matching state
21
+ and composing views; the moment it carries `className` or `style` it has taken
22
+ on a second job and starts growing the layout it should have delegated.
23
+ Move the markup into a `*.view.tsx` (or a shared UI primitive) and let the
24
+ screen pass it data.
25
+ See the simple-components skill (§two-tier) and report §9.
26
+ rule:
27
+ all:
28
+ - kind: jsx_attribute
29
+ - regex: '^(className|style)='
@@ -0,0 +1,17 @@
1
+ id: silent-default-return
2
+ language: typescript
3
+ severity: warning
4
+ message: |
5
+ Silent-default return on negation. Hides upstream bugs.
6
+ Throw with a contextual message, or return an explicit discriminated failure variant.
7
+ See defensive-errors Rule 1.
8
+ rule:
9
+ any:
10
+ - pattern: "if (!$X) return null"
11
+ - pattern: "if (!$X) return []"
12
+ - pattern: "if (!$X) return 0"
13
+ - pattern: "if (!$X) return \"\""
14
+ - pattern: "if ($X === undefined) return null"
15
+ - pattern: "if ($X === undefined) return []"
16
+ - pattern: "if ($X == null) return null"
17
+ - pattern: "if ($X == null) return []"
@@ -0,0 +1,102 @@
1
+ # ── the .tsx half of `state-loading-boolean-shape` ──
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 `state-loading-boolean-shape.yml`.
10
+ # Suppressing needs THIS id: `ast-grep-ignore: state-loading-boolean-shape-tsx`.
11
+ #
12
+ # ── wave 1, store rule 4 of 7 ──────────────────────────────────────────
13
+ #
14
+ # The boolean-soup async shape: `{ loading: boolean, data?: T, error?: E }`.
15
+ # Four booleans' worth of states, of which most are nonsense — loading AND
16
+ # error, loaded with no data, error with data — and nothing stops any of them
17
+ # being constructed. It is the exact shape `Resource<T>` exists to replace.
18
+ #
19
+ # SCOPE, measured rather than guessed. The naive test — "a `loading: boolean`
20
+ # field" — hits 71 sites park-wide, and most are legitimate: a `LoadMoreButton`
21
+ # taking `loading?: boolean` as a prop is one boolean describing one thing, and
22
+ # there is no invalid state to make unrepresentable. What makes the shape a
23
+ # defect is that the async OUTCOME is recorded in more than one independently
24
+ # settable place. So the rule requires a second field: another outcome boolean
25
+ # (`isLoaded`, `isError`, `success`…), or a payload/error field that is optional
26
+ # or null-unioned. A lone `loading?: boolean` stays silent, correctly.
27
+ #
28
+ # What that produces: 8 in `.ts` + 13 in `.tsx` = 21. The report's target of 114
29
+ # is a DIFFERENT population — the migration target for `Resource<T>` adoption,
30
+ # which also counts the 81 class fields spelled `loading = false` (a class
31
+ # property, not a type declaration) plus hook-local boolean pairs. This rule is
32
+ # scoped to type and interface declarations, so 21 is its honest number and the
33
+ # remaining ~93 are not silently claimed.
34
+ id: state-loading-boolean-shape-tsx
35
+ language: tsx
36
+ severity: error
37
+ ignores:
38
+ - '**/*.test.tsx'
39
+ - '**/*.spec.tsx'
40
+ - '**/__tests__/**'
41
+ utils:
42
+ bool-prop:
43
+ kind: property_signature
44
+ has:
45
+ stopBy: end
46
+ kind: predefined_type
47
+ regex: '^boolean$'
48
+ loading-flag:
49
+ all:
50
+ - matches: bool-prop
51
+ - has:
52
+ field: name
53
+ regex: '^(loading|isLoading|fetching|isFetching|busy|isBusy|pending|isPending)$'
54
+ # A SECOND place the same outcome is recorded. Deliberately excludes the
55
+ # loading family so a lone loading flag cannot satisfy both halves.
56
+ outcome-flag:
57
+ all:
58
+ - matches: bool-prop
59
+ - has:
60
+ field: name
61
+ regex: '^(loaded|isLoaded|error|isError|hasError|success|isSuccess|succeeded|failed|isFailed|done|isDone|ready|isReady)$'
62
+ # A payload or error that may or may not be there — the other way the shape
63
+ # admits "loaded but empty" and "errored but full".
64
+ payload-field:
65
+ kind: property_signature
66
+ all:
67
+ - has:
68
+ field: name
69
+ regex: '^(data|error|value|result|items|response|payload)$'
70
+ - any:
71
+ - regex: '\?\s*:'
72
+ - has:
73
+ stopBy: end
74
+ kind: union_type
75
+ regex: '(null|undefined)'
76
+ message: |
77
+ An async state recorded as a boolean plus optional payload. The type admits
78
+ states that cannot happen — loading with an error, done with no data, an
79
+ error carrying data — so every reader has to re-derive which combination is
80
+ real, and they disagree.
81
+ Use a discriminated union, where the payload lives in the branch that has it:
82
+ { loading: boolean; data?: T; error?: string }
83
+ → | { status: "idle" }
84
+ | { status: "loading" }
85
+ | { status: "ready"; data: T }
86
+ | { status: "error"; message: string }
87
+ Then `match(state).with({ status: "ready" }, ({ data }) => …).exhaustive()`
88
+ makes adding a state a build error instead of a silent fallthrough.
89
+ For fetch-shaped data prefer the shared Query/`Resource<T>` rather than
90
+ hand-rolling the union again. See defensive-errors Rule 1 and the
91
+ mobx-models skill.
92
+ rule:
93
+ all:
94
+ - any:
95
+ - kind: interface_body
96
+ - kind: object_type
97
+ - has:
98
+ matches: loading-flag
99
+ - has:
100
+ any:
101
+ - matches: outcome-flag
102
+ - matches: payload-field
@@ -0,0 +1,91 @@
1
+ # ── wave 1, store rule 4 of 7 ──────────────────────────────────────────
2
+ #
3
+ # The boolean-soup async shape: `{ loading: boolean, data?: T, error?: E }`.
4
+ # Four booleans' worth of states, of which most are nonsense — loading AND
5
+ # error, loaded with no data, error with data — and nothing stops any of them
6
+ # being constructed. It is the exact shape `Resource<T>` exists to replace.
7
+ #
8
+ # SCOPE, measured rather than guessed. The naive test — "a `loading: boolean`
9
+ # field" — hits 71 sites park-wide, and most are legitimate: a `LoadMoreButton`
10
+ # taking `loading?: boolean` as a prop is one boolean describing one thing, and
11
+ # there is no invalid state to make unrepresentable. What makes the shape a
12
+ # defect is that the async OUTCOME is recorded in more than one independently
13
+ # settable place. So the rule requires a second field: another outcome boolean
14
+ # (`isLoaded`, `isError`, `success`…), or a payload/error field that is optional
15
+ # or null-unioned. A lone `loading?: boolean` stays silent, correctly.
16
+ #
17
+ # What that produces: 8 in `.ts` + 13 in `.tsx` = 21. The report's target of 114
18
+ # is a DIFFERENT population — the migration target for `Resource<T>` adoption,
19
+ # which also counts the 81 class fields spelled `loading = false` (a class
20
+ # property, not a type declaration) plus hook-local boolean pairs. This rule is
21
+ # scoped to type and interface declarations, so 21 is its honest number and the
22
+ # remaining ~93 are not silently claimed.
23
+ id: state-loading-boolean-shape
24
+ language: typescript
25
+ severity: error
26
+ ignores:
27
+ - '**/*.test.ts'
28
+ - '**/*.spec.ts'
29
+ - '**/__tests__/**'
30
+ utils:
31
+ bool-prop:
32
+ kind: property_signature
33
+ has:
34
+ stopBy: end
35
+ kind: predefined_type
36
+ regex: '^boolean$'
37
+ loading-flag:
38
+ all:
39
+ - matches: bool-prop
40
+ - has:
41
+ field: name
42
+ regex: '^(loading|isLoading|fetching|isFetching|busy|isBusy|pending|isPending)$'
43
+ # A SECOND place the same outcome is recorded. Deliberately excludes the
44
+ # loading family so a lone loading flag cannot satisfy both halves.
45
+ outcome-flag:
46
+ all:
47
+ - matches: bool-prop
48
+ - has:
49
+ field: name
50
+ regex: '^(loaded|isLoaded|error|isError|hasError|success|isSuccess|succeeded|failed|isFailed|done|isDone|ready|isReady)$'
51
+ # A payload or error that may or may not be there — the other way the shape
52
+ # admits "loaded but empty" and "errored but full".
53
+ payload-field:
54
+ kind: property_signature
55
+ all:
56
+ - has:
57
+ field: name
58
+ regex: '^(data|error|value|result|items|response|payload)$'
59
+ - any:
60
+ - regex: '\?\s*:'
61
+ - has:
62
+ stopBy: end
63
+ kind: union_type
64
+ regex: '(null|undefined)'
65
+ message: |
66
+ An async state recorded as a boolean plus optional payload. The type admits
67
+ states that cannot happen — loading with an error, done with no data, an
68
+ error carrying data — so every reader has to re-derive which combination is
69
+ real, and they disagree.
70
+ Use a discriminated union, where the payload lives in the branch that has it:
71
+ { loading: boolean; data?: T; error?: string }
72
+ → | { status: "idle" }
73
+ | { status: "loading" }
74
+ | { status: "ready"; data: T }
75
+ | { status: "error"; message: string }
76
+ Then `match(state).with({ status: "ready" }, ({ data }) => …).exhaustive()`
77
+ makes adding a state a build error instead of a silent fallthrough.
78
+ For fetch-shaped data prefer the shared Query/`Resource<T>` rather than
79
+ hand-rolling the union again. See defensive-errors Rule 1 and the
80
+ mobx-models skill.
81
+ rule:
82
+ all:
83
+ - any:
84
+ - kind: interface_body
85
+ - kind: object_type
86
+ - has:
87
+ matches: loading-flag
88
+ - has:
89
+ any:
90
+ - matches: outcome-flag
91
+ - matches: payload-field
@@ -0,0 +1,108 @@
1
+ # ── the .tsx half of `store-async-method` ──
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.
7
+ #
8
+ # Keep this file's `rule:` block IDENTICAL to `store-async-method.yml`.
9
+ # Suppressing needs THIS id: `ast-grep-ignore: store-async-method-tsx`.
10
+ #
11
+ # ── wave 1, store rule 2 of 7 ──────────────────────────────────────────
12
+ #
13
+ # The other half of `store-no-runinaction`, and the reason that rule has so much
14
+ # to find: an `async` method's continuation after each `await` is NOT covered by
15
+ # the action `makeAutoObservable` created. Every assignment past the first await
16
+ # is an unbatched mutation outside an action.
17
+ #
18
+ # Deliberately does NOT fire when the method already contains `runInAction` —
19
+ # that method is reported by `store-no-runinaction` at `error`, and the fix is
20
+ # the same conversion. Two findings prescribing one change read as two problems,
21
+ # and the error-tier one is the one worth showing.
22
+ #
23
+ # So this rule catches the quieter half: the async store method that mutates
24
+ # without even the hand-patched `runInAction`, where nothing marks the hole.
25
+ id: store-async-method-tsx
26
+ language: tsx
27
+ severity: warning
28
+ ignores:
29
+ - '**/*.test.tsx'
30
+ - '**/*.spec.tsx'
31
+ - '**/__tests__/**'
32
+ utils:
33
+ store-class:
34
+ kind: class_declaration
35
+ has:
36
+ stopBy: end
37
+ kind: call_expression
38
+ has:
39
+ field: function
40
+ kind: identifier
41
+ regex: '^make(Auto)?Observable$'
42
+ contains-runinaction:
43
+ has:
44
+ stopBy: end
45
+ kind: call_expression
46
+ has:
47
+ field: function
48
+ kind: identifier
49
+ regex: '^runInAction$'
50
+ # An observable MUTATION — which is the whole defect. Without this clause the
51
+ # rule fires on any async method, and in SiteCraft 10 of its 12 hits were
52
+ # private I/O helpers (`walkDir`, `collectAllFiles`, `uploadFile`) that await
53
+ # a filesystem and never touch a field. Those have no action coverage to lose,
54
+ # so at warning tier they were pure noise.
55
+ mutates-this:
56
+ has:
57
+ stopBy: end
58
+ any:
59
+ # this.field = … / this.field += …
60
+ - all:
61
+ - any:
62
+ - kind: assignment_expression
63
+ - kind: augmented_assignment_expression
64
+ - has:
65
+ field: left
66
+ kind: member_expression
67
+ has:
68
+ field: object
69
+ kind: this
70
+ # this.list.push(…), this.map.set(…), … — mutation through a method.
71
+ - all:
72
+ - kind: call_expression
73
+ - has:
74
+ field: function
75
+ kind: member_expression
76
+ all:
77
+ - has:
78
+ field: property
79
+ regex: '^(push|pop|shift|unshift|splice|sort|reverse|set|delete|clear|add)$'
80
+ - has:
81
+ field: object
82
+ kind: member_expression
83
+ has:
84
+ field: object
85
+ kind: this
86
+ message: |
87
+ An `async` method on a store class. The action that `makeAutoObservable`
88
+ wrapped around it ends at the first `await` — everything after resumes
89
+ OUTSIDE it, so each assignment there is an unbatched mutation and observers
90
+ see the half-updated store between them.
91
+ Use a `flow`, which resumes inside an action at every yield:
92
+ async load() { this.x = await f(); this.y = await g() }
93
+ → load = flow(function* (this: Store) { this.x = yield f(); this.y = yield g() })
94
+ Scope: this fires only when the method actually MUTATES an observable —
95
+ assigns `this.field` or calls `push`/`set`/`delete` on one. A private async
96
+ helper that only does I/O and returns has no action coverage to lose and stays
97
+ silent.
98
+ See the mobx-models skill.
99
+ rule:
100
+ all:
101
+ - kind: method_definition
102
+ - regex: '^((private|public|protected|static|override|abstract)\s+)*async\s'
103
+ - inside:
104
+ stopBy: end
105
+ matches: store-class
106
+ - matches: mutates-this
107
+ - not:
108
+ matches: contains-runinaction