@chengzhao-dev/dsh-cpp-workflow 0.1.1 → 0.3.0

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 (82) hide show
  1. package/README.en.md +18 -15
  2. package/README.md +18 -15
  3. package/lib/index.js +7 -8
  4. package/lib/workspace.js +14 -14
  5. package/locale/en.json +1 -1
  6. package/locale/zh.json +1 -1
  7. package/package.json +7 -5
  8. package/skills/building-cpp/SKILL.md +8 -6
  9. package/skills/building-cpp/assets/config/.clang-format +5 -38
  10. package/skills/building-cpp/assets/config/.clang-tidy +8 -39
  11. package/skills/building-cpp/assets/config/.clangd +2 -5
  12. package/skills/building-cpp/assets/config/.vscode/extensions.json +2 -1
  13. package/skills/building-cpp/assets/config/.vscode/settings.json +16 -1
  14. package/skills/building-cpp/assets/templates/projects/common/running-build.sh +29 -18
  15. package/skills/building-cpp/assets/templates/projects/multi/CMakeLists.txt +9 -5
  16. package/skills/building-cpp/assets/templates/projects/multi/include/greeting.h +5 -3
  17. package/skills/building-cpp/assets/templates/projects/multi/src/greeting.cc +18 -0
  18. package/skills/building-cpp/assets/templates/projects/multi/src/main.cc +8 -0
  19. package/skills/building-cpp/assets/templates/projects/shared-library/CMakeLists.txt +8 -3
  20. package/skills/building-cpp/assets/templates/projects/shared-library/greeting/CMakeLists.txt +15 -5
  21. package/skills/building-cpp/assets/templates/projects/shared-library/greeting/include/greeting.h +5 -3
  22. package/skills/building-cpp/assets/templates/projects/shared-library/greeting/src/greeting.cc +18 -0
  23. package/skills/building-cpp/assets/templates/projects/shared-library/src/main.cc +8 -0
  24. package/skills/building-cpp/assets/templates/projects/single/CMakeLists.txt +7 -3
  25. package/skills/building-cpp/assets/templates/projects/single/src/main.cc +15 -0
  26. package/skills/building-cpp/assets/templates/projects/static-library/CMakeLists.txt +8 -3
  27. package/skills/building-cpp/assets/templates/projects/static-library/greeting/CMakeLists.txt +15 -5
  28. package/skills/building-cpp/assets/templates/projects/static-library/greeting/include/greeting.h +5 -3
  29. package/skills/building-cpp/assets/templates/projects/static-library/greeting/src/greeting.cc +18 -0
  30. package/skills/building-cpp/assets/templates/projects/static-library/src/main.cc +8 -0
  31. package/skills/building-cpp/examples/cmake.md +17 -17
  32. package/skills/building-cpp/examples/environment.md +4 -3
  33. package/skills/building-cpp/examples/tooling.md +31 -16
  34. package/skills/building-cpp/references/build-script.md +2 -2
  35. package/skills/building-cpp/references/cmake.md +35 -17
  36. package/skills/building-cpp/references/encoding.md +49 -0
  37. package/skills/building-cpp/references/environment.md +17 -13
  38. package/skills/building-cpp/references/linking.md +6 -6
  39. package/skills/building-cpp/references/standards/README.md +165 -0
  40. package/skills/building-cpp/references/standards/c++11/README.md +50 -0
  41. package/skills/building-cpp/references/standards/c++11/cmake-snippet.cmake +7 -0
  42. package/skills/building-cpp/references/standards/c++14/README.md +48 -0
  43. package/skills/building-cpp/references/standards/c++14/cmake-snippet.cmake +7 -0
  44. package/skills/building-cpp/references/standards/c++17/README.md +49 -0
  45. package/skills/building-cpp/references/standards/c++17/cmake-snippet.cmake +7 -0
  46. package/skills/building-cpp/references/standards/c++20/README.md +49 -0
  47. package/skills/building-cpp/references/standards/c++20/cmake-snippet.cmake +7 -0
  48. package/skills/building-cpp/references/standards/c++23/README.md +50 -0
  49. package/skills/building-cpp/references/standards/c++23/cmake-snippet.cmake +7 -0
  50. package/skills/building-cpp/references/static-analysis.md +27 -15
  51. package/skills/building-cpp/scripts/checking-encoding.mjs +241 -0
  52. package/skills/building-cpp/scripts/checking-environment.mjs +21 -21
  53. package/skills/building-cpp/steps/01-build.md +10 -8
  54. package/skills/building-cpp/steps/02-tooling.md +9 -7
  55. package/skills/building-cpp/steps/03-check.md +9 -7
  56. package/skills/developing-cpp/SKILL.md +8 -6
  57. package/skills/developing-cpp/examples/comments.md +39 -8
  58. package/skills/developing-cpp/examples/exceptions.md +64 -45
  59. package/skills/developing-cpp/examples/naming.md +2 -2
  60. package/skills/developing-cpp/examples/structure.md +12 -9
  61. package/skills/developing-cpp/references/comments.md +28 -8
  62. package/skills/developing-cpp/references/declaration-order.md +7 -4
  63. package/skills/developing-cpp/references/exceptions.md +83 -46
  64. package/skills/developing-cpp/references/glossary.md +6 -6
  65. package/skills/developing-cpp/references/google-cpp-style.md +27 -37
  66. package/skills/developing-cpp/references/language.md +4 -4
  67. package/skills/developing-cpp/references/layout.md +14 -14
  68. package/skills/developing-cpp/references/naming.md +15 -6
  69. package/skills/developing-cpp/references/project-design.md +12 -3
  70. package/skills/developing-cpp/references/standard-library.md +1 -1
  71. package/skills/developing-cpp/steps/02-boundary.md +8 -7
  72. package/skills/developing-cpp/steps/03-declaration.md +9 -8
  73. package/skills/developing-cpp/steps/04-implementation.md +11 -7
  74. package/templates/cpp-workflow-project/SKILL.md +1 -1
  75. package/templates/cpp-workflow-project/environment.json +2 -1
  76. package/skills/building-cpp/assets/templates/projects/multi/src/greeting.cpp +0 -13
  77. package/skills/building-cpp/assets/templates/projects/multi/src/main.cpp +0 -8
  78. package/skills/building-cpp/assets/templates/projects/shared-library/greeting/src/greeting.cpp +0 -13
  79. package/skills/building-cpp/assets/templates/projects/shared-library/src/main.cpp +0 -8
  80. package/skills/building-cpp/assets/templates/projects/single/src/main.cpp +0 -9
  81. package/skills/building-cpp/assets/templates/projects/static-library/greeting/src/greeting.cpp +0 -13
  82. package/skills/building-cpp/assets/templates/projects/static-library/src/main.cpp +0 -8
@@ -0,0 +1,49 @@
1
+ # C++20 标准档案
2
+
3
+ [返回标准索引](../README.md)
4
+
5
+ | 项 | 值 |
6
+ | --- | --- |
7
+ | `standard` | `c++20` |
8
+ | CMake meta-feature | `cxx_std_20` |
9
+ | `CXX_STANDARD` | `20` |
10
+ | `__cplusplus` | `202002L` |
11
+ | clang-format 21/22 | `Standard: c++20` |
12
+ | 模板实用下限 | GCC 13 / Clang 16(`std::format`) |
13
+
14
+ ## 最重要的三件事
15
+
16
+ 1. `std::format` 是这一档的库门槛:libstdc++ 13+,libc++ 14+(特性宏到 19 才稳定),不是有 `-std=c++20` 就自动可用。
17
+ 2. concepts/ranges/`std::span`/`std::bit_cast` 可以用;`std::print`、`std::expected`、`if consteval` 仍不能假设。
18
+ 3. 模板在 `__cplusplus >= 202002L` 时切到 `std::format`;降回 C++17 时必须把它换回 `std::ostringstream`。
19
+
20
+ ## 本档能用
21
+
22
+ - C++17 的全部内容。
23
+ - concepts、requires、ranges、`std::span`、`std::bit_cast`、`std::source_location`。
24
+ - `std::format`、协程、`std::jthread`、三路比较。
25
+ - 模块语法(工具链支持不完整,模板不使用)。
26
+
27
+ ## 本档不能假设
28
+
29
+ - 没有 `std::print`、`std::expected`、`std::stacktrace`、`if consteval`、显式对象参数。
30
+ - `std::format` 不只看语言核心:libstdc++ 13+;libc++ 14+ 实现完成,但特性宏到 19 才稳定。
31
+ - 模块、协程的构建支持因编译器/构建系统而异;模板不依赖它们。
32
+
33
+ ## CMake
34
+
35
+ 把 [cmake-snippet.cmake](cmake-snippet.cmake) 的 target 名换成实际 target。模板从这一档开始会在 `__cplusplus >= 202002L` 时使用 `std::format`,此前用字符串拼接。
36
+
37
+ ## 工具
38
+
39
+ - `.clang-format`:`Standard: c++20`。
40
+ - `.clangd`:不写 `-std`,从 `compile_commands.json` 读。
41
+ - `.clang-tidy`:Google 基线;不要为了 concepts 打开一整族非 Google 检查。
42
+
43
+ ## 降档/升级
44
+
45
+ 从 C++23 降到这里时,把 `std::print` 改成 `std::format` + `std::cout`,把 `std::expected` 改成 `std::optional` 或项目结果类型,去掉 `if consteval`。升级到 C++23 后仍要逐项确认库实现,不要因为 `-std=c++23` 通过就假定 `print`/`stacktrace` 可用。
46
+
47
+ ## 验证
48
+
49
+ configure、build、run 必测;`compile_commands.json` 的最后一个 `-std=` 必须是 `-std=c++20`。模板在 C++20 下应走 `std::format` 分支。
@@ -0,0 +1,7 @@
1
+ # c++20:把 target 名换成实际 target;与 environment.json 的 standard 一致。
2
+ target_compile_features(app PRIVATE cxx_std_20)
3
+ set_target_properties(app PROPERTIES
4
+ CXX_STANDARD 20
5
+ CXX_STANDARD_REQUIRED ON
6
+ CXX_EXTENSIONS OFF
7
+ )
@@ -0,0 +1,50 @@
1
+ # C++23 标准档案
2
+
3
+ [返回标准索引](../README.md)
4
+
5
+ | 项 | 值 |
6
+ | --- | --- |
7
+ | `standard` | `c++23` |
8
+ | CMake meta-feature | `cxx_std_23` |
9
+ | `CXX_STANDARD` | `23` |
10
+ | `__cplusplus` | `202302L` |
11
+ | clang-format 21/22 | `Standard: Latest` |
12
+ | 模板实用下限 | GCC 13 / Clang 17(模板只用 `std::format`) |
13
+
14
+ ## 最重要的三件事
15
+
16
+ 1. C++23 在 GCC/Clang 上仍是 Partial;`std::print` 要 libstdc++ 14+/libc++ 19+,`std::stacktrace` 还要链接库。
17
+ 2. clang-format 21/22 不认识 `c++23`,配置写 `Standard: Latest`;`.clangd` 不写 `-std`,否则会把编译数据库降回 C++20。
18
+ 3. 模板默认 23,但模板只强依赖 `std::format`;不要因为默认 23 就在公共头里用 `std::expected`/`std::print`,先确认所有 target 和库门槛。
19
+
20
+ ## 本档能用
21
+
22
+ - C++20 的全部内容。
23
+ - `std::print` / `std::println`、`std::expected`、`std::stacktrace`。
24
+ - `if consteval`、显式对象参数(deducing this)、`std::mdspan`、`std::flat_map`。
25
+ - ranges 的 C++23 增补(工具链实现为准)。
26
+
27
+ ## 本档不能假设
28
+
29
+ - C++23 在 GCC/Clang 上仍是 Partial:具体设施要查编译器状态页,不能只看 `-std=c++23`。
30
+ - `std::print` 的库门槛是 libstdc++ 14+ / libc++ 19+;`std::stacktrace` 还要链接对应库,链接失败不是语法错误。
31
+ - `std::expected` 的 monadic 接口、`std::flat_map` 等设施在旧标准库上可能缺失。
32
+ - 模板只用 `std::format`,不会因为默认 C++23 就强依赖 `print`/`stacktrace`。
33
+
34
+ ## CMake
35
+
36
+ 把 [cmake-snippet.cmake](cmake-snippet.cmake) 的 target 名换成实际 target。默认模板的 `cxx_std_23` 和 `CXX_STANDARD 23` 必须与 `environment.json` 的 `standard` 一致。
37
+
38
+ ## 工具
39
+
40
+ - `.clang-format`:`Standard: Latest`(21/22 不接受 `c++23`;`Latest` 按工具支持的最高标准解析)。
41
+ - `.clangd`:不写 `-std`,从 `compile_commands.json` 读;写死 `-std=c++20` 会让 `<print>` 和 `if consteval` 解析失败。
42
+ - `.clang-tidy`:Google 基线;不要因为 C++23 就打开 `cppcoreguidelines-*` 整包。
43
+
44
+ ## 降档/升级
45
+
46
+ 从 C++23 降到 C++20 见 [C++20 档案](../c++20/README.md)。升级到 C++26 前先确认 CMake 3.25+、编译器/标准库支持、clang-format 的 `c++26` 枚举;新增目录流程见 [标准索引](../README.md#新增标准)。
47
+
48
+ ## 验证
49
+
50
+ configure、build、run 必测;`compile_commands.json` 的最后一个 `-std=` 必须是 `-std=c++23`。模板在 C++23 下走 `std::format` 分支;若使用 `print`/`stacktrace`,另做链接测试。
@@ -0,0 +1,7 @@
1
+ # c++23:把 target 名换成实际 target;与 environment.json 的 standard 一致。
2
+ target_compile_features(app PRIVATE cxx_std_23)
3
+ set_target_properties(app PROPERTIES
4
+ CXX_STANDARD 23
5
+ CXX_STANDARD_REQUIRED ON
6
+ CXX_EXTENSIONS OFF
7
+ )
@@ -1,18 +1,18 @@
1
1
  # 静态分析与语言服务
2
2
 
3
- 三件工具各管一段:`clang-format` 排版、`clang-tidy` 诊断、`clangd` 提供补全/跳转/诊断。它们都读同一份编译数据库,但职责不重叠。
3
+ 三件语言工具各管一段:`clang-format` 排版、`clang-tidy` 诊断、`clangd` 提供补全/跳转/诊断。它们都读同一份编译数据库,但职责不重叠。编辑器侧只保留 CMake Tools 作为配置、构建、运行和调试入口;C/C++ 扩展只做 `CMake: Debug` 的调试后端,不参与语言服务。
4
4
 
5
5
  ## clang-format
6
6
 
7
7
  ```bash
8
- clang-format --dry-run --Werror src/*.cpp include/*.h tests/*.cpp
8
+ clang-format --dry-run --Werror src/*.cc include/*.h tests/*.cc
9
9
  ```
10
10
 
11
11
  - 检查用 `--dry-run --Werror`;需要自动修复时单独跑 `clang-format -i`,不要在构建脚本里悄悄改源码。
12
12
  - 工程必须有根 `.clang-format`;缺失时先复制配置,不要回退到内置风格,回退会掩盖配置缺失。
13
13
  - 只扫描项目源码:`src/`、`include/`、`apps/`、`tests/` 等实际存在的目录;不扫描 `build/`、第三方、生成文件。
14
14
  - 配置文件在工程根 `.clang-format`;不要依赖用户主目录或编辑器内置配置。
15
- - `ColumnLimit: 100`、`Standard: c++20`、`BasedOnStyle: Google`;`IncludeCategories` 维持包含顺序。
15
+ - `BasedOnStyle: Google`、`Standard: Latest`;clang-format 21/22 不接受 `c++23`,`Latest` 按工具支持的最高标准解析。列宽 80、2 空格、包含顺序都取 Google 预设,不另写 `ColumnLimit` 或 `IncludeCategories`。
16
16
  - 格式化失败就是失败,不要用 `// clang-format off` 绕过;确需关闭时只包住无法排版的宏或表格,并写一句理由。
17
17
 
18
18
  ## clang-tidy
@@ -21,7 +21,7 @@ clang-format --dry-run --Werror src/*.cpp include/*.h tests/*.cpp
21
21
  clang-tidy -p build/clang \
22
22
  --header-filter="^$(pwd -P)/(include|src|apps|tests)/" \
23
23
  --warnings-as-errors='*' \
24
- src/*.cpp apps/*.cpp tests/*.cpp
24
+ src/*.cc apps/*.cc tests/*.cc
25
25
  ```
26
26
 
27
27
  - `-p` 指向当前编译器的构建目录,读取 `compile_commands.json`。
@@ -30,7 +30,7 @@ clang-tidy -p build/clang \
30
30
  - 系统头默认被排除,不要加 `--system-headers`。
31
31
  - 只扫描项目源文件;不要把 `build/` 或第三方源码传给 clang-tidy。
32
32
  - `--warnings-as-errors='*'` 把警告当失败;修代码,不要新增 `NOLINT`。确需抑制时写 `// NOLINT(检查名): 理由`,理由必须具体。
33
- - 配置在工程根 `.clang-tidy`;关闭项逐条写理由,不整族关闭。`cppcoreguidelines-pro-bounds-constant-array-index` 不能整族关闭;优先把下标换成 `at()` 或带边界检查的辅助函数,并补负向越界测试。
33
+ - 配置在工程根 `.clang-tidy`;默认集合是 `google-*` 加 `readability-identifier-naming`。关闭某条时逐条写理由,不整族关闭;命名规则只配项目确实统一的项,不设 `FunctionCase`。
34
34
  - 没有编译数据库时先回到 `building-cpp` 的定构建生成;不要用 `--` 手工拼编译参数绕过。
35
35
  - `clang-tidy` 的诊断可能指向头文件;修头文件里的声明或实现,不要只改调用点。
36
36
 
@@ -41,17 +41,13 @@ clang-tidy -p build/clang \
41
41
  ```yaml
42
42
  CompileFlags:
43
43
  CompilationDatabase: build/clang
44
- Add:
45
- - -std=c++20
46
- - -Wall
47
- - -Wextra
48
44
  Diagnostics:
49
45
  Suppress:
50
46
  - pp_file_not_found
51
47
  ```
52
48
 
53
49
  - `CompilationDatabase` 指向当前编译器目录;GCC 模式改 `build/gcc`。
54
- - `Add` 只补编译数据库里没有的公共选项;不要重复 `-I` 项目 include(数据库里已有)。
50
+ - 不要在 `.clangd` 写 `-std`:它会覆盖 `compile_commands.json`,把 C++23 工程降成旧标准。标准只由构建声明和编译数据库决定。
55
51
  - 静态检查规则只在 `.clang-tidy` 里维护;clangd 读取同一份配置,不要在这里复制检查列表。
56
52
  - `Suppress` 只放确实无法避免的解析噪声(如 `pp_file_not_found`),并写理由。
57
53
 
@@ -82,7 +78,11 @@ Diagnostics:
82
78
 
83
79
  ```json
84
80
  {
85
- "recommendations": ["llvm-vs-code-extensions.vscode-clangd", "ms-vscode.cmake-tools"]
81
+ "recommendations": [
82
+ "llvm-vs-code-extensions.vscode-clangd",
83
+ "ms-vscode.cmake-tools",
84
+ "ms-vscode.cpptools"
85
+ ]
86
86
  }
87
87
  ```
88
88
 
@@ -96,13 +96,25 @@ Diagnostics:
96
96
  "--background-index"
97
97
  ],
98
98
  "cmake.buildDirectory": "${workspaceFolder}/build/clang",
99
- "cmake.generator": "Ninja"
99
+ "cmake.generator": "Ninja",
100
+ "cmake.buildBeforeRun": true,
101
+ "cmake.exportCompileCommandsFile": true,
102
+ "cmake.debugConfig": {
103
+ "type": "cppdbg",
104
+ "request": "launch",
105
+ "MIMode": "lldb",
106
+ "miDebuggerPath": "/usr/bin/lldb",
107
+ "stopAtEntry": false
108
+ },
109
+ "C_Cpp.intelliSenseEngine": "disabled"
100
110
  }
101
111
  ```
102
112
 
113
+ - 配置、构建、运行和调试只从 CMake Tools 进入;不要同时维护 `tasks.json` 或 `launch.json`,否则会出现第二套构建或启动路径。
114
+ - C/C++ 扩展只作为 CMake 快速调试的 `cppdbg` 后端;IntelliSense 关闭,补全、跳转、诊断和格式化只由 clangd 提供。
103
115
  - 不推荐与 clangd 冲突的 C++ 智能提示插件;只保留一个语言服务。
104
116
  - 不把 `compile_commands.json` 提交进仓库;它是构建产物。
105
- - 切换编译器时同时改 `.clangd`、`settings.json` 和构建目录;三处不一致会让诊断指向旧代码。
117
+ - 切换编译器时同时改 `.clangd`、`settings.json` 和构建目录;三处不一致会让诊断指向旧代码。GCC 模式把 `MIMode` 改为 `gdb`、`miDebuggerPath` 改为 `/usr/bin/gdb`。
106
118
 
107
119
  ## 门禁分级
108
120
 
@@ -110,9 +122,9 @@ Diagnostics:
110
122
 
111
123
  | 级别 | 典型检查 | 处理 |
112
124
  | --- | --- | --- |
113
- | 失败 | `bugprone-*`、`readability-math-missing-parentheses`、`readability-identifier-naming`、项目声明的致命集 | 修代码;不允许用 NOLINT 掩盖 |
125
+ | 失败 | `google-*`、`readability-identifier-naming`、项目声明的致命集 | 修代码;不允许用 NOLINT 掩盖 |
114
126
  | 失败 | 编译诊断、链接诊断、格式检查 | 直接失败 |
115
- | 观察 | 迁移期暂时无法修完的 `modernize-*` | 在项目技能里列清单、写负责人和退出条件;不默认全仓关闭 |
127
+ | 观察 | 迁移期暂时无法修完的个别 `google-*` 子项 | 在项目技能里列清单、写负责人和退出条件;不默认全仓关闭 |
116
128
 
117
129
  - 致命集宁可小、可解释,也不要把所有检查一刀切。
118
130
  - 观察项不是免死金牌:新代码不应新增观察项,已有项只能减少。
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ // Read-only encoding audit for a Git work tree.
3
+ //
4
+ // Default profile "portable" is the cross-platform baseline: UTF-8 without BOM,
5
+ // LF line endings, one final newline. Profile "msvc" allows a UTF-8 BOM and CRLF
6
+ // only for MSVC-compiled C/C++ sources; Markdown, CMake, scripts and configs stay
7
+ // portable. It never rewrites files.
8
+ //
9
+ // Usage:
10
+ // node checking-encoding.mjs [--root DIR] [--profile portable|msvc] [--json] [--quiet]
11
+ //
12
+ // Exit codes: 0 clean, 1 issues found, 2 usage or Git error.
13
+ import { execFileSync } from 'node:child_process'
14
+ import { existsSync, readFileSync, statSync } from 'node:fs'
15
+ import path from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+
18
+ export const EXIT = { ok: 0, issues: 1, usage: 2 }
19
+
20
+ const UTF8_BOM = [0xef, 0xbb, 0xbf]
21
+ const UTF16_LE = [0xff, 0xfe]
22
+ const UTF16_BE = [0xfe, 0xff]
23
+ const UTF32_LE = [0xff, 0xfe, 0x00, 0x00]
24
+ const UTF32_BE = [0x00, 0x00, 0xfe, 0xff]
25
+
26
+ const CRLF_REQUIRED = new Set(['.bat', '.cmd'])
27
+ const MSVC_SOURCE = new Set([
28
+ '.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.hxx', '.inl', '.ixx', '.cppm', '.rc',
29
+ ])
30
+ const ALWAYS_LF = new Set([
31
+ '.md', '.qmd', '.txt', '.rst', '.yml', '.yaml', '.json', '.jsonc', '.json5', '.toml',
32
+ '.ini', '.cfg', '.conf', '.sh', '.bash', '.zsh', '.fish', '.ps1', '.py', '.mjs', '.cjs',
33
+ '.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.sass', '.less', '.html', '.htm', '.xml',
34
+ '.svg', '.cmake', '.lua', '.sql', '.gitignore', '.gitattributes', '.editorconfig',
35
+ ])
36
+ const NO_EXTENSION_LF = new Set(['CMakeLists.txt', 'Makefile', 'Justfile', 'Dockerfile', 'LICENSE'])
37
+
38
+ function usage(message) {
39
+ if (message) process.stderr.write(`错误:${message}\n`)
40
+ process.stderr.write('用法:node checking-encoding.mjs [--root DIR] [--profile portable|msvc] [--json] [--quiet]\n')
41
+ return EXIT.usage
42
+ }
43
+
44
+ function parseArgs(argv) {
45
+ const options = { root: undefined, profile: 'portable', json: false, quiet: false }
46
+ for (let index = 0; index < argv.length; index += 1) {
47
+ const arg = argv[index]
48
+ if (arg === '--root') {
49
+ options.root = argv[index + 1]
50
+ index += 1
51
+ } else if (arg === '--profile') {
52
+ options.profile = argv[index + 1]
53
+ index += 1
54
+ } else if (arg === '--json') {
55
+ options.json = true
56
+ } else if (arg === '--quiet') {
57
+ options.quiet = true
58
+ } else if (arg === '--help' || arg === '-h') {
59
+ options.help = true
60
+ } else {
61
+ throw new Error(`未知参数 ${arg}`)
62
+ }
63
+ }
64
+ if (options.profile !== 'portable' && options.profile !== 'msvc') {
65
+ throw new Error(`--profile 只能是 portable 或 msvc,收到 ${options.profile}`)
66
+ }
67
+ return options
68
+ }
69
+
70
+ function gitRoot(from) {
71
+ try {
72
+ return execFileSync('git', ['-C', from, 'rev-parse', '--show-toplevel'], {
73
+ encoding: 'utf8',
74
+ stdio: ['ignore', 'pipe', 'ignore'],
75
+ }).trim()
76
+ } catch {
77
+ return undefined
78
+ }
79
+ }
80
+
81
+ function listFiles(root) {
82
+ const output = execFileSync('git', ['-C', root, 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
83
+ encoding: 'utf8',
84
+ maxBuffer: 64 * 1024 * 1024,
85
+ })
86
+ return output
87
+ .split('\0')
88
+ .filter(Boolean)
89
+ .map((item) => path.join(root, item))
90
+ .filter((file) => {
91
+ try {
92
+ return statSync(file).isFile()
93
+ } catch {
94
+ return false
95
+ }
96
+ })
97
+ }
98
+
99
+ function startsWith(buffer, bytes) {
100
+ return bytes.every((byte, index) => buffer[index] === byte)
101
+ }
102
+
103
+ function bomOf(buffer) {
104
+ if (startsWith(buffer, UTF32_LE)) return 'UTF-32 LE'
105
+ if (startsWith(buffer, UTF32_BE)) return 'UTF-32 BE'
106
+ if (startsWith(buffer, UTF8_BOM)) return 'UTF-8'
107
+ if (startsWith(buffer, UTF16_LE)) return 'UTF-16 LE'
108
+ if (startsWith(buffer, UTF16_BE)) return 'UTF-16 BE'
109
+ return undefined
110
+ }
111
+
112
+ function isBinary(buffer) {
113
+ const limit = Math.min(buffer.length, 8192)
114
+ for (let index = 0; index < limit; index += 1) {
115
+ if (buffer[index] === 0) return true
116
+ }
117
+ return false
118
+ }
119
+
120
+ function inspect(file, root, profile) {
121
+ const rel = path.relative(root, file).split(path.sep).join('/')
122
+ const extension = path.extname(file).toLowerCase()
123
+ const source = MSVC_SOURCE.has(extension)
124
+ const alwaysLf = ALWAYS_LF.has(extension) || NO_EXTENSION_LF.has(path.basename(file))
125
+ const issues = []
126
+ const add = (kind, message) => issues.push({ path: rel, kind, message })
127
+
128
+ let buffer
129
+ try {
130
+ buffer = readFileSync(file)
131
+ } catch (error) {
132
+ add('read', `无法读取(${error.message})`)
133
+ return { rel, issues }
134
+ }
135
+
136
+ const bom = bomOf(buffer)
137
+ if (bom === 'UTF-8') {
138
+ if (profile !== 'msvc' || !source) {
139
+ add('bom', profile === 'msvc' && !source ? '只有 MSVC 编译的 C/C++ 源码可以带 BOM' : '不应带 BOM')
140
+ }
141
+ } else if (bom) {
142
+ add('bom', `发现 ${bom} BOM;只接受 UTF-8`)
143
+ }
144
+
145
+ if (bom && bom !== 'UTF-8') {
146
+ return { rel, issues }
147
+ }
148
+ if (isBinary(buffer)) {
149
+ return { rel, issues }
150
+ }
151
+
152
+ let text
153
+ try {
154
+ text = new TextDecoder('utf-8', { fatal: true }).decode(buffer)
155
+ } catch {
156
+ add('utf8', '不是合法 UTF-8')
157
+ return { rel, issues }
158
+ }
159
+
160
+ const body = text.startsWith('\uFEFF') ? text.slice(1) : text
161
+ if (body.includes('\uFEFF')) {
162
+ add('bom', '文件中间含 U+FEFF')
163
+ }
164
+
165
+ if (buffer.length > 0 && !body.endsWith('\n')) {
166
+ add('newline', '缺少末尾换行')
167
+ }
168
+
169
+ const crlf = (body.match(/\r\n/g) ?? []).length
170
+ const lf = (body.match(/(?<!\r)\n/g) ?? []).length
171
+ const cr = (body.match(/\r(?!\n)/g) ?? []).length
172
+
173
+ if (CRLF_REQUIRED.has(extension)) {
174
+ if (crlf === 0 || lf > 0 || cr > 0) add('eol', '应为 CRLF')
175
+ } else if (profile === 'msvc' && source && !alwaysLf) {
176
+ if ((crlf > 0 && lf > 0) || cr > 0) add('eol', '不要混用 CRLF 与 LF')
177
+ } else if (crlf > 0 || cr > 0) {
178
+ add('eol', '应为 LF')
179
+ }
180
+
181
+ return { rel, issues }
182
+ }
183
+
184
+ export function checkEncoding(root, profile = 'portable') {
185
+ const files = listFiles(root)
186
+ const results = []
187
+ for (const file of files) {
188
+ results.push(inspect(file, root, profile))
189
+ }
190
+ const issues = results.flatMap((result) => result.issues)
191
+ const counts = { files: results.length, issues: issues.length, bom: 0, eol: 0, utf8: 0, newline: 0, read: 0 }
192
+ for (const issue of issues) counts[issue.kind] = (counts[issue.kind] ?? 0) + 1
193
+ return { root, profile, files: results.length, issues, counts }
194
+ }
195
+
196
+ function main() {
197
+ let options
198
+ try {
199
+ options = parseArgs(process.argv.slice(2))
200
+ } catch (error) {
201
+ return usage(error.message)
202
+ }
203
+ if (options.help) {
204
+ process.stdout.write('检查 Git 工作树的 UTF-8/BOM、行尾与末尾换行;默认 portable。\n')
205
+ return EXIT.ok
206
+ }
207
+
208
+ const base = options.root ? path.resolve(options.root) : process.cwd()
209
+ if (!existsSync(base)) return usage(`根目录不存在:${base}`)
210
+ const root = gitRoot(base)
211
+ if (!root) return usage(`不是 Git 工作树:${base}`)
212
+
213
+ let report
214
+ try {
215
+ report = checkEncoding(root, options.profile)
216
+ } catch (error) {
217
+ process.stderr.write(`错误:${error.message}\n`)
218
+ return EXIT.usage
219
+ }
220
+
221
+ if (options.json) {
222
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
223
+ } else if (!options.quiet || report.issues.length > 0) {
224
+ if (report.issues.length === 0) {
225
+ process.stdout.write(`编码检查:${report.files} 个文件,未发现问题(profile ${report.profile})。\n`)
226
+ } else {
227
+ process.stdout.write(`编码检查:${report.files} 个文件,发现 ${report.issues.length} 个问题(profile ${report.profile})。\n`)
228
+ for (const issue of report.issues) {
229
+ process.stdout.write(` ${issue.path}: [${issue.kind}] ${issue.message}\n`)
230
+ }
231
+ process.stdout.write(
232
+ `汇总:BOM ${report.counts.bom},行尾 ${report.counts.eol},UTF-8 ${report.counts.utf8},末尾换行 ${report.counts.newline}\n`,
233
+ )
234
+ }
235
+ }
236
+ return report.issues.length === 0 ? EXIT.ok : EXIT.issues
237
+ }
238
+
239
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
240
+ process.exitCode = main()
241
+ }
@@ -15,6 +15,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
15
15
  export const HOSTS = ['windows', 'linux', 'macos']
16
16
  export const BUILD_HOSTS = ['native', 'wsl']
17
17
  export const COMPILERS = ['clang', 'gcc']
18
+ export const STANDARDS = ['c++11', 'c++14', 'c++17', 'c++20', 'c++23']
19
+ export const DEFAULT_STANDARD = 'c++23'
18
20
  const PROJECT_SKILL_DIR = path.join('.agents', 'skills', 'cpp-workflow-project')
19
21
  const MAX_ASCENT = 64
20
22
 
@@ -34,8 +36,9 @@ function normalize(value) {
34
36
  }
35
37
 
36
38
  /**
37
- * Validate the three axes and their combination. Pure: no filesystem or PATH.
38
- * @returns {{ok: boolean, issues: string[], notes: string[], axes: {host?: string, build_host?: string, compiler?: string}}}
39
+ * Validate the three environment axes, the language standard, and their combination.
40
+ * Pure: no filesystem or PATH.
41
+ * @returns {{ok: boolean, issues: string[], notes: string[], axes: {host?: string, build_host?: string, compiler?: string, standard?: string}}}
39
42
  */
40
43
  export function validateEnvironment(value) {
41
44
  const issues = []
@@ -53,6 +56,18 @@ export function validateEnvironment(value) {
53
56
  host: normalize(value.host),
54
57
  build_host: normalize(value.build_host),
55
58
  compiler: normalize(value.compiler),
59
+ standard: normalize(value.standard),
60
+ }
61
+
62
+ if (axes.standard === undefined || axes.standard === null || axes.standard === '') {
63
+ axes.standard = DEFAULT_STANDARD
64
+ notes.push(`standard 未声明或为空;按默认 ${DEFAULT_STANDARD} 处理,请在 environment.json 中显式写出`)
65
+ } else if (Array.isArray(axes.standard)) {
66
+ issues.push(`standard 只能声明一个值,当前是数组 [${axes.standard.join(', ')}]`)
67
+ } else if (typeof axes.standard !== 'string') {
68
+ issues.push(`standard 必须是字符串,当前是 ${typeof axes.standard}`)
69
+ } else if (!STANDARDS.includes(axes.standard)) {
70
+ issues.push(`standard 的值 "${axes.standard}" 非法;允许:${STANDARDS.join(' / ')}`)
56
71
  }
57
72
 
58
73
  for (const [key, allowed] of [
@@ -85,12 +100,12 @@ export function validateEnvironment(value) {
85
100
  }
86
101
 
87
102
  if (axes.host === 'windows' && axes.build_host === 'native') {
88
- notes.push('windows + native 是合法声明,但本版本没有 PowerShell 构建脚本;--live 会报告缺 POSIX 壳,而不是缺编译器')
103
+ issues.push('本版本不能在 Windows 原生壳里构建')
89
104
  }
90
105
 
91
106
  for (const key of Object.keys(value)) {
92
- if (!['host', 'build_host', 'compiler'].includes(key)) {
93
- notes.push(`忽略未知键 "${key}";环境只认 host / build_host / compiler`)
107
+ if (!['host', 'build_host', 'compiler', 'standard'].includes(key)) {
108
+ notes.push(`忽略未知键 "${key}";环境只认 host / build_host / compiler / standard`)
94
109
  }
95
110
  }
96
111
 
@@ -245,25 +260,10 @@ export function checkLive(declaration, options = {}) {
245
260
  }
246
261
  const { host, build_host: buildHost, compiler } = validation.axes
247
262
 
248
- if (host === 'windows' && buildHost === 'native') {
249
- return {
250
- ok: false,
251
- issues: [
252
- 'Windows 原生构建缺少 POSIX 壳;本版本没有 PowerShell 构建脚本。缺的是壳,不是编译器。请改用 WSL 并把 build_host 改成 wsl,或补一个 PowerShell 版本。',
253
- ],
254
- notes: validation.notes,
255
- checks,
256
- }
257
- }
258
-
259
263
  if (buildHost === 'wsl') {
260
264
  if (platform !== 'linux' || !isWsl(env)) {
261
265
  issues.push('声明 build_host=wsl,但当前进程不在 WSL 内;请在 WSL 里运行 --live')
262
266
  }
263
- } else if (host === 'windows') {
264
- if (platform !== 'win32') {
265
- issues.push(`声明 host=windows + build_host=native,但当前平台是 ${platform};请在 Windows 上运行 --live`)
266
- }
267
267
  } else if (host === 'linux' && platform !== 'linux') {
268
268
  issues.push(`声明 host=linux,但当前平台是 ${platform};请在 Linux 上运行 --live`)
269
269
  } else if (host === 'macos' && platform !== 'darwin') {
@@ -358,7 +358,7 @@ export function checkLive(declaration, options = {}) {
358
358
  ['clang-tidy', tidyVersion],
359
359
  ]) {
360
360
  if (version && version.major < 16) {
361
- issues.push(`${role} ${versionText(version)} 过旧;需要 >= 16(配置使用 BreakAfterAttributes)`)
361
+ issues.push(`${role} ${versionText(version)} 过旧;本工作流要求 >= 16`)
362
362
  }
363
363
  }
364
364
 
@@ -16,28 +16,29 @@ build/
16
16
 
17
17
  ## 通用情况
18
18
 
19
- 先读 `environment.json`。三个轴分别表示:
19
+ 先读 `environment.json`。源文件清单来自 `developing-cpp` 的定边界和定实现;定构建只把它们写进 target,不重新决定库与程序的边界。三个环境轴加语言标准分别表示:
20
20
 
21
21
  | 轴 | 取值 | 含义 |
22
22
  | --- | --- | --- |
23
23
  | `host` | `windows` / `linux` / `macos` | 编辑器与代理壳所在系统 |
24
- | `build_host` | `native` / `wsl` | 执行编译和运行产物的环境;`native` 表示 `host` 本机,`wsl` 表示 Windows 的 WSL,且只允许 `host=windows` |
24
+ | `build_host` | `native` / `wsl` | 执行编译和运行产物的环境;`native` 表示 `host` 本机,`wsl` 表示 Windows 的 WSL,且只允许 `host=windows`;`windows` + `native` 本版本不支持 |
25
25
  | `compiler` | `clang` / `gcc` | 用哪个编译器;一次只配一个 |
26
+ | `standard` | `c++11`–`c++23` | 一个工程一个语言标准;新工程默认 `c++23`,已有工程按原声明 |
26
27
 
27
- 轴为空、值未知或组合非法(如 `host=linux` + `build_host=wsl`)时,停在定构建,先让用户把 `environment.json` 填对。不要替用户选 WSL、Clang 或构建目录。
28
+ 环境轴为空、`standard` 非法或组合非法(如 `host=linux` + `build_host=wsl`、`host=windows` + `build_host=native`)时,停在定构建,先让用户把 `environment.json` 填对。不要替用户选 WSL、Clang 或构建目录。`windows` + `native` 只给一句「本版本不能在 Windows 原生壳里构建」,不要建议改 WSL、不要提出补 PowerShell 脚本。
28
29
 
29
30
  构建规则:
30
31
 
31
- - CMake 最低 3.20;`project(... LANGUAGES CXX)`;只有确有 C 源时才写 `LANGUAGES C CXX`。目标标准 C++20。
32
+ - CMake 最低 3.20;`project(... LANGUAGES CXX)`;只有确有 C 源时才写 `LANGUAGES C CXX`。新工程默认标准 C++23;已有工程按 `environment.json` 的 `standard` 或现有 target 声明,不擅自升降。
32
33
  - 生成器固定单配置 Ninja,不用 Ninja Multi-Config:同一源文件会写多条命令,clangd 默认只取第一条,容易读错数据库(clangd#2026);构建目录固定 `build/clang` 或 `build/gcc`。
33
34
  - 一个 configure 只用一个编译器:`-DCMAKE_CXX_COMPILER=clang++` 或 `g++`。
34
35
  - 程序 target 固定叫 `app`;库 target 用组件名,程序链接库。库与程序分开,不把 `main()` 编进库。
35
36
  - 用 `target_include_directories`、`target_compile_features`、`target_link_libraries` 表达依赖;禁止全局 `include_directories()`、`add_definitions()`、`CMAKE_CXX_FLAGS` 污染所有 target。
36
- - 库的公共 include 根用 `PUBLIC`;实现私有头用 `PRIVATE`;测试 target 只链接被测库的公共接口。
37
+ - 库的公共 include 根用 `PUBLIC`;实现私有头用 `PRIVATE`;测试 target 只链接被测库的公共接口。头文件按 Google 规则从仓库根写完整路径,构建时把仓库根加入搜索路径,不要改成组件的 `include/` 根后再写裸文件名。
37
38
  - `set(CMAKE_EXPORT_COMPILE_COMMANDS ON)`;Ninja 生成器下会产出 `compile_commands.json`。
38
39
  - 输出目录统一:可执行文件到 `build/<compiler>/bin`,库到 `build/<compiler>/lib`;不要用源码目录当输出目录。
39
40
  - 开启 `CMAKE_EXPORT_COMPILE_COMMANDS` 后,把 `build/<compiler>/compile_commands.json` 符号链接到 `build/compile_commands.json`,让 clangd 默认能找到当前数据库。本插件固定产出 `build/<compiler>/` 并在 `build/` 下维护链接,不生成 `out/build/`,也不把两套构建根混用。
40
- - 目标标准写在 target 上:`target_compile_features(app PRIVATE cxx_std_20)`;库用 `PUBLIC` 传播标准。每个 target 再加 `CXX_EXTENSIONS OFF`,避免 CMake 默认的 `-std=gnu++20`。
41
+ - 目标标准写在 target 上:`target_compile_features(app PRIVATE cxx_std_23)`;库用 `PUBLIC` 传播标准。再加 `set_target_properties(... CXX_STANDARD 23 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF)`:meta-feature 只保证“至少”,`CXX_STANDARD_REQUIRED` 防 decay,`CXX_EXTENSIONS OFF` 把 `-std=gnu++23` 收成 `-std=c++23`。configure 后读 `compile_commands.json` 的最后一个 `-std=`,必须与 `environment.json` 的 `standard` 一致。
41
42
  - 工程确有 C 源时才写 `LANGUAGES C CXX`,并且 `CMAKE_C_COMPILER` 与 `CMAKE_CXX_COMPILER` 必须来自同一套编译器(`clang`/`clang++` 或 `gcc`/`g++`);没有 C 源时只启用 CXX。
42
43
  - 不写 `file(GLOB ...)`;源文件显式列出,新增文件时改 CMake。
43
44
  - 不加 `-march=native`、`-ffast-math` 等改变语义或可移植性的选项;警告选项按团队约定统一加。
@@ -59,11 +60,11 @@ GCC 模式把目录和编译器换成 `build/gcc`、`g++`。工程确有 C 源
59
60
 
60
61
  ## 情况
61
62
 
62
- **新工程**:从 `assets/templates/projects/` 选形态:`single` 单文件、`multi` 多文件单程序、`static-library` 静态库 + 程序、`shared-library` 动态库 + 程序。只复制目标工程缺的文件;已有的 `CMakeLists.txt`、`.clang-*`、源码不覆盖。模板里的 `{{PROJECT_NAME}}` 换成真实工程名。
63
+ **新工程**:从 `assets/templates/projects/` 选形态:`single` 单文件、`multi` 多文件单程序、`static-library` 静态库 + 程序、`shared-library` 动态库 + 程序。只复制目标工程缺的文件;已有的 `CMakeLists.txt`、`.clang-*`、源码不覆盖。模板里的 `{{PROJECT_NAME}}` 换成真实工程名,默认标准 C++23;四套模板已按 `__cplusplus` 条件使用 `std::format`,把 `standard`、`cxx_std_*`、`CXX_STANDARD` 和 `.clang-format` 的解析档位一起降到 C++11–C++20 后仍应能构建,降完跑 `references/standards/README.md` 的五档矩阵。模板源码是 `.cc`,头文件用路径式 `#define` 守卫;新增文件沿用 `.cc` / `.h`,已有工程保持原有扩展名。
63
64
 
64
65
  **已有工程**:先读现有 `CMakeLists.txt`,找出全局设置、GLOB、输出目录和 target 命名;只改与当前任务相关的一处。大改前先让通过检查的项目入口跑通,保留可回退点。
65
66
 
66
- **库 + 程序**:库 target 的 `PUBLIC` include 根指向 `<component>/include`;程序 target 只链接库,不直接包含库的 `src/`。静态库与动态库的接口相同;动态库额外设置运行时库路径。
67
+ **库 + 程序**:库 target 的 `PUBLIC` include 根按项目约定设置;本工作流把仓库根作为源码根,程序按 `#include "component/include/name.h"` 包含公共头,不直接包含库的 `src/`。静态库与动态库的接口相同;动态库额外设置运行时库路径。
67
68
 
68
69
  **测试**:`enable_testing()` 放在顶层,`add_subdirectory(tests)` 在 `add_subdirectory(<component>)` 之后;测试 target 用 `add_test(NAME ... COMMAND ...)` 注册。没有测试时不要为了「看起来完整」加空测试。
69
70
 
@@ -74,3 +75,4 @@ GCC 模式把目录和编译器换成 `build/gcc`、`g++`。工程确有 C 源
74
75
  - 全局 include、GLOB、把 `main()` 编进库:`examples/cmake.md`。
75
76
  - 正确的最小库 + 程序:`examples/cmake.md`「最小可用形状」。
76
77
  - target 与目录的完整说明:`references/cmake.md`。
78
+ - 五档标准的下限、降档替换和工具映射:`references/standards/README.md`。