@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.
- package/README.md +239 -0
- package/candidates/README.md +30 -0
- package/candidates/r10-as-cast.yml +24 -0
- package/candidates/r1d-scattered-numeric-const-in-component.yml +13 -0
- package/candidates/r2-helper-fn-in-tsx.yml +22 -0
- package/candidates/r3-hooks-outside-layout.yml +21 -0
- package/candidates/r6-jsx-comment-smell.yml +14 -0
- package/candidates/r7-raw-hsl-outside-tokens.yml +17 -0
- package/candidates/r8-inline-style-prop.yml +13 -0
- package/candidates/r9-optional-store-arg.yml +24 -0
- package/configs/biome.json +96 -0
- package/configs/tsconfig.base.json +21 -0
- package/dist/astGrep.d.ts +16 -0
- package/dist/astGrep.js +48 -0
- package/dist/astGrep.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +138 -0
- package/dist/cli.js.map +1 -0
- package/dist/deploy.d.ts +1 -0
- package/dist/deploy.js +34 -0
- package/dist/deploy.js.map +1 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +102 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fileWalk.d.ts +2 -0
- package/dist/fileWalk.js +45 -0
- package/dist/fileWalk.js.map +1 -0
- package/dist/findings.d.ts +12 -0
- package/dist/findings.js +8 -0
- package/dist/findings.js.map +1 -0
- package/dist/gateRed.d.ts +1 -0
- package/dist/gateRed.js +93 -0
- package/dist/gateRed.js.map +1 -0
- package/dist/gitDiff.d.ts +18 -0
- package/dist/gitDiff.js +90 -0
- package/dist/gitDiff.js.map +1 -0
- package/dist/hookPostedit.d.ts +1 -0
- package/dist/hookPostedit.js +117 -0
- package/dist/hookPostedit.js.map +1 -0
- package/dist/hookStop.d.ts +1 -0
- package/dist/hookStop.js +56 -0
- package/dist/hookStop.js.map +1 -0
- package/dist/hookStopTest.d.ts +1 -0
- package/dist/hookStopTest.js +71 -0
- package/dist/hookStopTest.js.map +1 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +172 -0
- package/dist/init.js.map +1 -0
- package/dist/initTest.d.ts +1 -0
- package/dist/initTest.js +88 -0
- package/dist/initTest.js.map +1 -0
- package/dist/iterate.d.ts +9 -0
- package/dist/iterate.js +79 -0
- package/dist/iterate.js.map +1 -0
- package/dist/iterateTest.d.ts +1 -0
- package/dist/iterateTest.js +91 -0
- package/dist/iterateTest.js.map +1 -0
- package/dist/leakPatterns.d.ts +5 -0
- package/dist/leakPatterns.js +22 -0
- package/dist/leakPatterns.js.map +1 -0
- package/dist/leaks.d.ts +8 -0
- package/dist/leaks.js +211 -0
- package/dist/leaks.js.map +1 -0
- package/dist/leaksRed.d.ts +1 -0
- package/dist/leaksRed.js +62 -0
- package/dist/leaksRed.js.map +1 -0
- package/dist/metrics.d.ts +10 -0
- package/dist/metrics.js +113 -0
- package/dist/metrics.js.map +1 -0
- package/dist/metricsAggregate.d.ts +5 -0
- package/dist/metricsAggregate.js +78 -0
- package/dist/metricsAggregate.js.map +1 -0
- package/dist/metricsBaseline.d.ts +21 -0
- package/dist/metricsBaseline.js +88 -0
- package/dist/metricsBaseline.js.map +1 -0
- package/dist/metricsCollect.d.ts +8 -0
- package/dist/metricsCollect.js +295 -0
- package/dist/metricsCollect.js.map +1 -0
- package/dist/metricsCycle.d.ts +1 -0
- package/dist/metricsCycle.js +79 -0
- package/dist/metricsCycle.js.map +1 -0
- package/dist/metricsTypes.d.ts +102 -0
- package/dist/metricsTypes.js +2 -0
- package/dist/metricsTypes.js.map +1 -0
- package/dist/packagePaths.d.ts +7 -0
- package/dist/packagePaths.js +11 -0
- package/dist/packagePaths.js.map +1 -0
- package/dist/pluginApi.d.ts +24 -0
- package/dist/pluginApi.js +12 -0
- package/dist/pluginApi.js.map +1 -0
- package/dist/pluginResolve.d.ts +6 -0
- package/dist/pluginResolve.js +48 -0
- package/dist/pluginResolve.js.map +1 -0
- package/dist/screenScope.d.ts +21 -0
- package/dist/screenScope.js +81 -0
- package/dist/screenScope.js.map +1 -0
- package/dist/screenScopeTest.d.ts +1 -0
- package/dist/screenScopeTest.js +116 -0
- package/dist/screenScopeTest.js.map +1 -0
- package/dist/severity.d.ts +9 -0
- package/dist/severity.js +62 -0
- package/dist/severity.js.map +1 -0
- package/dist/severityTest.d.ts +1 -0
- package/dist/severityTest.js +63 -0
- package/dist/severityTest.js.map +1 -0
- package/dist/sourceLines.d.ts +9 -0
- package/dist/sourceLines.js +52 -0
- package/dist/sourceLines.js.map +1 -0
- package/dist/structure.d.ts +2 -0
- package/dist/structure.js +98 -0
- package/dist/structure.js.map +1 -0
- package/dist/testRules.d.ts +1 -0
- package/dist/testRules.js +285 -0
- package/dist/testRules.js.map +1 -0
- package/dist/textGrep.d.ts +2 -0
- package/dist/textGrep.js +52 -0
- package/dist/textGrep.js.map +1 -0
- package/dist/tomlTable.d.ts +2 -0
- package/dist/tomlTable.js +26 -0
- package/dist/tomlTable.js.map +1 -0
- package/dist/toolResolve.d.ts +18 -0
- package/dist/toolResolve.js +50 -0
- package/dist/toolResolve.js.map +1 -0
- package/dist/verify.d.ts +3 -0
- package/dist/verify.js +49 -0
- package/dist/verify.js.map +1 -0
- package/dist/verifyDiff.d.ts +18 -0
- package/dist/verifyDiff.js +126 -0
- package/dist/verifyDiff.js.map +1 -0
- package/dist/workspaces.d.ts +6 -0
- package/dist/workspaces.js +84 -0
- package/dist/workspaces.js.map +1 -0
- package/mise.toml +9 -0
- package/package.json +62 -0
- package/rules/__fixtures__/as-any-escape/bad.ts +8 -0
- package/rules/__fixtures__/as-any-escape/good.ts +14 -0
- package/rules/__fixtures__/catch-empty/bad.ts +7 -0
- package/rules/__fixtures__/catch-empty/good.ts +14 -0
- package/rules/__fixtures__/direct-store-import/bad.tsx +23 -0
- package/rules/__fixtures__/direct-store-import/good.tsx +34 -0
- package/rules/__fixtures__/direct-store-import/showcase/ignored.tsx +9 -0
- package/rules/__fixtures__/discriminator-ternary/bad.tsx +10 -0
- package/rules/__fixtures__/discriminator-ternary/good.tsx +23 -0
- package/rules/__fixtures__/hardcoded-url-in-component/components/bad.tsx +8 -0
- package/rules/__fixtures__/hardcoded-url-in-component/components/good.tsx +14 -0
- package/rules/__fixtures__/inline-map-row/bad.tsx +72 -0
- package/rules/__fixtures__/inline-map-row/good.tsx +51 -0
- package/rules/__fixtures__/instanceof-map-set/bad.ts +7 -0
- package/rules/__fixtures__/instanceof-map-set/good.ts +8 -0
- package/rules/__fixtures__/literal-union-in-component/components/bad.ts +8 -0
- package/rules/__fixtures__/literal-union-in-component/components/good.ts +23 -0
- package/rules/__fixtures__/missing-observer/bad.tsx +66 -0
- package/rules/__fixtures__/missing-observer/good.tsx +69 -0
- package/rules/__fixtures__/no-local-kit-clone/bad.ts +30 -0
- package/rules/__fixtures__/no-local-kit-clone/bad.tsx +30 -0
- package/rules/__fixtures__/no-local-kit-clone/good.ts +31 -0
- package/rules/__fixtures__/no-local-kit-clone/good.tsx +31 -0
- package/rules/__fixtures__/non-exhaustive-match/bad.ts +8 -0
- package/rules/__fixtures__/non-exhaustive-match/good.ts +14 -0
- package/rules/__fixtures__/page-store-bare-usememo/bad.ts +15 -0
- package/rules/__fixtures__/page-store-bare-usememo/bad.tsx +13 -0
- package/rules/__fixtures__/page-store-bare-usememo/good.ts +17 -0
- package/rules/__fixtures__/page-store-bare-usememo/good.tsx +14 -0
- package/rules/__fixtures__/screen-file-styling/bad.screen.tsx +10 -0
- package/rules/__fixtures__/screen-file-styling/good.screen.tsx +8 -0
- package/rules/__fixtures__/state-loading-boolean-shape/bad.ts +36 -0
- package/rules/__fixtures__/state-loading-boolean-shape/bad.tsx +36 -0
- package/rules/__fixtures__/state-loading-boolean-shape/good.ts +44 -0
- package/rules/__fixtures__/state-loading-boolean-shape/good.tsx +44 -0
- package/rules/__fixtures__/store-async-method/bad.ts +37 -0
- package/rules/__fixtures__/store-async-method/bad.tsx +37 -0
- package/rules/__fixtures__/store-async-method/good.ts +73 -0
- package/rules/__fixtures__/store-async-method/good.tsx +73 -0
- package/rules/__fixtures__/store-context-provider/bad.ts +16 -0
- package/rules/__fixtures__/store-context-provider/bad.tsx +24 -0
- package/rules/__fixtures__/store-context-provider/good.ts +13 -0
- package/rules/__fixtures__/store-context-provider/good.tsx +11 -0
- package/rules/__fixtures__/store-delegation-getter/bad.ts +30 -0
- package/rules/__fixtures__/store-delegation-getter/bad.tsx +30 -0
- package/rules/__fixtures__/store-delegation-getter/good.ts +65 -0
- package/rules/__fixtures__/store-delegation-getter/good.tsx +65 -0
- package/rules/__fixtures__/store-new-map/bad.ts +22 -0
- package/rules/__fixtures__/store-new-map/bad.tsx +22 -0
- package/rules/__fixtures__/store-new-map/good.ts +41 -0
- package/rules/__fixtures__/store-new-map/good.tsx +41 -0
- package/rules/__fixtures__/store-no-runinaction/bad.ts +67 -0
- package/rules/__fixtures__/store-no-runinaction/bad.tsx +67 -0
- package/rules/__fixtures__/store-no-runinaction/good.ts +39 -0
- package/rules/__fixtures__/store-no-runinaction/good.tsx +39 -0
- package/rules/__fixtures__/store-no-setinterval-poll/bad.ts +19 -0
- package/rules/__fixtures__/store-no-setinterval-poll/bad.tsx +19 -0
- package/rules/__fixtures__/store-no-setinterval-poll/good.ts +29 -0
- package/rules/__fixtures__/store-no-setinterval-poll/good.tsx +29 -0
- package/rules/__fixtures__/ts-pattern-dangling-match/bad.ts +20 -0
- package/rules/__fixtures__/ts-pattern-dangling-match/good.ts +35 -0
- package/rules/__fixtures__/ui-imports-app-store/src/ui/bad.tsx +13 -0
- package/rules/__fixtures__/ui-imports-app-store/src/ui/good.tsx +23 -0
- package/rules/__fixtures__/view-file-logic/bad.view.tsx +16 -0
- package/rules/__fixtures__/view-file-logic/good.view.tsx +7 -0
- package/rules/__fixtures__/view-imports-store/bad.view.tsx +13 -0
- package/rules/__fixtures__/view-imports-store/good.view.tsx +23 -0
- package/rules/__fixtures__/zod-optional-nullable/bad.ts +10 -0
- package/rules/__fixtures__/zod-optional-nullable/bad.tsx +17 -0
- package/rules/__fixtures__/zod-optional-nullable/good.ts +16 -0
- package/rules/__fixtures__/zod-optional-nullable/good.tsx +20 -0
- package/rules/as-any-escape-tsx.yml +53 -0
- package/rules/as-any-escape.yml +29 -0
- package/rules/catch-empty-tsx.yml +55 -0
- package/rules/catch-empty.yml +31 -0
- package/rules/classname-not-composed.yml +28 -0
- package/rules/classname-visual-identity.yml +34 -0
- package/rules/demo-mode-by-default-ts.yml +76 -0
- package/rules/demo-mode-by-default.yml +54 -0
- package/rules/direct-store-import.yml +118 -0
- package/rules/discriminator-ternary.yml +70 -0
- package/rules/hardcoded-url-in-component.yml +44 -0
- package/rules/inline-map-row.yml +107 -0
- package/rules/instanceof-map-set-tsx.yml +46 -0
- package/rules/instanceof-map-set.yml +22 -0
- package/rules/json-roundtrip.yml +9 -0
- package/rules/jsx-cond-and.yml +16 -0
- package/rules/jsx-ternary.yml +35 -0
- package/rules/kind-if-without-match.yml +13 -0
- package/rules/literal-union-in-component.yml +71 -0
- package/rules/match-bool-to-null.yml +15 -0
- package/rules/missing-observer.yml +189 -0
- package/rules/mobx-effect-observable-dep.yml +23 -0
- package/rules/mobx-effect-store-write.yml +21 -0
- package/rules/mobx-usestate-from-store.yml +20 -0
- package/rules/no-local-kit-clone-tsx.yml +104 -0
- package/rules/no-local-kit-clone.yml +93 -0
- package/rules/non-exhaustive-match.yml +34 -0
- package/rules/page-store-bare-usememo-tsx.yml +47 -0
- package/rules/page-store-bare-usememo.yml +43 -0
- package/rules/screen-file-styling.yml +29 -0
- package/rules/silent-default-return.yml +17 -0
- package/rules/state-loading-boolean-shape-tsx.yml +102 -0
- package/rules/state-loading-boolean-shape.yml +91 -0
- package/rules/store-async-method-tsx.yml +108 -0
- package/rules/store-async-method.yml +98 -0
- package/rules/store-context-provider-tsx.yml +68 -0
- package/rules/store-context-provider.yml +78 -0
- package/rules/store-delegation-getter-tsx.yml +104 -0
- package/rules/store-delegation-getter.yml +93 -0
- package/rules/store-new-map-tsx.yml +84 -0
- package/rules/store-new-map.yml +73 -0
- package/rules/store-no-runinaction-tsx.yml +71 -0
- package/rules/store-no-runinaction.yml +60 -0
- package/rules/store-no-setinterval-poll-tsx.yml +62 -0
- package/rules/store-no-setinterval-poll.yml +51 -0
- package/rules/ts-pattern-dangling-match-tsx.yml +41 -0
- package/rules/ts-pattern-dangling-match.yml +63 -0
- package/rules/ui-imports-app-store.yml +49 -0
- package/rules/view-file-logic.yml +53 -0
- package/rules/view-imports-store.yml +64 -0
- package/rules/zod-optional-nullable-tsx.yml +56 -0
- package/rules/zod-optional-nullable.yml +29 -0
- package/sgconfig.yml +8 -0
- package/structure/rules/component-decl.yml +48 -0
- 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'
|