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