@etus/seven-skill 0.1.0-beta.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/.claude/skills/seven/SKILL.md +162 -0
- package/.claude/skills/seven/reference/audit.md +120 -0
- package/.claude/skills/seven/reference/brand.md +24 -0
- package/.claude/skills/seven/reference/clarify.md +76 -0
- package/.claude/skills/seven/reference/color-and-contrast.md +84 -0
- package/.claude/skills/seven/reference/motion-design.md +71 -0
- package/.claude/skills/seven/reference/polish.md +55 -0
- package/.claude/skills/seven/reference/product.md +45 -0
- package/.claude/skills/seven/reference/shape.md +85 -0
- package/.claude/skills/seven/reference/spatial-design.md +60 -0
- package/.claude/skills/seven/reference/typography.md +43 -0
- package/.claude/skills/seven/reference/ux-writing.md +47 -0
- package/.claude/skills/seven/scripts/load-context.mjs +84 -0
- package/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +190 -0
- package/NOTICE.md +26 -0
- package/README.md +118 -0
- package/cli/bin/commands/skills.mjs +664 -0
- package/cli/bin/seven.mjs +68 -0
- package/cli/engine/browser/injected/index.mjs +84 -0
- package/cli/engine/cli/main.mjs +215 -0
- package/cli/engine/detect-antipatterns-browser.js +3014 -0
- package/cli/engine/detect-antipatterns.mjs +44 -0
- package/cli/engine/engines/browser/detect-url.mjs +108 -0
- package/cli/engine/engines/regex/detect-text.mjs +508 -0
- package/cli/engine/engines/static-html/css-cascade.mjs +957 -0
- package/cli/engine/engines/static-html/detect-html.mjs +211 -0
- package/cli/engine/engines/visual/screenshot-contrast.mjs +192 -0
- package/cli/engine/findings.mjs +28 -0
- package/cli/engine/node/file-system.mjs +212 -0
- package/cli/engine/profile/profiler.mjs +169 -0
- package/cli/engine/registry/seven-antipatterns.mjs +494 -0
- package/cli/engine/rules/checks.mjs +1518 -0
- package/cli/engine/shared/color.mjs +204 -0
- package/cli/engine/shared/constants.mjs +91 -0
- package/cli/engine/shared/page.mjs +9 -0
- package/cli/engine/shared/tokens.mjs +153 -0
- package/cli/engine/token-data.generated.mjs +691 -0
- package/docs/detector-rules.md +605 -0
- package/docs/figma-token-rule-exploration.md +185 -0
- package/package.json +76 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
4
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Seven CLI entry.
|
|
8
|
+
*
|
|
9
|
+
* npx seven detect [file-or-dir-or-url...] Scan for design anti-patterns
|
|
10
|
+
* npx seven skills help|install|check Manage the Seven skill
|
|
11
|
+
* npx seven --help
|
|
12
|
+
* npx seven --version
|
|
13
|
+
*
|
|
14
|
+
* Exit codes: 0 clean / 1 usage error / 2 findings present.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const command = args[0]
|
|
24
|
+
|
|
25
|
+
function printUsage() {
|
|
26
|
+
process.stdout.write(`Usage: seven <command> [options]
|
|
27
|
+
|
|
28
|
+
Commands:
|
|
29
|
+
detect [file-or-dir-or-url...] Scan for Seven design anti-patterns
|
|
30
|
+
skills help List the Seven skill and its commands
|
|
31
|
+
skills install Copy the built skill into ./.claude/skills
|
|
32
|
+
skills check Report skill install status
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--help Show this help message
|
|
36
|
+
--version Show version number
|
|
37
|
+
|
|
38
|
+
Run 'seven detect --help' for detector options.
|
|
39
|
+
`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!command || command === '--help' || command === '-h') {
|
|
43
|
+
printUsage()
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (command === '--version' || command === '-v') {
|
|
48
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'))
|
|
49
|
+
process.stdout.write(`${pkg.version}\n`)
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === 'detect') {
|
|
54
|
+
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)]
|
|
55
|
+
const { detectCli } = await import('../engine/cli/main.mjs')
|
|
56
|
+
await detectCli()
|
|
57
|
+
} else if (command === 'skills') {
|
|
58
|
+
const { run } = await import('./commands/skills.mjs')
|
|
59
|
+
await run(args.slice(1))
|
|
60
|
+
} else if (command === 'help') {
|
|
61
|
+
printUsage()
|
|
62
|
+
process.exit(0)
|
|
63
|
+
} else {
|
|
64
|
+
// Bare `seven src/` shorthand: treat unknown leading args as detect targets.
|
|
65
|
+
process.argv = [process.argv[0], process.argv[1], ...args]
|
|
66
|
+
const { detectCli } = await import('../engine/cli/main.mjs')
|
|
67
|
+
await detectCli()
|
|
68
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
3
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* In-browser detector. This file is NOT a standalone module — it is
|
|
7
|
+
* concatenated by build/build-browser-detector.mjs into a single IIFE bundle
|
|
8
|
+
* (cli/engine/detect-antipatterns-browser.js) together with the registry,
|
|
9
|
+
* color helpers, constants, and the rule checks. It therefore references
|
|
10
|
+
* those symbols (`checkElement*DOM`, `checkPageQualityDOM`,
|
|
11
|
+
* `checkHtmlPatterns`) as free identifiers resolved within the bundle scope.
|
|
12
|
+
*
|
|
13
|
+
* The browser engine (engines/browser/detect-url.mjs) injects the bundle into
|
|
14
|
+
* a Puppeteer page and calls `window.sevenDetect()`. Each element is checked
|
|
15
|
+
* with the live `getComputedStyle`, so cascade-, layout-, and runtime-CSS-
|
|
16
|
+
* dependent drift surfaces — the per-element work the static-html engine does
|
|
17
|
+
* against a resolved cascade, here against the real browser.
|
|
18
|
+
*
|
|
19
|
+
* Impeccable's injected script also carries an inspector overlay, decoration,
|
|
20
|
+
* visual-contrast capture, and the Chrome-extension bridge. Those are
|
|
21
|
+
* extension UI, not detection — Seven has no extension, so they are omitted.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
;(function sevenInstallBrowserDetector() {
|
|
25
|
+
if (typeof window === 'undefined') return
|
|
26
|
+
|
|
27
|
+
function collectFindings() {
|
|
28
|
+
const out = []
|
|
29
|
+
const push = (arr) => {
|
|
30
|
+
if (!arr) return
|
|
31
|
+
for (const f of arr) out.push({ type: f.id, detail: f.snippet })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const el of document.querySelectorAll('*')) {
|
|
35
|
+
if (el === document.body || el === document.documentElement) continue
|
|
36
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : ''
|
|
37
|
+
if (tag === 'script' || tag === 'style' || tag === 'head' || tag === 'meta' || tag === 'link') {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
push(checkElementBordersDOM(el))
|
|
42
|
+
push(checkElementColorsDOM(el))
|
|
43
|
+
push(checkElementGlowDOM(el))
|
|
44
|
+
push(checkElementMotionDOM(el))
|
|
45
|
+
push(checkElementTactilePressDOM(el))
|
|
46
|
+
push(checkElementPillOnButtonDOM(el))
|
|
47
|
+
push(checkElementLucideIconDOM(el))
|
|
48
|
+
push(checkElementQualityDOM(el))
|
|
49
|
+
} catch {
|
|
50
|
+
// A malformed element must not abort the whole scan.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
push(checkPageQualityDOM())
|
|
56
|
+
} catch {
|
|
57
|
+
/* page-quality check failed — continue */
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Regex-on-markup checks over the fully rendered HTML.
|
|
62
|
+
push(checkHtmlPatterns(document.documentElement.outerHTML))
|
|
63
|
+
} catch {
|
|
64
|
+
/* html-pattern check failed — continue */
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Public entry the browser engine calls. Returns a flat array of
|
|
72
|
+
* { type, detail } findings; detect-url.mjs maps it to normalized findings.
|
|
73
|
+
*/
|
|
74
|
+
window.sevenDetect = function sevenDetect() {
|
|
75
|
+
try {
|
|
76
|
+
return collectFindings()
|
|
77
|
+
} catch {
|
|
78
|
+
return []
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Alias kept for parity with the impeccable browser API surface.
|
|
83
|
+
window.sevenScan = window.sevenDetect
|
|
84
|
+
})()
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// ETUS Digital — Seven Design System. Detector architecture adapted from
|
|
3
|
+
// impeccable (Paul Bakaus, Apache-2.0). See packages/seven-skill/NOTICE.md.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { createBrowserDetector, detectUrl } from '../engines/browser/detect-url.mjs'
|
|
9
|
+
import { detectHtml } from '../engines/static-html/detect-html.mjs'
|
|
10
|
+
import { detectText } from '../engines/regex/detect-text.mjs'
|
|
11
|
+
import {
|
|
12
|
+
HTML_EXTENSIONS,
|
|
13
|
+
detectFrameworkConfig,
|
|
14
|
+
findOwningPackageName,
|
|
15
|
+
isPortListening,
|
|
16
|
+
walkDir,
|
|
17
|
+
} from '../node/file-system.mjs'
|
|
18
|
+
import { FIXTURES_PATH_SEGMENT } from '../shared/constants.mjs'
|
|
19
|
+
import { getRuleExemption, getRuleIds } from '../registry/seven-antipatterns.mjs'
|
|
20
|
+
|
|
21
|
+
// ─── Exemption resolution ───────────────────────────────────────────────────
|
|
22
|
+
// Decide which rules may run against a given file. Test fixtures are always
|
|
23
|
+
// fully scanned. Otherwise a rule is skipped when the file sits in an exempt
|
|
24
|
+
// package (resolved by the nearest package.json name) or under an exempt path
|
|
25
|
+
// segment. This keeps the detector quiet on token-defining and skill-defining
|
|
26
|
+
// source while still catching real consumer drift.
|
|
27
|
+
|
|
28
|
+
function allowedRulesForFile(filePath, only = null, skip = null) {
|
|
29
|
+
let base = only && only.length > 0 ? getRuleIds().filter((id) => only.includes(id)) : getRuleIds()
|
|
30
|
+
if (skip && skip.length > 0) base = base.filter((id) => !skip.includes(id))
|
|
31
|
+
if (filePath.includes(FIXTURES_PATH_SEGMENT)) return base
|
|
32
|
+
const pkgName = findOwningPackageName(filePath)
|
|
33
|
+
return base.filter((id) => {
|
|
34
|
+
const exemption = getRuleExemption(id)
|
|
35
|
+
if (exemption.skipPathSegments.some((seg) => filePath.includes(seg))) return false
|
|
36
|
+
if (pkgName && exemption.skipPackages.has(pkgName)) return false
|
|
37
|
+
return true
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Output formatting ──────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
export function formatFindings(findings, jsonMode) {
|
|
44
|
+
if (jsonMode) return JSON.stringify(findings, null, 2)
|
|
45
|
+
if (findings.length === 0) return 'No anti-patterns found.'
|
|
46
|
+
|
|
47
|
+
const grouped = {}
|
|
48
|
+
for (const f of findings) {
|
|
49
|
+
if (!grouped[f.file]) grouped[f.file] = []
|
|
50
|
+
grouped[f.file].push(f)
|
|
51
|
+
}
|
|
52
|
+
const out = []
|
|
53
|
+
for (const [file, items] of Object.entries(grouped)) {
|
|
54
|
+
out.push(`\n${file}`)
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
out.push(` ${item.line ? `line ${item.line}: ` : ''}[${item.antipattern}] ${item.snippet}`)
|
|
57
|
+
out.push(` -> ${item.description}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
out.push(`\n${findings.length} anti-pattern${findings.length === 1 ? '' : 's'} found.`)
|
|
61
|
+
return out.join('\n')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Per-target scanning ────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
async function scanFile(filePath, { fast = false, only = null, skip = null } = {}) {
|
|
67
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
68
|
+
const ruleIds = allowedRulesForFile(filePath, only, skip)
|
|
69
|
+
if (ruleIds.length === 0) return []
|
|
70
|
+
if (!fast && HTML_EXTENSIONS.has(ext)) {
|
|
71
|
+
return detectHtml(filePath, { ruleIds })
|
|
72
|
+
}
|
|
73
|
+
let content
|
|
74
|
+
try {
|
|
75
|
+
content = fs.readFileSync(filePath, 'utf-8')
|
|
76
|
+
} catch {
|
|
77
|
+
return []
|
|
78
|
+
}
|
|
79
|
+
return detectText(content, filePath, { ruleIds })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Programmatic scan. Resolves each path (URL, file, or directory), routes it
|
|
84
|
+
* to the matching engine, applies per-file rule exemptions, and returns the
|
|
85
|
+
* combined findings. No formatting, no process.exit — `detectCli` wraps this.
|
|
86
|
+
*
|
|
87
|
+
* @param {string[]} paths
|
|
88
|
+
* @param {{ fast?: boolean, only?: string[], skip?: string[] }} [options]
|
|
89
|
+
* @returns {Promise<Array>} normalized findings
|
|
90
|
+
*/
|
|
91
|
+
export async function collectFindings(paths, options = {}) {
|
|
92
|
+
const { fast = false, only = null, skip = null } = options
|
|
93
|
+
const urlTargets = paths.filter((t) => /^https?:\/\//i.test(t))
|
|
94
|
+
const browserDetector = urlTargets.length > 1 ? await createBrowserDetector() : null
|
|
95
|
+
const findings = []
|
|
96
|
+
const restrict = (raw) => {
|
|
97
|
+
let out = raw
|
|
98
|
+
if (only && only.length > 0) out = out.filter((f) => only.includes(f.antipattern))
|
|
99
|
+
if (skip && skip.length > 0) out = out.filter((f) => !skip.includes(f.antipattern))
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
for (const target of paths) {
|
|
104
|
+
if (/^https?:\/\//i.test(target)) {
|
|
105
|
+
const scan = browserDetector ? (u) => browserDetector.detectUrl(u, { only }) : (u) => detectUrl(u, { only })
|
|
106
|
+
findings.push(...restrict(await scan(target)))
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const resolved = path.resolve(target)
|
|
110
|
+
let stat
|
|
111
|
+
try {
|
|
112
|
+
stat = fs.statSync(resolved)
|
|
113
|
+
} catch {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if (stat.isDirectory()) {
|
|
117
|
+
for (const file of walkDir(resolved)) {
|
|
118
|
+
findings.push(...(await scanFile(file, { fast, only, skip })))
|
|
119
|
+
}
|
|
120
|
+
} else if (stat.isFile()) {
|
|
121
|
+
findings.push(...(await scanFile(resolved, { fast, only, skip })))
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
if (browserDetector) await browserDetector.close()
|
|
126
|
+
}
|
|
127
|
+
return findings
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printUsage() {
|
|
131
|
+
process.stdout.write(`Usage: seven detect [options] [file-or-dir-or-url...]
|
|
132
|
+
|
|
133
|
+
Scan files or URLs for Seven Design System anti-patterns.
|
|
134
|
+
|
|
135
|
+
Options:
|
|
136
|
+
--fast Regex-only mode (skip linked-stylesheet resolution)
|
|
137
|
+
--json Output results as JSON
|
|
138
|
+
--only=<ids> Comma-separated rule IDs to run (default: all)
|
|
139
|
+
--help Show this help message
|
|
140
|
+
|
|
141
|
+
Detection engines:
|
|
142
|
+
Source files Regex scan of the file text
|
|
143
|
+
HTML files Static-HTML scan, resolves <link rel=stylesheet>
|
|
144
|
+
URLs Browser render via Puppeteer (auto-detected; optional dep)
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
seven detect src/
|
|
148
|
+
seven detect packages/ui/src/components/Button
|
|
149
|
+
seven detect index.html
|
|
150
|
+
seven detect https://localhost:5173
|
|
151
|
+
seven detect --fast --json .
|
|
152
|
+
`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function detectCli() {
|
|
156
|
+
let args = process.argv.slice(2)
|
|
157
|
+
if (args[0] === 'detect') args = args.slice(1)
|
|
158
|
+
const jsonMode = args.includes('--json')
|
|
159
|
+
const helpMode = args.includes('--help') || args.includes('-h')
|
|
160
|
+
const fastMode = args.includes('--fast')
|
|
161
|
+
const onlyArg = args.find((a) => a.startsWith('--only='))
|
|
162
|
+
const only = onlyArg ? onlyArg.slice(7).split(',').map((s) => s.trim()).filter(Boolean) : null
|
|
163
|
+
const skipArg = args.find((a) => a.startsWith('--skip='))
|
|
164
|
+
const skip = skipArg ? skipArg.slice(7).split(',').map((s) => s.trim()).filter(Boolean) : null
|
|
165
|
+
const targets = args.filter((a) => !a.startsWith('-'))
|
|
166
|
+
|
|
167
|
+
if (helpMode) {
|
|
168
|
+
printUsage()
|
|
169
|
+
process.exit(0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const paths = targets.length > 0 ? targets : [process.cwd()]
|
|
173
|
+
|
|
174
|
+
// One-time framework dev-server nudge for directory scans.
|
|
175
|
+
if (!jsonMode) {
|
|
176
|
+
for (const target of paths) {
|
|
177
|
+
if (/^https?:\/\//i.test(target)) continue
|
|
178
|
+
let stat
|
|
179
|
+
try {
|
|
180
|
+
stat = fs.statSync(path.resolve(target))
|
|
181
|
+
} catch {
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
if (!stat.isDirectory()) continue
|
|
185
|
+
const fwConfig = detectFrameworkConfig(path.resolve(target))
|
|
186
|
+
if (!fwConfig) continue
|
|
187
|
+
const probe = await isPortListening(fwConfig.port, fwConfig.fingerprint)
|
|
188
|
+
if (probe.listening && probe.matched) {
|
|
189
|
+
process.stderr.write(
|
|
190
|
+
`\n${fwConfig.name} dev server detected on localhost:${fwConfig.port}.\n` +
|
|
191
|
+
`For results that include runtime CSS, scan the running site:\n` +
|
|
192
|
+
` npx seven detect http://localhost:${fwConfig.port}\n\n`
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let findings = []
|
|
199
|
+
try {
|
|
200
|
+
findings = await collectFindings(paths, { fast: fastMode, only, skip })
|
|
201
|
+
} catch (e) {
|
|
202
|
+
process.stderr.write(`detect failed: ${e?.message ?? e}\n`)
|
|
203
|
+
process.exit(1)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (findings.length > 0) {
|
|
207
|
+
const text = formatFindings(findings, jsonMode)
|
|
208
|
+
if (jsonMode) process.stdout.write(text + '\n')
|
|
209
|
+
else process.stderr.write(text + '\n')
|
|
210
|
+
process.exit(2)
|
|
211
|
+
}
|
|
212
|
+
if (jsonMode) process.stdout.write('[]\n')
|
|
213
|
+
else process.stdout.write('No anti-patterns found.\n')
|
|
214
|
+
process.exit(0)
|
|
215
|
+
}
|