@starci/eslint-canon-fe 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +33 -16
  3. package/brand-values.mjs +172 -0
  4. package/class-names.mjs +9 -9
  5. package/client-boundary.mjs +287 -0
  6. package/comments.mjs +137 -190
  7. package/docs/brand-values.md +30 -0
  8. package/docs/client-boundary.md +124 -0
  9. package/docs/e2e-shape.md +161 -0
  10. package/docs/env-owner.md +52 -0
  11. package/docs/formatting.md +30 -0
  12. package/docs/grammar-boundary.md +33 -0
  13. package/docs/hooks-folder.md +29 -0
  14. package/docs/hygiene.md +122 -0
  15. package/docs/lint-escape-hatch.md +34 -0
  16. package/docs/lists.md +74 -0
  17. package/docs/native-controls.md +73 -0
  18. package/docs/next-conventions.md +232 -0
  19. package/docs/size-and-state-budget.md +73 -0
  20. package/docs/size-growth.md +29 -0
  21. package/docs/spec-quality.md +147 -0
  22. package/docs/status-colors.md +32 -0
  23. package/docs/translation.md +54 -0
  24. package/docs/transport.md +195 -0
  25. package/docs/type-safety.md +95 -0
  26. package/e2e-shape.mjs +345 -0
  27. package/env-owner.mjs +134 -0
  28. package/file-layout.mjs +105 -104
  29. package/formatting.mjs +104 -0
  30. package/grammar-boundary.mjs +85 -9
  31. package/hooks-folder.mjs +120 -0
  32. package/hygiene.mjs +513 -0
  33. package/icon.mjs +212 -212
  34. package/index.mjs +243 -142
  35. package/landmark.mjs +0 -11
  36. package/lib/ast.mjs +94 -0
  37. package/lib/bindings.mjs +144 -0
  38. package/lib/config.mjs +79 -0
  39. package/lib/hfs.mjs +48 -0
  40. package/lib/next.mjs +6 -0
  41. package/lib/path.mjs +2 -0
  42. package/lib/platform.mjs +66 -0
  43. package/lib/react.mjs +28 -0
  44. package/lib/scope.mjs +142 -0
  45. package/lib/status-tones.generated.mjs +9 -0
  46. package/lib/types.mjs +63 -0
  47. package/lib/why.mjs +334 -0
  48. package/lint-escape-hatch.mjs +93 -56
  49. package/lists.mjs +197 -0
  50. package/loading.mjs +156 -164
  51. package/naming.mjs +149 -143
  52. package/native-controls.mjs +112 -0
  53. package/next-conventions.mjs +430 -0
  54. package/package.json +15 -6
  55. package/props-and-slots.mjs +19 -13
  56. package/runtime/engine/plain-object.mjs +5 -0
  57. package/runtime/engine/runtime-root.mjs +32 -0
  58. package/runtime/engine/yaml.mjs +161 -0
  59. package/runtime/knowledge/hfs/slots.yaml +1072 -0
  60. package/runtime/scripts/lib/git.mjs +53 -0
  61. package/runtime/scripts/lib/glob.mjs +23 -0
  62. package/runtime/scripts/lib/hfs-allows.mjs +42 -0
  63. package/runtime/scripts/lib/hfs-slots.mjs +722 -0
  64. package/runtime/scripts/lib/hfs-view.mjs +68 -0
  65. package/runtime/scripts/lib/next-contract.mjs +14 -0
  66. package/runtime/scripts/lib/path-key.mjs +17 -0
  67. package/runtime/scripts/lib/recorded-lines.mjs +27 -0
  68. package/served-locale.mjs +174 -160
  69. package/shape-slot.mjs +22 -21
  70. package/size-and-state-budget.mjs +214 -0
  71. package/size-growth.mjs +58 -0
  72. package/spec-quality.mjs +318 -0
  73. package/status-colors.mjs +139 -0
  74. package/the-split.mjs +133 -134
  75. package/tokens.mjs +259 -268
  76. package/translation.mjs +295 -122
  77. package/transport.mjs +752 -0
  78. package/type-safety.mjs +147 -74
  79. package/typography.mjs +78 -83
  80. package/vendor-boundary.mjs +5 -27
  81. package/lint-adoption.mjs +0 -116
package/docs/lists.md ADDED
@@ -0,0 +1,74 @@
1
+ # Lists
2
+
3
+ Law module: `lists.mjs`. Catalogue: R65 FE_SIZE_AND_STATE_BUDGET, sub-check `FE_LIST_KEY`.
4
+
5
+ A list is items with identity. React reconciles a rendered list by `key`; a missing, positional or random key hands a deleted row's state to its neighbour or remounts every row. A component prop written as an object or array literal inside `.map` is a new value per row per render, so a memoised row never hits.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/list-item-has-key`
10
+
11
+ Every element a `.map` callback returns carries a `key`.
12
+
13
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
14
+
15
+ ```tsx
16
+ rows.map((row) => <Row row={row} />)
17
+ ```
18
+
19
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
20
+
21
+ ```tsx
22
+ rows.map((row) => <Row key={row.id} row={row} />)
23
+ ```
24
+
25
+ **Finding code:** `FE_LIST_KEY`
26
+
27
+ **Vì sao (why):** Phần tử trả từ `.map` ở `<file>` không có `key`, nên React nhận diện hàng theo vị trí và hàng bị đổi state khi xoá hoặc sắp xếp lại.
28
+
29
+ **Cách sửa:** Thêm `key` lấy từ id của dữ liệu; với fragment dùng `<Fragment key={...}>`.
30
+
31
+ ## `starci-fe/no-index-key`
32
+
33
+ A list key is stable data identity, never the map index or a random value. A static array written in place, or a callback that ignores its item (`(_, index) =>`), is exempt.
34
+
35
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
36
+
37
+ ```tsx
38
+ rows.map((row, index) => <Row key={index} row={row} />)
39
+ ```
40
+
41
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
42
+
43
+ ```tsx
44
+ rows.map((row) => <Row key={row.id} row={row} />)
45
+ ```
46
+
47
+ **Finding code:** `FE_LIST_KEY`
48
+
49
+ **Vì sao (why):** `key` ở `<file>` lấy từ chỉ số của `.map` hoặc giá trị sinh ngẫu nhiên; chỉ số là vị trí, không phải danh tính.
50
+
51
+ **Cách sửa:** Dùng id mà dữ liệu mang (`key={row.id}`); không dùng index, `Math.random`, `Date.now`.
52
+
53
+ ## `starci-fe/no-inline-literal-prop-in-list`
54
+
55
+ No object or array literal as a prop of a component rendered inside `.map`.
56
+
57
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
58
+
59
+ ```tsx
60
+ rows.map((row) => <Avatar key={row.id} props={{ size: "sm" }} />)
61
+ ```
62
+
63
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
64
+
65
+ ```tsx
66
+ const AVATAR_PROPS = { size: "sm" } as const
67
+ rows.map((row) => <Avatar key={row.id} props={AVATAR_PROPS} />)
68
+ ```
69
+
70
+ **Finding code:** `FE_LIST_KEY`
71
+
72
+ **Vì sao (why):** Component trong `.map` ở `<file>` nhận object hoặc array literal làm prop: mỗi hàng mỗi lần render một giá trị mới, memo không bao giờ trúng.
73
+
74
+ **Cách sửa:** Nâng hằng số ra ngoài component hoặc dựng một lần trước `.map`.
@@ -0,0 +1,73 @@
1
+ # No native form controls
2
+
3
+ Law module: `native-controls.mjs`. Catalogue: R62 FE_NATIVE_FORM_CONTROL.
4
+
5
+ `<select>`, `<input>`, `<textarea>` and `<button>` are rendered by the grammar in product source, at every tier. If the grammar lacks the control, add it to the grammar.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/no-native-form-control`
10
+
11
+ Use the grammar's renderers; never a bare select, input, textarea or button.
12
+
13
+ **Invalid** (`src/components/blocks/Feed/index.tsx`)
14
+
15
+ ```ts
16
+ <button onClick={go} />
17
+ ```
18
+
19
+ **Valid** (`src/components/blocks/Feed/index.tsx`)
20
+
21
+ ```ts
22
+ <Button onPress={go}>{t("go")}</Button>
23
+ ```
24
+
25
+ **Finding code:** `FE_NATIVE_FORM_CONTROL`
26
+
27
+ **Vì sao (why):** `<tag>` thô ở `<file>`. Dùng renderer của grammar.
28
+
29
+ **Cách sửa:** Thay bằng thành phần của grammar; nếu chưa có, thêm vào grammar thay vì vẽ tại chỗ.
30
+
31
+ ## `starci-fe/no-native-img`
32
+
33
+ Use `next/image`, never a bare `<img>` (catalogue R62, sub-check `FE_NATIVE_IMAGE`).
34
+
35
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
36
+
37
+ ```tsx
38
+ <img src={src} alt={t("cover")} />
39
+ ```
40
+
41
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
42
+
43
+ ```tsx
44
+ <Image src={src} alt={t("cover")} width={40} height={40} />
45
+ ```
46
+
47
+ **Finding code:** `FE_NATIVE_IMAGE`
48
+
49
+ **Vì sao (why):** `<img>` thô ở `<file>`: tải ảnh gốc, không giữ chỗ nên trang nhảy khi ảnh tải xong.
50
+
51
+ **Cách sửa:** Dùng `Image` của `next/image` với `width` và `height` (hoặc `fill` và `sizes`) và `alt` từ catalog.
52
+
53
+ ## `starci-fe/image-has-size`
54
+
55
+ `next/image` carries `width` and `height`, or `fill` with `sizes` (sub-check `FE_NATIVE_IMAGE`).
56
+
57
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
58
+
59
+ ```tsx
60
+ <Image src={src} alt={t("cover")} />
61
+ ```
62
+
63
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
64
+
65
+ ```tsx
66
+ <Image src={src} alt={t("cover")} width={40} height={40} />
67
+ ```
68
+
69
+ **Finding code:** `FE_NATIVE_IMAGE`
70
+
71
+ **Vì sao (why):** `Image` ở `<file>` thiếu `width` và `height` (hoặc `fill` mà thiếu `sizes`): trình duyệt không giữ chỗ được.
72
+
73
+ **Cách sửa:** Cho đủ `width` và `height`, hoặc `fill` trong khung có kích thước kèm `sizes`.
@@ -0,0 +1,232 @@
1
+ # Next conventions and the one locale pattern
2
+
3
+ Law module: `next-conventions.mjs`. Catalogue: FE_NEXT_CONVENTIONS, R58 (html lang), R59 (i18n placement, single-file half).
4
+
5
+ One locale pattern everywhere: `next-intl`, a `[locale]` segment, default `vi`, `localePrefix: "as-needed"`, and `proxy.ts` (Next 16) instead of `middleware.ts`.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/no-middleware-file`
10
+
11
+ `middleware.ts` is forbidden: the request interceptor is `proxy.ts` and exports `proxy`.
12
+
13
+ **Invalid** (`src/middleware.ts`)
14
+
15
+ ```ts
16
+ export function middleware() {}
17
+ ```
18
+
19
+ **Valid** (`src/proxy.ts`)
20
+
21
+ ```ts
22
+ export function proxy(request) { return request }
23
+ ```
24
+
25
+ **Finding code:** `FE_NEXT_CONVENTIONS`
26
+
27
+ **Vì sao (why):** `<file>` dùng tên `middleware`. Từ Next 16 bộ chặn request là `proxy.ts` và xuất `proxy`.
28
+
29
+ **Cách sửa:** Đổi tên tệp thành `proxy.ts` và đổi export thành `proxy`.
30
+
31
+ ## `starci-fe/locale-segment-is-locale`
32
+
33
+ The locale route segment is `[locale]`, never `[lang]`.
34
+
35
+ **Invalid** (`src/app/[lang]/page.tsx`)
36
+
37
+ ```ts
38
+ export default function Page() {}
39
+ ```
40
+
41
+ **Valid** (`src/app/[locale]/page.tsx`)
42
+
43
+ ```ts
44
+ export default function Page() {}
45
+ ```
46
+
47
+ **Finding code:** `FE_I18N_PLACEMENT`
48
+
49
+ **Vì sao (why):** Tệp `<file>` nằm dưới segment ngôn ngữ không phải `[locale]`. Chỉ có một mẫu: `next-intl`, `[locale]`, mặc định `vi`.
50
+
51
+ **Cách sửa:** Đổi tên thư mục segment thành `[locale]`.
52
+
53
+ ## `starci-fe/no-second-i18n-stack`
54
+
55
+ `next-intl` is the only i18n library.
56
+
57
+ **Invalid** (`any file`)
58
+
59
+ ```ts
60
+ import { useTranslation } from "react-i18next"
61
+ ```
62
+
63
+ **Valid** (`any file`)
64
+
65
+ ```ts
66
+ import { useTranslations } from "next-intl"
67
+ ```
68
+
69
+ **Finding code:** `FE_I18N_PLACEMENT`
70
+
71
+ **Vì sao (why):** `<file>` nhập một thư viện i18n thứ hai. App chỉ dùng `next-intl` qua `modules/i18n`.
72
+
73
+ **Cách sửa:** Chuyển sang `next-intl`; xóa thư viện còn lại.
74
+
75
+ ## `starci-fe/html-lang-from-locale`
76
+
77
+ `<html lang>` is taken from the `[locale]` segment, never a literal.
78
+
79
+ **Invalid** (`src/app/[locale]/layout.tsx`)
80
+
81
+ ```ts
82
+ <html lang="en">
83
+ ```
84
+
85
+ **Valid** (`src/app/[locale]/layout.tsx`)
86
+
87
+ ```ts
88
+ <html lang={locale}>
89
+ ```
90
+
91
+ **Finding code:** `FE_I18N_LITERAL`
92
+
93
+ **Vì sao (why):** `<html lang>` ở `<file>` là chữ cứng. Thuộc tính `lang` phải lấy từ segment `[locale]`.
94
+
95
+ **Cách sửa:** Dùng `lang={locale}` từ tham số của layout `[locale]`.
96
+
97
+ ## `starci-fe/page-exports-metadata`
98
+
99
+ Every `page.tsx` exports `metadata` or `generateMetadata` (catalogue R54, sub-check `FE_PAGE_METADATA_MISSING`).
100
+
101
+ **Invalid** (`src/app/[locale]/courses/page.tsx`)
102
+
103
+ ```tsx
104
+ export default function Page() { return <CoursesPage /> }
105
+ ```
106
+
107
+ **Valid** (`src/app/[locale]/courses/page.tsx`)
108
+
109
+ ```tsx
110
+ export async function generateMetadata() { return { title: t("courses.title") } }
111
+ export default function Page() { return <CoursesPage /> }
112
+ ```
113
+
114
+ **Finding code:** `FE_PAGE_METADATA_MISSING`
115
+
116
+ **Vì sao (why):** `<file>` là `page.tsx` nhưng không export `metadata` hoặc `generateMetadata`; mọi trang trong nhánh mang cùng một tiêu đề.
117
+
118
+ **Cách sửa:** Export `metadata` (hoặc `generateMetadata` khi tiêu đề lấy từ dữ liệu) với tiêu đề và mô tả lấy từ catalog thông điệp.
119
+
120
+ ## `starci-fe/no-null-suspense-fallback`
121
+
122
+ `<Suspense>` has a fallback that renders something; `fallback={null}` is forbidden (catalogue R53, sub-check `FE_SUSPENSE_NULL_FALLBACK`).
123
+
124
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
125
+
126
+ ```tsx
127
+ <Suspense fallback={null}><Feed /></Suspense>
128
+ ```
129
+
130
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
131
+
132
+ ```tsx
133
+ <Suspense fallback={<FeedSkeleton />}><Feed /></Suspense>
134
+ ```
135
+
136
+ **Finding code:** `FE_SUSPENSE_NULL_FALLBACK`
137
+
138
+ **Vì sao (why):** `<Suspense>` ở `<file>` có `fallback` rỗng: người dùng thấy khoảng trống thay vì trạng thái đang tải.
139
+
140
+ **Cách sửa:** Truyền skeleton (trạng thái loading) của đúng thứ đang tải làm `fallback`.
141
+
142
+ ## `starci-fe/navigation-from-intl`
143
+
144
+ `Link`, `useRouter`, `usePathname` and `redirect` come from `modules/i18n/navigation`, not from Next (catalogue R59, sub-check `FE_I18N_NAVIGATION`).
145
+
146
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
147
+
148
+ ```tsx
149
+ import { useRouter } from "next/navigation"
150
+ ```
151
+
152
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
153
+
154
+ ```tsx
155
+ import { useRouter } from "@/modules/i18n/navigation"
156
+ ```
157
+
158
+ **Finding code:** `FE_I18N_NAVIGATION`
159
+
160
+ **Vì sao (why):** `<file>` nhập `Link`, `useRouter`, `usePathname` hoặc `redirect` từ Next, không biết locale: đường dẫn mất tiền tố `[locale]`.
161
+
162
+ **Cách sửa:** Nhập từ `modules/i18n/navigation`, được next-intl dựng từ `routing.ts`.
163
+
164
+ ## `starci-fe/no-native-anchor`
165
+
166
+ An internal link is `Link` from `modules/i18n/navigation`; an external `_blank` link carries `rel` (catalogue R59, sub-check `FE_I18N_NAVIGATION`).
167
+
168
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
169
+
170
+ ```tsx
171
+ <a href="/courses">{t("courses")}</a>
172
+ ```
173
+
174
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
175
+
176
+ ```tsx
177
+ <Link href={routes.courses()}>{t("courses")}</Link>
178
+ ```
179
+
180
+ **Finding code:** `FE_I18N_NAVIGATION`
181
+
182
+ **Vì sao (why):** `<a>` ở `<file>` trỏ route nội bộ (tải lại cả trang, mất locale, mất prefetch) hoặc mở tab mới mà không có `rel`.
183
+
184
+ **Cách sửa:** Dùng `Link` từ `modules/i18n/navigation` với href dựng bởi `modules/routes`; liên kết `_blank` thêm `rel="noopener noreferrer"`.
185
+
186
+ ## `starci-fe/no-hardcoded-route`
187
+
188
+ A route path is built by `modules/routes`, never written as a literal at a link or a navigation call (catalogue R57, sub-check `FE_ROUTE_HARDCODED`).
189
+
190
+ **Invalid** (`src/components/blocks/Feed/component.tsx`)
191
+
192
+ ```tsx
193
+ router.push("/agentos/workspaces/new")
194
+ ```
195
+
196
+ **Valid** (`src/components/blocks/Feed/component.tsx`)
197
+
198
+ ```tsx
199
+ router.push(routes.newWorkspace())
200
+ ```
201
+
202
+ **Finding code:** `FE_ROUTE_HARDCODED`
203
+
204
+ **Vì sao (why):** `<file>` viết thẳng đường dẫn route ở nơi gọi; route đổi chỗ thì bản sao trỏ vào 404.
205
+
206
+ **Cách sửa:** Dựng href bằng hàm của `modules/routes` và dùng đúng hàm đó ở mọi nơi.
207
+
208
+ ## `starci-fe/i18n-stack-in-one-module`
209
+
210
+ next-intl's routing, navigation, request config and middleware factories are called only in the i18n package (or the only app's `modules/i18n`).
211
+
212
+ `defineRouting` (`next-intl/routing`), `createNavigation` (`next-intl/navigation`), `getRequestConfig` (`next-intl/server`) and `createMiddleware` (`next-intl/middleware`), resolved by the import that binds the called name (a renamed import or a namespace member counts), are called only in a file of slot `fe.package.i18n` (`packages/<family>-i18n`, exporting `createAppI18n`), or in a `fe.modules.i18n` file of a repository that declares exactly one app. In a multi-app repository each app's `modules/i18n` calls the package factory. It sits beside `no-second-i18n-stack`, which refuses another i18n library at its import; this rule refuses a second copy of next-intl's own stack.
213
+
214
+ **Invalid** (`apps/web/src/modules/i18n/request.ts`, a repository with two apps)
215
+
216
+ ```ts
217
+ import { getRequestConfig } from "next-intl/server"
218
+ export default getRequestConfig(async () => ({ locale: "vi", messages: {} }))
219
+ ```
220
+
221
+ **Valid** (`apps/web/src/modules/i18n/index.ts`)
222
+
223
+ ```ts
224
+ import { createAppI18n } from "@nivo/i18n"
225
+ export const i18n = createAppI18n({ locales: ["vi"] })
226
+ ```
227
+
228
+ **Finding code:** `FE_I18N_PLACEMENT`
229
+
230
+ **Vì sao (why):** `<file>` tự dựng một tầng của stack next-intl (`defineRouting`, `createNavigation`, `getRequestConfig`, `createMiddleware`). Stack chỉ được viết một lần cho cả repo.
231
+
232
+ **Cách sửa:** Gọi factory `createAppI18n` của gói i18n dùng chung trong `modules/i18n/index.ts` và nhập kết quả; repo một app thì chỉ viết stack trong `modules/i18n` của app đó.
@@ -0,0 +1,73 @@
1
+ # Size and state budget
2
+
3
+ Law module: `size-and-state-budget.mjs`. Catalogue: R65 FE_SIZE_AND_STATE_BUDGET.
4
+
5
+ A component file is at most 300 lines; a connected unit holds at most 6 `useState` and reads at most 6 data hooks; there is no hand-written polling loop. `HFS_SIZE_GROWTH` (no growth against the parent commit) is the architecture machine's; these are the absolute file-local limits.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/component-line-budget`
10
+
11
+ A component file has at most 300 lines.
12
+
13
+ **Invalid** (`src/components/blocks/Feed/index.tsx`)
14
+
15
+ ```ts
16
+ // a 301-line component file
17
+ ```
18
+
19
+ **Valid** (`src/components/blocks/Feed/index.tsx`)
20
+
21
+ ```ts
22
+ // 300 lines or fewer
23
+ ```
24
+
25
+ **Finding code:** `FE_SIZE_AND_STATE_BUDGET`
26
+
27
+ **Vì sao (why):** `<file>` dài `<n>` dòng, vượt ngưỡng 300 dòng của một component.
28
+
29
+ **Cách sửa:** Tách phần vẽ khỏi phần dữ liệu và tách từng khu vực thành đơn vị riêng.
30
+
31
+ ## `starci-fe/unit-hook-budget`
32
+
33
+ A unit has at most 6 state hooks and 6 data hooks.
34
+
35
+ **Invalid** (`src/components/blocks/Feed/index.tsx`)
36
+
37
+ ```ts
38
+ const Feed = () => { /* 7 x useState */ }
39
+ ```
40
+
41
+ **Valid** (`src/components/blocks/Feed/index.tsx`)
42
+
43
+ ```ts
44
+ const Feed = () => { /* up to 6 x useState and 6 data hooks */ }
45
+ ```
46
+
47
+ **Finding code:** `FE_SIZE_AND_STATE_BUDGET`
48
+
49
+ **Vì sao (why):** `<unit>` có `<n>` hook dữ liệu / `<m>` useState, vượt ngưỡng 6.
50
+
51
+ **Cách sửa:** Gom trạng thái đổi cùng nhau vào reducer hoặc hook riêng; tách mỗi vùng dữ liệu thành block kết nối riêng.
52
+
53
+ ## `starci-fe/no-hand-rolled-polling`
54
+
55
+ No `setInterval` and no self-scheduling `setTimeout`: one refresh mechanism per resource.
56
+
57
+ **Invalid** (`src/components/blocks/Feed/index.tsx`)
58
+
59
+ ```ts
60
+ useEffect(() => { const t = setInterval(load, 5000) }, [])
61
+ ```
62
+
63
+ **Valid** (`src/components/blocks/Feed/index.tsx`)
64
+
65
+ ```ts
66
+ const d = useQueryFeedSwr({ refreshInterval: 5000 })
67
+ ```
68
+
69
+ **Finding code:** `FE_SIZE_AND_STATE_BUDGET`
70
+
71
+ **Vì sao (why):** `<unit>` tự viết vòng poll (`setInterval` hoặc `setTimeout` tự gọi lại).
72
+
73
+ **Cách sửa:** Làm tươi qua hook dữ liệu (`refreshInterval`) hoặc socket, mỗi resource đúng một cơ chế.
@@ -0,0 +1,29 @@
1
+ # Size growth
2
+
3
+ Law module: `size-growth.mjs`. Catalogue: R20 HFS_SIZE_GROWTH.
4
+
5
+ A source file has a line budget, `ruleParams.fe.fileLines` of `knowledge/hfs/slots.yaml`, read through the slot view `hfsOf(context).ruleParams` (the package ships its own copy of the manifest in `runtime/`). A file over the budget may not be new and may not be longer than it was at the parent commit; a file within the budget is never a finding. The rule takes no option, there is no baseline file and no allowlist: the record is the repository's own history. `*.d.ts` declaration files are not governed.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/file-size-growth`
10
+
11
+ A source file over the budget is neither new nor longer than its recorded size.
12
+
13
+ **Invalid** (`src/components/Card/index.tsx`)
14
+
15
+ ```ts
16
+ // a new file with more lines than the budget, or an existing file over the budget that gained a line
17
+ ```
18
+
19
+ **Valid** (`src/components/Card/index.tsx`)
20
+
21
+ ```ts
22
+ // a file within the budget, or an oversized file that stayed the same size or shrank
23
+ ```
24
+
25
+ **Finding code:** `HFS_SIZE_GROWTH`
26
+
27
+ **Vì sao (why):** `<file>` vượt ngân sách dòng: file mới đã dài quá mức, hoặc file cũ dài hơn bản ở commit cha. Mỗi file phải nhỏ để một người đọc hết và một bài test phủ hết.
28
+
29
+ **Cách sửa:** Tách phần mới sang file riêng theo trách nhiệm; một file đã vượt ngân sách chỉ được giữ nguyên hoặc ngắn lại.
@@ -0,0 +1,147 @@
1
+ # Spec quality
2
+
3
+ Law module: `spec-quality.mjs`. Catalogue: R67 FE_SPEC_QUALITY, R60 (real catalogue in specs).
4
+
5
+ A spec is a claim about one unit's behaviour: no class-string pinning, it imports the unit beside it, no spec for a barrel, no `as unknown as`, an axe assertion for every connected screen, and a real `next-intl` catalogue instead of a mock.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/no-class-string-in-spec`
10
+
11
+ A component spec asserts behaviour, never a class string.
12
+
13
+ **Invalid** (`src/components/leaves/Chip/Chip.test.tsx`)
14
+
15
+ ```ts
16
+ expect(el).toHaveClass("bg-primary")
17
+ ```
18
+
19
+ **Valid** (`src/components/leaves/Chip/Chip.test.tsx`)
20
+
21
+ ```ts
22
+ expect(screen.getByRole("button", { name: "Save" })).toBeEnabled()
23
+ ```
24
+
25
+ **Finding code:** `FE_SPEC_QUALITY`
26
+
27
+ **Vì sao (why):** Spec `<file>` ghim chuỗi class. Spec kiểm hành vi, không kiểm stylesheet.
28
+
29
+ **Cách sửa:** Assert theo role, tên, trạng thái hoặc chữ hiển thị.
30
+
31
+ ## `starci-fe/spec-tests-its-neighbour`
32
+
33
+ A spec imports its sibling subject: `X.test.tsx` imports `./X`.
34
+
35
+ **Invalid** (`src/components/leaves/Chip/Chip.test.tsx`)
36
+
37
+ ```ts
38
+ import { Row } from "../Row"
39
+ ```
40
+
41
+ **Valid** (`src/components/leaves/Chip/Chip.test.tsx`)
42
+
43
+ ```ts
44
+ import { Chip } from "./Chip"
45
+ ```
46
+
47
+ **Finding code:** `FE_SPEC_QUALITY`
48
+
49
+ **Vì sao (why):** Spec `<file>` không import chủ thể nằm cạnh nó.
50
+
51
+ **Cách sửa:** Đưa spec về cạnh chủ thể, hoặc test đúng đơn vị nằm cạnh.
52
+
53
+ ## `starci-fe/no-barrel-spec`
54
+
55
+ No `index.spec.ts(x)` beside an `index.ts(x)` that holds only imports and re-exports. The sibling is read and parsed: an `index.ts` that declares a function, class or value (implementation) may have its spec; a missing sibling is no finding.
56
+
57
+ **Invalid** (`src/hooks/index.spec.ts`, beside `src/hooks/index.ts` = `export * from "./useFeed"`)
58
+
59
+ ```ts
60
+ // file: src/hooks/index.spec.ts
61
+ ```
62
+
63
+ **Valid** (`src/modules/browser-storage/index.spec.ts`, beside an `index.ts` that declares `createStore` and exports two stores)
64
+
65
+ ```ts
66
+ // file: src/modules/browser-storage/index.spec.ts
67
+ ```
68
+
69
+ **Finding code:** `FE_SPEC_QUALITY`
70
+
71
+ **Vì sao (why):** Spec `<file>` đặt cạnh một barrel. Barrel không có hành vi để test.
72
+
73
+ **Cách sửa:** Xóa spec; test từng đơn vị mà barrel xuất lại, cạnh chính đơn vị đó.
74
+
75
+ ## `starci-fe/no-double-cast-in-spec`
76
+
77
+ A spec builds its values with the type's own shape, not `as unknown as`.
78
+
79
+ **Invalid** (`any spec`)
80
+
81
+ ```ts
82
+ const x = { id: 1 } as unknown as Course
83
+ ```
84
+
85
+ **Valid** (`any spec`)
86
+
87
+ ```ts
88
+ const x = mock<Course>()
89
+ ```
90
+
91
+ **Finding code:** `FE_SPEC_QUALITY`
92
+
93
+ **Vì sao (why):** Spec `<file>` ép kiểu hai lần (`as unknown as`).
94
+
95
+ **Cách sửa:** Dựng giá trị bằng factory có kiểu (`mock<T>()` hoặc fixture builder).
96
+
97
+ ## `starci-fe/connected-spec-has-axe`
98
+
99
+ The spec of a connected block or feature runs an axe assertion.
100
+
101
+ **Invalid** (`src/components/blocks/Feed/index.test.tsx`)
102
+
103
+ ```ts
104
+ it("renders", () => {})
105
+ ```
106
+
107
+ **Valid** (`src/components/blocks/Feed/index.test.tsx`)
108
+
109
+ ```ts
110
+ it("a11y", async () => { expect(await axe(container)).toHaveNoViolations() })
111
+ ```
112
+
113
+ **Finding code:** `FE_SPEC_QUALITY`
114
+
115
+ **Vì sao (why):** Spec `<file>` của màn hình kết nối không có assert axe.
116
+
117
+ **Cách sửa:** Thêm `expect(await axe(container)).toHaveNoViolations()`.
118
+
119
+ ## `starci-fe/no-mocked-translations`
120
+
121
+ A spec renders with the real catalogue; it never mocks `next-intl`. A `vi.mock("next-intl/server", factory)` in the spec of a server helper (no provider exists for it) is allowed when the factory - or a `vi.hoisted` block it reads - uses the app's real catalogue (an import that resolves into `modules/i18n/messages/`); a literal or key-echoing server mock, an automock, and any mock of the client `next-intl` stay findings.
122
+
123
+ **Invalid** (`any spec`)
124
+
125
+ ```ts
126
+ vi.mock("next-intl", () => ({}))
127
+ ```
128
+
129
+ **Valid** (`any spec`)
130
+
131
+ ```ts
132
+ render(<NextIntlClientProvider messages={messages} locale="vi" />)
133
+ ```
134
+
135
+ ```ts
136
+ import messages from "@/modules/i18n/messages/vi.json"
137
+ vi.mock("next-intl/server", async () => {
138
+ const { createTranslator } = await import("next-intl")
139
+ return { getTranslations: async () => createTranslator({ locale: "vi", messages }) }
140
+ })
141
+ ```
142
+
143
+ **Finding code:** `FE_I18N_CATALOG`
144
+
145
+ **Vì sao (why):** Spec `<file>` giả lập `next-intl`, nên chỉ kiểm khóa chứ không kiểm catalog thật.
146
+
147
+ **Cách sửa:** Component client: render trong `NextIntlClientProvider` với `messages/<locale>.json` thật. Helper server (`next-intl/server`): factory của mock dựng translator từ catalog thật (`createTranslator` với `modules/i18n/messages/<locale>.json`).
@@ -0,0 +1,32 @@
1
+ # Status colours: text and icons take the soft pair
2
+
3
+ Law module: `status-colors.mjs`. Catalogue: R61 FE_STYLE_TOKEN_ONLY (the class-name half of the soft-pair contrast obligation; the brand layer's own contrast is `starci/status-contrast` of @starci/stylelint-canon).
4
+
5
+ A status tone has two faces. The solid tone (`--success`) is a fill, with `--success-foreground` ink on it. A status shown as text or as an icon takes the soft pair, the way a HeroUI flat Chip does: `--success-soft-foreground` ink, on `--success-soft` when it sits on a tint. The soft pair is what the brand layer is checked to make readable (3:1 on the tint and on the page); the solid tone reads 3.8:1 or worse on white, so as a text colour it fails. The tones are the grammar's status tones (`success`, `warning`, `danger`, `info`), read from `lib/status-tones.generated.mjs`, which is generated from the grammar with the stylelint vocabulary.
6
+
7
+ Every rule below is an error in `starciFeConfig`; none can be switched off or suppressed inline.
8
+
9
+ ## `starci-fe/status-text-uses-soft-foreground`
10
+
11
+ No class that paints text, an icon or a text decoration with a solid status tone: `text-<tone>`, `fill-<tone>`, `stroke-<tone>`, `decoration-<tone>` (with any variant, opacity or `!`), read from `className`/`class`, from `cn()`/`clsx()` arguments, conditionals, arrays, constants and `classes` entries.
12
+
13
+ **Invalid** (`src/components/leaves/StatusText/index.tsx`)
14
+
15
+ ```tsx
16
+ <span className="text-success" />
17
+ <svg className={cn("size-4", ok && "fill-danger")} />
18
+ ```
19
+
20
+ **Valid** (`src/components/leaves/StatusText/index.tsx`)
21
+
22
+ ```tsx
23
+ <span className="bg-success-soft text-success-soft-foreground" />
24
+ <button className="bg-success text-success-foreground" />
25
+ <div className="border-danger ring-2 ring-warning" />
26
+ ```
27
+
28
+ **Finding code:** `FE_STYLE_TOKEN_ONLY`
29
+
30
+ **Vì sao (why):** `<file>` tô chữ hoặc icon bằng tông trạng thái đặc (`<what>`, ví dụ `text-success`). Tông đặc là màu nền; dùng làm màu chữ nó đọc dưới 4.5:1 trên nền trang.
31
+
32
+ **Cách sửa:** Đổi thành cặp mềm: `text-<tông>-soft-foreground` (kèm `bg-<tông>-soft` nếu nằm trên nền nhạt); tông đặc chỉ dùng với `bg-<tông>` và `text-<tông>-foreground`.