| author | |
| committer | |
| log | 10882bea739a98f11b942c20c6570eb6126dd010 |
| tree | 258a6a03b0defb5961935b3ed30a5da0f53270cf |
| parent | 0e512db34d787b21b827f07c6a62fba7fb00369e |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>6 files changed, 4 insertions(+), 98 deletions(-)
.github/workflows/ci.yml-3| ... | @@ -52,9 +52,6 @@ jobs: | ... | @@ -52,9 +52,6 @@ jobs: |
| 52 | - name: Check | 52 | - name: Check |
| 53 | run: vp check | 53 | run: vp check |
| 54 | 54 | ||
| 55 | - name: Check className overrides on components/ui do not grow | ||
| 56 | run: vp run lint:restyle-ceiling | ||
| 57 | |||
| 58 | - name: Typecheck | 55 | - name: Typecheck |
| 59 | run: vpr typecheck | 56 | run: vpr typecheck |
| 60 | 57 |
AGENTS.md+1-1| ... | @@ -158,7 +158,7 @@ Full glossary with file links: `docs/internals/glossary.md` | ... | @@ -158,7 +158,7 @@ Full glossary with file links: `docs/internals/glossary.md` |
| 158 | ## Taste | 158 | ## Taste |
| 159 | 159 | ||
| 160 | - Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb. | 160 | - Complexity belongs at the adapter boundary. Orchestration stays pure, UI stays dumb. |
| 161 | - `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits, add a variant to the component, not classes at the call site. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` reports violations and CI caps their count. | 161 | - `apps/web/src/components/ui` exports own their look. Pick a `variant` or `size`; do not restyle one with `className`. If none fits and the look is a generic concept, add a variant to the component; a look that belongs to one feature stays in that feature's own component, not in `components/ui`. Layout classes (width, flex, margin, position) belong on the parent. `shadcn/no-restyle` fails lint on violations. |
| 162 | - Inferred types over annotations. `any` is the enemy. | 162 | - Inferred types over annotations. `any` is the enemy. |
| 163 | - Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior. | 163 | - Comments describe how a thing is used, and move when the code moves. To be used mostly to describe functions, not to annotate every line of behavior. |
| 164 | - Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays. | 164 | - Our users drive agents all day and notice a dropped frame, a lying spinner, and a stale label. No continuously repainting animations; they peg the GPU on high-refresh displays. |
package.json-1| ... | @@ -31,7 +31,6 @@ | ... | @@ -31,7 +31,6 @@ |
| 31 | "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", | 31 | "knip:check": "knip --include files,dependencies --no-config-hints && knip --workspace apps/server --workspace apps/desktop --workspace apps/web --workspace packages/client-runtime --workspace packages/contracts --workspace packages/effect-acp --workspace packages/effect-codex-app-server --workspace packages/shared --workspace packages/ssh --workspace packages/tailscale --exports --preprocessor ./scripts/knip-schemas.ts --no-config-hints", |
| 32 | "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", | 32 | "knip:production": "knip --production --preprocessor ./scripts/knip-schemas.ts", |
| 33 | "lint:mobile": "node scripts/mobile-native-static-check.ts", | 33 | "lint:mobile": "node scripts/mobile-native-static-check.ts", |
| 34 | "lint:restyle-ceiling": "node scripts/lint-restyle-ceiling.ts", | ||
| 35 | "test": "vp run -r test", | 34 | "test": "vp run -r test", |
| 36 | "test:resource-monitor": "cargo test --locked --manifest-path native/resource-monitor/Cargo.toml", | 35 | "test:resource-monitor": "cargo test --locked --manifest-path native/resource-monitor/Cargo.toml", |
| 37 | "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test", | 36 | "test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test", |
scripts/lint-restyle-ceiling.test.ts deleted-24| ... | @@ -1,24 +0,0 @@ | ||
| 1 | import { assert, describe, it } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import { countRestyleFindings, evaluateCeiling } from "./lint-restyle-ceiling.ts"; | ||
| 4 | |||
| 5 | describe("lint-restyle-ceiling", () => { | ||
| 6 | it("counts only no-restyle diagnostics", () => { | ||
| 7 | const count = countRestyleFindings({ | ||
| 8 | diagnostics: [ | ||
| 9 | { code: "shadcn(no-restyle)" }, | ||
| 10 | { code: "react(refs)" }, | ||
| 11 | { code: "shadcn(no-restyle)" }, | ||
| 12 | ], | ||
| 13 | }); | ||
| 14 | assert.strictEqual(count, 2); | ||
| 15 | }); | ||
| 16 | |||
| 17 | it("fails above the ceiling and passes at or below it", () => { | ||
| 18 | assert.isFalse(evaluateCeiling(11, 10).ok); | ||
| 19 | assert.isTrue(evaluateCeiling(10, 10).ok); | ||
| 20 | const below = evaluateCeiling(7, 10); | ||
| 21 | assert.isTrue(below.ok); | ||
| 22 | assert.include(below.message, "Lower RESTYLE_CEILING"); | ||
| 23 | }); | ||
| 24 | }); | ||
scripts/lint-restyle-ceiling.ts deleted-66| ... | @@ -1,66 +0,0 @@ | ||
| 1 | #!/usr/bin/env node | ||
| 2 | // @effect-diagnostics nodeBuiltinImport:off - one-shot CI gate over a child process. | ||
| 3 | import * as NodeChildProcess from "node:child_process"; | ||
| 4 | import * as NodePath from "node:path"; | ||
| 5 | import * as NodeURL from "node:url"; | ||
| 6 | |||
| 7 | // `shadcn/no-restyle` stays a warning while the existing className overrides on | ||
| 8 | // components/ui exports are migrated to variants (see vite.config.ts). This gate keeps the | ||
| 9 | // count from growing: CI fails when findings exceed the ceiling. Lower the ceiling when you | ||
| 10 | // migrate a file, and delete this script when the rule becomes an error. | ||
| 11 | export const RESTYLE_CEILING = 0; | ||
| 12 | |||
| 13 | const RULE = "shadcn(no-restyle)"; | ||
| 14 | |||
| 15 | const repoRoot = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), ".."); | ||
| 16 | |||
| 17 | interface LintReport { | ||
| 18 | readonly diagnostics: ReadonlyArray<{ readonly code: string }>; | ||
| 19 | } | ||
| 20 | |||
| 21 | export function countRestyleFindings(report: LintReport): number { | ||
| 22 | return report.diagnostics.filter((diagnostic) => diagnostic.code === RULE).length; | ||
| 23 | } | ||
| 24 | |||
| 25 | export function evaluateCeiling( | ||
| 26 | count: number, | ||
| 27 | ceiling: number, | ||
| 28 | ): { readonly ok: boolean; readonly message: string } { | ||
| 29 | if (count > ceiling) { | ||
| 30 | return { | ||
| 31 | ok: false, | ||
| 32 | message: | ||
| 33 | `${RULE}: ${count} findings exceed the ceiling of ${ceiling}. ` + | ||
| 34 | "Use a variant or size on the components/ui export instead of a className override " + | ||
| 35 | "(run `vp lint apps/web/src` for the list).", | ||
| 36 | }; | ||
| 37 | } | ||
| 38 | const slack = ceiling - count; | ||
| 39 | return { | ||
| 40 | ok: true, | ||
| 41 | message: | ||
| 42 | slack === 0 | ||
| 43 | ? `${RULE}: ${count} findings, at the ceiling.` | ||
| 44 | : `${RULE}: ${count} findings, ${slack} below the ceiling of ${ceiling}. Lower RESTYLE_CEILING in scripts/lint-restyle-ceiling.ts to ${count}.`, | ||
| 45 | }; | ||
| 46 | } | ||
| 47 | |||
| 48 | function main() { | ||
| 49 | const result = NodeChildProcess.spawnSync("vp", ["lint", "--format", "json", "apps/web/src"], { | ||
| 50 | cwd: repoRoot, | ||
| 51 | encoding: "utf8", | ||
| 52 | maxBuffer: 256 * 1024 * 1024, | ||
| 53 | }); | ||
| 54 | if (result.error) throw result.error; | ||
| 55 | const report = JSON.parse(result.stdout) as LintReport; | ||
| 56 | const verdict = evaluateCeiling(countRestyleFindings(report), RESTYLE_CEILING); | ||
| 57 | process.stdout.write(`${verdict.message}\n`); | ||
| 58 | process.exitCode = verdict.ok ? 0 : 1; | ||
| 59 | } | ||
| 60 | |||
| 61 | if ( | ||
| 62 | process.argv[1] !== undefined && | ||
| 63 | NodePath.resolve(process.argv[1]) === NodeURL.fileURLToPath(import.meta.url) | ||
| 64 | ) { | ||
| 65 | main(); | ||
| 66 | } | ||
vite.config.ts+3-3| ... | @@ -200,13 +200,13 @@ export default defineConfig({ | ... | @@ -200,13 +200,13 @@ export default defineConfig({ |
| 200 | { | 200 | { |
| 201 | // components/ui exports own their look. App code picks a variant or size instead | 201 | // components/ui exports own their look. App code picks a variant or size instead |
| 202 | // of restyling with className; layout classes (width, flex, margin, position) stay | 202 | // of restyling with className; layout classes (width, flex, margin, position) stay |
| 203 | // allowed because placement belongs to the parent. Warn-only until the existing | 203 | // allowed because placement belongs to the parent. components/ui is for generic |
| 204 | // overrides are migrated to variants; the ceiling below stops the count growing. | 204 | // primitives: a look that belongs to one feature stays in that feature's component. |
| 205 | files: ["apps/web/src/**"], | 205 | files: ["apps/web/src/**"], |
| 206 | excludeFiles: ["apps/web/src/components/ui/**"], | 206 | excludeFiles: ["apps/web/src/components/ui/**"], |
| 207 | rules: { | 207 | rules: { |
| 208 | "shadcn/no-restyle": [ | 208 | "shadcn/no-restyle": [ |
| 209 | "warn", | 209 | "error", |
| 210 | { | 210 | { |
| 211 | allow: ["layout"], | 211 | allow: ["layout"], |
| 212 | contracts: [ | 212 | contracts: [ |