@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,60 @@
1
+ # ── wave 1, store rule 1 of 7 ──────────────────────────────────────────
2
+ #
3
+ # The report's gap number 1: the most-repeated store rule in the corpus had no
4
+ # mechanical check at all, and the one script that did grep for it
5
+ # (mobx-models/scripts/lint-mobx.sh) was wired to nothing and has since been
6
+ # deleted.
7
+ #
8
+ # Scoped by the CLASS, not by the path. `**/stores/**` would have been the
9
+ # obvious glob and it is the wrong one twice over: it misses a store that lives
10
+ # beside its feature, and it catches plain helper classes that happen to sit in
11
+ # the folder. "Has a make(Auto)Observable call in it" is the actual definition
12
+ # of the thing the rule is about.
13
+ #
14
+ # Note the callee is matched as an identifier rather than with
15
+ # `pattern: makeAutoObservable($$$)`. A pattern does not match a call carrying
16
+ # explicit type arguments, and SiteCraft writes
17
+ # `makeAutoObservable<this, '_buildTimer' | 'buildManager'>(this, {…})` — which
18
+ # silently cost the whole of project.store.ts, 7 real call sites, until the
19
+ # count came up short against the grep.
20
+ id: store-no-runinaction
21
+ language: typescript
22
+ severity: error
23
+ ignores:
24
+ # A test may drive a store's state directly to set up a scenario; that is the
25
+ # sanctioned way to test a store without a render. All 6 of tg-gallery's
26
+ # runInAction sites are tests, which is why the report counts it as 0.
27
+ - '**/*.test.ts'
28
+ - '**/*.spec.ts'
29
+ - '**/__tests__/**'
30
+ utils:
31
+ store-class:
32
+ kind: class_declaration
33
+ has:
34
+ stopBy: end
35
+ kind: call_expression
36
+ has:
37
+ field: function
38
+ kind: identifier
39
+ regex: '^make(Auto)?Observable$'
40
+ message: |
41
+ `runInAction` inside a store class. Every method is already an action —
42
+ `makeAutoObservable` made it one — so this wraps an action in an action and
43
+ buys nothing. Where it appears is the tell: after an `await`, because the
44
+ continuation of an async method is NOT covered by the method's action, and
45
+ each `runInAction` is a hand-patched hole in that coverage.
46
+ Convert the method to a `flow`, which resumes inside an action at every yield
47
+ and needs no patching:
48
+ async load() { const x = await f(); runInAction(() => { this.x = x }) }
49
+ → load = flow(function* (this: Store) { this.x = yield f() })
50
+ See the mobx-models skill.
51
+ rule:
52
+ all:
53
+ - kind: call_expression
54
+ - has:
55
+ field: function
56
+ kind: identifier
57
+ regex: '^runInAction$'
58
+ - inside:
59
+ stopBy: end
60
+ matches: store-class
@@ -0,0 +1,62 @@
1
+ # ── the .tsx half of `store-no-setinterval-poll` ──
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-setinterval-poll.yml`.
10
+ # Suppressing needs THIS id: `ast-grep-ignore: store-no-setinterval-poll-tsx`.
11
+ #
12
+ # ── wave 1, store rule 7 of 7 ──────────────────────────────────────────
13
+ #
14
+ # `setInterval` inside a store class: a hand-rolled poll. Report §14 counts three
15
+ # independent sites rebuilding the same thing, and the failure mode is uniform —
16
+ # the interval outlives what it was polling for, keeps firing while the tab is
17
+ # hidden, and its disposer is either missing or forgotten (ObservableQuery, the
18
+ # reference implementation, shipped with exactly that leak: `dispose()` not
19
+ # removing its `visibilitychange` listener).
20
+ #
21
+ # Matches the CALL, not the identifier, so the common
22
+ # `private timer: ReturnType<typeof setInterval>` field annotation — a type
23
+ # position — stays silent.
24
+ id: store-no-setinterval-poll-tsx
25
+ language: tsx
26
+ severity: warning
27
+ ignores:
28
+ - '**/*.test.tsx'
29
+ - '**/*.spec.tsx'
30
+ - '**/__tests__/**'
31
+ utils:
32
+ store-class:
33
+ kind: class_declaration
34
+ has:
35
+ stopBy: end
36
+ kind: call_expression
37
+ has:
38
+ field: function
39
+ kind: identifier
40
+ regex: '^make(Auto)?Observable$'
41
+ message: |
42
+ A hand-rolled poll inside a store. An interval has to be cancelled on every
43
+ path that ends the thing it polls for, paused when the tab is hidden, and kept
44
+ from overlapping a request still in flight — and the copies in this codebase
45
+ each solve a different two of those three.
46
+ Use the Query object's `refetchInterval`, which owns the disposer, the
47
+ in-flight dedup and the focus/visibility handling in one place.
48
+ CARVE-OUT — process-shaped work (a render, an export, a git sync: something
49
+ with progress and no notion of revalidation) legitimately ticks on a timer.
50
+ There is no manifest glob for that yet, so mark those with a reason:
51
+ // ast-grep-ignore: store-no-setinterval-poll -- progress tick for a render job
52
+ See report §14 and the mobx-models skill.
53
+ rule:
54
+ all:
55
+ - kind: call_expression
56
+ - has:
57
+ field: function
58
+ kind: identifier
59
+ regex: '^setInterval$'
60
+ - inside:
61
+ stopBy: end
62
+ matches: store-class
@@ -0,0 +1,51 @@
1
+ # ── wave 1, store rule 7 of 7 ──────────────────────────────────────────
2
+ #
3
+ # `setInterval` inside a store class: a hand-rolled poll. Report §14 counts three
4
+ # independent sites rebuilding the same thing, and the failure mode is uniform —
5
+ # the interval outlives what it was polling for, keeps firing while the tab is
6
+ # hidden, and its disposer is either missing or forgotten (ObservableQuery, the
7
+ # reference implementation, shipped with exactly that leak: `dispose()` not
8
+ # removing its `visibilitychange` listener).
9
+ #
10
+ # Matches the CALL, not the identifier, so the common
11
+ # `private timer: ReturnType<typeof setInterval>` field annotation — a type
12
+ # position — stays silent.
13
+ id: store-no-setinterval-poll
14
+ language: typescript
15
+ severity: warning
16
+ ignores:
17
+ - '**/*.test.ts'
18
+ - '**/*.spec.ts'
19
+ - '**/__tests__/**'
20
+ utils:
21
+ store-class:
22
+ kind: class_declaration
23
+ has:
24
+ stopBy: end
25
+ kind: call_expression
26
+ has:
27
+ field: function
28
+ kind: identifier
29
+ regex: '^make(Auto)?Observable$'
30
+ message: |
31
+ A hand-rolled poll inside a store. An interval has to be cancelled on every
32
+ path that ends the thing it polls for, paused when the tab is hidden, and kept
33
+ from overlapping a request still in flight — and the copies in this codebase
34
+ each solve a different two of those three.
35
+ Use the Query object's `refetchInterval`, which owns the disposer, the
36
+ in-flight dedup and the focus/visibility handling in one place.
37
+ CARVE-OUT — process-shaped work (a render, an export, a git sync: something
38
+ with progress and no notion of revalidation) legitimately ticks on a timer.
39
+ There is no manifest glob for that yet, so mark those with a reason:
40
+ // ast-grep-ignore: store-no-setinterval-poll -- progress tick for a render job
41
+ See report §14 and the mobx-models skill.
42
+ rule:
43
+ all:
44
+ - kind: call_expression
45
+ - has:
46
+ field: function
47
+ kind: identifier
48
+ regex: '^setInterval$'
49
+ - inside:
50
+ stopBy: end
51
+ matches: store-class
@@ -0,0 +1,41 @@
1
+ # PROMOTED to canon in wave 2 from faceless-photo-lib/.ast-grep (rule-triage).
2
+ # No canon equivalent existed: `non-exhaustive-match` is about PREFERRING
3
+ # `.exhaustive()` over `.otherwise()`, which is a style call the owner has
4
+ # explicitly deferred (decision #4). This is the different, non-negotiable
5
+ # half — a chain with NO terminator evaluates to a builder object, so every
6
+ # arm body is dead code and the truthy builder keeps the caller compiling.
7
+ # Verified on real code: took a terminated chain out of observatory, deleted
8
+ # `.exhaustive()` -> fires; left it -> silent; a dangling chain nested in a
9
+ # callback of an OUTER terminated chain -> fires on the inner one only;
10
+ # `"abc".match(/b/)` -> silent.
11
+ #
12
+ # TSX twin of ts-pattern-dangling-match (ast-grep rules are single-language, so
13
+ # .tsx components get their own rule). Same policy — see
14
+ # ts-pattern-dangling-match.yml for the full rationale. This twin carries most of
15
+ # the repo's JSX-embedded `{match(state)...}` render branches.
16
+ id: ts-pattern-dangling-match-tsx
17
+ language: tsx
18
+ severity: error
19
+ message: "ts-pattern `match()` chain has no terminator — it evaluates to a builder, not a value. Add `.exhaustive()` (project.md §4)."
20
+ note: "`.otherwise()` and `.run()` also terminate a chain; preferring `.exhaustive()` over `.otherwise()` is the separate ts-pattern-prefer-exhaustive rule."
21
+ rule:
22
+ all:
23
+ - kind: call_expression
24
+ has:
25
+ field: function
26
+ kind: identifier
27
+ regex: "^match$"
28
+ - not:
29
+ inside:
30
+ stopBy:
31
+ not:
32
+ any:
33
+ - kind: member_expression
34
+ - kind: call_expression
35
+ kind: call_expression
36
+ has:
37
+ field: function
38
+ kind: member_expression
39
+ has:
40
+ field: property
41
+ regex: "^(exhaustive|otherwise|run)$"
@@ -0,0 +1,63 @@
1
+ # PROMOTED to canon in wave 2 from faceless-photo-lib/.ast-grep (rule-triage).
2
+ # No canon equivalent existed: `non-exhaustive-match` is about PREFERRING
3
+ # `.exhaustive()` over `.otherwise()`, which is a style call the owner has
4
+ # explicitly deferred (decision #4). This is the different, non-negotiable
5
+ # half — a chain with NO terminator evaluates to a builder object, so every
6
+ # arm body is dead code and the truthy builder keeps the caller compiling.
7
+ # Verified on real code: took a terminated chain out of observatory, deleted
8
+ # `.exhaustive()` -> fires; left it -> silent; a dangling chain nested in a
9
+ # callback of an OUTER terminated chain -> fires on the inner one only;
10
+ # `"abc".match(/b/)` -> silent.
11
+ #
12
+ # ts-pattern-dangling-match (convention-rules-ci task 2.4; project.md §3 and §4
13
+ # "`match().exhaustive()` everywhere").
14
+ #
15
+ # A `match(...)` chain that is never terminated does not evaluate anything — it
16
+ # leaves a ts-pattern *builder object* where the caller expects a value. That is
17
+ # always a bug, and a silent one: the builder is truthy, so `if`/JSX/`return`
18
+ # paths keep working while every branch body is dead code.
19
+ #
20
+ # This rule is the hard gate: it fires only when a chain has NO terminator at
21
+ # all (`.exhaustive()`, `.otherwise()`, or `.run()`). Preferring `.exhaustive()`
22
+ # over `.otherwise()` is the separate, advisory `ts-pattern-prefer-exhaustive`
23
+ # rule, because `.otherwise()` is legitimate when matching an OPEN domain (an
24
+ # external boundary string, say) that no union can enumerate.
25
+ #
26
+ # Implementation notes:
27
+ # - The target is a call_expression whose callee is the bare identifier
28
+ # `match`, so `someString.match(re)` (the RegExp method) is never confused
29
+ # for ts-pattern.
30
+ # - The terminator search walks ONLY the member/call spine of this chain — the
31
+ # `stopBy` halts at the first ancestor that is neither, so an enclosing
32
+ # terminated chain cannot mask a dangling chain nested in one of its
33
+ # callbacks.
34
+ # - Terminators are matched structurally (`kind` + `regex`) rather than with a
35
+ # surface `pattern:`, because ast-grep's default `smart` strictness also
36
+ # compares interleaved `comment` nodes — and these chains carry per-arm
37
+ # comments constantly, which would produce false positives.
38
+ id: ts-pattern-dangling-match
39
+ language: typescript
40
+ severity: error
41
+ message: "ts-pattern `match()` chain has no terminator — it evaluates to a builder, not a value. Add `.exhaustive()` (project.md §4)."
42
+ note: "`.otherwise()` and `.run()` also terminate a chain; preferring `.exhaustive()` over `.otherwise()` is the separate ts-pattern-prefer-exhaustive rule."
43
+ rule:
44
+ all:
45
+ - kind: call_expression
46
+ has:
47
+ field: function
48
+ kind: identifier
49
+ regex: "^match$"
50
+ - not:
51
+ inside:
52
+ stopBy:
53
+ not:
54
+ any:
55
+ - kind: member_expression
56
+ - kind: call_expression
57
+ kind: call_expression
58
+ has:
59
+ field: function
60
+ kind: member_expression
61
+ has:
62
+ field: property
63
+ regex: "^(exhaustive|otherwise|run)$"
@@ -0,0 +1,49 @@
1
+ id: ui-imports-app-store
2
+ language: tsx
3
+ severity: error
4
+ message: |
5
+ A reusable UI component importing an app store. The moment it knows where the
6
+ data comes from it stops being reusable — and the boundary is the whole point:
7
+ a UI component consumes an interface (props, or a view-model interface the
8
+ store implements), never the store itself.
9
+ Take the values as props and let the app component do the reading. See the
10
+ ui-components skill.
11
+ files:
12
+ - '**/packages/ui/**'
13
+ - '**/components/ui/**'
14
+ - '**/src/ui/**'
15
+ utils:
16
+ # A type-only import: erased at build time, so it creates no dependency on the
17
+ # store — only on its shape. Covers all four spellings TypeScript allows.
18
+ type-only-import:
19
+ any:
20
+ - pattern: import type { $$$S } from $SRC
21
+ - pattern: import type $D from $SRC
22
+ - pattern: import type * as $NS from $SRC
23
+ # `import { type A, type B }` — every named specifier inline-typed.
24
+ - all:
25
+ - kind: import_statement
26
+ - has:
27
+ stopBy: end
28
+ kind: named_imports
29
+ - not:
30
+ has:
31
+ stopBy: end
32
+ all:
33
+ - kind: import_specifier
34
+ - not:
35
+ regex: '^type\s'
36
+ rule:
37
+ all:
38
+ - kind: import_statement
39
+ - has:
40
+ stopBy: end
41
+ kind: string_fragment
42
+ regex: '(^|/)stores?(/|$)'
43
+ # X7 (report §2): the view-model pattern this rule exists to protect tells a
44
+ # UI component to type itself against `IXxxModel` — which lives in the store
45
+ # module. Firing at `error` on that import made the rule contradict the
46
+ # pattern it enforces, so a compliant component could not be written without
47
+ # a suppression comment. A type import is the compliant shape, not a breach.
48
+ - not:
49
+ matches: type-only-import
@@ -0,0 +1,53 @@
1
+ # ── simple-components, rule 3 of 4 ─────────────────────────────────────
2
+ #
3
+ # The view half of the two-tier split (decision D8, report §9): a `*.view.tsx`
4
+ # is props in, JSX out. No hooks, no `match`, no ternary, no data transformation.
5
+ # That contract is what makes a view the file a cheap model writes without
6
+ # getting it wrong, and what lets its test mount it with nothing around it.
7
+ #
8
+ # WARNING TIER ON PURPOSE. D8 ships the split as a recommendation first, with a
9
+ # ratchet on the share of view files — not as a hard gate. Raising this to error
10
+ # would decide a question the owner deliberately parked. The store half of the
11
+ # same contract is the canon's `view-imports-store` (error), which is a narrower
12
+ # claim: no store access anywhere in the app's view files.
13
+ #
14
+ # `useTheme` / `useTokens` / `useStyles` / `useColorScheme` are exempt: the house
15
+ # styling idiom is RN inline objects read from a theme hook (report §10), so
16
+ # banning that hook would ban styling from the file that owns styling.
17
+ id: view-file-logic
18
+ language: tsx
19
+ severity: warning
20
+ files:
21
+ - '**/*.view.tsx'
22
+ message: |
23
+ Logic in a `*.view.tsx`. A view renders the props it was handed and decides
24
+ nothing: no hooks, no `match`, no ternary, no formatting, no icon-picking.
25
+ Move the decision up to the logic component or the model, and hand the view
26
+ the ANSWER — `tone="danger"`, `formattedDate`, `icon` — not the input it would
27
+ have to branch on. Passing `status` and mapping it to colours inside the view
28
+ is the same branch, laundered.
29
+ Exempt: `useTheme` / `useTokens` / `useStyles` / `useColorScheme` — styling is
30
+ what a view owns.
31
+ See the simple-components skill (§two-tier) and report §9.
32
+ rule:
33
+ any:
34
+ # Any hook call that is not a styling hook.
35
+ - all:
36
+ - kind: call_expression
37
+ - has:
38
+ field: function
39
+ kind: identifier
40
+ regex: '^use[A-Z]'
41
+ - not:
42
+ has:
43
+ field: function
44
+ kind: identifier
45
+ regex: '^use(Theme|Tokens|Styles|ColorScheme|Colors)$'
46
+ # Branching that belongs to the logic tier.
47
+ - all:
48
+ - kind: call_expression
49
+ - has:
50
+ field: function
51
+ kind: identifier
52
+ regex: '^match$'
53
+ - kind: ternary_expression
@@ -0,0 +1,64 @@
1
+ # ── G1, rule 3 of 3 (second half): stores are banned outright in views ─
2
+ #
3
+ # Split from `direct-store-import.yml` for one mechanical reason: ast-grep scopes
4
+ # `files:` per rule file, and the two halves need different globs. This is not a
5
+ # fourth rule so much as the `*.view.tsx` arm of the third.
6
+ #
7
+ # Report §9 fixes the view contract as props → JSX and nothing else: no hooks, no
8
+ # `match`, no data transformation, no store. That is stricter than the rest of
9
+ # `.tsx`, where decision D1 makes a `rootStore` import legal at screen level — a
10
+ # view is never a screen, so `rootStore` is banned here too, and so is `useStore`.
11
+ #
12
+ # The only store-path import a view may keep is a type: C40 has the view take
13
+ # `model={store}` typed by `IXxxModel`, which lives in the store module and
14
+ # erases at build time.
15
+ #
16
+ # Expected hit count today: zero. `*.view.tsx` does not exist anywhere in the
17
+ # park yet (the two-tier split ships as a recommendation first, report §9 /
18
+ # decision #8). This is a guardrail waiting at the convention's door, not a
19
+ # finding about current code.
20
+ id: view-imports-store
21
+ language: tsx
22
+ severity: error
23
+ files:
24
+ - '**/*.view.tsx'
25
+ utils:
26
+ type-only-import:
27
+ any:
28
+ - pattern: import type { $$$S } from $SRC
29
+ - pattern: import type $D from $SRC
30
+ - pattern: import type * as $NS from $SRC
31
+ - all:
32
+ - kind: import_statement
33
+ - has:
34
+ stopBy: end
35
+ kind: named_imports
36
+ - not:
37
+ has:
38
+ stopBy: end
39
+ all:
40
+ - kind: import_specifier
41
+ - not:
42
+ regex: '^type\s'
43
+ message: |
44
+ A `*.view.tsx` importing from a stores directory. A view is props in, JSX out
45
+ — it has no hooks, no branching and no data access, which is exactly what
46
+ makes it the file a cheap model writes without getting it wrong, and what
47
+ lets its test mount it with nothing around it.
48
+ Move the read up to the logic component and hand the view what it renders.
49
+ Unlike the rest of the app, the global `rootStore` is not an exception here:
50
+ a view is never a screen.
51
+ `import type { IXxxModel }` IS allowed — typing the view against the model
52
+ interface it renders is the prescribed shape (C40). See report §9 and the
53
+ mobx-components skill.
54
+ rule:
55
+ all:
56
+ - kind: import_statement
57
+ - has:
58
+ field: source
59
+ has:
60
+ stopBy: end
61
+ kind: string_fragment
62
+ regex: '(^|/)stores?(/|$)'
63
+ - not:
64
+ matches: type-only-import
@@ -0,0 +1,56 @@
1
+ # ── rule body adopted from faceless-photo-lib `zod-no-optional-nullable` (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
+ # `z.$T().optional()` only matched a DIRECT call on `z` — it missed both
5
+ # `UserSchema.optional()` and any chained builder (`z.string().min(1).optional()`).
6
+ # The receiver anchor keeps it to real zod schemas. Measured: +14 findings, 0 lost.
7
+ #
8
+ # The .tsx half of `zod-optional-nullable`.
9
+ #
10
+ # ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
11
+ # at `language: typescript` matches `.ts` and NEVER `.tsx`, so
12
+ # `zod-optional-nullable` was blind to every React component file — exactly
13
+ # where a form's validation schema often lives, right next to the component
14
+ # that uses it. Verified by effect — byte-identical content in `p.ts` and
15
+ # `p.tsx`, the first flagged and the second silent.
16
+ #
17
+ # Three ways to cover both were tried and two were rejected on measurement:
18
+ # - `language: [typescript, tsx]` — ast-grep refuses to parse the rule.
19
+ # - `languageGlobs: {tsx: ["*.ts"]}` — reassigns `.ts` to the tsx language, so
20
+ # EVERY remaining `language: typescript` rule goes dark. Measured corpus-wide:
21
+ # 5 rules blinded, 6,094 findings lost against 334 gained. It would have
22
+ # looked like a coverage win.
23
+ # - two files with the SAME id — "Error: Duplicate rule id".
24
+ # So: a separate id per language. The trailing `-tsx` is what you suppress with
25
+ # in a .tsx file, and the finding prints the id, so copying from the output is
26
+ # always right.
27
+ #
28
+ # Keep this file's `rule:` block IDENTICAL to `zod-optional-nullable.yml`. They
29
+ # are one rule wearing two ids because the tool has no way to say "both
30
+ # languages".
31
+ id: zod-optional-nullable-tsx
32
+ language: tsx
33
+ severity: warning
34
+ message: |
35
+ `z.X().optional()` / `z.X().nullable()` makes invalid states representable.
36
+ Prefer a discriminated union with explicit states so the absent case is a named variant.
37
+ See zod-schemas skill and defensive-errors philosophy.
38
+ Suppressing this one needs the id you see above — `ast-grep-ignore: zod-optional-nullable-tsx`.
39
+ `zod-optional-nullable` is a DIFFERENT rule (the .ts arm) and does not cover it; ast-grep
40
+ matches suppression ids exactly. A bare `// ast-grep-ignore` suppresses every
41
+ rule on the line, and a wrong id is reported back as `unused-suppression`.
42
+ rule:
43
+ all:
44
+ - kind: call_expression
45
+ has:
46
+ field: function
47
+ kind: member_expression
48
+ has:
49
+ field: property
50
+ regex: "^(optional|nullable|nullish)$"
51
+ # Anchor: the receiver chain mentions `z` or a `*Schema` binding, i.e. this
52
+ # really is a zod schema and not an unrelated builder with the same method.
53
+ - has:
54
+ stopBy: end
55
+ kind: identifier
56
+ regex: "^(z|.*Schema)$"
@@ -0,0 +1,29 @@
1
+ # ── rule body adopted from faceless-photo-lib `zod-no-optional-nullable` (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
+ # `z.$T().optional()` only matched a DIRECT call on `z` — it missed both
5
+ # `UserSchema.optional()` and any chained builder (`z.string().min(1).optional()`).
6
+ # The receiver anchor keeps it to real zod schemas. Measured: +14 findings, 0 lost.
7
+ #
8
+ id: zod-optional-nullable
9
+ language: typescript
10
+ severity: warning
11
+ message: |
12
+ `z.X().optional()` / `z.X().nullable()` makes invalid states representable.
13
+ Prefer a discriminated union with explicit states so the absent case is a named variant.
14
+ See zod-schemas skill and defensive-errors philosophy.
15
+ rule:
16
+ all:
17
+ - kind: call_expression
18
+ has:
19
+ field: function
20
+ kind: member_expression
21
+ has:
22
+ field: property
23
+ regex: "^(optional|nullable|nullish)$"
24
+ # Anchor: the receiver chain mentions `z` or a `*Schema` binding, i.e. this
25
+ # really is a zod schema and not an unrelated builder with the same method.
26
+ - has:
27
+ stopBy: end
28
+ kind: identifier
29
+ regex: "^(z|.*Schema)$"
package/sgconfig.yml ADDED
@@ -0,0 +1,8 @@
1
+ # ast-grep config for the @agentvibes/guardrails rule canon.
2
+ # Invoke directly: ast-grep scan -c <abs path to this file> <target>
3
+ # Or via the CLI: guardrails verify [paths]
4
+ #
5
+ # candidates/ is deliberately NOT listed: those rules are un-triaged imports
6
+ # (see candidates/README.md) and load nowhere until promoted into rules/.
7
+ ruleDirs:
8
+ - rules
@@ -0,0 +1,48 @@
1
+ id: component-decl
2
+ language: tsx
3
+ severity: info
4
+ # Same out-of-scope surface every canon rule declares: tests, stories, and
5
+ # showcase files are legitimate multi-component / oversized territory (a
6
+ # showcase screen IS a bundle of demo components). Found by the first real
7
+ # adoption (observatory): without this, a reformat touching a showcase file's
8
+ # declaration lines turned the verify-diff ratchet red on fixture code.
9
+ # Metrics already excluded these paths on its own; now the structure findings
10
+ # agree with it.
11
+ ignores:
12
+ - '**/*.test.tsx'
13
+ - '**/*.spec.tsx'
14
+ - '**/__tests__/**'
15
+ - '**/showcase/**'
16
+ - '**/*.stories.tsx'
17
+ message: |
18
+ Marker rule — matches every top-level React component declaration so
19
+ check-structure.sh can count them per file and measure their spans.
20
+ Never reported directly (severity info is invisible to the post-edit hook
21
+ and to the CI diff gate, both of which only look at error/warning tiers).
22
+ rule:
23
+ all:
24
+ # `const Foo = <anything>` or `function Foo(...) { ... }`
25
+ - any:
26
+ - pattern: const $N = $INIT
27
+ - pattern: |
28
+ function $N($$$) { $$$ }
29
+ # ...that renders JSX somewhere inside. Excludes `const StoreContext =
30
+ # createContext(...)`, capitalised constant maps, class instances, etc.
31
+ - has:
32
+ stopBy: end
33
+ any:
34
+ - kind: jsx_element
35
+ - kind: jsx_self_closing_element
36
+ # Only top-level declarations count. A component defined inside another
37
+ # function is a different (worse) problem — see noNestedComponentDefinitions.
38
+ not:
39
+ inside:
40
+ stopBy: end
41
+ any:
42
+ - kind: arrow_function
43
+ - kind: function_declaration
44
+ - kind: function_expression
45
+ - kind: method_definition
46
+ constraints:
47
+ N:
48
+ regex: '^[A-Z][A-Za-z0-9]*$'
@@ -0,0 +1,7 @@
1
+ # ast-grep config for the React structure check (guardrails verify / verify-diff).
2
+ # Separate from ../sgconfig.yml on purpose: this dir holds a marker rule, not
3
+ # a reportable finding — the reportable findings (react-multi-component,
4
+ # react-component-too-long, react-component-needs-folder) are synthesised by
5
+ # the CLI from the marker's per-file counts and line spans.
6
+ ruleDirs:
7
+ - rules