diff --git a/openspec/changes/archive/2026-09-22-fill-linter-config-files/.openspec.yaml b/openspec/changes/archive/2026-09-22-fill-linter-config-files/.openspec.yaml new file mode 100644 index 0000000..a9eca53 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-fill-linter-config-files/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-22 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-fill-linter-config-files/design.md b/openspec/changes/archive/2026-09-22-fill-linter-config-files/design.md new file mode 100644 index 0000000..5d1605e --- /dev/null +++ b/openspec/changes/archive/2026-09-22-fill-linter-config-files/design.md @@ -0,0 +1,57 @@ +## Context + +- `hooks/env_check.py::detect_linter_config`(M8 后):`for lang_id, lang_def in REGISTRY.items(): files = lang_def.get("linter_config_files") or []` → 存在性过滤 → `{lang: [files]}`。**填错无害**(不存在的文件名永远不命中),这是本 change 敢于「保守估计 + 宽清单」的依据。 +- 现状覆盖:java / rust / typescript / python 四条(M8 迁移自旧硬编码常量)。 +- 关键区分(M8 design 遗留):`requiresConfig` **不等于** linter 配置——java 的 `requiresConfig` 是 `pom.xml` 等项目文件(gate 判定「项目是否接入」用),**不是** linter 配置文件;直接把 requiresConfig 拷进 linter_config_files 会造成误报(SessionStart 会说「linter 配置:pom.xml」)。只有 requiresConfig 本身就是 linter 配置的条目(yaml 的 `.yamllint*`、markdown 的 `.markdownlint*`、scala 的 `.scalafmt.conf`、erlang 的 `elvis.config*`、protobuf 的 `buf.yaml`、vue/svelte/astro 的 eslint 系)可直接复制。 + +## Decisions + +### 保守清单(只填确定存在的标准配置名) + +按语言分组(~30 条): + +- go: `.golangci.yml`, `.golangci.yaml` +- shell: `.shellcheckrc` +- dockerfile: `.hadolint.yaml`, `.hadolint.yml` +- yaml: `.yamllint`, `.yamllint.yaml`, `.yamllint.yml`(requiresConfig 即 linter 配置,直接复制) +- markdown: `.markdownlint-cli2.*` / `.markdownlint.*` 九件套(同上复制) +- vue / svelte / astro: eslint 系(requiresConfig 直接复制) +- ruby: `.rubocop.yml`, `.rubocop.yaml` +- php: `phpstan.neon`, `phpstan.neon.dist`, `psalm.xml` +- swift: `.swiftlint.yml`, `.swiftlint.yaml` +- kotlin: `detekt.yml`, `detekt.yaml` +- dart: `analysis_options.yaml` +- elixir: `.credo.exs` +- c / cpp / objc / cuda: `.clang-tidy`, `.clang-format` +- css: `.stylelintrc`, `.stylelintrc.json`, `.stylelintrc.yml`, `stylelint.config.js` +- html: `.htmlhintrc` +- terraform: `.tflint.hcl`, `tflint.hcl` +- sql: `.sqlfluff` +- haskell: `.hlint.yaml`, `.hlint.yml` +- ocaml: `.ocamlformat` +- perl: `.perlcriticrc` +- clojure: `.clj-kondo/config.edn` +- powershell: `PSScriptAnalyzerSettings.psd1` +- crystal: `.ameba.yml` +- lua: `.luacheckrc` +- r: `.lintr` +- ansible: `.ansible-lint` +- solidity: `.solhint.json`, `.solhintrc` +- scala: `.scalafmt.conf`(requiresConfig 复制) +- erlang: `elvis.config`, `elvis.config.js`(requiresConfig 复制) +- protobuf: `buf.yaml`, `buf.gen.yaml`, `buf.work.yaml`(requiresConfig 复制) + +**明确不填**(低置信或无标准配置名):graphql、toml、zig、nim、julia、pascal、liquid、luau、nix、groovy、cfml、fsharp、vbnet、csharp、elm、objc→已填 clang、cfml 等——留空数组是诚实状态。 + +### 守护测试 + +`test_linter_config_files_coverage`:stable/beta 中非空 `linter_config_files` 数量 ≥ 25(当前应 ~34)。防止未来语言注册表重构静默清空填充。 + +### 不动 requiresConfig 语义 + +两条字段语义分离是 M8 的设计决定,本 change 遵守:requiresConfig 服务 gate 判定,linter_config_files 服务 SessionStart 盘点。 + +## Risks / Trade-offs + +- 个别配置名猜错 → 该文件不存在 → 盘点不报 → 无副作用。 +- 个别语言存在更流行的配置名(如 graphql 的 `.graphqlrc`)→ 未填 → 盘点少报一项 → 未来按需补,本 change 不冒进。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-fill-linter-config-files/proposal.md b/openspec/changes/archive/2026-09-22-fill-linter-config-files/proposal.md new file mode 100644 index 0000000..d7b236f --- /dev/null +++ b/openspec/changes/archive/2026-09-22-fill-linter-config-files/proposal.md @@ -0,0 +1,23 @@ +## Why + +M8(v0.6.5)把 `hooks/env_check.py` 的 linter 配置盘点改为从 `scripts/languages.json` 的 `linter_config_files` 字段读取,但当时只迁移了 java/rust/typescript/python 四条历史记录——54 条 stable/beta 语言中仍有约 30 条填着默认空数组,SessionStart 钩子对它们的项目依旧报告「未配置任何 linter」,即使项目里躺着 `.golangci.yml`、`.rubocop.yml`、`.shellcheckrc` 等真实配置。这就是评审遗留的「detect_linter_config 推全局」。 + +## What Changes + +- `scripts/languages.json`:为约 30 条 stable/beta 语言填充 `linter_config_files`(**保守清单**:只填确定存在的业界标准配置文件名,如 `.golangci.yml` / `.shellcheckrc` / `.rubocop.yml` / `.swiftlint.yml` / `analysis_options.yaml` / `.credo.exs` / `.clang-tidy` / `.stylelintrc*` / `.shellcheckrc` 等)。填错无副作用——`detect_linter_config` 只报告**实际存在**的文件,永不误报;填漏只影响盘点广度,不影响正确性。 +- `tests/test_validate_languages_json.py` 新增 1 条守护:stable/beta 语言中非空 `linter_config_files` 的数量 ≥ 25(防止未来重构把填充结果清空)。 +- 不改 `hooks/env_check.py`(M8 已 registry 驱动);不改 `scripts/languages.json` 其他字段;不改 `docs/LANGUAGES.md` 生成逻辑(生成器不读该字段,已实测)。 + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `registry-driven-config`: 新增 Requirement——已知配置文件名的 stable/beta 语言 SHALL 填充该字段,SessionStart 盘点覆盖面由数据而非代码决定。 + +## Impact + +约 30 条语言 JSON 条目各加 1-4 个字符串;1 条测试;spec delta 1 条 Requirement + 2 Scenario。零运行时行为变化(除 SessionStart 报告更全)。 \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-fill-linter-config-files/specs/registry-driven-config/spec.md b/openspec/changes/archive/2026-09-22-fill-linter-config-files/specs/registry-driven-config/spec.md new file mode 100644 index 0000000..58b94b7 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-fill-linter-config-files/specs/registry-driven-config/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Known linter config filenames SHALL be populated for stable/beta languages + +For each stable/beta language whose linter has well-known project-level configuration filenames, `scripts/languages.json` SHALL populate that entry's `linter_config_files` with those filenames. The population SHALL be conservative: only filenames that genuinely exist as that linter's configuration. Entries whose linter has no standard config filename (or where the filename is uncertain) MAY keep the empty list. `hooks/env_check.py` SHALL continue to report only files that actually exist, so an over-guessed filename never produces a false report. + +#### Scenario: A Go project carries .golangci.yml + +- **WHEN** the SessionStart hook runs in a repository whose root contains `.golangci.yml` +- **THEN** `detect_linter_config` reports `{"go": [".golangci.yml"]}` because `go.linter_config_files` includes it + +#### Scenario: A guessed filename does not exist in the project + +- **WHEN** a language lists a config filename that the current project does not contain +- **THEN** the hook reports nothing for that language/file pair — existence filtering prevents false positives + +### Requirement: Linter config coverage SHALL be guarded against regression + +`tests/test_validate_languages_json.py` SHALL assert that at least 25 stable/beta languages have a non-empty `linter_config_files` list, so a registry refactor cannot silently wipe the populated coverage. + +#### Scenario: A refactor drops the populated fields + +- **WHEN** a future edit removes `linter_config_files` from most stable/beta entries +- **THEN** the coverage test fails with the current populated count \ No newline at end of file diff --git a/openspec/changes/archive/2026-09-22-fill-linter-config-files/tasks.md b/openspec/changes/archive/2026-09-22-fill-linter-config-files/tasks.md new file mode 100644 index 0000000..bdb0c45 --- /dev/null +++ b/openspec/changes/archive/2026-09-22-fill-linter-config-files/tasks.md @@ -0,0 +1,19 @@ +## 1. Data fill + +- [x] 1.1 Apply the conservative per-language map from design.md to `scripts/languages.json` (~30 stable/beta entries). +- [x] 1.2 `python3 scripts/validate_languages_json.py` — exit 0 (type rule covers the new values). + +## 2. Coverage guard test + +- [x] 2.1 Add `test_linter_config_files_coverage` (≥25 stable/beta with non-empty list) to `tests/test_validate_languages_json.py`. + +## 3. Validation + +- [x] 3.1 `python3 -m unittest discover -s tests -p 'test_*.py' -v` — all pass. +- [x] 3.2 `python3 tests/run_all.py` — 141 / 0 / 0. +- [x] 3.3 `git diff --check` — clean; `openspec validate --all --strict` — pass. + +## 4. Release + +- [x] 4.1 bump patch → 0.6.8; in-repo marketplace pins → v0.6.8; feat PR → merge; release PR → merge; tag v0.6.8; market repo catalog + README row → 0.6.8. +- [x] 4.2 `openspec archive fill-linter-config-files`. \ No newline at end of file diff --git a/openspec/specs/registry-driven-config/spec.md b/openspec/specs/registry-driven-config/spec.md index 3bdb213..a13c385 100644 --- a/openspec/specs/registry-driven-config/spec.md +++ b/openspec/specs/registry-driven-config/spec.md @@ -26,3 +26,26 @@ Every language entry in `scripts/languages.json` SHALL have a `linter_config_fil - **WHEN** the project's root contains `ruff.toml` but no other linter configs - **THEN** `env_check.detect_linter_config` returns `{"python": ["ruff.toml"]}` (or equivalent from the registry) and excludes all other languages +### Requirement: Known linter config filenames SHALL be populated for stable/beta languages + +For each stable/beta language whose linter has well-known project-level configuration filenames, `scripts/languages.json` SHALL populate that entry's `linter_config_files` with those filenames. The population SHALL be conservative: only filenames that genuinely exist as that linter's configuration. Entries whose linter has no standard config filename (or where the filename is uncertain) MAY keep the empty list. `hooks/env_check.py` SHALL continue to report only files that actually exist, so an over-guessed filename never produces a false report. + +#### Scenario: A Go project carries .golangci.yml + +- **WHEN** the SessionStart hook runs in a repository whose root contains `.golangci.yml` +- **THEN** `detect_linter_config` reports `{"go": [".golangci.yml"]}` because `go.linter_config_files` includes it + +#### Scenario: A guessed filename does not exist in the project + +- **WHEN** a language lists a config filename that the current project does not contain +- **THEN** the hook reports nothing for that language/file pair — existence filtering prevents false positives + +### Requirement: Linter config coverage SHALL be guarded against regression + +`tests/test_validate_languages_json.py` SHALL assert that at least 25 stable/beta languages have a non-empty `linter_config_files` list, so a registry refactor cannot silently wipe the populated coverage. + +#### Scenario: A refactor drops the populated fields + +- **WHEN** a future edit removes `linter_config_files` from most stable/beta entries +- **THEN** the coverage test fails with the current populated count + diff --git a/scripts/languages.json b/scripts/languages.json index 327d451..a1c96c8 100644 --- a/scripts/languages.json +++ b/scripts/languages.json @@ -187,6 +187,10 @@ "install_hint": "内置于 Go 工具链;聚合 lint 可装 golangci-lint", "requiresConfig": [ "go.mod" + ], + "linter_config_files": [ + ".golangci.yml", + ".golangci.yaml" ] }, { @@ -237,6 +241,10 @@ "build.gradle.kts", "settings.gradle", "pom.xml" + ], + "linter_config_files": [ + "detekt.yml", + "detekt.yaml" ] }, { @@ -257,7 +265,11 @@ "swiftlint", "--fix" ], - "install_hint": "brew install swiftlint" + "install_hint": "brew install swiftlint", + "linter_config_files": [ + ".swiftlint.yml", + ".swiftlint.yaml" + ] }, { "id": "php", @@ -284,6 +296,11 @@ "bash", "-c", "find . -name '*.php' -type f -not -path '*/vendor/*' -print0 | xargs -0 -n1 php -l" + ], + "linter_config_files": [ + "phpstan.neon", + "phpstan.neon.dist", + "psalm.xml" ] }, { @@ -304,7 +321,11 @@ "rubocop", "-A" ], - "install_hint": "gem install rubocop" + "install_hint": "gem install rubocop", + "linter_config_files": [ + ".rubocop.yml", + ".rubocop.yaml" + ] }, { "id": "scala", @@ -329,6 +350,9 @@ "install_hint": "coursier install scalafmt", "requiresConfig": [ ".scalafmt.conf" + ], + "linter_config_files": [ + ".scalafmt.conf" ] }, { @@ -356,6 +380,9 @@ "bash", "-c", "find . \\( -name '*.sh' -o -name '*.bash' -o -name '*.zsh' \\) -type f -not -path '*/node_modules/*' -print0 | xargs -0 shellcheck --severity=warning" + ], + "linter_config_files": [ + ".shellcheckrc" ] }, { @@ -380,6 +407,10 @@ "bash", "-c", "find . \\( -name 'Dockerfile' -o -name 'Dockerfile.*' -o -name 'Containerfile' \\) -type f -not -path '*/node_modules/*' -print0 | xargs -0 -r hadolint" + ], + "linter_config_files": [ + ".hadolint.yaml", + ".hadolint.yml" ] }, { @@ -405,6 +436,11 @@ ".yamllint", ".yamllint.yaml", ".yamllint.yml" + ], + "linter_config_files": [ + ".yamllint", + ".yamllint.yaml", + ".yamllint.yml" ] }, { @@ -431,6 +467,9 @@ "install_hint": "项目需配置 credo 依赖", "requiresConfig": [ "mix.exs" + ], + "linter_config_files": [ + ".credo.exs" ] }, { @@ -463,6 +502,12 @@ "--no-install", "stylelint", "--version" + ], + "linter_config_files": [ + ".stylelintrc", + ".stylelintrc.json", + ".stylelintrc.yml", + "stylelint.config.js" ] }, { @@ -490,6 +535,10 @@ "bash", "-c", "find . -name '*.c' -o -name '*.h' | xargs -r clang-tidy --quiet" + ], + "linter_config_files": [ + ".clang-tidy", + ".clang-format" ] }, { @@ -518,6 +567,10 @@ "bash", "-c", "find . \\( -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' \\) -type f -print0 | xargs -0 -r clang-tidy --quiet" + ], + "linter_config_files": [ + ".clang-tidy", + ".clang-format" ] }, { @@ -545,6 +598,10 @@ "bash", "-c", "find . -name '*.m' -o -name '*.mm' | xargs -r clang-tidy --quiet" + ], + "linter_config_files": [ + ".clang-tidy", + ".clang-format" ] }, { @@ -570,6 +627,9 @@ "install_hint": "Dart SDK 内置", "requiresConfig": [ "pubspec.yaml" + ], + "linter_config_files": [ + "analysis_options.yaml" ] }, { @@ -615,6 +675,18 @@ ".pre-commit-config.yaml", ".eslintrc", ".eslintrc.cjs" + ], + "linter_config_files": [ + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", + "eslint.config.ts", + ".eslintrc.json", + ".eslintrc.js", + ".eslintrc.yml", + ".pre-commit-config.yaml", + ".eslintrc", + ".eslintrc.cjs" ] }, { @@ -654,6 +726,16 @@ ".eslintrc.js", ".eslintrc.yml", ".pre-commit-config.yaml" + ], + "linter_config_files": [ + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", + "eslint.config.ts", + ".eslintrc.json", + ".eslintrc.js", + ".eslintrc.yml", + ".pre-commit-config.yaml" ] }, { @@ -693,6 +775,16 @@ ".eslintrc.js", ".eslintrc.yml", ".pre-commit-config.yaml" + ], + "linter_config_files": [ + "eslint.config.js", + "eslint.config.mjs", + "eslint.config.cjs", + "eslint.config.ts", + ".eslintrc.json", + ".eslintrc.js", + ".eslintrc.yml", + ".pre-commit-config.yaml" ] }, { @@ -713,7 +805,11 @@ "--plugin=prettier-plugin-solidity", "--write" ], - "install_hint": "npm install -g solhint prettier prettier-plugin-solidity" + "install_hint": "npm install -g solhint prettier prettier-plugin-solidity", + "linter_config_files": [ + ".solhint.json", + ".solhintrc" + ] }, { "id": "terraform", @@ -733,7 +829,11 @@ "terraform", "fmt" ], - "install_hint": "brew install tflint" + "install_hint": "brew install tflint", + "linter_config_files": [ + ".tflint.hcl", + "tflint.hcl" + ] }, { "id": "nix", @@ -792,6 +892,9 @@ "bash", "-c", "find . \\( -name '*.html' -o -name '*.htm' \\) -type f -not -path '*/node_modules/*' -exec grep -L '<%' {} + | xargs -0 -r npx --no-install htmlhint" + ], + "linter_config_files": [ + ".htmlhintrc" ] }, { @@ -817,6 +920,9 @@ "bash", "-c", "find . -name '*.sql' -type f -not -path '*/node_modules/*' -print0 | xargs -0 -r sqlfluff lint" + ], + "linter_config_files": [ + ".sqlfluff" ] }, { @@ -875,6 +981,11 @@ "buf.yaml", "buf.gen.yaml", "buf.work.yaml" + ], + "linter_config_files": [ + "buf.yaml", + "buf.gen.yaml", + "buf.work.yaml" ] }, { @@ -919,6 +1030,17 @@ ".markdownlint.yaml", ".markdownlint.yml", ".markdownlint.ini" + ], + "linter_config_files": [ + ".markdownlint-cli2.jsonc", + ".markdownlint-cli2.json", + ".markdownlint-cli2.yaml", + ".markdownlint-cli2.yml", + ".markdownlint.jsonc", + ".markdownlint.json", + ".markdownlint.yaml", + ".markdownlint.yml", + ".markdownlint.ini" ] }, { @@ -963,7 +1085,11 @@ "-i", "." ], - "install_hint": "brew install hlint fourmolu" + "install_hint": "brew install hlint fourmolu", + "linter_config_files": [ + ".hlint.yaml", + ".hlint.yml" + ] }, { "id": "ocaml", @@ -985,7 +1111,10 @@ "-i", "." ], - "install_hint": "opam install ocamlformat" + "install_hint": "opam install ocamlformat", + "linter_config_files": [ + ".ocamlformat" + ] }, { "id": "fsharp", @@ -1033,6 +1162,9 @@ "gate": [ "perlcritic", "." + ], + "linter_config_files": [ + ".perlcriticrc" ] }, { @@ -1087,7 +1219,10 @@ "format": [ "cljstyle" ], - "install_hint": "brew install clj-kondo cljstyle" + "install_hint": "brew install clj-kondo cljstyle", + "linter_config_files": [ + ".clj-kondo/config.edn" + ] }, { "id": "powershell", @@ -1111,7 +1246,10 @@ "-Command", "Invoke-Formatter" ], - "install_hint": "Install-Module PSScriptAnalyzer" + "install_hint": "Install-Module PSScriptAnalyzer", + "linter_config_files": [ + "PSScriptAnalyzerSettings.psd1" + ] }, { "id": "zig", @@ -1185,6 +1323,9 @@ "install_hint": "brew install crystal-ameba", "requiresConfig": [ "shard.yml" + ], + "linter_config_files": [ + ".ameba.yml" ] }, { @@ -1246,7 +1387,10 @@ "stylua", "." ], - "install_hint": "luarocks install luacheck; cargo install stylua" + "install_hint": "luarocks install luacheck; cargo install stylua", + "linter_config_files": [ + ".luacheckrc" + ] }, { "id": "luau", @@ -1313,7 +1457,10 @@ "::", "style_dir" ], - "install_hint": "Rscript -e 'install.packages(c(\"lintr\",\"styler\"))'" + "install_hint": "Rscript -e 'install.packages(c(\"lintr\",\"styler\"))'", + "linter_config_files": [ + ".lintr" + ] }, { "id": "cfml", @@ -1401,6 +1548,10 @@ "requiresConfig": [ "elvis.config", "elvis.config.js" + ], + "linter_config_files": [ + "elvis.config", + "elvis.config.js" ] }, { @@ -1470,6 +1621,10 @@ "bash", "-c", "find . -name '*.cu' | xargs -r clang-tidy --quiet" + ], + "linter_config_files": [ + ".clang-tidy", + ".clang-format" ] }, { @@ -1489,7 +1644,10 @@ "ansible-lint", "--fix" ], - "install_hint": "playbook 检查由 ansible-lint 承接(yaml 通道);语义级检查待集成" + "install_hint": "playbook 检查由 ansible-lint 承接(yaml 通道);语义级检查待集成", + "linter_config_files": [ + ".ansible-lint" + ] } ] } diff --git a/tests/test_validate_languages_json.py b/tests/test_validate_languages_json.py index 4cb1848..aeb3b1d 100644 --- a/tests/test_validate_languages_json.py +++ b/tests/test_validate_languages_json.py @@ -111,3 +111,11 @@ def test_linter_config_files_must_be_list_of_str(self) -> None: def test_linter_config_files_can_be_empty(self) -> None: reg = _mutate(_baseline(), "rust", linter_config_files=[]) self.assertEqual([], check(reg)) + +class LinterConfigCoverageTest(unittest.TestCase): + def test_linter_config_files_coverage(self) -> None: + """stable/beta 中至少 25 条填了 linter_config_files(防重构清空)。""" + reg = _baseline() + n = sum(1 for l in reg["languages"] + if l.get("status") in ("stable", "beta") and l.get("linter_config_files")) + self.assertGreaterEqual(n, 25, f"only {n} stable/beta languages have linter_config_files")