-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathlint-rules.ts
More file actions
157 lines (131 loc) · 8.42 KB
/
lint-rules.ts
File metadata and controls
157 lines (131 loc) · 8.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* TUIkit lint rules configuration
*
* Central definition of all frontmatter schemas, body section requirements,
* rule metadata, and validation patterns. The linter imports this config
* and uses it to drive all checks — no rule definitions live in lint.ts.
*/
import { z } from "zod";
// ── Types ──────────────────────────────────────────────────────────────────
export type Severity = "error" | "warn";
export type SpecKind = "component" | "token" | "test" | "preview" | "target";
export interface RuleDef {
id: string;
severity: Severity;
description: string;
kinds: SpecKind[] | "*";
fix?: string;
}
// ── Constants ──────────────────────────────────────────────────────────────
export const VALID_CATEGORIES = [
"input",
"display",
"navigation",
"layout",
"feedback",
] as const;
// ── Frontmatter schemas (one per kind) ─────────────────────────────────────
export const ComponentFM = z.object({
kind: z.literal("component"),
name: z.string().min(1),
description: z.string().min(1),
version: z.number().int().nonnegative(),
category: z.enum(VALID_CATEGORIES),
dependencies: z.unknown(),
}).passthrough();
export const TokenFM = z.object({
kind: z.literal("token"),
name: z.string().min(1),
description: z.string().min(1),
version: z.number().int().nonnegative(),
}).passthrough();
export const TestFM = z.object({
kind: z.literal("test"),
component: z.string().min(1),
version: z.number().int().nonnegative(),
}).passthrough();
export const PreviewFM = z.object({
kind: z.literal("preview"),
component: z.string().min(1),
version: z.number().int().nonnegative(),
}).passthrough();
export const TargetFM = z.object({
kind: z.literal("target"),
name: z.string().min(1),
language: z.string().min(1),
runtime: z.string().min(1),
framework: z.unknown().refine((v) => v != null, "framework is required"),
}).passthrough();
export const SchemaByKind: Record<string, z.ZodTypeAny> = {
component: ComponentFM,
token: TokenFM,
test: TestFM,
preview: PreviewFM,
target: TargetFM,
};
// ── Required body sections ─────────────────────────────────────────────────
export const REQUIRED_SECTIONS: Record<string, { sections: string[]; severity: Severity; exact: boolean }> = {
component: {
sections: ["Visual rules", "Rendering example", "Dependencies"],
severity: "error",
exact: true,
},
target: {
sections: [
"Architecture pattern", "Type mapping", "Callback translation",
"State machine translation", "Token access", "Composition",
"Test pattern", "Key mapping", "Dependencies", "Demo CLI",
],
severity: "warn",
exact: false, // fuzzy match (lowercase includes)
},
};
// ── RFC 2119 patterns ──────────────────────────────────────────────────────
export const RFC2119_KEYWORDS = /\b(MUST NOT|MUST|SHOULD NOT|SHOULD|MAY)\b/;
export const INFORMAL_WORDS = /\b(always|never|should(?!\s+NOT))\b/i;
export const NORMATIVE_SECTIONS = ["Visual rules", "Behavior", "Edge cases"];
// ── Rule registry ──────────────────────────────────────────────────────────
export const RULES: RuleDef[] = [
// Frontmatter (zod-driven, auto-generated rule IDs like fm-<field>)
{ id: "fm-kind", severity: "error", description: "kind field is present and valid", kinds: "*" },
{ id: "fm-name", severity: "error", description: "name is present", kinds: ["component", "token", "target"] },
{ id: "fm-name-case", severity: "warn", description: "name follows PascalCase convention", kinds: ["component"] },
{ id: "fm-description", severity: "error", description: "description is present", kinds: ["component", "token"] },
{ id: "fm-version", severity: "error", description: "version is present and numeric", kinds: ["component", "token", "test", "preview"] },
{ id: "fm-category", severity: "error", description: "category is present and valid", kinds: ["component"] },
{ id: "fm-dependencies", severity: "error", description: "dependencies section is present", kinds: ["component"] },
{ id: "fm-props", severity: "warn", description: "props section is present", kinds: ["component"] },
{ id: "fm-tokens-deps-sync", severity: "warn", description: "tokens and dependencies are in sync", kinds: ["component"] },
{ id: "fm-component", severity: "error", description: "test/preview spec references a component", kinds: ["test", "preview"] },
{ id: "fm-component-ref", severity: "error", description: "referenced component spec exists", kinds: ["test", "preview"] },
{ id: "fm-language", severity: "error", description: "target language is specified", kinds: ["target"] },
{ id: "fm-runtime", severity: "error", description: "target runtime is specified", kinds: ["target"] },
{ id: "fm-framework", severity: "error", description: "target framework is specified", kinds: ["target"] },
// Accessibility
{ id: "fm-a11y-interactive", severity: "error", description: "interactive components define accessibility", kinds: ["component"],
fix: "Add accessibility: with role, announce.on_mount, and screen_reader_adaptations" },
{ id: "fm-a11y-display", severity: "warn", description: "display components should define accessibility", kinds: ["component"] },
// Body sections
{ id: "body-visual-rules", severity: "error", description: '"## Visual rules" section exists', kinds: ["component"] },
{ id: "body-rendering-example", severity: "error", description: '"## Rendering example" section exists', kinds: ["component"] },
{ id: "body-dependencies", severity: "error", description: '"## Dependencies" section exists', kinds: ["component"] },
{ id: "body-behavior", severity: "warn", description: "interactive components have behavior section", kinds: ["component"] },
{ id: "target-section", severity: "warn", description: "target spec has recommended body sections", kinds: ["target"] },
{ id: "test-empty", severity: "warn", description: "test spec has at least one test case", kinds: ["test"] },
// RFC 2119
{ id: "rfc2119-missing", severity: "warn", description: "normative sections use RFC 2119 keywords", kinds: ["component"],
fix: "Replace informal language with MUST/SHOULD/MAY" },
{ id: "rfc2119-informal", severity: "warn", description: "no informal language in normative sections", kinds: ["component"],
fix: "Replace with RFC 2119 keyword" },
// Cross-references
{ id: "xref-token", severity: "warn", description: "token references resolve to known tokens", kinds: ["component"] },
// File naming
{ id: "naming-dir", severity: "warn", description: "component directory is PascalCase", kinds: ["component"] },
{ id: "naming-spec", severity: "error", description: "{Name}.md exists in component directory", kinds: ["component"] },
{ id: "naming-test", severity: "error", description: "{Name}.test.md exists in component directory", kinds: ["component"] },
// Links
{ id: "link-broken", severity: "error", description: "internal markdown links resolve to existing files", kinds: "*",
fix: "Fix path or remove link" },
];
// Lookup helper
export const RULES_BY_ID = new Map(RULES.map((r) => [r.id, r]));