@agentvibes/guardrails 0.1.0 → 0.2.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 +131 -5
- package/dist/astGrep.d.ts +3 -0
- package/dist/astGrep.js +15 -2
- package/dist/astGrep.js.map +1 -1
- package/dist/doctor.js +38 -2
- package/dist/doctor.js.map +1 -1
- package/dist/hookPostedit.js +18 -1
- package/dist/hookPostedit.js.map +1 -1
- package/dist/presetDrift.d.ts +31 -0
- package/dist/presetDrift.js +226 -0
- package/dist/presetDrift.js.map +1 -0
- package/dist/presetDriftTest.d.ts +1 -0
- package/dist/presetDriftTest.js +136 -0
- package/dist/presetDriftTest.js.map +1 -0
- package/dist/repoRules.d.ts +27 -0
- package/dist/repoRules.js +86 -0
- package/dist/repoRules.js.map +1 -0
- package/dist/repoRulesTest.d.ts +1 -0
- package/dist/repoRulesTest.js +122 -0
- package/dist/repoRulesTest.js.map +1 -0
- package/dist/screenScope.d.ts +16 -5
- package/dist/screenScope.js +66 -21
- package/dist/screenScope.js.map +1 -1
- package/dist/screenScopeTest.js +67 -7
- package/dist/screenScopeTest.js.map +1 -1
- package/dist/siblingCloneTest.d.ts +1 -0
- package/dist/siblingCloneTest.js +201 -0
- package/dist/siblingCloneTest.js.map +1 -0
- package/dist/testRules.js +77 -0
- package/dist/testRules.js.map +1 -1
- package/dist/twinCoverageTest.d.ts +1 -0
- package/dist/twinCoverageTest.js +262 -0
- package/dist/twinCoverageTest.js.map +1 -0
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +33 -8
- package/dist/verify.js.map +1 -1
- package/dist/verifyDiff.js +6 -5
- package/dist/verifyDiff.js.map +1 -1
- package/package.json +7 -3
- package/rules/__fixtures__/demo-mode-by-default/bad.ts +22 -0
- package/rules/__fixtures__/demo-mode-by-default/bad.tsx +22 -0
- package/rules/__fixtures__/demo-mode-by-default/good.ts +21 -0
- package/rules/__fixtures__/demo-mode-by-default/good.tsx +21 -0
- package/rules/__fixtures__/json-roundtrip/bad.ts +6 -0
- package/rules/__fixtures__/json-roundtrip/bad.tsx +8 -0
- package/rules/__fixtures__/json-roundtrip/good.ts +5 -0
- package/rules/__fixtures__/json-roundtrip/good.tsx +7 -0
- package/rules/__fixtures__/kind-if-without-match/bad.ts +17 -0
- package/rules/__fixtures__/kind-if-without-match/bad.tsx +18 -0
- package/rules/__fixtures__/kind-if-without-match/good.ts +14 -0
- package/rules/__fixtures__/kind-if-without-match/good.tsx +13 -0
- package/rules/__fixtures__/literal-union-in-component/components/bad.tsx +16 -0
- package/rules/__fixtures__/literal-union-in-component/components/good.tsx +25 -0
- package/rules/__fixtures__/non-exhaustive-match/bad.tsx +12 -0
- package/rules/__fixtures__/non-exhaustive-match/good.tsx +14 -0
- package/rules/__fixtures__/silent-default-return/bad.ts +9 -0
- package/rules/__fixtures__/silent-default-return/bad.tsx +10 -0
- package/rules/__fixtures__/silent-default-return/good.ts +17 -0
- package/rules/__fixtures__/silent-default-return/good.tsx +11 -0
- package/rules/demo-mode-by-default-ts.yml +17 -1
- package/rules/demo-mode-by-default.yml +17 -1
- package/rules/json-roundtrip-tsx.yml +26 -0
- package/rules/kind-if-without-match-tsx.yml +31 -0
- package/rules/literal-union-in-component-tsx.yml +81 -0
- package/rules/no-local-kit-clone-tsx.yml +16 -0
- package/rules/no-local-kit-clone.yml +16 -0
- package/rules/non-exhaustive-match-tsx.yml +48 -0
- package/rules/silent-default-return-tsx.yml +33 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// NOTHING in this file may be reported by `kind-if-without-match-tsx`.
|
|
2
|
+
import { match } from "ts-pattern"
|
|
3
|
+
|
|
4
|
+
export const A = (r: R) =>
|
|
5
|
+
match(r)
|
|
6
|
+
.with({ kind: "idle" }, () => <Spinner />)
|
|
7
|
+
.with({ kind: "ready" }, () => <List />)
|
|
8
|
+
.exhaustive()
|
|
9
|
+
|
|
10
|
+
export const B = (r: R) => {
|
|
11
|
+
if (r.count === 0) return <Empty />
|
|
12
|
+
return <List />
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Every type here MUST be reported by `literal-union-in-component-tsx`.
|
|
2
|
+
// Path matters: the rule is scoped to component/screen/app directories.
|
|
3
|
+
//
|
|
4
|
+
// This is the file the rule was named for and never saw. Under
|
|
5
|
+
// `language: typescript` the `**/components/**/*.tsx` glob in the rule could
|
|
6
|
+
// not match anything — the `.tsx` arm makes those glob lines real.
|
|
7
|
+
|
|
8
|
+
// Enum-style UI state redeclared in the file that renders it.
|
|
9
|
+
export type Tab = "tasks" | "habits" | "stream"
|
|
10
|
+
export type LoadPhase = "idle" | "loading" | "ready" | "error"
|
|
11
|
+
type Align = "start" | "center" | "end"
|
|
12
|
+
export type Tone = "neutral" | "danger" | "success" | "warning"
|
|
13
|
+
|
|
14
|
+
export const TabBar = ({ tab, align }: { tab: Tab; align: Align }) => (
|
|
15
|
+
<nav data-align={align}>{tab}</nav>
|
|
16
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// NOTHING in this file may be reported by `literal-union-in-component-tsx`.
|
|
2
|
+
|
|
3
|
+
// The fix: consume the shared type, do not restate it.
|
|
4
|
+
import type { Tab } from "../../stores/uiStore"
|
|
5
|
+
import type { Tone } from "../../lib/tone"
|
|
6
|
+
|
|
7
|
+
export interface TabBarProps {
|
|
8
|
+
tab: Tab
|
|
9
|
+
tone: Tone
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A single-member alias is not an enum-style union.
|
|
13
|
+
export type Only = "one"
|
|
14
|
+
|
|
15
|
+
// A union of non-literals is a different thing entirely.
|
|
16
|
+
export type Id = string | number
|
|
17
|
+
export type Handler = (() => void) | undefined
|
|
18
|
+
|
|
19
|
+
// A discriminated union of OBJECT shapes is the pattern this codebase wants
|
|
20
|
+
// everywhere — it is not a bare string enum and must not be flagged.
|
|
21
|
+
export type Resource<T> =
|
|
22
|
+
| { status: "idle" }
|
|
23
|
+
| { status: "ready"; data: T }
|
|
24
|
+
|
|
25
|
+
export const TabBar = ({ tab, tone }: TabBarProps) => <nav data-tone={tone}>{tab}</nav>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Every chain here MUST be reported by `non-exhaustive-match-tsx`.
|
|
2
|
+
// The .ts arm (`non-exhaustive-match`) does NOT see this file: ast-grep's
|
|
3
|
+
// `typescript` and `tsx` languages are disjoint.
|
|
4
|
+
import { match } from "ts-pattern"
|
|
5
|
+
|
|
6
|
+
export const A = (r: R) => match(r).with({ kind: "idle" }, () => <Spinner />).otherwise(() => null)
|
|
7
|
+
|
|
8
|
+
export const B = (r: R) =>
|
|
9
|
+
match(r)
|
|
10
|
+
.with({ kind: "idle" }, () => <Spinner />)
|
|
11
|
+
.with({ kind: "ready" }, () => <List />)
|
|
12
|
+
.otherwise(() => <Empty />)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// NOTHING in this file may be reported by `non-exhaustive-match-tsx`.
|
|
2
|
+
import { match } from "ts-pattern"
|
|
3
|
+
|
|
4
|
+
export const A = (r: R) =>
|
|
5
|
+
match(r)
|
|
6
|
+
.with({ kind: "idle" }, () => <Spinner />)
|
|
7
|
+
.with({ kind: "ready" }, () => <List />)
|
|
8
|
+
.exhaustive()
|
|
9
|
+
|
|
10
|
+
// The false-positive pins carried over from good.ts: the rule anchors to a
|
|
11
|
+
// ts-pattern `match()` chain, so a method merely NAMED `otherwise` is not a
|
|
12
|
+
// finding. A revert to the old bare `$X.otherwise($$$)` body fails here.
|
|
13
|
+
export const q = someBuilder.otherwise(() => 1)
|
|
14
|
+
export const r = config.fallback.otherwise
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Every return here MUST be reported by `silent-default-return`.
|
|
2
|
+
export const a = (x?: T) => { if (!x) return null; return use(x) }
|
|
3
|
+
export const b = (x?: T[]) => { if (!x) return []; return x }
|
|
4
|
+
export const c = (x?: number) => { if (!x) return 0; return x }
|
|
5
|
+
export const d = (x?: string) => { if (!x) return ""; return x }
|
|
6
|
+
export const e = (x?: T) => { if (x === undefined) return null; return use(x) }
|
|
7
|
+
export const f = (x?: T[]) => { if (x === undefined) return []; return x }
|
|
8
|
+
export const g = (x?: T) => { if (x == null) return null; return use(x) }
|
|
9
|
+
export const h = (x?: T[]) => { if (x == null) return []; return x }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Every return here MUST be reported by `silent-default-return-tsx`.
|
|
2
|
+
// The .ts arm (`silent-default-return`) does NOT see this file.
|
|
3
|
+
export const A = (x?: T) => { if (!x) return null; return <Use v={x} /> }
|
|
4
|
+
export const b = (x?: T[]) => { if (!x) return []; return x }
|
|
5
|
+
export const c = (x?: number) => { if (!x) return 0; return x }
|
|
6
|
+
export const d = (x?: string) => { if (!x) return ""; return x }
|
|
7
|
+
export const E = (x?: T) => { if (x === undefined) return null; return <Use v={x} /> }
|
|
8
|
+
export const f = (x?: T[]) => { if (x === undefined) return []; return x }
|
|
9
|
+
export const G = (x?: T) => { if (x == null) return null; return <Use v={x} /> }
|
|
10
|
+
export const h = (x?: T[]) => { if (x == null) return []; return x }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// NOTHING in this file may be reported by `silent-default-return`.
|
|
2
|
+
export const a = (x?: T) => {
|
|
3
|
+
if (!x) throw new Error("a(): x is required; caller passed nothing")
|
|
4
|
+
return use(x)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// An explicit discriminated failure variant is the sanctioned shape.
|
|
8
|
+
export const b = (x?: T) => {
|
|
9
|
+
if (!x) return { kind: "failure", reason: "missing-x" } as const
|
|
10
|
+
return { kind: "success", value: use(x) } as const
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The rule is the SILENT default, not every early return.
|
|
14
|
+
export const c = (x?: T) => {
|
|
15
|
+
if (!x) return fallbackFor(x)
|
|
16
|
+
return use(x)
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// NOTHING in this file may be reported by `silent-default-return-tsx`.
|
|
2
|
+
export const A = (x?: T) => {
|
|
3
|
+
if (!x) throw new Error("A(): x is required; the caller rendered it with nothing")
|
|
4
|
+
return <Use v={x} />
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Rendering an explicit empty state is not a silent default.
|
|
8
|
+
export const B = (x?: T) => {
|
|
9
|
+
if (!x) return <Empty reason="no-x" />
|
|
10
|
+
return <Use v={x} />
|
|
11
|
+
}
|
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
# Validated against the real pre-fix store.tsx: 1 hit at the exact declaration,
|
|
36
36
|
# 0 on the fixed version, 0 across substrate-app, tg-gallery webapp and
|
|
37
37
|
# agent-workbench web.
|
|
38
|
+
#
|
|
39
|
+
# 2026-09-06 (is-9e66ec5a): the value matcher was widened to accept
|
|
40
|
+
# `as const` / `as <Type>` / `satisfies <Type>` around the literal. This is an
|
|
41
|
+
# error-tier rule, so the widening was measured before landing: 0 new findings
|
|
42
|
+
# in the seven repos that run this gate, and 0 across every repo under
|
|
43
|
+
# ~/gits/github — the only hits corpus-wide are this package's own fixtures.
|
|
38
44
|
id: demo-mode-by-default-ts
|
|
39
45
|
language: typescript
|
|
40
46
|
severity: error
|
|
@@ -52,9 +58,19 @@ rule:
|
|
|
52
58
|
- has:
|
|
53
59
|
field: key
|
|
54
60
|
regex: '^(kind|mode|backend|source|dataSource|driver|adapter|transport|client)$'
|
|
61
|
+
# The literal, optionally wrapped in `as const` / `as Mode` /
|
|
62
|
+
# `satisfies Mode` (is-9e66ec5a). Without the tail, `{ kind: "demo" }` was
|
|
63
|
+
# a finding and `{ kind: "demo" as const }` was not — and `as const` is the
|
|
64
|
+
# spelling people reach for precisely when the object has no type
|
|
65
|
+
# annotation to carry the literal type. DataSpool's retired local
|
|
66
|
+
# `no-fake-defaults` caught it, so adopting the canon had been a coverage
|
|
67
|
+
# regression; the reviewer sent both gate PRs back over it.
|
|
68
|
+
#
|
|
69
|
+
# The leading and trailing `.` are the quote character, so `"demo-ish"`
|
|
70
|
+
# still does not match: the alternation must be the WHOLE string body.
|
|
55
71
|
- has:
|
|
56
72
|
field: value
|
|
57
|
-
regex: '^.(demo|mock|fixture|fake|stub|sample|seed)
|
|
73
|
+
regex: '^.(demo|mock|fixture|fake|stub|sample|seed).( +(as|satisfies) +.+)?$'
|
|
58
74
|
- not:
|
|
59
75
|
inside:
|
|
60
76
|
stopBy: end
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
# Validated against the real pre-fix store.tsx: 1 hit at the exact declaration,
|
|
16
16
|
# 0 on the fixed version, 0 across substrate-app, tg-gallery webapp and
|
|
17
17
|
# agent-workbench web.
|
|
18
|
+
#
|
|
19
|
+
# 2026-09-06 (is-9e66ec5a): the value matcher was widened to accept
|
|
20
|
+
# `as const` / `as <Type>` / `satisfies <Type>` around the literal. This is an
|
|
21
|
+
# error-tier rule, so the widening was measured before landing: 0 new findings
|
|
22
|
+
# in the seven repos that run this gate, and 0 across every repo under
|
|
23
|
+
# ~/gits/github — the only hits corpus-wide are this package's own fixtures.
|
|
18
24
|
id: demo-mode-by-default
|
|
19
25
|
language: tsx
|
|
20
26
|
severity: error
|
|
@@ -30,9 +36,19 @@ rule:
|
|
|
30
36
|
- has:
|
|
31
37
|
field: key
|
|
32
38
|
regex: '^(kind|mode|backend|source|dataSource|driver|adapter|transport|client)$'
|
|
39
|
+
# The literal, optionally wrapped in `as const` / `as Mode` /
|
|
40
|
+
# `satisfies Mode` (is-9e66ec5a). Without the tail, `{ kind: "demo" }` was
|
|
41
|
+
# a finding and `{ kind: "demo" as const }` was not — and `as const` is the
|
|
42
|
+
# spelling people reach for precisely when the object has no type
|
|
43
|
+
# annotation to carry the literal type. DataSpool's retired local
|
|
44
|
+
# `no-fake-defaults` caught it, so adopting the canon had been a coverage
|
|
45
|
+
# regression; the reviewer sent both gate PRs back over it.
|
|
46
|
+
#
|
|
47
|
+
# The leading and trailing `.` are the quote character, so `"demo-ish"`
|
|
48
|
+
# still does not match: the alternation must be the WHOLE string body.
|
|
33
49
|
- has:
|
|
34
50
|
field: value
|
|
35
|
-
regex: '^.(demo|mock|fixture|fake|stub|sample|seed)
|
|
51
|
+
regex: '^.(demo|mock|fixture|fake|stub|sample|seed).( +(as|satisfies) +.+)?$'
|
|
36
52
|
- not:
|
|
37
53
|
inside:
|
|
38
54
|
stopBy: end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# ── the .tsx half of `json-roundtrip` ──
|
|
2
|
+
#
|
|
3
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
4
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`. `json-roundtrip`
|
|
5
|
+
# was therefore blind to every React component file. The mechanism, and the
|
|
6
|
+
# three alternatives that were tried and rejected on measurement, are written
|
|
7
|
+
# out once in `catch-empty-tsx.yml`; the short version is that ast-grep has no
|
|
8
|
+
# way to say "both languages", so a twin id per language is the only option.
|
|
9
|
+
#
|
|
10
|
+
# Measured 2026-09-06 across the same five repos: 0 findings. A clean zero,
|
|
11
|
+
# not a broken one — the rule fires on a known positive in its fixture. Closing
|
|
12
|
+
# a language gap at zero cost is the same call `demo-mode-by-default-ts` made.
|
|
13
|
+
#
|
|
14
|
+
# Keep this file's `rule:` block IDENTICAL to `json-roundtrip.yml`.
|
|
15
|
+
#
|
|
16
|
+
id: json-roundtrip-tsx
|
|
17
|
+
language: tsx
|
|
18
|
+
severity: warning
|
|
19
|
+
message: |
|
|
20
|
+
JSON.parse(JSON.stringify(x)) loses Maps, Sets, TypedArrays, class instances, circular refs.
|
|
21
|
+
Use a typed serializer with Zod validation at the boundary.
|
|
22
|
+
See defensive-errors Rule 5.
|
|
23
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: json-roundtrip-tsx`.
|
|
24
|
+
`json-roundtrip` is a DIFFERENT rule (the .ts arm) and does not cover it.
|
|
25
|
+
rule:
|
|
26
|
+
pattern: "JSON.parse(JSON.stringify($X))"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# ── the .tsx half of `kind-if-without-match` ──
|
|
2
|
+
#
|
|
3
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
4
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`. `kind-if-without-match`
|
|
5
|
+
# was therefore blind to every React component file. The mechanism, and the
|
|
6
|
+
# three alternatives that were tried and rejected on measurement, are written
|
|
7
|
+
# out once in `catch-empty-tsx.yml`; the short version is that ast-grep has no
|
|
8
|
+
# way to say "both languages", so a twin id per language is the only option.
|
|
9
|
+
#
|
|
10
|
+
# Measured 2026-09-06 across the same five repos: 29 findings in `.tsx`
|
|
11
|
+
# (SiteCraft 1, faceless 2, observatory 6, tg-gallery 2, merkle-substrate 18).
|
|
12
|
+
# `if (x.kind === …)` branching lives in components at least as often as in
|
|
13
|
+
# plain modules, which is precisely where the union grows a variant unnoticed.
|
|
14
|
+
#
|
|
15
|
+
# Keep this file's `rule:` block IDENTICAL to `kind-if-without-match.yml`.
|
|
16
|
+
#
|
|
17
|
+
id: kind-if-without-match-tsx
|
|
18
|
+
language: tsx
|
|
19
|
+
severity: warning
|
|
20
|
+
message: |
|
|
21
|
+
Discriminated-union branching with if/else silently ignores new variants.
|
|
22
|
+
Use match($X).with({kind: ...}, ...).exhaustive() from ts-pattern.
|
|
23
|
+
See defensive-errors Rule 2 and the pattern-matching skill.
|
|
24
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: kind-if-without-match-tsx`.
|
|
25
|
+
`kind-if-without-match` is a DIFFERENT rule (the .ts arm) and does not cover it.
|
|
26
|
+
rule:
|
|
27
|
+
any:
|
|
28
|
+
- pattern: "if ($X.kind === $_) $_"
|
|
29
|
+
- pattern: "if ($X.type === $_) $_"
|
|
30
|
+
- pattern: "if ($X.status === $_) $_"
|
|
31
|
+
- pattern: "if ($X._obj === $_) $_"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# ── the .tsx half of `literal-union-in-component` ──
|
|
2
|
+
#
|
|
3
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
4
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`. `literal-union-in-component`
|
|
5
|
+
# was therefore blind to every React component file. The mechanism, and the
|
|
6
|
+
# three alternatives that were tried and rejected on measurement, are written
|
|
7
|
+
# out once in `catch-empty-tsx.yml`; the short version is that ast-grep has no
|
|
8
|
+
# way to say "both languages", so a twin id per language is the only option.
|
|
9
|
+
#
|
|
10
|
+
# Measured 2026-09-06 across the same five repos: 41 findings in `.tsx`
|
|
11
|
+
# (SiteCraft 1, faceless 7, observatory 9, tg-gallery 6, merkle-substrate 18).
|
|
12
|
+
# This twin restores intent rather than extending it: the `files:` globs below
|
|
13
|
+
# already list `**/components/**/*.tsx` and `**/screens/**/*.tsx`, and under
|
|
14
|
+
# `language: typescript` those three lines could never match anything. The rule
|
|
15
|
+
# is named `-in-component` and was blind to components.
|
|
16
|
+
#
|
|
17
|
+
# Keep this file's `rule:` block IDENTICAL to `literal-union-in-component.yml`.
|
|
18
|
+
#
|
|
19
|
+
id: literal-union-in-component-tsx
|
|
20
|
+
language: tsx
|
|
21
|
+
severity: warning
|
|
22
|
+
files:
|
|
23
|
+
- '**/components/**/*.ts'
|
|
24
|
+
- '**/components/**/*.tsx'
|
|
25
|
+
- '**/screens/**/*.ts'
|
|
26
|
+
- '**/screens/**/*.tsx'
|
|
27
|
+
- '**/app/**/*.ts'
|
|
28
|
+
- '**/app/**/*.tsx'
|
|
29
|
+
ignores:
|
|
30
|
+
- '**/__tests__/**'
|
|
31
|
+
- '**/*.test.ts'
|
|
32
|
+
- '**/*.test.tsx'
|
|
33
|
+
- '**/_fixture.ts'
|
|
34
|
+
- '**/_fixtures.ts'
|
|
35
|
+
message: |
|
|
36
|
+
A string-literal union declared inside a component file. Every component that
|
|
37
|
+
touches the same concept redeclares it, and the copies drift — one file grows
|
|
38
|
+
a `"archived"` member the others never hear about, and nothing fails to build
|
|
39
|
+
because each copy is internally consistent.
|
|
40
|
+
Move it to where the state it describes lives:
|
|
41
|
+
- a UI-tier store, next to the actions that mutate it, when it IS state
|
|
42
|
+
(`type Tab = "tasks" | "habits"` belongs to the store that owns the tab), or
|
|
43
|
+
- a shared module as a typed constant + alias when it is purely lexical
|
|
44
|
+
(file kinds, palette names) with no state attached.
|
|
45
|
+
Component prop types then consume the shared type instead of restating it.
|
|
46
|
+
See the mobx-models skill and report §8.
|
|
47
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: literal-union-in-component-tsx`.
|
|
48
|
+
`literal-union-in-component` is a DIFFERENT rule (the .ts arm) and does not cover it.
|
|
49
|
+
rule:
|
|
50
|
+
all:
|
|
51
|
+
- kind: type_alias_declaration
|
|
52
|
+
# Two or more STRING members on the union's own spine. The spine matters:
|
|
53
|
+
# tree-sitter nests unions left-associatively, so a 3-member union is
|
|
54
|
+
# `union_type(union_type(lit, lit), lit)` and a 2-member one is
|
|
55
|
+
# `union_type(lit, lit)` — hence the two arms. Searching with `stopBy: end`
|
|
56
|
+
# instead would reach literals nested INSIDE object members and flag
|
|
57
|
+
# `{ status: "idle" } | { status: "ready" }`, the discriminated union this
|
|
58
|
+
# codebase wants everywhere.
|
|
59
|
+
- has:
|
|
60
|
+
field: value
|
|
61
|
+
kind: union_type
|
|
62
|
+
any:
|
|
63
|
+
# three or more members
|
|
64
|
+
- has:
|
|
65
|
+
kind: union_type
|
|
66
|
+
has:
|
|
67
|
+
kind: literal_type
|
|
68
|
+
has:
|
|
69
|
+
kind: string
|
|
70
|
+
# exactly two members
|
|
71
|
+
- has:
|
|
72
|
+
all:
|
|
73
|
+
- kind: literal_type
|
|
74
|
+
- has:
|
|
75
|
+
kind: string
|
|
76
|
+
- follows:
|
|
77
|
+
stopBy: end
|
|
78
|
+
all:
|
|
79
|
+
- kind: literal_type
|
|
80
|
+
- has:
|
|
81
|
+
kind: string
|
|
@@ -34,7 +34,23 @@
|
|
|
34
34
|
# `Resource` must carry type parameters to match. Without that the rule hits
|
|
35
35
|
# `interface Resource { name; url; type; description }` in VolumePhotography — a
|
|
36
36
|
# page listing conference links, nothing to do with async state.
|
|
37
|
+
# CARVE-OUT 1 IS NOW A REPO DECLARATION, not only a glob (is-086a90ac).
|
|
38
|
+
# `@agentvibes/kit` exists since 2026-09 and lives in its own repo, where verify
|
|
39
|
+
# sees its files as `src/resource/resource.ts` — neither `**/packages/kit/**`
|
|
40
|
+
# nor `**/agentvibes-kit/**` matches that, and no path glob can tell that tree
|
|
41
|
+
# from any other repo's `src/`. So the kit repo says so itself:
|
|
42
|
+
#
|
|
43
|
+
# [verify]
|
|
44
|
+
# kit_source = "^src/"
|
|
45
|
+
#
|
|
46
|
+
# together with the `appliesTo: not-kit-source` marker below. Same mechanism
|
|
47
|
+
# `[verify] screens` uses (is-7067a0b8): the repo states a fact about itself,
|
|
48
|
+
# the canon says which rules stand down for it. The globs stay for monorepos
|
|
49
|
+
# that vendor the kit under `packages/kit/`.
|
|
50
|
+
#
|
|
37
51
|
id: no-local-kit-clone-tsx
|
|
52
|
+
metadata:
|
|
53
|
+
appliesTo: not-kit-source
|
|
38
54
|
language: tsx
|
|
39
55
|
severity: error
|
|
40
56
|
ignores:
|
|
@@ -23,7 +23,23 @@
|
|
|
23
23
|
# `Resource` must carry type parameters to match. Without that the rule hits
|
|
24
24
|
# `interface Resource { name; url; type; description }` in VolumePhotography — a
|
|
25
25
|
# page listing conference links, nothing to do with async state.
|
|
26
|
+
# CARVE-OUT 1 IS NOW A REPO DECLARATION, not only a glob (is-086a90ac).
|
|
27
|
+
# `@agentvibes/kit` exists since 2026-09 and lives in its own repo, where verify
|
|
28
|
+
# sees its files as `src/resource/resource.ts` — neither `**/packages/kit/**`
|
|
29
|
+
# nor `**/agentvibes-kit/**` matches that, and no path glob can tell that tree
|
|
30
|
+
# from any other repo's `src/`. So the kit repo says so itself:
|
|
31
|
+
#
|
|
32
|
+
# [verify]
|
|
33
|
+
# kit_source = "^src/"
|
|
34
|
+
#
|
|
35
|
+
# together with the `appliesTo: not-kit-source` marker below. Same mechanism
|
|
36
|
+
# `[verify] screens` uses (is-7067a0b8): the repo states a fact about itself,
|
|
37
|
+
# the canon says which rules stand down for it. The globs stay for monorepos
|
|
38
|
+
# that vendor the kit under `packages/kit/`.
|
|
39
|
+
#
|
|
26
40
|
id: no-local-kit-clone
|
|
41
|
+
metadata:
|
|
42
|
+
appliesTo: not-kit-source
|
|
27
43
|
language: typescript
|
|
28
44
|
severity: error
|
|
29
45
|
ignores:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# ── the .tsx half of `non-exhaustive-match` ──
|
|
2
|
+
#
|
|
3
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
4
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`. `non-exhaustive-match`
|
|
5
|
+
# was therefore blind to every React component file. The mechanism, and the
|
|
6
|
+
# three alternatives that were tried and rejected on measurement, are written
|
|
7
|
+
# out once in `catch-empty-tsx.yml`; the short version is that ast-grep has no
|
|
8
|
+
# way to say "both languages", so a twin id per language is the only option.
|
|
9
|
+
#
|
|
10
|
+
# Measured 2026-09-06 across the five repos that consume guardrails
|
|
11
|
+
# (SiteCraftMonorepo 40, faceless-photo-lib 28, agent-session-observatory 18,
|
|
12
|
+
# tg-gallery 47, merkle-substrate 52): 185 `.otherwise()` chains in `.tsx` the
|
|
13
|
+
# `.ts` arm never saw. Adding them was the owner's call — decision D4 in
|
|
14
|
+
# doc-e4526f1a parks the question of banning `.otherwise()` outright, so the
|
|
15
|
+
# owner was asked whether to surface the .tsx half before D4 lands and answered
|
|
16
|
+
# «1» (add the twin now, severity warning) on 2026-09-06, is-8385d5dd.
|
|
17
|
+
#
|
|
18
|
+
# Keep this file's `rule:` block IDENTICAL to `non-exhaustive-match.yml`.
|
|
19
|
+
#
|
|
20
|
+
id: non-exhaustive-match-tsx
|
|
21
|
+
language: tsx
|
|
22
|
+
severity: warning
|
|
23
|
+
message: |
|
|
24
|
+
`.otherwise()` catches unmatched variants silently. Prefer `.exhaustive()` so adding a
|
|
25
|
+
new union variant becomes a compile error everywhere it's branched on.
|
|
26
|
+
See defensive-errors Rule 2 and pattern-matching skill.
|
|
27
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: non-exhaustive-match-tsx`.
|
|
28
|
+
`non-exhaustive-match` is a DIFFERENT rule (the .ts arm) and does not cover it.
|
|
29
|
+
rule:
|
|
30
|
+
all:
|
|
31
|
+
- kind: call_expression
|
|
32
|
+
has:
|
|
33
|
+
field: function
|
|
34
|
+
kind: identifier
|
|
35
|
+
regex: "^match$"
|
|
36
|
+
- inside:
|
|
37
|
+
stopBy:
|
|
38
|
+
not:
|
|
39
|
+
any:
|
|
40
|
+
- kind: member_expression
|
|
41
|
+
- kind: call_expression
|
|
42
|
+
kind: call_expression
|
|
43
|
+
has:
|
|
44
|
+
field: function
|
|
45
|
+
kind: member_expression
|
|
46
|
+
has:
|
|
47
|
+
field: property
|
|
48
|
+
regex: "^otherwise$"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ── the .tsx half of `silent-default-return` ──
|
|
2
|
+
#
|
|
3
|
+
# ast-grep's `typescript` and `tsx` languages are DISJOINT, not nested: a rule
|
|
4
|
+
# at `language: typescript` matches `.ts` and NEVER `.tsx`. `silent-default-return`
|
|
5
|
+
# was therefore blind to every React component file. The mechanism, and the
|
|
6
|
+
# three alternatives that were tried and rejected on measurement, are written
|
|
7
|
+
# out once in `catch-empty-tsx.yml`; the short version is that ast-grep has no
|
|
8
|
+
# way to say "both languages", so a twin id per language is the only option.
|
|
9
|
+
#
|
|
10
|
+
# Measured 2026-09-06 across the same five repos: 24 findings in `.tsx`
|
|
11
|
+
# (SiteCraft 3, faceless 0, observatory 9, tg-gallery 0, merkle-substrate 12).
|
|
12
|
+
#
|
|
13
|
+
# Keep this file's `rule:` block IDENTICAL to `silent-default-return.yml`.
|
|
14
|
+
#
|
|
15
|
+
id: silent-default-return-tsx
|
|
16
|
+
language: tsx
|
|
17
|
+
severity: warning
|
|
18
|
+
message: |
|
|
19
|
+
Silent-default return on negation. Hides upstream bugs.
|
|
20
|
+
Throw with a contextual message, or return an explicit discriminated failure variant.
|
|
21
|
+
See defensive-errors Rule 1.
|
|
22
|
+
Suppressing this one needs the id you see above — `ast-grep-ignore: silent-default-return-tsx`.
|
|
23
|
+
`silent-default-return` is a DIFFERENT rule (the .ts arm) and does not cover it.
|
|
24
|
+
rule:
|
|
25
|
+
any:
|
|
26
|
+
- pattern: "if (!$X) return null"
|
|
27
|
+
- pattern: "if (!$X) return []"
|
|
28
|
+
- pattern: "if (!$X) return 0"
|
|
29
|
+
- pattern: "if (!$X) return \"\""
|
|
30
|
+
- pattern: "if ($X === undefined) return null"
|
|
31
|
+
- pattern: "if ($X === undefined) return []"
|
|
32
|
+
- pattern: "if ($X == null) return null"
|
|
33
|
+
- pattern: "if ($X == null) return []"
|