sparkle-design-cli 2.0.7-beta.9 → 2.0.7-rc.3

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/README.md CHANGED
@@ -4,24 +4,13 @@ Sparkle Design のプロジェクト初期セットアップ、CSS 生成、導
4
4
 
5
5
  ## クイックスタート
6
6
 
7
- 既存の Next.js / Vite プロジェクトに Sparkle Design を導入する場合、以下 1 コマンドで完了します:
7
+ 既存の Next.js / Vite プロジェクトで以下を実行するだけで導入完了します。詳細は [`setup` セクション](#setup-プロジェクトのフルセットアップ) を参照してください。
8
8
 
9
9
  ```bash
10
10
  npx --yes sparkle-design-cli setup --assistant claude
11
11
  ```
12
12
 
13
- setup は次を自動で行います:
14
-
15
- 1. パッケージマネージャー(pnpm / npm / yarn / bun)を自動検出
16
- 2. `sparkle-design` を dependencies、`tailwindcss` + `@tailwindcss/postcss` を devDependencies に追加
17
- 3. 未作成の場合のみ初期ファイルを生成:
18
- - `sparkle.config.json`(デフォルトは blue / BIZ UDPGothic / BIZ UDGothic / md — sparkle-design 本体のデフォルトに合わせて統一)
19
- - `postcss.config.mjs`
20
- - Tailwind エントリ CSS — Next.js App Router なら `src/app/globals.css`、Vite なら `src/index.css`、それ以外は `src/globals.css`(プロジェクト構成から自動判定)
21
- 4. AI 指示ファイル(`CLAUDE.md` / `AGENTS.md` / Cursor rules)に Sparkle Design Guard ブロックを追加
22
- 5. `sparkle-design-cli generate` を実行し、`sparkle-design.css` と `SparkleHead.tsx` を生成
23
-
24
- 既存ファイルは上書きされません。
13
+ `--assistant` は `claude` / `cursor` / `codex` / `generic` から選べます。既存ファイルは上書きされません。
25
14
 
26
15
  ## インストール
27
16
 
@@ -33,22 +22,24 @@ npm install -g sparkle-design-cli
33
22
 
34
23
  ### リリースチャネル
35
24
 
36
- npm の dist-tag でチャネルを分離しています。通常は latest を使い、品質保証中の変更を試したい場合のみ beta を指定してください。
25
+ npm の dist-tag でチャネルを分けています。通常は `latest` を使い、RC や beta は先行試用時のみ指定してください。
37
26
 
38
- | チャネル | dist-tag | 用途 | 指定方法 |
39
- | -------- | -------- | ------------------------------------------------------------- | ----------------------------------- |
40
- | 安定版 | `latest` | 本番運用向け。`npx --yes sparkle-design-cli` は常にこれを取得 | (デフォルト) |
41
- | Beta | `beta` | 品質保証中の検証用 | `npx --yes sparkle-design-cli@beta` |
27
+ | チャネル | dist-tag | バージョン形式 | 用途 |
28
+ | --- | --- | --- | --- |
29
+ | 安定版 | `latest` | `X.Y.Z` | 本番運用向け(デフォルト) |
30
+ | Release Candidate | `next` | `X.Y.Z-rc.N` | GA 直前の検証、チーム展開前 |
31
+ | Beta | `beta` | `X.Y.Z-beta.N` | 品質保証中の検証用 |
42
32
 
43
33
  ```bash
44
- # beta で setup を試す
45
- npx --yes sparkle-design-cli@beta setup --assistant claude
34
+ # latest を使う(デフォルト)
35
+ npx --yes sparkle-design-cli setup --assistant claude
46
36
 
47
- # beta で check を試す
48
- npx --yes sparkle-design-cli@beta check src --strict
49
- ```
37
+ # RC を使う
38
+ npx --yes sparkle-design-cli@next setup --assistant claude
50
39
 
51
- Beta バージョンは `X.Y.Z-beta.N` 形式で publish されます。GA(正式版)へ昇格するときは、beta の -beta.N を外した `X.Y.Z` を改めて publish します。
40
+ # beta を使う
41
+ npx --yes sparkle-design-cli@beta setup --assistant claude
42
+ ```
52
43
 
53
44
  ## 使用方法
54
45
 
@@ -231,6 +222,111 @@ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaf
231
222
 
232
223
  `setup` は通常実行時も JSON サマリーを stdout に表示します。`--dry-run` を付けると、その JSON を表示したままファイル変更だけを抑止します。
233
224
 
225
+ ### 手動セットアップ(Next.js / Vite 以外のプロジェクト向け)
226
+
227
+ `setup` コマンドは **Next.js App Router** と **Vite** を前提に scaffold / entry CSS の配置を自動判定します。Remix / Astro / TanStack Start など他のフレームワークや、独自ディレクトリ構成のプロジェクトでは自動判定が外れるため、以下の手順で手動セットアップすることを推奨します。
228
+
229
+ #### 手順
230
+
231
+ **1. パッケージをインストール**
232
+
233
+ ```bash
234
+ # 本体
235
+ pnpm add sparkle-design # or npm install / yarn add / bun add
236
+
237
+ # Tailwind v4
238
+ pnpm add -D tailwindcss @tailwindcss/postcss
239
+ ```
240
+
241
+ **2. `sparkle.config.json` をプロジェクトルートに作成**
242
+
243
+ ```json
244
+ {
245
+ "primary": "blue",
246
+ "font-pro": "Inter",
247
+ "font-mono": "JetBrains Mono",
248
+ "radius": "md"
249
+ }
250
+ ```
251
+
252
+ 選択肢の詳細は本 README の「[設定オプション](#設定オプション)」を参照してください。
253
+
254
+ **3. `postcss.config.mjs` をプロジェクトルートに作成**
255
+
256
+ ```js
257
+ export default {
258
+ plugins: {
259
+ '@tailwindcss/postcss': {},
260
+ },
261
+ };
262
+ ```
263
+
264
+ **4. Tailwind エントリ CSS を自前で用意**
265
+
266
+ 既存プロジェクトに組み込む場合、`@import "tailwindcss";` を含む CSS ファイル(例: `src/styles/app.css`)があればそれを使います。無ければ作成してアプリケーションのエントリから import します。
267
+
268
+ **5. `generate` を実行**
269
+
270
+ ```bash
271
+ npx --yes sparkle-design-cli generate
272
+ ```
273
+
274
+ これで `sparkle-design.css` が `src/app/sparkle-design.css` に生成され、`SparkleHead.tsx` も同じ場所に出ます。エントリ CSS の検出に失敗する場合は `sparkle.config.json` の `extend.globals-path` に明示指定してください。
275
+
276
+ ```json
277
+ {
278
+ "primary": "blue",
279
+ "extend": {
280
+ "globals-path": "src/styles/app.css"
281
+ }
282
+ }
283
+ ```
284
+
285
+ **6. フォントの `<link>` タグを手動で配置**
286
+
287
+ Next.js / Vite 以外ではアプリケーションフレームワークごとに「`<head>` 相当」の配置方法が異なります。自動生成される `SparkleHead.tsx` はそのまま使えるはずですが、使えない場合は中身(`preconnect` + Google Fonts の `<link>` タグ群)を自前のレイアウトにコピーしてください。
288
+
289
+ たとえば Astro なら `src/layouts/BaseLayout.astro` の `<head>` に直接書きます:
290
+
291
+ ```html
292
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
293
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
294
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:FILL,wght@0..1,500&display=block" />
295
+ <!-- sparkle.config.json の font-pro / font-mono に合わせた Google Fonts URL -->
296
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" />
297
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" />
298
+ ```
299
+
300
+ **7. アンチパターン検査を package.json に追加(任意)**
301
+
302
+ ```json
303
+ {
304
+ "scripts": {
305
+ "lint:sparkle": "npx --yes sparkle-design-cli check src --strict",
306
+ "lint:sparkle:json": "npx --yes sparkle-design-cli check src --format json"
307
+ }
308
+ }
309
+ ```
310
+
311
+ **8. AI エージェントを使うなら Guard ブロックと hook を手動で配置**
312
+
313
+ AI ガード(`CLAUDE.md` / `AGENTS.md` / `.cursor/rules/*.mdc`)と hook 設定(`.claude/settings.json` / `.cursor/hooks.json` / `.codex/hooks.json`)は `setup` に任せるのが最も楽です。
314
+
315
+ ```bash
316
+ # ガード追加・hook 設定のみ(パッケージインストールや scaffold はスキップ)
317
+ npx --yes sparkle-design-cli setup --assistant claude --skip-install --skip-scaffold --skip-generate
318
+ ```
319
+
320
+ この `--skip-*` 組み合わせは手動セットアップ済みのプロジェクトに対して Guard + hook だけ差し込むのに使えます。
321
+
322
+ #### 既知の未対応ケース
323
+
324
+ - **モノレポ(workspaces)**: `generate` は実行した cwd からパッケージマネージャーを遡及検出しないため、サブパッケージで直接実行するのが確実です。
325
+ - **CSS 以外のエントリ(CSS-in-JS / vanilla-extract 等)**: Sparkle Design は Tailwind v4 の `@source` + CSS variable 前提で作られているため、ビルドパイプラインの外で CSS を扱うソリューションは対象外です。
326
+ - **Tailwind v3**: `@source` ディレクティブ依存のため v4 必須です。
327
+
328
+ Next.js / Vite 以外で導入したい方で困ったときは Issue で教えていただけると助かります。
329
+
234
330
  ## 設定ファイル (sparkle.config.json)
235
331
 
236
332
  ### 設定ファイルの作成
@@ -305,39 +401,15 @@ Figma プラグインが出力する基本4項目に加えて、プロジェク
305
401
  - カスタム出力先: `-o` オプションで指定可能
306
402
  - 実行場所を基準として相対パスで処理されます
307
403
 
308
- ## 自動フォント管理
309
-
310
- **v1.2.0 以降の新機能**
311
-
312
- CLI は自動的にフォント管理を行います:
313
-
314
- 1. **フォント検出**: `sparkle-design.css` から Google Fonts の `@import` 文を検出
315
- 2. **エントリ CSS への移動**: フォントの `@import` を Tailwind エントリ CSS(`globals.css` / `index.css` 等、自動検出)の先頭に移動
316
- 3. **sparkle-design.css から削除**: 元のファイルからフォント `@import` を削除
317
-
318
- ### なぜこの処理が必要なのか?
319
-
320
- CSS の仕様では、`@import` 文は `@charset` と `@layer` 以外のすべてのルールより前に記述する必要があります。Tailwind CSS v4 と組み合わせた場合、この順序が正しくないとビルド時に警告が発生します。
321
-
322
- この機能により、以下の警告が自動的に解決されます:
323
-
324
- ```text
325
- ⚠️ @import rules must precede all rules aside from @charset and @layer statements
326
- ```
327
-
328
- ### 動作条件
404
+ ## フォントと entry CSS の自動管理
329
405
 
330
- - Tailwind エントリ CSS(`@import "tailwindcss"` を含む CSS ファイル)が `sparkle-design.css` と同じディレクトリに存在する場合のみ実行されます(ファイル名に依存しません)
331
- - エントリ CSS が存在しない場合は、フォント管理処理はスキップされます
406
+ CLI は **Tailwind エントリ CSS(`@import "tailwindcss"` を含む CSS ファイル)** を自動検出し、以下を 1 回で揃えます:
332
407
 
333
- ### 出力例
408
+ - `sparkle-design.css` の `@import`
409
+ - `@source "../node_modules/sparkle-design/dist"`(v4 が node_modules のクラスを拾うのに必要)
410
+ - フォント `<link>` タグ(React 向けは `SparkleHead.tsx`、Vite 向けは `index.html` の managed block に自動注入)
334
411
 
335
- ```text
336
- 📦 フォント管理処理を開始します...
337
- 📝 3個のフォントimportを検出しました
338
- ✅ エントリ CSS にフォントimportを追加しました
339
- ✅ sparkle-design.css からフォントimportを削除しました
340
- ```
412
+ CSS 仕様上 `@import` は他の at-rule より前に書く必要があるため、順序も適切に整えます。`@import "tailwindcss"` が欠けている場合は先頭に自動追記されます。
341
413
 
342
414
  ## 開発
343
415
 
@@ -354,71 +426,20 @@ npm link
354
426
  ### 開発用コマンド
355
427
 
356
428
  ```bash
357
- # テスト実行
358
- npm test
359
-
360
- # テスト監視モード
361
- npm run test:watch
362
-
363
- # Lint実行
364
- npm run lint
365
-
366
- # Lintエラーを自動修正
367
- npm run lint:fix
368
-
369
- # コードフォーマット
370
- npm run format
371
-
372
- # フォーマットチェック
373
- npm run format:check
374
- ```
375
-
376
- ### CLI実行テスト
377
-
378
- ```bash
379
- # CSS生成
380
- sparkle-design-cli generate
381
-
382
- # ヘルプ表示
383
- sparkle-design-cli --help
384
- sparkle-design-cli generate --help
385
-
386
- # 検査
387
- sparkle-design-cli check src --strict
429
+ npm test # Node.js test runner
430
+ npm run lint # ESLint
431
+ npm run format # Prettier
388
432
  ```
389
433
 
390
434
  ### リリース手順(メンテナ向け)
391
435
 
392
- npm publish は GitHub Actions (`Publish to npm`) 経由で行います。ローカルから `npm publish` しないでください。
393
-
394
- #### 安定版(latest)
395
-
396
- 1. `main` ブランチに変更をマージ
397
- 2. `package.json` の `version` を SemVer で更新(例: `2.0.6` → `2.1.0`)し、`CHANGELOG.md` に該当セクションを追加
398
- 3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `latest`)で手動実行
399
- 4. workflow は lint / test を流したうえで `npm publish`(dist-tag = latest)を実行
400
-
401
- #### Beta(beta)
436
+ publish は GitHub Actions の **Publish to npm** workflow 経由。ローカル `npm publish` は禁止。
402
437
 
403
- 品質保証中の変更を先行公開したいときに使います。latest には影響しません。
438
+ 1. `package.json` の `version` を更新(安定版: `X.Y.Z` / RC: `X.Y.Z-rc.N` / Beta: `X.Y.Z-beta.N`)
439
+ 2. `CHANGELOG.md` に該当セクションを追加
440
+ 3. PR をマージ後、**Publish to npm** workflow を `channel: auto` で実行
404
441
 
405
- 1. `package.json` の `version` を `X.Y.Z-beta.N` 形式に更新(例: `2.1.0-beta.0`、次の beta は `2.1.0-beta.1`)
406
- 2. `CHANGELOG.md` に `[X.Y.Z-beta.N]` セクションを追加
407
- 3. PR をマージ後、Actions の **Publish to npm** を `channel: auto`(または `beta`)で手動実行
408
- 4. workflow が version を判定して `npm publish --tag beta` を実行
409
- 5. 利用者側は `npx --yes sparkle-design-cli@beta ...` で検証
410
-
411
- #### Beta から GA(latest)へ昇格
412
-
413
- 同じ成果物を latest にする場合は、新しい安定版 `X.Y.Z` を publish するか、既存 beta バージョンに latest タグを付け替えます。
414
-
415
- ```bash
416
- # 選択肢 A: 新たに X.Y.Z を publish する(推奨)
417
- # version を X.Y.Z に更新 → workflow を latest で実行
418
-
419
- # 選択肢 B: 既存の X.Y.Z-beta.N に latest タグを付け替える
420
- npm dist-tag add sparkle-design-cli@X.Y.Z-beta.N latest
421
- ```
442
+ `channel: auto` は `package.json` の version 形式から dist-tag を自動判定します(`-beta.N` → `beta` / `-rc.N` → `next` / それ以外 → `latest`)。既存 RC / Beta を latest に昇格させる場合は、新しい `X.Y.Z` として改めて publish します(`npm dist-tag add` での手動付け替えも可能ですが、version 管理が明確になる前者を推奨)。
422
443
 
423
444
  ## ライセンス
424
445
 
@@ -925,7 +925,7 @@ const ANTI_PATTERN_GROUPS = [
925
925
  check: {
926
926
  description: 'Tailwind デフォルト typography を Sparkle Design コンポーネント内で使わない',
927
927
  recommendation:
928
- 'text-sm / text-xs / text-base / font-medium / font-semibold / font-bold は character-* utility に置き換えてください。',
928
+ 'text-sm / text-xs / text-base / font-medium / font-semibold / font-bold は character-* utility に置き換えてください。ただし character-* に対応する token が無いサイズ(text-[10px] 等の arbitrary value、あるいは意図的に token 外のサイズを使う場合)は、同一行または直前行に `// sparkle-disable-line tailwind-typography` コメントを付けて例外扱いとして残すこともできます。',
929
929
  pattern: /\b(text-(?:xs|sm|base|lg|xl|2xl)|font-(?:medium|semibold|bold|normal|light))\b/g,
930
930
  },
931
931
  featureSection: lines([
@@ -937,9 +937,15 @@ const ANTI_PATTERN_GROUPS = [
937
937
  '',
938
938
  '// ❌ Wrong — Tailwind デフォルトの typography を使わない',
939
939
  '<span className="text-sm font-medium">テキスト</span>',
940
+ '',
941
+ '// 例外 — character-* に対応するサイズが無い場合は arbitrary value を使うか',
942
+ '// suppress コメントを付けて残す',
943
+ '<span className="text-[10px] text-text-low">12px 未満の極小メタ情報</span>',
944
+ '{/* sparkle-disable-next-line tailwind-typography */}',
945
+ '<span className="text-xs">どうしても text-xs で残したいケース</span>',
940
946
  '```',
941
947
  '',
942
- 'Sparkle Design コンポーネント内では `character-*-pro` / `character-*-mono` を使用する。Tailwind の `text-sm` / `font-medium` 等は使わない。',
948
+ 'Sparkle Design コンポーネント内では `character-*-pro` / `character-*-mono` を使用する。character-1(12px)より小さい指定や、対応 token が無いサイズは Tailwind の arbitrary value (`text-[10px]` 等) で表現するか、`// sparkle-disable-line tailwind-typography` で個別に例外指定する。',
943
949
  ]),
944
950
  jsdocTargets: [],
945
951
  },
@@ -1049,8 +1055,28 @@ const ANTI_PATTERN_GROUPS = [
1049
1055
  description: 'Card 系コンポーネントのデフォルト padding を安易に上書きしない',
1050
1056
  recommendation:
1051
1057
  'CardHeader / CardContent のデフォルト padding(px-6 py-2)をそのまま使ってください。上書きは本当に必要な場合のみ。',
1052
- pattern:
1053
- /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*(?:"[^"]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^"]*"|'[^']*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^']*'|\{[^}]*\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)[^}]*\})[^>]*>/g,
1058
+ // 旧 pattern は `[^"]*\b...[^"]*` の 2 つの `[^"]*` が同じ文字列を食い合い
1059
+ // quadratic backtracking になっていた(閉じ quote 無しの壊れた className
1060
+ // で n=80k / 12s の regression)。2-pass 化して linear time を保証する:
1061
+ // 1. `<CardHeader className="..." ...>` / `{...}` 形式の className 値
1062
+ // を確定マッチで抽出
1063
+ // 2. 抽出した className 値に padding utility が含まれているか
1064
+ // `\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)` で test する
1065
+ // en: Switch to a 2-pass matcher. The old regex had quadratic
1066
+ // backtracking on malformed input; now we first capture the whole
1067
+ // className value, then test for padding utilities in isolation.
1068
+ match: (content) => {
1069
+ const OPEN_TAG =
1070
+ /<Card(?:Header|Content)\b[^>]*\bclassName\s*=\s*("[^"]*"|'[^']*'|\{[^}]*\})[^>]*>/g;
1071
+ const PADDING_UTIL = /\b(?:p-|px-|py-|pt-|pb-|pl-|pr-)/;
1072
+ const results = [];
1073
+ for (const m of content.matchAll(OPEN_TAG)) {
1074
+ if (PADDING_UTIL.test(m[1])) {
1075
+ results.push({ index: m.index, text: m[0] });
1076
+ }
1077
+ }
1078
+ return results;
1079
+ },
1054
1080
  },
1055
1081
  featureSection: lines([
1056
1082
  '### Card 系コンポーネントの padding を上書きしない',
@@ -1146,7 +1172,18 @@ const ANTI_PATTERN_GROUPS = [
1146
1172
  description: 'Icon の children にテキストを渡さない',
1147
1173
  recommendation:
1148
1174
  'Icon には icon prop でアイコン名を渡してください。children にテキストを渡すのは旧 Material Icons の書き方です。',
1149
- pattern: /<Icon\b(?:[^>]|\/(?!>))*>[^<]+<\/Icon>/g,
1175
+ // 旧 pattern は `(?:[^>]|\/(?!>))*` が `/ ` を両分岐にマッチさせるため、
1176
+ // 閉じ `>` が無い入力で exponential backtracking する ReDoS の種だった
1177
+ // (n=200 で 48s 消費)。属性部を `[^>]*` の単一分岐に戻して linear に
1178
+ // する。代償として自己閉じ `<Icon ... />` にも `<Icon ... >...</Icon>`
1179
+ // として表面的にはマッチし得るが、自己閉じは `/` が末尾に来るので
1180
+ // `>[^<]+` の後続(children text)に空白以外が必要になり、現実の JSX
1181
+ // では自己閉じと open+close タグが入り乱れる頻度は低い。1-pass で拾い
1182
+ // きれないケースは 2-pass に引き上げることで将来対応する。
1183
+ // en: Old pattern suffered exponential ReDoS because `/ ` fit both
1184
+ // branches of the alternation. Use a linear `[^>]*` and accept a
1185
+ // small risk of self-closing false positives (rare in practice).
1186
+ pattern: /<Icon\b[^>]*>[^<]+<\/Icon>/g,
1150
1187
  },
1151
1188
  featureSection: lines([
1152
1189
  '### Icon の children にテキストを渡さない',
package/lib/check.js CHANGED
@@ -11,7 +11,18 @@ const RULES = getCheckRules();
11
11
  const BASE_MANUAL_REVIEW_REMINDERS = getManualReviewReminders();
12
12
 
13
13
  const SPARKLE_HEAD_JSX_PATTERN = /<SparkleHead\s*\/?\s*>/;
14
- const CSP_PATTERN = /content-security-policy|contentSecurityPolicy|Content-Security-Policy/i;
14
+ // CSP key が「実際の設定 key として記述されている」コンテキストに絞る。
15
+ // 以前は単純に `/content-security-policy|.../i` だったため、コメント行
16
+ // (`// TODO: Add Content-Security-Policy later` 等)にだけマッチして
17
+ // false-positive の finding を emit していた。
18
+ // - `'Content-Security-Policy': ...` (object property with quoted string key)
19
+ // - `contentSecurityPolicy: ...` (JS camelCase property)
20
+ // - `"Content-Security-Policy"` を value として持つ header エントリ(`key: 'Content-Security-Policy'`)
21
+ // を許容し、単体コメント文字列には反応しないようにする。
22
+ // en: Restrict CSP detection to contexts that look like actual config entries,
23
+ // avoiding false positives from mere comment mentions.
24
+ const CSP_PATTERN =
25
+ /(?:['"]Content-Security-Policy['"]\s*[:,]|contentSecurityPolicy\s*[:=]|key\s*:\s*['"]Content-Security-Policy['"])/;
15
26
  const NEXT_CONFIG_CANDIDATES = [
16
27
  'next.config.js',
17
28
  'next.config.ts',
@@ -74,8 +85,66 @@ function formatSnippet(text) {
74
85
  return text.replace(/\s+/g, ' ').trim().slice(0, 120);
75
86
  }
76
87
 
88
+ /**
89
+ * ESLint 風の suppression コメントをサポートする。以下のいずれかが findings
90
+ * と同じ行、または直前行にあれば該当 rule の finding は emit しない:
91
+ *
92
+ * // sparkle-disable-line <rule-id>
93
+ * // sparkle-disable-next-line <rule-id>
94
+ * {/* sparkle-disable-line <rule-id> *\/} (JSX コメント)
95
+ *
96
+ * ユーザーがどうしても character-* に移行できない typography(token に
97
+ * 存在しないサイズなど)や、特定 file だけ例外扱いしたい場合の escape
98
+ * hatch として用意する。ルール ID は複数カンマ区切り可(例:
99
+ * `sparkle-disable-line tailwind-typography, card-padding-override`)。
100
+ *
101
+ * en: ESLint-style suppression comments so specific findings can be opted
102
+ * out when character-* / Sparkle tokens don't map cleanly (e.g. sub-token
103
+ * font sizes). Same-line or previous-line comment is honored.
104
+ */
105
+ const SUPPRESS_LINE = /(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-line\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
106
+ const SUPPRESS_NEXT_LINE =
107
+ /(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-next-line\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
108
+
109
+ function extractSuppressedRules(source, regex) {
110
+ if (!source) return null;
111
+ const m = source.match(regex);
112
+ if (!m) return null;
113
+ return new Set(
114
+ m[1]
115
+ .split(',')
116
+ .map((id) => id.trim())
117
+ .filter(Boolean)
118
+ );
119
+ }
120
+
121
+ function isSuppressed(ruleId, contentLines, lineNumber) {
122
+ // lineNumber は 1-origin
123
+ // en: 1-based line numbers from getLineNumber.
124
+ const current = contentLines[lineNumber - 1] ?? '';
125
+ const previous = contentLines[lineNumber - 2] ?? '';
126
+ const sameLine = extractSuppressedRules(current, SUPPRESS_LINE);
127
+ if (sameLine && sameLine.has(ruleId)) return true;
128
+ const prevLine = extractSuppressedRules(previous, SUPPRESS_NEXT_LINE);
129
+ if (prevLine && prevLine.has(ruleId)) return true;
130
+ return false;
131
+ }
132
+
77
133
  function collectFindings(filePath, content) {
78
134
  const findings = [];
135
+ const contentLines = content.split(/\r?\n/);
136
+ const pushFinding = (rule, index, snippet) => {
137
+ const line = getLineNumber(content, index ?? 0);
138
+ if (isSuppressed(rule.id, contentLines, line)) return;
139
+ findings.push({
140
+ filePath,
141
+ id: rule.id,
142
+ description: rule.description,
143
+ recommendation: rule.recommendation,
144
+ line,
145
+ snippet,
146
+ });
147
+ };
79
148
 
80
149
  for (const rule of RULES) {
81
150
  if (typeof rule.match === 'function') {
@@ -83,26 +152,12 @@ function collectFindings(filePath, content) {
83
152
  // 複雑な照合が必要なルール向けの opt-in API。2-pass 方式などで regex 単体の
84
153
  // backtracking リスクを回避したいときに使う。index は content 内の絶対オフセット。
85
154
  for (const hit of rule.match(content)) {
86
- findings.push({
87
- filePath,
88
- id: rule.id,
89
- description: rule.description,
90
- recommendation: rule.recommendation,
91
- line: getLineNumber(content, hit.index ?? 0),
92
- snippet: formatSnippet(hit.text ?? ''),
93
- });
155
+ pushFinding(rule, hit.index ?? 0, formatSnippet(hit.text ?? ''));
94
156
  }
95
157
  continue;
96
158
  }
97
159
  for (const match of content.matchAll(rule.pattern)) {
98
- findings.push({
99
- filePath,
100
- id: rule.id,
101
- description: rule.description,
102
- recommendation: rule.recommendation,
103
- line: getLineNumber(content, match.index ?? 0),
104
- snippet: formatMatch(match),
105
- });
160
+ pushFinding(rule, match.index ?? 0, formatMatch(match));
106
161
  }
107
162
  }
108
163
 
@@ -160,22 +215,38 @@ function collectNextjsCspFindings(cwd) {
160
215
  let content;
161
216
  try {
162
217
  content = fs.readFileSync(configPath, 'utf8');
163
- } catch {
218
+ } catch (error) {
219
+ // ENOENT は候補が存在しないだけなので静かに次へ。EACCES などは
220
+ // デバッグ価値があるので warn する(silent skip で「check したが
221
+ // CSP 設定が無かった」と誤解されないように)。
222
+ // en: ENOENT = config file absent, move on quietly. Other codes
223
+ // are worth surfacing so the user knows detection was skipped.
224
+ if (error.code !== 'ENOENT') {
225
+ console.warn(
226
+ `⚠️ ${candidate} の読み込みに失敗したため CSP 検査をスキップします (${error.code ?? error.message})`
227
+ );
228
+ }
164
229
  continue;
165
230
  }
166
231
 
167
232
  if (!CSP_PATTERN.test(content)) continue;
168
233
 
234
+ // host の `.` を regex escape しないと `fontsXgoogleapisXcom` などにも
235
+ // マッチし、本来足りない allowlist を「足りている」と誤判定する false
236
+ // negative が起きる。literal `.` として検査する。
237
+ // en: Escape `.` so we match the literal hostname and don't accept
238
+ // arbitrary strings as an allowlist match.
239
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
169
240
  const missingDomains = [
170
241
  {
171
242
  domain: 'fonts.googleapis.com',
172
243
  directive: 'style-src',
173
- pattern: new RegExp(FONT_DOMAINS.GOOGLEAPIS.replace('https://', '')),
244
+ pattern: new RegExp(escapeRegex(FONT_DOMAINS.GOOGLEAPIS.replace('https://', ''))),
174
245
  },
175
246
  {
176
247
  domain: 'fonts.gstatic.com',
177
248
  directive: 'font-src',
178
- pattern: new RegExp(FONT_DOMAINS.GSTATIC.replace('https://', '')),
249
+ pattern: new RegExp(escapeRegex(FONT_DOMAINS.GSTATIC.replace('https://', ''))),
179
250
  },
180
251
  ].filter(({ pattern }) => !pattern.test(content));
181
252
 
@@ -268,9 +339,37 @@ function printTextReport(report, options = {}) {
268
339
  }
269
340
  }
270
341
 
271
- console.log('Manual review reminders:');
272
- for (const reminder of report.manualReviewReminders) {
273
- console.log(`- [${reminder.id}] ${reminder.message}`);
342
+ const reminders = report.manualReviewReminders ?? [];
343
+ if (reminders.length === 0) {
344
+ // 0 件のときは従来通り静かに済ます。
345
+ // en: Nothing to review — keep quiet.
346
+ } else {
347
+ // Guard block から参照される acknowledgment フォーマットと合わせるため、
348
+ // AI 向けに「各 ID を response に echo して確認したことを示してほしい」
349
+ // と明示的に要求する。hook による exit blocking はしない(判断事項のため)。
350
+ // en: Ask the AI to echo each reminder ID in its final response, so humans
351
+ // can see which reminders were considered. This is not enforced by hook
352
+ // exit code — reminders are judgment calls, not hard failures.
353
+ console.log('');
354
+ console.log('=== Manual review reminders (must acknowledge each ID in your response) ===');
355
+ console.log(
356
+ 'These are judgment calls the linter cannot detect. For every item below,'
357
+ );
358
+ console.log(
359
+ 'explicitly state the reminder ID in your reply together with whether the'
360
+ );
361
+ console.log(
362
+ 'current code already satisfies it, or what change is needed. Silence = skipped.'
363
+ );
364
+ console.log('');
365
+ for (const reminder of reminders) {
366
+ console.log(`- [${reminder.id}] ${reminder.message}`);
367
+ }
368
+ console.log('');
369
+ console.log(
370
+ 'Example acknowledgment: "[badge-tag-semantics] reviewed — Badge used only for counts (OK). [dialog-modal-ux] reviewed — changed Dialog to Modal for form case."'
371
+ );
372
+ console.log('=========================================================================');
274
373
  }
275
374
 
276
375
  if (options.strict && report.findings.length > 0) {
@@ -281,6 +380,7 @@ function printTextReport(report, options = {}) {
281
380
  function printJsonReport(report, options = {}) {
282
381
  const strictMode = Boolean(options.strict);
283
382
  const passed = !(strictMode && report.findings.length > 0);
383
+ const reminders = report.manualReviewReminders ?? [];
284
384
 
285
385
  console.log(
286
386
  JSON.stringify(
@@ -289,6 +389,18 @@ function printJsonReport(report, options = {}) {
289
389
  targetCount: report.targets.length,
290
390
  checkedFileCount: report.checkedFiles.length,
291
391
  findingCount: report.findings.length,
392
+ reminderCount: reminders.length,
393
+ // AI 向けの attention フラグ。reminder が 1 件でもあれば true になり、
394
+ // AI は最終 response で各 reminder ID を echo する必要がある。
395
+ // exit code には寄与しない(判断事項を hook で block するのは過剰)。
396
+ // en: AI attention flag. When reminders exist, the AI MUST echo each
397
+ // reminder ID in its final reply. Not tied to exit code — reminders
398
+ // are judgment calls, not hard errors.
399
+ manualReviewRequired: reminders.length > 0,
400
+ reminderAcknowledgmentFormat:
401
+ reminders.length > 0
402
+ ? 'Respond with each reminder ID followed by your review, e.g. `[badge-tag-semantics] reviewed — Badge is used only for counts (OK)`.'
403
+ : null,
292
404
  strictMode,
293
405
  passed,
294
406
  exitCode: passed ? 0 : 1,