@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.
Files changed (42) hide show
  1. package/.claude/skills/seven/SKILL.md +162 -0
  2. package/.claude/skills/seven/reference/audit.md +120 -0
  3. package/.claude/skills/seven/reference/brand.md +24 -0
  4. package/.claude/skills/seven/reference/clarify.md +76 -0
  5. package/.claude/skills/seven/reference/color-and-contrast.md +84 -0
  6. package/.claude/skills/seven/reference/motion-design.md +71 -0
  7. package/.claude/skills/seven/reference/polish.md +55 -0
  8. package/.claude/skills/seven/reference/product.md +45 -0
  9. package/.claude/skills/seven/reference/shape.md +85 -0
  10. package/.claude/skills/seven/reference/spatial-design.md +60 -0
  11. package/.claude/skills/seven/reference/typography.md +43 -0
  12. package/.claude/skills/seven/reference/ux-writing.md +47 -0
  13. package/.claude/skills/seven/scripts/load-context.mjs +84 -0
  14. package/.claude-plugin/marketplace.json +34 -0
  15. package/.claude-plugin/plugin.json +12 -0
  16. package/LICENSE +190 -0
  17. package/NOTICE.md +26 -0
  18. package/README.md +118 -0
  19. package/cli/bin/commands/skills.mjs +664 -0
  20. package/cli/bin/seven.mjs +68 -0
  21. package/cli/engine/browser/injected/index.mjs +84 -0
  22. package/cli/engine/cli/main.mjs +215 -0
  23. package/cli/engine/detect-antipatterns-browser.js +3014 -0
  24. package/cli/engine/detect-antipatterns.mjs +44 -0
  25. package/cli/engine/engines/browser/detect-url.mjs +108 -0
  26. package/cli/engine/engines/regex/detect-text.mjs +508 -0
  27. package/cli/engine/engines/static-html/css-cascade.mjs +957 -0
  28. package/cli/engine/engines/static-html/detect-html.mjs +211 -0
  29. package/cli/engine/engines/visual/screenshot-contrast.mjs +192 -0
  30. package/cli/engine/findings.mjs +28 -0
  31. package/cli/engine/node/file-system.mjs +212 -0
  32. package/cli/engine/profile/profiler.mjs +169 -0
  33. package/cli/engine/registry/seven-antipatterns.mjs +494 -0
  34. package/cli/engine/rules/checks.mjs +1518 -0
  35. package/cli/engine/shared/color.mjs +204 -0
  36. package/cli/engine/shared/constants.mjs +91 -0
  37. package/cli/engine/shared/page.mjs +9 -0
  38. package/cli/engine/shared/tokens.mjs +153 -0
  39. package/cli/engine/token-data.generated.mjs +691 -0
  40. package/docs/detector-rules.md +605 -0
  41. package/docs/figma-token-rule-exploration.md +185 -0
  42. 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
+ }