division-ds 0.1.2 → 0.2.1
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/AGENTS.md +23 -3
- package/README.md +11 -0
- package/division-ds-0.2.1.tgz +0 -0
- package/package.json +2 -2
- package/public/docs/components/accordion.md +207 -0
- package/public/docs/components/ai-label.md +90 -0
- package/public/docs/components/alert-dialog.md +292 -0
- package/public/docs/components/alert.md +175 -0
- package/public/docs/components/approval-request.md +148 -0
- package/public/docs/components/auth-form.md +132 -0
- package/public/docs/components/avatar-group.md +152 -0
- package/public/docs/components/avatar.md +179 -0
- package/public/docs/components/badge.md +178 -0
- package/public/docs/components/breadcrumbs.md +112 -0
- package/public/docs/components/button.md +204 -0
- package/public/docs/components/card.md +257 -0
- package/public/docs/components/carousel.md +130 -0
- package/public/docs/components/cart.md +172 -0
- package/public/docs/components/chart-panel.md +358 -0
- package/public/docs/components/chat-message.md +157 -0
- package/public/docs/components/chat-thread.md +244 -0
- package/public/docs/components/checkbox.md +149 -0
- package/public/docs/components/checkout-form.md +128 -0
- package/public/docs/components/command-palette.md +147 -0
- package/public/docs/components/data-table.md +401 -0
- package/public/docs/components/description-list.md +103 -0
- package/public/docs/components/dialog.md +197 -0
- package/public/docs/components/divider.md +97 -0
- package/public/docs/components/dropdown-menu.md +222 -0
- package/public/docs/components/empty-state.md +145 -0
- package/public/docs/components/footer.md +397 -0
- package/public/docs/components/form-field.md +216 -0
- package/public/docs/components/icon-button.md +127 -0
- package/public/docs/components/input.md +200 -0
- package/public/docs/components/kanban-board.md +300 -0
- package/public/docs/components/kbd.md +72 -0
- package/public/docs/components/link.md +98 -0
- package/public/docs/components/list-item.md +241 -0
- package/public/docs/components/loading-animation.md +72 -0
- package/public/docs/components/multi-select.md +202 -0
- package/public/docs/components/navbar.md +144 -0
- package/public/docs/components/newsletter-form.md +112 -0
- package/public/docs/components/page-header.md +151 -0
- package/public/docs/components/pagination.md +212 -0
- package/public/docs/components/product-card.md +172 -0
- package/public/docs/components/progress.md +101 -0
- package/public/docs/components/prompt-input.md +151 -0
- package/public/docs/components/quantity-stepper.md +140 -0
- package/public/docs/components/radio.md +177 -0
- package/public/docs/components/rating.md +106 -0
- package/public/docs/components/score-ring.md +95 -0
- package/public/docs/components/search.md +116 -0
- package/public/docs/components/section-heading.md +113 -0
- package/public/docs/components/segmented-control.md +126 -0
- package/public/docs/components/select.md +224 -0
- package/public/docs/components/sheet.md +145 -0
- package/public/docs/components/sidebar.md +193 -0
- package/public/docs/components/skeleton.md +135 -0
- package/public/docs/components/slider.md +114 -0
- package/public/docs/components/sparkline.md +128 -0
- package/public/docs/components/spinner.md +98 -0
- package/public/docs/components/stat.md +106 -0
- package/public/docs/components/status-tracker.md +145 -0
- package/public/docs/components/stepper.md +147 -0
- package/public/docs/components/switch.md +153 -0
- package/public/docs/components/tabs.md +180 -0
- package/public/docs/components/tag.md +105 -0
- package/public/docs/components/textarea.md +148 -0
- package/public/docs/components/timeline.md +152 -0
- package/public/docs/components/toast.md +194 -0
- package/public/docs/components/tool-call.md +107 -0
- package/public/docs/components/tooltip.md +108 -0
- package/public/manifest/components.json +198 -15
- package/public/manifest/tokens.json +75 -5
- package/scripts/division-ds.mjs +78 -42
- package/scripts/lib/agent-docs.mjs +80 -6
- package/scripts/lib/ai-copy.mjs +120 -0
- package/scripts/lib/button-spacing.mjs +5 -5
- package/scripts/lib/check-text.mjs +57 -0
- package/scripts/lib/contrast.mjs +111 -0
- package/scripts/lib/host-inline.mjs +59 -0
- package/scripts/lib/mcp-server.mjs +116 -0
- package/scripts/lib/mcp-tools.mjs +266 -0
- package/scripts/lib/props-validator.mjs +3 -2
- package/scripts/lib/rules-installer.mjs +42 -4
- package/scripts/lib/token-discipline.mjs +10 -3
- package/skills/division-ds/SKILL.md +90 -0
- package/skills/division-ds/commands/add.md +25 -0
- package/skills/division-ds/commands/build.md +46 -0
- package/skills/division-ds/commands/fix.md +28 -0
- package/skills/division-ds/commands/review.md +25 -0
- package/skills/division-ds/topics/ai.md +8 -0
- package/skills/division-ds/topics/colour.md +11 -0
- package/skills/division-ds/topics/feedback.md +9 -0
- package/skills/division-ds/topics/forms.md +22 -0
- package/skills/division-ds/topics/layout.md +36 -0
- package/skills/division-ds/topics/motion.md +10 -0
- package/skills/division-ds/topics/overlays.md +9 -0
package/scripts/division-ds.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// division-ds: the system's gates and rules, for a project that USES the components.
|
|
3
3
|
//
|
|
4
|
-
// division-ds check [files, dirs or globs...] token discipline, touching buttons, props
|
|
4
|
+
// division-ds check [files, dirs or globs...] token discipline, touching buttons, props, AI copy
|
|
5
5
|
// division-ds rules [--target <dir>] ... install the agent rules into a project
|
|
6
|
+
// division-ds skill [--target <dir>] ... install the agent skill into a project
|
|
7
|
+
// division-ds mcp serve the docs, tokens and gates to an agent (MCP)
|
|
6
8
|
// division-ds help
|
|
7
9
|
//
|
|
8
10
|
// Inside this repository `npm run verify` is the loop. A consumer project has none of that: an
|
|
@@ -17,28 +19,33 @@
|
|
|
17
19
|
import { existsSync, globSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
18
20
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
19
21
|
import { fileURLToPath } from 'node:url'
|
|
20
|
-
import { extractRules } from './lib/agent-docs.mjs'
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
22
|
+
import { DOCS_SITE, extractRules, SKILL_SOURCE } from './lib/agent-docs.mjs'
|
|
23
|
+
import { checkText } from './lib/check-text.mjs'
|
|
24
|
+
import { createServer } from './lib/mcp-server.mjs'
|
|
25
|
+
import { createTools } from './lib/mcp-tools.mjs'
|
|
26
|
+
import { buildIndex } from './lib/props-validator.mjs'
|
|
27
|
+
import { applyPlan, diff, planRules, planSkill, SKILL_TARGETS, TARGETS } from './lib/rules-installer.mjs'
|
|
25
28
|
|
|
26
29
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
27
30
|
const DEFAULT_MANIFEST = join(ROOT, 'public/manifest/components.json')
|
|
28
|
-
const CHECKED = /\.(tsx|jsx|ts)$/
|
|
31
|
+
const CHECKED = /\.(tsx|jsx|ts|css)$/
|
|
29
32
|
const SKIPPED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage'])
|
|
30
33
|
|
|
31
34
|
const HELP = `division-ds: Division DS gates and agent rules for a project that uses the components.
|
|
32
35
|
|
|
33
36
|
Usage:
|
|
34
37
|
division-ds check [paths...] [--manifest <path|url>] [--tokens <path|url>] [--json]
|
|
35
|
-
Checks .tsx, .jsx and .
|
|
36
|
-
glob ("src/**/*.tsx"). Runs
|
|
38
|
+
Checks .tsx, .jsx, .ts and .css files (default: ./src; a .css file gets the colour gate only). A path may be a file, a directory or a
|
|
39
|
+
glob ("src/**/*.tsx"). Runs five gates and exits 1 on any finding:
|
|
37
40
|
token-discipline raw colours, Tailwind palette classes, token names that do not exist
|
|
38
41
|
button-spacing two Buttons/IconButtons side by side with no gap on their parent
|
|
42
|
+
host-inline an element marked data-host-mode="inline" (an MCP Apps inline card)
|
|
43
|
+
with over 2 actions, a scroll container, or a menu/popover inside
|
|
39
44
|
props an unknown prop or an enum value outside the union, on elements
|
|
40
45
|
imported from Division DS (components/atoms|molecules|organisms, or
|
|
41
46
|
the package name); a component of your own with the same name is skipped
|
|
47
|
+
ai-copy "AI" in a button label; "answer", "please" or "!" in the labels of
|
|
48
|
+
the assistant components (ChatThread, PromptInput, ApprovalRequest, ...)
|
|
42
49
|
--manifest components.json to check props against (default: the one beside this script)
|
|
43
50
|
--tokens tokens.json for token names (default: tokens.json beside the manifest)
|
|
44
51
|
--json one JSON object on stdout instead of text
|
|
@@ -49,6 +56,17 @@ Usage:
|
|
|
49
56
|
.cursor/rules/division-ds.mdc. --dry-run prints the changes and writes nothing. --remove
|
|
50
57
|
takes the blocks out and deletes the .mdc (and a file left holding nothing).
|
|
51
58
|
|
|
59
|
+
division-ds skill [--target <dir>] [--only agents,claude] [--dry-run] [--remove]
|
|
60
|
+
Copies the Division DS skill (SKILL.md, topics/, commands/) into .agents/skills/division-ds/
|
|
61
|
+
(Codex and Cursor read it) and .claude/skills/division-ds/ (Claude Code). The folders are
|
|
62
|
+
replaced whole on re-run. --dry-run lists the changes and writes nothing; --remove deletes them.
|
|
63
|
+
|
|
64
|
+
division-ds mcp [--manifest <path|url>] [--tokens <path|url>]
|
|
65
|
+
An MCP server on stdio, for Claude Code, Cursor and other agents. Tools: list_components,
|
|
66
|
+
get_component, search, get_tokens, validate_code (the gates above, on code the agent just
|
|
67
|
+
wrote), check_contrast, get_add_command, get_review_checklist. Register it once:
|
|
68
|
+
claude mcp add division-ds -- npx -y division-ds mcp
|
|
69
|
+
|
|
52
70
|
division-ds help
|
|
53
71
|
`
|
|
54
72
|
|
|
@@ -115,27 +133,7 @@ async function check(options) {
|
|
|
115
133
|
const findings = []
|
|
116
134
|
for (const file of files) {
|
|
117
135
|
const path = relative(process.cwd(), file).split('\\').join('/')
|
|
118
|
-
|
|
119
|
-
for (const v of tokenDiscipline(text, path, tokenNames)) {
|
|
120
|
-
findings.push({ gate: 'token-discipline', path, line: v.line, rule: v.rule, found: v.found, fix: v.fix, text: formatTokenDiscipline(v) })
|
|
121
|
-
}
|
|
122
|
-
if (/\.[jt]sx$/.test(path)) {
|
|
123
|
-
for (const v of buttonSpacing(text, path)) {
|
|
124
|
-
findings.push({
|
|
125
|
-
gate: 'button-spacing',
|
|
126
|
-
path,
|
|
127
|
-
line: v.line,
|
|
128
|
-
column: v.column,
|
|
129
|
-
rule: 'touching-buttons',
|
|
130
|
-
found: `<${v.tag}> with ${v.count} button-like children`,
|
|
131
|
-
fix: 'Wrap the buttons in one parent with a flex/grid class and a gap- utility (flex flex-wrap items-center gap-3), and move any margin from the buttons onto it.',
|
|
132
|
-
text: formatButtonSpacing({ ...v, file: path }),
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
for (const v of findPropViolations(text, path, index)) {
|
|
137
|
-
findings.push({ gate: 'props', path, line: v.line, column: v.column, rule: v.rule, found: v.found, fix: v.fix, text: formatPropViolation(v) })
|
|
138
|
-
}
|
|
136
|
+
findings.push(...checkText(readFileSync(file, 'utf8'), path, index, tokenNames))
|
|
139
137
|
}
|
|
140
138
|
|
|
141
139
|
if (options.json) {
|
|
@@ -143,7 +141,7 @@ async function check(options) {
|
|
|
143
141
|
console.log(JSON.stringify({ files: files.length, manifest: manifestSource, findings: out }, null, 2))
|
|
144
142
|
} else if (findings.length > 0) {
|
|
145
143
|
console.error(`\ndivision-ds check FAILED: ${findings.length} finding(s) in ${files.length} file(s).\n`)
|
|
146
|
-
for (const gate of ['token-discipline', 'button-spacing', 'props']) {
|
|
144
|
+
for (const gate of ['token-discipline', 'button-spacing', 'host-inline', 'props', 'ai-copy']) {
|
|
147
145
|
const ofGate = findings.filter(finding => finding.gate === gate)
|
|
148
146
|
if (ofGate.length === 0) continue
|
|
149
147
|
console.error(`${gate} (${ofGate.length})`)
|
|
@@ -151,41 +149,79 @@ async function check(options) {
|
|
|
151
149
|
if (gate === 'button-spacing') console.error(` ${ofGate[0].fix}`)
|
|
152
150
|
console.error('')
|
|
153
151
|
}
|
|
154
|
-
console.error(
|
|
152
|
+
console.error(`Each component's props, values and examples: ${DOCS_SITE}docs/components/<slug>.md. Tokens: ${DOCS_SITE}llms-tokens.txt.`)
|
|
155
153
|
} else {
|
|
156
154
|
console.log(`division-ds check passed: ${files.length} file(s), ${index.size} components, ${tokenNames.length} tokens, 0 findings.`)
|
|
157
155
|
// It reads code, not the running page: say so, or a pass reads as "the screen works" (the
|
|
158
156
|
// 2026-10-20 outside test shipped three runtime bugs past a clean check).
|
|
159
|
-
console.log(
|
|
157
|
+
console.log(`Checked: colours, tokens, button spacing, inline host cards, props and AI copy. Not checked: how the screen behaves; run the review checklist in a browser (commands/review.md from \`npx division-ds skill\`, or ${SKILL_SOURCE}commands/review.md).`)
|
|
160
158
|
}
|
|
161
159
|
process.exit(findings.length > 0 ? 1 : 0)
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
162
|
+
async function mcp(options) {
|
|
163
|
+
const manifestSource = options.manifest ?? DEFAULT_MANIFEST
|
|
164
|
+
const tokensSource = options.tokens ?? manifestSource.replace(/components\.json$/, 'tokens.json')
|
|
165
|
+
const [manifest, tokens] = await Promise.all([load(manifestSource), load(tokensSource)])
|
|
166
|
+
const version = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version
|
|
167
|
+
const readIfThere = path => (existsSync(path) ? readFileSync(path, 'utf8') : undefined)
|
|
168
|
+
const tools = createTools({
|
|
169
|
+
manifest,
|
|
170
|
+
tokens,
|
|
171
|
+
version,
|
|
172
|
+
// Shipped beside the manifests in the npm package (build-cli-package.mjs); a slug comes from
|
|
173
|
+
// the manifest, never from the agent, so it cannot reach outside the docs folder.
|
|
174
|
+
readDoc: slug => readIfThere(join(ROOT, 'public/docs/components', `${slug}.md`)),
|
|
175
|
+
readChecklist: () => readIfThere(join(ROOT, 'skills/division-ds/commands/review.md')),
|
|
176
|
+
})
|
|
177
|
+
createServer({ tools, version }).listen(process.stdin, process.stdout)
|
|
178
|
+
// stdout is the protocol channel: this goes to stderr, where MCP clients put server logs.
|
|
179
|
+
console.error(`division-ds ${version} MCP server on stdio: ${tools.length} tools, ${manifest.components.length} components.`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** --only and --target, checked the same way for `rules` and `skill`. */
|
|
183
|
+
function targetsOf(options, all) {
|
|
184
|
+
const only = options.only ? options.only.split(',').map(target => target.trim()) : all
|
|
185
|
+
const unknown = only.filter(target => !all.includes(target))
|
|
186
|
+
if (unknown.length) usage(`--only takes ${all.join(', ')}; not ${unknown.join(', ')}.`)
|
|
168
187
|
const targetDir = resolve(options.target ?? '.')
|
|
169
188
|
if (!existsSync(targetDir) || !statSync(targetDir).isDirectory()) usage(`--target is not a directory: ${targetDir}`)
|
|
170
|
-
// This repository's AGENTS.md
|
|
171
|
-
// read twice by every agent here, and regenerated nowhere.
|
|
189
|
+
// This repository's AGENTS.md and skills/ are the source; a copy of its own rules beside them
|
|
190
|
+
// would be read twice by every agent here, and regenerated nowhere.
|
|
172
191
|
if (resolve(targetDir) === resolve(ROOT)) usage('--target is Division DS itself. Point it at the project that uses the components.')
|
|
192
|
+
return { only, targetDir }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function rules(options) {
|
|
196
|
+
const { only, targetDir } = targetsOf(options, TARGETS)
|
|
173
197
|
const source = readFileSync(join(ROOT, 'AGENTS.md'), 'utf8').replace(/\r\n/g, '\n')
|
|
174
|
-
|
|
198
|
+
report(planRules({ targetDir, rules: extractRules(source), only, remove: Boolean(options.remove) }), options)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function skill(options) {
|
|
202
|
+
const { only, targetDir } = targetsOf(options, SKILL_TARGETS)
|
|
203
|
+
const plan = planSkill({ targetDir, sourceDir: join(ROOT, 'skills/division-ds'), only, remove: Boolean(options.remove) })
|
|
204
|
+
// A diff of every skill file is pages long; the list of files is the review.
|
|
205
|
+
report(plan, { ...options, quiet: true }, targetDir)
|
|
206
|
+
}
|
|
175
207
|
|
|
208
|
+
/** Prints a plan (with a diff per file on --dry-run, unless quiet) and applies it unless --dry-run. */
|
|
209
|
+
function report(plan, options, stopAt) {
|
|
176
210
|
for (const change of plan) {
|
|
177
211
|
const shown = relative(process.cwd(), change.path) || change.path
|
|
178
212
|
console.log(`${options['dry-run'] ? 'would ' : ''}${change.action.padEnd(9)} ${shown}`)
|
|
179
|
-
if (options['dry-run'] && change.action !== 'unchanged' && change.action !== 'absent') {
|
|
213
|
+
if (options['dry-run'] && !options.quiet && change.action !== 'unchanged' && change.action !== 'absent') {
|
|
180
214
|
console.log(diff(change.before, change.after).replace(/^/gm, ' '))
|
|
181
215
|
}
|
|
182
216
|
}
|
|
183
|
-
if (!options['dry-run']) applyPlan(plan)
|
|
217
|
+
if (!options['dry-run']) applyPlan(plan, stopAt)
|
|
184
218
|
}
|
|
185
219
|
|
|
186
220
|
const [command, ...rest] = process.argv.slice(2)
|
|
187
221
|
const options = parseArgs(rest)
|
|
188
222
|
if (command === 'check') await check(options)
|
|
189
223
|
else if (command === 'rules') rules(options)
|
|
224
|
+
else if (command === 'skill') skill(options)
|
|
225
|
+
else if (command === 'mcp') await mcp(options)
|
|
190
226
|
else if (command === undefined || command === 'help' || command === '--help' || command === '-h') console.log(HELP)
|
|
191
227
|
else usage(`Unknown command "${command}".`)
|
|
@@ -8,6 +8,8 @@ const TIER_NOUN = { atoms: 'Atom', molecules: 'Molecule', organisms: 'Organism'
|
|
|
8
8
|
export const DOCS_SITE = 'https://victor-ilioiu.vercel.app/systems/division-ds/'
|
|
9
9
|
/** Where shadcn downloads the components from: public/r/ in the public GitHub repository. */
|
|
10
10
|
export const REGISTRY = 'https://raw.githubusercontent.com/Victor-Ilioiu/division-ds/main/public/r'
|
|
11
|
+
/** The skill's files in the public repository, for a reader who has the package but not the skill. */
|
|
12
|
+
export const SKILL_SOURCE = 'https://github.com/Victor-Ilioiu/division-ds/blob/main/skills/division-ds/'
|
|
11
13
|
|
|
12
14
|
/** A table cell: one line, with the pipes that would end the cell escaped. */
|
|
13
15
|
const cell = text => String(text ?? '').replace(/\s*\n\s*/g, ' ').replace(/\|/g, '\\|')
|
|
@@ -19,14 +21,29 @@ export function importLines(entry) {
|
|
|
19
21
|
return entry.imports.map(({ names, from }) => `import { ${names.join(', ')} } from '${from}'`).join('\n')
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** The other components a page documents, by export name: ChatMessage's ResponseFeedback,
|
|
25
|
+
* ProductCard's ProductGrid. Parts named after the component (CardBody, RadioGroup) and helpers
|
|
26
|
+
* (lower case) are left out. The 2026-10-29 outside test could not find ResponseFeedback because
|
|
27
|
+
* only its parent was listed anywhere an agent looks first. */
|
|
28
|
+
export function alsoExports(entry) {
|
|
29
|
+
return entry.imports
|
|
30
|
+
.flatMap(({ names }) => names)
|
|
31
|
+
.filter(name => /^[A-Z][a-z]/.test(name) && !name.startsWith(entry.name) && !name.endsWith('Provider'))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Said under every Import block: the paths are the repository's, not an installed project's. */
|
|
35
|
+
export const INSTALLED_PATHS = 'Paths are from the Division DS repository root. Installed with shadcn they start `src/division-ds/` instead of `src/` (`src/division-ds/components/...`); import them relative to your file.'
|
|
36
|
+
|
|
22
37
|
/** One component as a markdown page. `entry` is a manifest entry (see `manifestEntry`). */
|
|
23
38
|
export function componentMarkdown(entry) {
|
|
24
39
|
const out = [`# ${entry.name}`, '', `> ${entry.summary}`, '']
|
|
25
40
|
out.push(`- **Best for:** ${entry.useWhen}`, `- **Not for:** ${entry.notFor}`)
|
|
41
|
+
const also = alsoExports(entry)
|
|
42
|
+
if (also.length) out.push(`- **Also on this page:** ${also.map(code).join(', ')}`)
|
|
26
43
|
out.push(`- ${TIER_NOUN[entry.tier]}. Source: \`${entry.source}\``)
|
|
27
44
|
out.push(`- Rendered docs page: \`/components/${entry.slug}\``, '')
|
|
28
45
|
|
|
29
|
-
out.push('## Import', '', fence(importLines(entry)), '')
|
|
46
|
+
out.push('## Import', '', fence(importLines(entry)), '', INSTALLED_PATHS, '')
|
|
30
47
|
|
|
31
48
|
if (entry.requires?.length) {
|
|
32
49
|
out.push('## Requires', '', ...entry.requires.map(item => `- ${item}`), '')
|
|
@@ -108,8 +125,10 @@ const byTier = entries =>
|
|
|
108
125
|
export const firstSentence = text => text.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? text
|
|
109
126
|
|
|
110
127
|
/** One component's line in llms.txt: what it is, then best for and not for. */
|
|
111
|
-
export const indexLine = entry =>
|
|
112
|
-
|
|
128
|
+
export const indexLine = entry => {
|
|
129
|
+
const also = alsoExports(entry)
|
|
130
|
+
return `- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)} Best for: ${entry.useWhen} Not for: ${entry.notFor}${also.length ? ` Also: ${also.join(', ')}.` : ''}`
|
|
131
|
+
}
|
|
113
132
|
|
|
114
133
|
/** llms.txt, in the llmstxt.org shape: a title, a summary, then sections of links. */
|
|
115
134
|
export function llmsIndex(entries, { rules }) {
|
|
@@ -124,6 +143,7 @@ export function llmsIndex(entries, { rules }) {
|
|
|
124
143
|
'## Start here',
|
|
125
144
|
'',
|
|
126
145
|
'- [Rules](llms-guidelines.txt): imports, colour, providers, forms. Short. Read it first.',
|
|
146
|
+
'- [DESIGN.md](DESIGN.md): the system in one file (colour, type, spacing, motion, every component in a line, the rules), for a tool that takes a single design file.',
|
|
127
147
|
'- [Tokens](llms-tokens.txt): every semantic and component token with when to use it.',
|
|
128
148
|
'- [Helpers](docs/helpers.md): the shared functions and types under src/lib (form errors, money, links, theme, streamed text).',
|
|
129
149
|
'- [All components, compact](llms-components.txt): every prop signature in one file.',
|
|
@@ -145,7 +165,7 @@ export function ruleHeadlines(rules) {
|
|
|
145
165
|
.split(/\n\s*\n/)
|
|
146
166
|
.map(block => block.replace(/\s*\n\s*/g, ' ').trim())
|
|
147
167
|
.filter(block => block.startsWith('**'))
|
|
148
|
-
.map(block => block.match(/^\*\*[^*]+\*\*\s
|
|
168
|
+
.map(block => block.match(/^\*\*[^*]+\*\*\s+.*?\.(?=\s|$)/)?.[0] ?? block)
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
/** Every component's contract in one compact file: no examples, no prose beyond the summary. */
|
|
@@ -203,6 +223,57 @@ export function tokensBundle(manifest) {
|
|
|
203
223
|
].join('\n')
|
|
204
224
|
}
|
|
205
225
|
|
|
226
|
+
/** DESIGN.md: the whole system in one short file, for a tool that takes a single design file.
|
|
227
|
+
* It carries the choosing layer only (semantic colour, spacing, motion, type, one line per
|
|
228
|
+
* component, the rule headlines) and links the full pages rather than inlining them. `fonts` is
|
|
229
|
+
* `[{ name, stack }]` from the `--font-*` theme variables in src/styles/tailwind.css. */
|
|
230
|
+
export function designFile(entries, manifest, { fonts, rules }) {
|
|
231
|
+
const value = token => (typeof token.value === 'string' ? token.value : `dark ${token.value.dark}, light ${token.value.light}`)
|
|
232
|
+
const table = layer => [
|
|
233
|
+
'| Token | Use | Value |',
|
|
234
|
+
'|---|---|---|',
|
|
235
|
+
...manifest.tokens
|
|
236
|
+
.filter(token => token.layer === layer)
|
|
237
|
+
.map(token => `| ${code(token.name)} | ${cell(token.use)} | ${code(value(token))} |`),
|
|
238
|
+
'',
|
|
239
|
+
]
|
|
240
|
+
const out = [
|
|
241
|
+
'# Division DS: design',
|
|
242
|
+
'',
|
|
243
|
+
`> A React 19, Tailwind 4 and Radix design system: ${entries.length} components, design tokens in three layers, light and dark themes, WCAG 2.2 AA. This file is the system in one page; each component links its full page with props and examples.`,
|
|
244
|
+
'',
|
|
245
|
+
'Links are relative to this file. Generated by `npm run agent-docs`.',
|
|
246
|
+
'',
|
|
247
|
+
'## Colour',
|
|
248
|
+
'',
|
|
249
|
+
manifest.rule,
|
|
250
|
+
'',
|
|
251
|
+
'Write a token as a CSS variable: `bg-[var(--color-surface-base)]`. Use only the names below. Component tokens and the full list: [llms-tokens.txt](llms-tokens.txt).',
|
|
252
|
+
'',
|
|
253
|
+
...table('semantic'),
|
|
254
|
+
'## Type',
|
|
255
|
+
'',
|
|
256
|
+
...fonts.map(font => `- ${code(`font-${font.name}`)}: ${code(font.stack)}`),
|
|
257
|
+
'',
|
|
258
|
+
'## Spacing',
|
|
259
|
+
'',
|
|
260
|
+
...table('spacing'),
|
|
261
|
+
'## Motion',
|
|
262
|
+
'',
|
|
263
|
+
...table('motion'),
|
|
264
|
+
]
|
|
265
|
+
for (const [tier, group] of byTier(entries)) {
|
|
266
|
+
out.push(`## ${tier[0].toUpperCase()}${tier.slice(1)}`, '')
|
|
267
|
+
for (const entry of group) {
|
|
268
|
+
const also = alsoExports(entry)
|
|
269
|
+
out.push(`- [${entry.name}](${entry.docs.replace(/^\//, '')}): ${firstSentence(entry.summary)}${also.length ? ` Also: ${also.join(', ')}.` : ''}`)
|
|
270
|
+
}
|
|
271
|
+
out.push('')
|
|
272
|
+
}
|
|
273
|
+
out.push('## Rules', '', 'In full: [llms-guidelines.txt](llms-guidelines.txt).', '', ...ruleHeadlines(rules).map(line => `- ${line}`), '')
|
|
274
|
+
return out.join('\n')
|
|
275
|
+
}
|
|
276
|
+
|
|
206
277
|
/** The shared helpers under src/lib, one section per file: exported types whole, functions as
|
|
207
278
|
* signatures, each with its doc comment. `helpers` is `[{ from, api }]`. */
|
|
208
279
|
export function helpersPage(helpers) {
|
|
@@ -265,6 +336,7 @@ export const SKILL_TOPICS = {
|
|
|
265
336
|
overlays: { title: 'Overlays', when: 'adding a Dialog, an AlertDialog or a Sheet' },
|
|
266
337
|
feedback: { title: 'Feedback', when: 'a screen reports a result or a wait: a toast, an alert, progress, a spinner' },
|
|
267
338
|
motion: { title: 'Motion', when: 'animating anything yourself' },
|
|
339
|
+
ai: { title: 'Assistant copy', when: 'wording a chat' },
|
|
268
340
|
}
|
|
269
341
|
|
|
270
342
|
/**
|
|
@@ -397,6 +469,8 @@ const SKILL_COMMANDS = {
|
|
|
397
469
|
'| `[unknown-prop]` "has no prop" | The prop does not exist. Open the page the message names and use a prop from its table. |',
|
|
398
470
|
'| `[unknown-prop]` "types X away on purpose" | The component sets that attribute itself. Use the prop it names (`label` for a name, `invalid` for an error state). |',
|
|
399
471
|
'| `[invalid-value]` | The value is not in the prop\'s union. The message lists the allowed values. |',
|
|
472
|
+
'| `[ai-in-cta]` | A button says "AI". Name what it does instead: "Summarize", "Draft a reply". |',
|
|
473
|
+
'| `[assistant-copy]` | An assistant component\'s label says "answer", "please" or "!". Write "response", drop "please", drop the exclamation point. |',
|
|
400
474
|
'| "Vocabulary gate FAILED" (this repository only) | Reword the line. If it is an ordinary idiom, ask before adding it to ALLOWED_IDIOMS. |',
|
|
401
475
|
'| "Agent docs are stale" (this repository only) | Run `npm run agent-docs`, then `npm run verify`. |',
|
|
402
476
|
'| A props-table test fails in `meta.test.ts` (this repository only) | The `<Name>.meta.ts` table and the real props disagree. Change the table to match the code, both ways. |',
|
|
@@ -410,7 +484,7 @@ const SKILL_COMMANDS = {
|
|
|
410
484
|
body: [
|
|
411
485
|
'Go through every line. A line that does not hold is a change to make, not a note to leave.',
|
|
412
486
|
'',
|
|
413
|
-
'- [ ] **States.** Every region has its empty, loading, error, success and disabled or no-permission state, as planned
|
|
487
|
+
'- [ ] **States.** Every region has its empty, loading, error, success and disabled or no-permission state, as planned before the code (the regions-and-states plan of `commands/build.md`). An empty list says what is empty and offers the action that fills it.',
|
|
414
488
|
'- [ ] **Headings.** One `PageHeader`, so one `h1`. Section headings continue the outline: `h2` under the `h1`, `h3` inside an `h2`, no level skipped. `SectionHeading` is given the level.',
|
|
415
489
|
'- [ ] **Tokens only.** No hex, `rgb()`, palette class or invented token name. `division-ds check` passes.',
|
|
416
490
|
'- [ ] **Buttons.** Two or more side by side share a parent with a gap. One primary button per view. A busy button takes `loading`; a disabled one says why nearby. Icon-only actions are `IconButton` with a `label`.',
|
|
@@ -505,7 +579,7 @@ export function skillFiles(split, componentCount) {
|
|
|
505
579
|
'',
|
|
506
580
|
// A project that installed the components has no public/ folder of ours (the 2026-10-20
|
|
507
581
|
// outside test followed these steps to files that did not exist).
|
|
508
|
-
`Paths starting \`public/\` are in the Division DS repository. In a project that uses the components, read them online: \`public/llms.txt\` is \`${DOCS_SITE}llms.txt\`, and so on.`,
|
|
582
|
+
`Paths starting \`public/\` are in the Division DS repository. In a project that uses the components, read them online: \`public/llms.txt\` is \`${DOCS_SITE}llms.txt\`, and so on. The other procedures named here (\`commands/build.md\` and the rest) are in the skill: \`${SKILL_SOURCE}commands/<name>.md\`, or \`npx division-ds skill\` to install them.`,
|
|
509
583
|
'',
|
|
510
584
|
...command.body,
|
|
511
585
|
'',
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The AI copy gate: the parts of the "Assistant copy" rule (AGENTS.md, topic `ai`) that a script
|
|
2
|
+
// can check without guessing. Run in a consumer project through `division-ds check` and the MCP
|
|
3
|
+
// `validate_code` (both via check-text.mjs).
|
|
4
|
+
//
|
|
5
|
+
// Where the rules come from: GitLab Pajamas (no "AI" in a call-to-action label: name the action),
|
|
6
|
+
// Cloudscape's generative-AI chat pattern ("response", not "answer"; no exclamation points; no
|
|
7
|
+
// "please") and Intuit's content design guide on writing about AI. The rest of that guidance
|
|
8
|
+
// (who speaks in which person, hedging, naming the model) needs a reader, so it stays prose.
|
|
9
|
+
//
|
|
10
|
+
// Scope is kept narrow on purpose, because a false positive teaches an agent to stop reading the
|
|
11
|
+
// gate:
|
|
12
|
+
// - `ai-in-cta`: the word "AI" (upper case, a whole word) in a button's own label: the text of a
|
|
13
|
+
// <Button> or <button>, an <IconButton label>, an `aria-label` on either, and the button props
|
|
14
|
+
// of the assistant components (approveLabel, rejectLabel, jumpLabel). Any button, imported from
|
|
15
|
+
// Division DS or not: a copy rule is about the words, not the component.
|
|
16
|
+
// - `assistant-copy`: "answer", "please" or "!" in the interface text of the assistant components
|
|
17
|
+
// (ChatThread, ChatMessage, PromptInput, ApprovalRequest, ToolCall): their label-like string
|
|
18
|
+
// props only. Message bodies, a ToolCall's input and output and PromptInput's suggestions are
|
|
19
|
+
// content written by the person or the model, not the interface, and are never read. A button
|
|
20
|
+
// placed inside one of them (a Button in ChatMessage's `actions`) is its interface too: its label
|
|
21
|
+
// and aria-label are checked the same way (missed by the 2026-10-29 outside test's probe).
|
|
22
|
+
// Only literals are read (a string attribute, JSX text, a ternary of two literals). A variable is
|
|
23
|
+
// not judged, the same as the props gate.
|
|
24
|
+
import ts from 'typescript'
|
|
25
|
+
import { scriptKind } from './props-validator.mjs'
|
|
26
|
+
|
|
27
|
+
const BUTTONS = new Set(['Button', 'button'])
|
|
28
|
+
/** Each assistant component's interface-text props. Everything else on them is content or data. */
|
|
29
|
+
const ASSISTANT_COPY = {
|
|
30
|
+
ChatThread: ['label', 'busyLabel', 'jumpLabel'],
|
|
31
|
+
ChatMessage: ['author', 'timeLabel'],
|
|
32
|
+
PromptInput: ['label', 'placeholder', 'hint'],
|
|
33
|
+
ApprovalRequest: ['title', 'approveLabel', 'rejectLabel', 'approvedLabel', 'rejectedLabel'],
|
|
34
|
+
ToolCall: ['title'],
|
|
35
|
+
}
|
|
36
|
+
const BUTTON_PROPS = { IconButton: ['label', 'aria-label'], Button: ['aria-label'], button: ['aria-label'], ApprovalRequest: ['approveLabel', 'rejectLabel'], ChatThread: ['jumpLabel'] }
|
|
37
|
+
|
|
38
|
+
const AI_WORD = /\bAI\b/
|
|
39
|
+
const ASSISTANT_WORDS = [
|
|
40
|
+
{ pattern: /\banswer(s|ed|ing)?\b/i, fix: 'Write "response" (or "reply"), not "answer": the assistant responds; it is not always right.' },
|
|
41
|
+
{ pattern: /\bplease\b/i, fix: 'Drop "please": the interface states the action plainly.' },
|
|
42
|
+
{ pattern: /!/, fix: 'Drop the exclamation point: assistant copy stays calm, whatever the result.' },
|
|
43
|
+
]
|
|
44
|
+
const AI_FIX = 'Name the action ("Summarize", "Draft a reply"), not the technology: a call to action never says "AI".'
|
|
45
|
+
|
|
46
|
+
/** The literal strings an attribute can hold, as nodes; none when it is not a literal. */
|
|
47
|
+
function literals(initializer) {
|
|
48
|
+
if (!initializer) return []
|
|
49
|
+
if (ts.isStringLiteral(initializer)) return [initializer]
|
|
50
|
+
if (!ts.isJsxExpression(initializer) || !initializer.expression) return []
|
|
51
|
+
const collect = node => {
|
|
52
|
+
while (ts.isParenthesizedExpression(node)) node = node.expression
|
|
53
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return [node]
|
|
54
|
+
if (ts.isConditionalExpression(node)) return [...collect(node.whenTrue), ...collect(node.whenFalse)]
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
return collect(initializer.expression)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Every AI copy finding in one file: `{ path, line, column, rule, found, fix }`. */
|
|
61
|
+
export function findAiCopyViolations(text, path) {
|
|
62
|
+
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, scriptKind(path))
|
|
63
|
+
const violations = []
|
|
64
|
+
const add = (node, rule, found, fix) => {
|
|
65
|
+
const at = source.getLineAndCharacterOfPosition(node.getStart(source))
|
|
66
|
+
violations.push({ path, line: at.line + 1, column: at.character + 1, rule, found, fix })
|
|
67
|
+
}
|
|
68
|
+
// How many assistant components the walk is inside: a button in there speaks for the assistant.
|
|
69
|
+
let insideAssistant = 0
|
|
70
|
+
const checkCta = (node, value, where) => {
|
|
71
|
+
if (AI_WORD.test(value)) add(node, 'ai-in-cta', `${where}: "${value.trim()}"`, AI_FIX)
|
|
72
|
+
if (insideAssistant > 0) {
|
|
73
|
+
for (const word of ASSISTANT_WORDS) {
|
|
74
|
+
if (word.pattern.test(value)) add(node, 'assistant-copy', `${where} in an assistant component: "${value.trim()}"`, word.fix)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const tagOf = node =>
|
|
79
|
+
ts.isJsxElement(node) ? node.openingElement.tagName.getText(source) : ts.isJsxSelfClosingElement(node) ? node.tagName.getText(source) : undefined
|
|
80
|
+
|
|
81
|
+
const visit = node => {
|
|
82
|
+
const opensAssistant = tagOf(node) in ASSISTANT_COPY
|
|
83
|
+
if (opensAssistant) insideAssistant++
|
|
84
|
+
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
85
|
+
const tag = node.tagName.getText(source)
|
|
86
|
+
for (const attribute of node.attributes.properties) {
|
|
87
|
+
if (!ts.isJsxAttribute(attribute)) continue
|
|
88
|
+
const prop = attribute.name.getText(source)
|
|
89
|
+
for (const value of literals(attribute.initializer)) {
|
|
90
|
+
if (BUTTON_PROPS[tag]?.includes(prop)) checkCta(value, value.text, `<${tag} ${prop}>`)
|
|
91
|
+
if (ASSISTANT_COPY[tag]?.includes(prop)) {
|
|
92
|
+
for (const word of ASSISTANT_WORDS) {
|
|
93
|
+
if (word.pattern.test(value.text)) add(value, 'assistant-copy', `<${tag} ${prop}>: "${value.text}"`, word.fix)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (ts.isJsxElement(node) && BUTTONS.has(node.openingElement.tagName.getText(source))) {
|
|
100
|
+
const tag = node.openingElement.tagName.getText(source)
|
|
101
|
+
const parts = []
|
|
102
|
+
for (const child of node.children) {
|
|
103
|
+
if (ts.isJsxText(child)) parts.push(child.text)
|
|
104
|
+
else if (ts.isJsxExpression(child) && child.expression && (ts.isStringLiteral(child.expression) || ts.isNoSubstitutionTemplateLiteral(child.expression))) {
|
|
105
|
+
parts.push(child.expression.text)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const label = parts.join(' ').replace(/\s+/g, ' ')
|
|
109
|
+
if (label.trim()) checkCta(node, label, `<${tag}> label`)
|
|
110
|
+
}
|
|
111
|
+
ts.forEachChild(node, visit)
|
|
112
|
+
if (opensAssistant) insideAssistant--
|
|
113
|
+
}
|
|
114
|
+
visit(source)
|
|
115
|
+
return violations.sort((a, b) => a.line - b.line || a.column - b.column)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function formatAiCopyViolation({ path, line, column, rule, found, fix }) {
|
|
119
|
+
return `${path}:${line}:${column} [${rule}] ${found}\n ${fix}`
|
|
120
|
+
}
|
|
@@ -91,7 +91,7 @@ function scanAttrs(text, start) {
|
|
|
91
91
|
|
|
92
92
|
/** Every JSX tag token in `text`, in source order: `{ type: 'open' | 'close' | 'self', name,
|
|
93
93
|
* attrsText, start }`. `start` is the index of the tag's leading `<`. */
|
|
94
|
-
function tokenizeTags(text) {
|
|
94
|
+
export function tokenizeTags(text) {
|
|
95
95
|
const tokens = []
|
|
96
96
|
let i = 0
|
|
97
97
|
while (i < text.length) {
|
|
@@ -306,7 +306,7 @@ function hasWorkingGap(className) {
|
|
|
306
306
|
* on the stack, otherwise it is ignored -- real TSX is always balanced; this only has to not throw
|
|
307
307
|
* on the odd case this scanner misreads.
|
|
308
308
|
*/
|
|
309
|
-
function buildTree(tokens) {
|
|
309
|
+
export function buildTree(tokens) {
|
|
310
310
|
const root = { name: null, attrsText: '', attrs: new Map(), start: -1, children: [] }
|
|
311
311
|
const stack = [root]
|
|
312
312
|
for (const token of tokens) {
|
|
@@ -343,7 +343,7 @@ function buildTree(tokens) {
|
|
|
343
343
|
* only child element is a button -- the `SheetClose asChild><Button/></SheetClose>` shape that is
|
|
344
344
|
* exactly what shipped broken. Only one level of unwrapping: an `asChild` wrapper around an
|
|
345
345
|
* `asChild` wrapper is not chased further. */
|
|
346
|
-
function isButtonLike(node) {
|
|
346
|
+
export function isButtonLike(node) {
|
|
347
347
|
if (BUTTON_TAGS.has(node.name)) return true
|
|
348
348
|
if (node.attrs.get('asChild') === 'true' && node.children.length === 1) {
|
|
349
349
|
return BUTTON_TAGS.has(node.children[0].name)
|
|
@@ -351,7 +351,7 @@ function isButtonLike(node) {
|
|
|
351
351
|
return false
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
function lineIndex(text) {
|
|
354
|
+
export function lineIndex(text) {
|
|
355
355
|
const offsets = [0]
|
|
356
356
|
for (let i = 0; i < text.length; i++) {
|
|
357
357
|
if (text[i] === '\n') offsets.push(i + 1)
|
|
@@ -359,7 +359,7 @@ function lineIndex(text) {
|
|
|
359
359
|
return offsets
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
function lineColAt(offsets, index) {
|
|
362
|
+
export function lineColAt(offsets, index) {
|
|
363
363
|
let lo = 0
|
|
364
364
|
let hi = offsets.length - 1
|
|
365
365
|
while (lo < hi) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// The five consumer gates over one file's text, as data. `division-ds check` runs it per file
|
|
2
|
+
// on disk; the MCP server's `validate_code` runs it on a snippet an agent passes in. One function,
|
|
3
|
+
// so the two can never disagree about what a finding is.
|
|
4
|
+
import { findAiCopyViolations, formatAiCopyViolation } from './ai-copy.mjs'
|
|
5
|
+
import { findViolations as buttonSpacing, formatViolation as formatButtonSpacing } from './button-spacing.mjs'
|
|
6
|
+
import { findInlineViolations, formatInlineViolation } from './host-inline.mjs'
|
|
7
|
+
import { findPropViolations, formatPropViolation } from './props-validator.mjs'
|
|
8
|
+
import { findViolations as tokenDiscipline, formatViolation as formatTokenDiscipline } from './token-discipline.mjs'
|
|
9
|
+
|
|
10
|
+
export const BUTTON_SPACING_FIX =
|
|
11
|
+
'Wrap the buttons in one parent with a flex/grid class and a gap- utility (flex flex-wrap items-center gap-3), and move any margin from the buttons onto it.'
|
|
12
|
+
|
|
13
|
+
/** A stylesheet with the value of every declaration of a real token blanked, newlines kept so line
|
|
14
|
+
* numbers hold. Setting a token is how a project themes the system (the theme builder writes
|
|
15
|
+
* exactly that into index.css), so a literal colour there is the point; anywhere else in the file,
|
|
16
|
+
* or on a custom property the system does not have, it is reported as in any other file. */
|
|
17
|
+
export function blankTokenOverrides(text, tokenNames) {
|
|
18
|
+
const known = new Set(tokenNames)
|
|
19
|
+
return text.replace(/(--[a-z0-9-]+)(\s*:)([^;}]*)/g, (match, name, colon, value) =>
|
|
20
|
+
// The var() references stay, so a token set from a name that does not exist is still caught.
|
|
21
|
+
known.has(name) ? name + colon + value.replace(/var\([^)]*\)|[^\n]/g, part => (part.length > 1 ? part : ' ')) : match
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Every finding in `text`. `path` decides which gates apply (button spacing and AI copy need JSX) and is
|
|
26
|
+
* echoed into each finding. `index` is buildIndex(manifest); `tokenNames` the token names. */
|
|
27
|
+
export function checkText(text, path, index, tokenNames) {
|
|
28
|
+
const findings = []
|
|
29
|
+
if (/\.css$/.test(path)) text = blankTokenOverrides(text, tokenNames)
|
|
30
|
+
for (const v of tokenDiscipline(text, path, tokenNames)) {
|
|
31
|
+
findings.push({ gate: 'token-discipline', path, line: v.line, rule: v.rule, found: v.found, fix: v.fix, text: formatTokenDiscipline(v) })
|
|
32
|
+
}
|
|
33
|
+
if (/\.[jt]sx$/.test(path)) {
|
|
34
|
+
for (const v of buttonSpacing(text, path)) {
|
|
35
|
+
findings.push({
|
|
36
|
+
gate: 'button-spacing',
|
|
37
|
+
path,
|
|
38
|
+
line: v.line,
|
|
39
|
+
column: v.column,
|
|
40
|
+
rule: 'touching-buttons',
|
|
41
|
+
found: `<${v.tag}> with ${v.count} button-like children`,
|
|
42
|
+
fix: BUTTON_SPACING_FIX,
|
|
43
|
+
text: formatButtonSpacing({ ...v, file: path }),
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
for (const v of findInlineViolations(text)) {
|
|
47
|
+
findings.push({ gate: 'host-inline', path, line: v.line, column: v.column, rule: v.rule, found: v.found, fix: v.fix, text: formatInlineViolation(v, path) })
|
|
48
|
+
}
|
|
49
|
+
for (const v of findAiCopyViolations(text, path)) {
|
|
50
|
+
findings.push({ gate: 'ai-copy', path, line: v.line, column: v.column, rule: v.rule, found: v.found, fix: v.fix, text: formatAiCopyViolation(v) })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const v of findPropViolations(text, path, index)) {
|
|
54
|
+
findings.push({ gate: 'props', path, line: v.line, column: v.column, rule: v.rule, found: v.found, fix: v.fix, text: formatPropViolation(v) })
|
|
55
|
+
}
|
|
56
|
+
return findings
|
|
57
|
+
}
|