@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,53 @@
1
+ # ── rule body adopted from faceless-photo-lib `no-as-any` (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
+ # surface `pattern:` is compared at ast-grep's default `smart` strictness, which
5
+ # also compares interleaved `comment` nodes — so `x as /* why */ any` slipped
6
+ # straight through. The structural form cannot be fooled that way.
7
+ #
8
+ # The .tsx half of `as-any-escape`.
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 `as-any-escape`
12
+ # was blind to every React component file. Verified by effect — byte-identical
13
+ # content in `p.ts` and `p.tsx`, the first flagged and the second silent.
14
+ #
15
+ # Three ways to cover both were tried and two were rejected on measurement:
16
+ # - `language: [typescript, tsx]` — ast-grep refuses to parse the rule.
17
+ # - `languageGlobs: {tsx: ["*.ts"]}` — reassigns `.ts` to the tsx language, so
18
+ # EVERY remaining `language: typescript` rule goes dark. Measured corpus-wide:
19
+ # 5 rules blinded, 6,094 findings lost against 334 gained. It would have
20
+ # looked like a coverage win.
21
+ # - two files with the SAME id — "Error: Duplicate rule id".
22
+ # So: a separate id per language. The trailing `-tsx` is what you suppress with
23
+ # in a .tsx file, and the finding prints the id, so copying from the output is
24
+ # always right.
25
+ #
26
+ # Keep this file's `rule:` block IDENTICAL to `as-any-escape.yml`. They are one
27
+ # rule wearing two ids because the tool has no way to say "both languages".
28
+ id: as-any-escape-tsx
29
+ language: tsx
30
+ severity: error
31
+ message: |
32
+ `as any` / `as unknown as` hides real type bugs that surface as runtime crashes.
33
+ Fix the type at the source (Zod schema, interface), use a type guard, or .parse() at the boundary.
34
+ See defensive-errors Rule 7.
35
+ Suppressing this one needs the id you see above — `ast-grep-ignore: as-any-escape-tsx`.
36
+ `as-any-escape` is a DIFFERENT rule (the .ts arm) and does not cover it; ast-grep
37
+ matches suppression ids exactly. A bare `// ast-grep-ignore` suppresses every
38
+ rule on the line, and a wrong id is reported back as `unused-suppression`.
39
+ rule:
40
+ any:
41
+ # `x as any`
42
+ - kind: as_expression
43
+ has:
44
+ kind: predefined_type
45
+ regex: "^any$"
46
+ # `x as unknown as T` — the double-cast that launders `unknown` back to a
47
+ # concrete type without validating anything.
48
+ - kind: as_expression
49
+ has:
50
+ kind: as_expression
51
+ has:
52
+ kind: predefined_type
53
+ regex: "^unknown$"
@@ -0,0 +1,29 @@
1
+ # ── rule body adopted from faceless-photo-lib `no-as-any` (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
+ # surface `pattern:` is compared at ast-grep's default `smart` strictness, which
5
+ # also compares interleaved `comment` nodes — so `x as /* why */ any` slipped
6
+ # straight through. The structural form cannot be fooled that way.
7
+ #
8
+ id: as-any-escape
9
+ language: typescript
10
+ severity: error
11
+ message: |
12
+ `as any` / `as unknown as` hides real type bugs that surface as runtime crashes.
13
+ Fix the type at the source (Zod schema, interface), use a type guard, or .parse() at the boundary.
14
+ See defensive-errors Rule 7.
15
+ rule:
16
+ any:
17
+ # `x as any`
18
+ - kind: as_expression
19
+ has:
20
+ kind: predefined_type
21
+ regex: "^any$"
22
+ # `x as unknown as T` — the double-cast that launders `unknown` back to a
23
+ # concrete type without validating anything.
24
+ - kind: as_expression
25
+ has:
26
+ kind: as_expression
27
+ has:
28
+ kind: predefined_type
29
+ regex: "^unknown$"
@@ -0,0 +1,55 @@
1
+ # ── rule body adopted from faceless-photo-lib `no-empty-catch` (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 patterns covered `catch {}` and `catch (e) {}` but NOT `catch (e:
5
+ # unknown) {}` / `catch (e: any) {}` — the type annotation is an extra named node.
6
+ # Measured on tg-gallery + observatory + byok + faceless: 0 files lost, 1 gained,
7
+ # and the finding now anchors on the `catch` keyword rather than the `try`.
8
+ #
9
+ # The .tsx half of `catch-empty`.
10
+ #
11
+ # ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
12
+ # at `language: typescript` matches `.ts` and NEVER `.tsx`, so `catch-empty`
13
+ # was blind to every React component file. Verified by effect — byte-identical
14
+ # content in `p.ts` and `p.tsx`, the first flagged and the second silent.
15
+ #
16
+ # Three ways to cover both were tried and two were rejected on measurement:
17
+ # - `language: [typescript, tsx]` — ast-grep refuses to parse the rule.
18
+ # - `languageGlobs: {tsx: ["*.ts"]}` — reassigns `.ts` to the tsx language, so
19
+ # EVERY remaining `language: typescript` rule goes dark. Measured corpus-wide:
20
+ # 5 rules blinded, 6,094 findings lost against 334 gained. It would have
21
+ # looked like a coverage win.
22
+ # - two files with the SAME id — "Error: Duplicate rule id".
23
+ # So: a separate id per language. The trailing `-tsx` is what you suppress with
24
+ # in a .tsx file, and the finding prints the id, so copying from the output is
25
+ # always right.
26
+ #
27
+ # Keep this file's `rule:` block IDENTICAL to `catch-empty.yml`. They are one
28
+ # rule wearing two ids because the tool has no way to say "both languages".
29
+ id: catch-empty-tsx
30
+ language: tsx
31
+ severity: error
32
+ message: |
33
+ Empty catch silently swallows errors. Either handle specifically OR log + rethrow OR
34
+ return an explicit discriminated failure variant.
35
+ See defensive-errors Rule 8.
36
+ Suppressing this one needs the id you see above — `ast-grep-ignore: catch-empty-tsx`.
37
+ `catch-empty` is a DIFFERENT rule (the .ts arm) and does not cover it; ast-grep
38
+ matches suppression ids exactly. A bare `// ast-grep-ignore` suppresses every
39
+ rule on the line, and a wrong id is reported back as `unused-suppression`.
40
+ rule:
41
+ any:
42
+ - pattern:
43
+ context: "try { work(); } catch { }"
44
+ selector: catch_clause
45
+ strictness: relaxed
46
+ - pattern:
47
+ context: "try { work(); } catch (e) { }"
48
+ selector: catch_clause
49
+ strictness: relaxed
50
+ # `catch (e: unknown)` / `catch (e: any)` — the annotation is an extra named
51
+ # node, so the untyped pattern above does not cover it.
52
+ - pattern:
53
+ context: "try { work(); } catch (e: unknown) { }"
54
+ selector: catch_clause
55
+ strictness: relaxed
@@ -0,0 +1,31 @@
1
+ # ── rule body adopted from faceless-photo-lib `no-empty-catch` (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 patterns covered `catch {}` and `catch (e) {}` but NOT `catch (e:
5
+ # unknown) {}` / `catch (e: any) {}` — the type annotation is an extra named node.
6
+ # Measured on tg-gallery + observatory + byok + faceless: 0 files lost, 1 gained,
7
+ # and the finding now anchors on the `catch` keyword rather than the `try`.
8
+ #
9
+ id: catch-empty
10
+ language: typescript
11
+ severity: error
12
+ message: |
13
+ Empty catch silently swallows errors. Either handle specifically OR log + rethrow OR
14
+ return an explicit discriminated failure variant.
15
+ See defensive-errors Rule 8.
16
+ rule:
17
+ any:
18
+ - pattern:
19
+ context: "try { work(); } catch { }"
20
+ selector: catch_clause
21
+ strictness: relaxed
22
+ - pattern:
23
+ context: "try { work(); } catch (e) { }"
24
+ selector: catch_clause
25
+ strictness: relaxed
26
+ # `catch (e: unknown)` / `catch (e: any)` — the annotation is an extra named
27
+ # node, so the untyped pattern above does not cover it.
28
+ - pattern:
29
+ context: "try { work(); } catch (e: unknown) { }"
30
+ selector: catch_clause
31
+ strictness: relaxed
@@ -0,0 +1,28 @@
1
+ id: classname-not-composed
2
+ language: tsx
3
+ severity: error
4
+ message: |
5
+ className built by string interpolation or a ternary. Class strings are a
6
+ vocabulary, not text to concatenate: compose them with `cn(...)` so
7
+ conflicting utilities resolve predictably, or — better — select a variant
8
+ prop on the UI component that owns those classes.
9
+ ❌ className={`card ... ${isOver ? "ring-2 ring-accent" : ""}`}
10
+ ✅ className={cn("card ...", isOver && "ring-2 ring-accent")}
11
+ ✅ <Card selected={isOver} />
12
+ See the ui-components skill (style vocabulary is centralized).
13
+ rule:
14
+ all:
15
+ # A bare `className={$EXPR}` pattern does not parse standalone — a JSX
16
+ # attribute is not a valid program — so it needs context + selector.
17
+ - pattern:
18
+ context: '<a className={$EXPR} />'
19
+ selector: jsx_attribute
20
+ - has:
21
+ stopBy: end
22
+ any:
23
+ - kind: template_string
24
+ - kind: ternary_expression
25
+ not:
26
+ has:
27
+ stopBy: end
28
+ pattern: cn($$$)
@@ -0,0 +1,34 @@
1
+ id: classname-visual-identity
2
+ language: tsx
3
+ severity: warning
4
+ message: |
5
+ Visual-identity utilities (colour, ring, shadow, radius, opacity, animation,
6
+ type scale) written in an app component. Those classes are the design
7
+ language — they belong to the UI component that owns them, selected through a
8
+ variant prop, not scattered across screens.
9
+ ❌ <button className="rounded-lg bg-red-500/20 px-2 text-red-200">
10
+ ✅ <Button tone="danger" size="sm">
11
+ Layout utilities (flex, grid, gap-*, p-*, w-*, items-*) stay legal here —
12
+ layout is per-screen composition, not identity. See the ui-components skill.
13
+ If this genuinely has no UI-component home yet, suppress with a justification.
14
+ ignores:
15
+ - '**/packages/ui/**'
16
+ - '**/components/ui/**'
17
+ - '**/src/ui/**'
18
+ - '**/*.test.tsx'
19
+ - '**/*.stories.tsx'
20
+ rule:
21
+ all:
22
+ # Matched by kind, not by pattern, so that BOTH attribute forms are covered:
23
+ # className="..." (plain string) and className={...} (expression container).
24
+ - kind: jsx_attribute
25
+ - has:
26
+ kind: property_identifier
27
+ regex: '^className$'
28
+ - has:
29
+ stopBy: end
30
+ any:
31
+ - kind: string_fragment
32
+ regex: '(^|[\s"''`:])(bg-|ring-|shadow-|animate-|rounded-|opacity-|backdrop-|text-(xs|sm|base|lg|xl|[2-9]xl|red|green|amber|blue|slate|gray|zinc|white|black)|border-(red|green|amber|blue|slate|gray|zinc|white|black))'
33
+ - kind: string_fragment
34
+ regex: 'text-tg-|bg-tg-|ring-tg-|border-tg-'
@@ -0,0 +1,76 @@
1
+ # ── the .ts half of `demo-mode-by-default` ─────────────────────────────
2
+ #
3
+ # Same disjoint-language problem the `-tsx` twins solve, running the other way:
4
+ # `demo-mode-by-default` is `language: tsx`, so it matched `.tsx` and NEVER
5
+ # `.ts`. Verified by effect — an `initialState()` returning `{ mode: "demo" }`
6
+ # flagged as `s.tsx` and completely silent as `s.ts`.
7
+ #
8
+ # That is the wrong half to be blind in. An `initialState` / `defaultState`
9
+ # function returning a demo source lives in a plain `.ts` store, reducer or
10
+ # config file at least as often as in a component, and this is the rule guarding
11
+ # `fake-data.md` — "NEVER make a demo/fixture/mock source the DEFAULT". A rule
12
+ # that stops at the component boundary is not guarding that.
13
+ #
14
+ # Costs nothing to close: 0 sites across all 69 repos under ~/gits/github,
15
+ # counted BY RULE ID. (Counting every finding the config emits instead counts
16
+ # `unused-suppression` and reads 205 — that mistake was made and caught.) And
17
+ # the 0 is clean rather than broken: the rule fires on a known positive.
18
+ #
19
+ # Keep this file's `rule:` block IDENTICAL to `demo-mode-by-default.yml`.
20
+ #
21
+ # A fixture/demo/mock source chosen as the DEFAULT data source.
22
+ #
23
+ # The failure this encodes: apps/substrate-app shipped with
24
+ # function initialState(boot, narrow): State { … mode: { kind: "demo" }, … }
25
+ # so the deployed site always opened on in-memory fixtures that accepted writes
26
+ # and forgot them at reload. The owner worked in it for days and lost the issues
27
+ # he had filed (2026-08-07).
28
+ #
29
+ # Scoped deliberately narrow — only the positions that decide what the app STARTS
30
+ # on: an `initialState`-family function, a DEFAULT_/INITIAL_ binding, or the
31
+ # initial argument of useState/useReducer. A demo literal inside a `.with(...)`
32
+ # match arm is the correct pattern (an explicit flag selecting the sandbox) and is
33
+ # excluded.
34
+ #
35
+ # Validated against the real pre-fix store.tsx: 1 hit at the exact declaration,
36
+ # 0 on the fixed version, 0 across substrate-app, tg-gallery webapp and
37
+ # agent-workbench web.
38
+ id: demo-mode-by-default-ts
39
+ language: typescript
40
+ severity: error
41
+ message: "A fixture/demo/mock source is the DEFAULT here. The default must be the real source; the sandbox belongs behind an explicit flag."
42
+ note: |
43
+ The default data source must be the real one. Put the sandbox behind an
44
+ explicit opt-in (`?mode=demo`, a flag, a separate entry) whose absence means
45
+ real, and make writes that cannot persist FAIL rather than report success.
46
+ See the fake-data-boundaries skill.
47
+ Suppressing this one needs the id you see above — `ast-grep-ignore: demo-mode-by-default-ts`.
48
+ `demo-mode-by-default` is a DIFFERENT rule (the .tsx arm) and does not cover it.
49
+ rule:
50
+ all:
51
+ - kind: pair
52
+ - has:
53
+ field: key
54
+ regex: '^(kind|mode|backend|source|dataSource|driver|adapter|transport|client)$'
55
+ - has:
56
+ field: value
57
+ regex: '^.(demo|mock|fixture|fake|stub|sample|seed).$'
58
+ - not:
59
+ inside:
60
+ stopBy: end
61
+ any:
62
+ - pattern: $OBJ.with($$$WARGS)
63
+ - pattern: $OBJ.when($$$WARGS)
64
+ - inside:
65
+ stopBy: end
66
+ any:
67
+ - kind: function_declaration
68
+ has:
69
+ field: name
70
+ regex: '^(initialState|createInitialState|defaultState|makeInitialState|initState|createDefaultState)$'
71
+ - kind: variable_declarator
72
+ has:
73
+ field: name
74
+ regex: '^(INITIAL_STATE|DEFAULT_STATE|INITIAL_MODE|DEFAULT_MODE|DEFAULT_BACKEND|initialState|defaultState)$'
75
+ - pattern: useState($$$ARGS)
76
+ - pattern: useReducer($$$ARGS)
@@ -0,0 +1,54 @@
1
+ # A fixture/demo/mock source chosen as the DEFAULT data source.
2
+ #
3
+ # The failure this encodes: apps/substrate-app shipped with
4
+ # function initialState(boot, narrow): State { … mode: { kind: "demo" }, … }
5
+ # so the deployed site always opened on in-memory fixtures that accepted writes
6
+ # and forgot them at reload. The owner worked in it for days and lost the issues
7
+ # he had filed (2026-08-07).
8
+ #
9
+ # Scoped deliberately narrow — only the positions that decide what the app STARTS
10
+ # on: an `initialState`-family function, a DEFAULT_/INITIAL_ binding, or the
11
+ # initial argument of useState/useReducer. A demo literal inside a `.with(...)`
12
+ # match arm is the correct pattern (an explicit flag selecting the sandbox) and is
13
+ # excluded.
14
+ #
15
+ # Validated against the real pre-fix store.tsx: 1 hit at the exact declaration,
16
+ # 0 on the fixed version, 0 across substrate-app, tg-gallery webapp and
17
+ # agent-workbench web.
18
+ id: demo-mode-by-default
19
+ language: tsx
20
+ severity: error
21
+ message: "A fixture/demo/mock source is the DEFAULT here. The default must be the real source; the sandbox belongs behind an explicit flag."
22
+ note: |
23
+ The default data source must be the real one. Put the sandbox behind an
24
+ explicit opt-in (`?mode=demo`, a flag, a separate entry) whose absence means
25
+ real, and make writes that cannot persist FAIL rather than report success.
26
+ See the fake-data-boundaries skill.
27
+ rule:
28
+ all:
29
+ - kind: pair
30
+ - has:
31
+ field: key
32
+ regex: '^(kind|mode|backend|source|dataSource|driver|adapter|transport|client)$'
33
+ - has:
34
+ field: value
35
+ regex: '^.(demo|mock|fixture|fake|stub|sample|seed).$'
36
+ - not:
37
+ inside:
38
+ stopBy: end
39
+ any:
40
+ - pattern: $OBJ.with($$$WARGS)
41
+ - pattern: $OBJ.when($$$WARGS)
42
+ - inside:
43
+ stopBy: end
44
+ any:
45
+ - kind: function_declaration
46
+ has:
47
+ field: name
48
+ regex: '^(initialState|createInitialState|defaultState|makeInitialState|initState|createDefaultState)$'
49
+ - kind: variable_declarator
50
+ has:
51
+ field: name
52
+ regex: '^(INITIAL_STATE|DEFAULT_STATE|INITIAL_MODE|DEFAULT_MODE|DEFAULT_BACKEND|initialState|defaultState)$'
53
+ - pattern: useState($$$ARGS)
54
+ - pattern: useReducer($$$ARGS)
@@ -0,0 +1,118 @@
1
+ # ── G1, rule 3 of 3: reaching past the store boundary ──────────────────
2
+ #
3
+ # Scoped to decision D1 (report §8): the global `rootStore` is a module-level
4
+ # singleton imported DIRECTLY at screen level — that is now the sanctioned
5
+ # channel, and this rule must not fight it. What stays banned is reaching past
6
+ # it: pulling a store CLASS into a component file, or grabbing one slice-store
7
+ # singleton so the component depends on an implementation instead of a model
8
+ # handed down as a prop.
9
+ #
10
+ # So the rule keys on the imported BINDING, not on the directory alone:
11
+ #
12
+ # import { rootStore } from "…/stores/rootStore" legal (D1)
13
+ # import { galleryStore } from "…/stores/gallery" flagged (slice store)
14
+ # import { GalleryStore } from "…/stores/gallery" flagged (store class)
15
+ # import { useStore } from "…/stores/StoreContext" ignored (access hook)
16
+ # import type { IGalleryModel } from "…/stores/…" ignored (C40 contract)
17
+ #
18
+ # NEAR-TERM SCOPE, stated plainly: "component file" is approximated by `.tsx`,
19
+ # and screens are not distinguished from leaf components — so a screen importing
20
+ # a page-store class is flagged alongside a leaf component doing the same. The
21
+ # real boundary needs the manifest's `screens` glob from `.agentvibes/project.toml`
22
+ # (report §8), which arrives with the `@agentvibes/guardrails` package. Until
23
+ # then this rule is the pragmatic half: it catches slice-store and store-class
24
+ # imports anywhere in `.tsx`, and leaves `rootStore` alone everywhere.
25
+ #
26
+ # `*.view.tsx` is stricter — a view may not touch a store by ANY route,
27
+ # `rootStore` included. That needs a different `files:` glob, which ast-grep
28
+ # scopes per rule file, so it lives in `view-imports-store.yml`.
29
+ id: direct-store-import
30
+ # A screen MUST import its page-store class — `usePageStore(() => new
31
+ # GalleryPageStore(rootStore, slug))` is report §8's prescribed shape — so this
32
+ # rule does not apply to files the repo declares as screens via
33
+ # `[verify] screens` in .agentvibes/project.toml. Without that declaration
34
+ # nothing changes and screens keep needing a justified suppression
35
+ # (is-7067a0b8).
36
+ metadata:
37
+ appliesTo: not-screens
38
+ language: tsx
39
+ severity: error
40
+ files:
41
+ - '**/*.tsx'
42
+ ignores:
43
+ # A test constructing a store directly is the PRESCRIBED shape, not a breach:
44
+ # report §8 puts `new GalleryPageStore(fakeRoot, "slug")` with no render at
45
+ # the centre of why the store is "отдельный легко тестируемый". Without this,
46
+ # 15 of observatory's 21 hits were tests doing exactly the right thing.
47
+ - '**/*.test.tsx'
48
+ - '**/*.spec.tsx'
49
+ - '**/__tests__/**'
50
+ # The composition root wires dependencies once before the first render —
51
+ # `initRootStore(deps)` in report §8. Observatory's `main.tsx` picks its data
52
+ # source here, which is also where fake-data.md REQUIRES the choice to be made.
53
+ - '**/main.tsx'
54
+ # A gallery, storybook or preview harness legitimately builds stores over
55
+ # fixtures — fake-data.md carves this out explicitly ("Fixtures are legitimate
56
+ # where the artifact IS the components"). 29 such files in the park today.
57
+ - '**/showcase/**'
58
+ - '**/*.stories.tsx'
59
+ utils:
60
+ type-only-import:
61
+ any:
62
+ - pattern: import type { $$$S } from $SRC
63
+ - pattern: import type $D from $SRC
64
+ - pattern: import type * as $NS from $SRC
65
+ - all:
66
+ - kind: import_statement
67
+ - has:
68
+ stopBy: end
69
+ kind: named_imports
70
+ - not:
71
+ has:
72
+ stopBy: end
73
+ all:
74
+ - kind: import_specifier
75
+ - not:
76
+ regex: '^type\s'
77
+ message: |
78
+ A store class or slice-store singleton imported straight into a component
79
+ file. The component is now welded to one implementation: it cannot be
80
+ rendered in a test, a story or a second screen without dragging that store
81
+ in with it.
82
+ Take the data as a prop — a `model` typed by the view-model interface the
83
+ store implements (`IXxxModel`) — and let the screen do the reading. The
84
+ global `rootStore` imported at screen level is legal (decision D1); this is
85
+ about everything reached past it.
86
+ `import type { IXxxModel } from ".../stores/..."` is the compliant shape and
87
+ does not fire. See the mobx-models and mobx-components skills.
88
+ rule:
89
+ all:
90
+ - kind: import_statement
91
+ - has:
92
+ field: source
93
+ has:
94
+ stopBy: end
95
+ kind: string_fragment
96
+ regex: '(^|/)stores?(/|$)'
97
+ # A binding whose name says "store", excluding the two legal ones: the
98
+ # global singleton (D1) and the `use…Store` access hooks.
99
+ - has:
100
+ stopBy: end
101
+ all:
102
+ - kind: identifier
103
+ - regex: 'Store$'
104
+ - not:
105
+ regex: '^rootStore$'
106
+ - not:
107
+ regex: '^use[A-Z]'
108
+ # X7 again, one level down: `import { NOTIFICATION_KINDS, type
109
+ # NotificationSettingsStore }` is a value import, so the whole-statement
110
+ # type check above does not exclude it — but the store name in it is
111
+ # still just a type. Observatory's NotificationsSettingsView.tsx was
112
+ # flagged for exactly this before the clause existed.
113
+ - not:
114
+ inside:
115
+ kind: import_specifier
116
+ regex: '^type\s'
117
+ - not:
118
+ matches: type-only-import
@@ -0,0 +1,70 @@
1
+ # PROMOTED to canon in wave 2 from PPA `r4-ternary-on-discriminator` (rule-triage).
2
+ #
3
+ # The only one of the 11 PPA R-rules that was actually firing (18 hits in the
4
+ # audit), and the triage question was whether it duplicates something canon
5
+ # already has. Measured on tg-gallery + observatory, it does not — the three
6
+ # rules partition the space cleanly:
7
+ #
8
+ # kind-if-without-match 27 hits `if` on a discriminator
9
+ # jsx-ternary 105 hits ternary whose branches are JSX
10
+ # this rule 18 hits ternary on a discriminator yielding VALUES
11
+ #
12
+ # r4 ∩ jsx-ternary = 0 r4 ∩ kind-if-without-match = 0
13
+ #
14
+ # The `not` clause below makes that disjointness structural rather than lucky:
15
+ # a ternary with a JSX branch belongs to `jsx-ternary` and is skipped here, so
16
+ # the two can never double-report the same line with the same fix.
17
+ #
18
+ # What it actually finds is the terse option-unwrap —
19
+ # `m.kind === "some" ? m.value : []` — which reads fine until the union grows a
20
+ # third member and the `:` branch silently absorbs it.
21
+ id: discriminator-ternary
22
+ language: tsx
23
+ severity: warning
24
+ utils:
25
+ # A JSX subtree, however many redundant parens wrap it. Same helper
26
+ # `jsx-ternary` uses, so the two rules agree on what "a JSX branch" means.
27
+ is-jsx:
28
+ any:
29
+ - kind: jsx_element
30
+ - kind: jsx_self_closing_element
31
+ - all:
32
+ - kind: parenthesized_expression
33
+ - has:
34
+ matches: is-jsx
35
+ message: |
36
+ A ternary branching on a discriminator field. Both arms are written, so
37
+ nothing breaks today — what breaks is the next variant: widen the union and
38
+ the `:` branch silently absorbs it, with no build error anywhere.
39
+ m.kind === "some" ? m.value : []
40
+ → match(m).with({ kind: "some" }, ({ value }) => value).with({ kind: "none" }, () => []).exhaustive()
41
+ Match the value, not a boolean derived from it — that is what turns adding a
42
+ variant into a type error.
43
+ Related but different: `kind-if-without-match` covers the `if` spelling, and
44
+ `jsx-ternary` covers ternaries that render JSX. This rule deliberately stands
45
+ down when a branch is JSX so the two never report the same line twice.
46
+ See defensive-errors Rule 1 & 2.
47
+ rule:
48
+ all:
49
+ - kind: ternary_expression
50
+ - has:
51
+ field: condition
52
+ kind: binary_expression
53
+ all:
54
+ - has:
55
+ field: operator
56
+ regex: '^(===|!==)$'
57
+ - has:
58
+ field: left
59
+ kind: member_expression
60
+ has:
61
+ field: property
62
+ regex: '^(kind|status|state|type|provider|providerId|reason|mode|variant|tag)$'
63
+ - not:
64
+ any:
65
+ - has:
66
+ field: consequence
67
+ matches: is-jsx
68
+ - has:
69
+ field: alternative
70
+ matches: is-jsx
@@ -0,0 +1,44 @@
1
+ # PROMOTED to canon in wave 2 from PPA `r1a-hardcoded-url-in-component` (rule-triage).
2
+ #
3
+ # Promoted because it enforces a rule that is already house-wide rather than
4
+ # PPA-local: CLAUDE.md's "No localhost" section — every service URL comes from
5
+ # the dev CLI, never a literal, and the legacy proxy zone is retired for new
6
+ # code. A URL baked into a component is also the thing that
7
+ # makes a component un-reusable and un-testable, which is the same boundary
8
+ # `ui-imports-app-store` guards from the other side.
9
+ #
10
+ # Only 2 hits across tg-gallery + observatory — a clean ratchet on new code
11
+ # rather than a cleanup campaign. The PPA original pointed at
12
+ # `apps/ppa/lib/constants/urls.ts`; the message below states the destination
13
+ # generically.
14
+ id: hardcoded-url-in-component
15
+ language: tsx
16
+ severity: warning
17
+ files:
18
+ - '**/components/**/*.ts'
19
+ - '**/components/**/*.tsx'
20
+ - '**/screens/**/*.ts'
21
+ - '**/screens/**/*.tsx'
22
+ ignores:
23
+ - '**/__tests__/**'
24
+ - '**/*.test.ts'
25
+ - '**/*.test.tsx'
26
+ - '**/showcase/**'
27
+ - '**/*.stories.tsx'
28
+ message: |
29
+ A hardcoded URL in a component file. The component now knows which
30
+ environment it is in, so it cannot be rendered in a test, a story or a second
31
+ deployment without editing it — and a URL that moves has to be found by grep
32
+ rather than changed in one place.
33
+ Put it in a constants module and read it from there (or from a store computed
34
+ when it varies at runtime). Service URLs in this codebase come from
35
+ the dev CLI, never a literal — see the house "No localhost" rule, which
36
+ also retires the legacy proxy zone for new code.
37
+ Documentation links and `xmlns` schema URLs are the honest exceptions;
38
+ suppress those with a reason.
39
+ rule:
40
+ all:
41
+ - kind: string
42
+ - regex: '^["'']https?://'
43
+ - not:
44
+ regex: 'w3\.org|schema\.org|json-schema\.org'