create-dwy 0.15.0 → 0.16.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 (89) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/index.js +5 -3
  3. package/package.json +3 -2
  4. package/src/searchable-select.js +419 -0
  5. package/src/skills-install.js +48 -7
  6. package/src/sync-all.js +17 -25
  7. package/src/sync.js +38 -32
  8. package/templates/ai-tools/CHANGELOG.md +76 -0
  9. package/templates/ai-tools/CLAUDE.md +27 -0
  10. package/templates/ai-tools/VERSION +1 -0
  11. package/templates/ai-tools/hook-manifests/hooks.json +7 -0
  12. package/templates/ai-tools/hooks/Git/pre-git-commit-ai-signature-check.sh +57 -0
  13. package/templates/ai-tools/rules/Android/dwy-android-core.md +215 -4
  14. package/templates/ai-tools/rules/Android/dwy-android-pitfalls.md +52 -0
  15. package/templates/ai-tools/rules/Docker/dwy-docker.md +52 -1
  16. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-build.md +50 -1
  17. package/templates/ai-tools/rules/HarmonyOS/dwy-arkts-ui-design.md +139 -0
  18. package/templates/ai-tools/rules/HarmonyOS/dwy-harmony-pitfalls.md +72 -0
  19. package/templates/ai-tools/rules/Python/dwy-python-backend.md +16 -63
  20. package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +89 -0
  21. package/templates/ai-tools/rules/Python/dwy-python-pitfalls.md +49 -0
  22. package/templates/ai-tools/rules/Vue/dwy-vue-base-lib.md +127 -0
  23. package/templates/ai-tools/rules/Vue/dwy-vue-core.md +22 -86
  24. package/templates/ai-tools/rules/Vue/dwy-vue-pitfalls.md +66 -0
  25. package/templates/ai-tools/rules/iOS/dwy-ios-pitfalls.md +7 -0
  26. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +59 -0
  27. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +9 -6
  28. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md +47 -2
  29. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/SKILL.md +511 -0
  30. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-data.md +126 -0
  31. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-network.md +88 -0
  32. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/checks-runtime.md +195 -0
  33. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/references/security-doc-search.md +179 -0
  34. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_db.sh +147 -0
  37. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_docker.sh +230 -0
  38. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_nginx.sh +100 -0
  42. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_secrets.sh +265 -0
  44. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_services.sh +94 -0
  45. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/ai-tools/skills/unitcircle/uc-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +3 -2
  48. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +51 -9
  49. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +58 -0
  50. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +2 -1
  51. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-explore/SKILL.md +83 -0
  52. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +2 -1
  53. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +1 -0
  54. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +14 -9
  55. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +65 -13
  56. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prototype/SKILL.md +30 -11
  57. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +3 -2
  58. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-design/SKILL.md +17 -10
  59. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-dev/SKILL.md +42 -10
  60. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-launch/SKILL.md +22 -10
  61. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-prd/SKILL.md +13 -4
  62. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-stage-ship/SKILL.md +13 -4
  63. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +12 -2
  64. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +15 -6
  65. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +2 -1
  66. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +1 -0
  67. package/templates/ai-tools/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +22 -1
  68. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/SKILL.md +1 -1
  69. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/references/checks-runtime.md +2 -2
  70. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_docker.sh +3 -3
  71. package/templates/ai-tools/skills//345/256/211/345/205/250/dwy-deploy-audit/scripts/check_secrets.sh +1 -1
  72. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/SKILL.md +176 -0
  73. package/templates/ai-tools/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-first/references/chapter-01-mirrors.md +112 -0
  74. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-ideate/SKILL.md +0 -21
  75. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/SKILL.md +0 -113
  76. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/templates/STATE.md +0 -56
  77. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dev-qa/SKILL.md +0 -117
  78. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-market-analysis/SKILL.md +0 -92
  79. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/SKILL.md +0 -72
  80. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/templates/prd-coarse.md +0 -61
  81. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/SKILL.md +0 -76
  82. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/templates/prd-detailed.md +0 -119
  83. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prototype/SKILL.md +0 -104
  84. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-spike/SKILL.md +0 -81
  85. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/SKILL.md +0 -212
  86. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/templates/impl-plan.md +0 -115
  87. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tech-feasibility/SKILL.md +0 -105
  88. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/SKILL.md +0 -64
  89. package/templates/ai-tools/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/templates/version-plan.md +0 -72
package/src/sync.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import fs from 'fs-extra'
2
2
  import path from 'path'
3
- import { groupMultiselect, isCancel, select } from '@clack/prompts'
3
+ import { isCancel } from '@clack/prompts'
4
+ import {
5
+ SEARCH_PLACEHOLDER,
6
+ searchableMultiselect,
7
+ searchableSelect,
8
+ } from './searchable-select.js'
4
9
  import { chalk, DEFAULT_TEMPLATE_REPO_URL, getDwyHomeDir, runGit } from './utils.js'
5
10
 
6
11
  const CATEGORIES = [
@@ -208,44 +213,42 @@ export async function scanExisting(projectTargetDir, typePlural) {
208
213
  return new Set(entries.filter(name => name !== '.gitkeep' && name !== '.DS_Store'))
209
214
  }
210
215
 
211
- function buildGroupedOptions(items) {
212
- const groups = new Map()
213
- for (const item of items) {
214
- const category = item.category || '其他'
215
- const options = groups.get(category) || []
216
- const hint = item.description
217
- ? item.description.length > 72
218
- ? `${item.description.slice(0, 72)}...`
219
- : item.description
220
- : undefined
221
-
222
- options.push({
223
- value: item.name,
224
- label: hint ? `${item.name}(${hint})` : item.name,
216
+ /**
217
+ * 将扫描项转为可搜索扁平 options。
218
+ * label 带分类前缀便于筛选;完整 description 进底部公共说明区(不截断塞 hint)。
219
+ *
220
+ * @param {Array<{ name: string, category?: string, description?: string }>} items
221
+ * @returns {Array<{ value: string, label: string, description?: string }>}
222
+ */
223
+ function buildSearchableOptions(items) {
224
+ return items
225
+ .slice()
226
+ .sort((a, b) => {
227
+ const catCmp = (a.category || '其他').localeCompare(b.category || '其他', 'zh')
228
+ if (catCmp !== 0) return catCmp
229
+ return a.name.localeCompare(b.name, 'zh')
230
+ })
231
+ .map(item => {
232
+ const category = item.category || '其他'
233
+ return {
234
+ value: item.name,
235
+ label: `${category} / ${item.name}`,
236
+ // 完整描述给底部公共区;搜索也匹配 description
237
+ description: item.description || undefined,
238
+ }
225
239
  })
226
- groups.set(category, options)
227
- }
228
-
229
- return Object.fromEntries(
230
- [...groups.entries()]
231
- .sort(([a], [b]) => a.localeCompare(b, 'zh'))
232
- .map(([category, options]) => [
233
- category,
234
- options.sort((a, b) => a.label.localeCompare(b.label, 'zh')),
235
- ]),
236
- )
237
240
  }
238
241
 
239
242
  async function promptSelection(items, label, defaultNames) {
240
243
  if (items.length === 0) return []
241
- const selectedNames = await groupMultiselect({
244
+ // 可搜索多选 + 底部说明区(聚焦项描述不跟在行尾)
245
+ const selectedNames = await searchableMultiselect({
242
246
  message: `选择 ${label}`,
243
- options: buildGroupedOptions(items),
247
+ options: buildSearchableOptions(items),
244
248
  initialValues: [...defaultNames],
245
249
  maxItems: 15,
246
250
  required: false,
247
- groupSpacing: 1,
248
- selectableGroups: true,
251
+ placeholder: SEARCH_PLACEHOLDER,
249
252
  })
250
253
  if (isCancel(selectedNames)) return null
251
254
  const selectedSet = new Set(selectedNames)
@@ -273,7 +276,8 @@ export async function interactiveSelect(scans, existing) {
273
276
 
274
277
  const nextLabel = CATEGORIES[i + 1].label
275
278
  const prevLabel = i > 0 ? CATEGORIES[i - 1].label : null
276
- const nav = await select({
279
+ // 导航步可搜索单选(选项少,交互与多选一致)
280
+ const nav = await searchableSelect({
277
281
  message: `${label} 已选 ${sel[key].length} 项。下一步:`,
278
282
  options: [
279
283
  { label: `继续选 ${nextLabel}`, value: 'next' },
@@ -281,6 +285,7 @@ export async function interactiveSelect(scans, existing) {
281
285
  { label: '取消同步', value: 'cancel' },
282
286
  ],
283
287
  initialValue: 'next',
288
+ placeholder: SEARCH_PLACEHOLDER,
284
289
  })
285
290
  if (isCancel(nav) || nav === 'cancel') return null
286
291
  i = nav === 'back' ? i - 1 : i + 1
@@ -292,7 +297,7 @@ export async function interactiveSelect(scans, existing) {
292
297
  for (const { key, label } of CATEGORIES) {
293
298
  console.log(chalk.gray(` ${label.padEnd(10)} ${sel[key].length} 项`))
294
299
  }
295
- const final = await select({
300
+ const final = await searchableSelect({
296
301
  message: '确认提交还是重选?',
297
302
  options: [
298
303
  { label: '确认提交', value: 'confirm' },
@@ -300,6 +305,7 @@ export async function interactiveSelect(scans, existing) {
300
305
  { label: '取消同步', value: 'cancel' },
301
306
  ],
302
307
  initialValue: 'confirm',
308
+ placeholder: SEARCH_PLACEHOLDER,
303
309
  })
304
310
  if (isCancel(final) || final === 'cancel') return null
305
311
  if (final === 'confirm') return sel
@@ -0,0 +1,76 @@
1
+ # ai-tools 内部版本
2
+
3
+ 本目录(`dwy-cli/templates/ai-tools`)rules / skills / hooks / commands / CLAUDE.md 等模板资产的**内部版本**,与 `create-dwy` npm 版本**解耦**。
4
+
5
+ - 当前版本:见同目录 `VERSION`(单一事实源)
6
+ - 用途:模板变更可追溯;发 `create-dwy` 时可对照本 CHANGELOG 摘发布说明;本地/多仓对照「模板包」新旧
7
+
8
+ ## 维护约定(强制)
9
+
10
+ **每次**修改本目录下任意内容(含新增 / 删除 / 重命名)时,同一变更必须:
11
+
12
+ 1. 按下方规则 bump `VERSION`
13
+ 2. 在本文件**顶部**(当前版本段之上)追加新版本段
14
+ 3. 条目写清路径与行为变化,不写空话
15
+
16
+ 禁止:改了模板却不 bump、不写 CHANGELOG。
17
+
18
+ ### 版本号(SemVer,0.x 阶段)
19
+
20
+ | 级别 | 何时 |
21
+ | ---- | ---- |
22
+ | **MAJOR**(`x.0.0`) | 破坏性:同步目录结构变更、托管块语义不兼容、删除已被广泛依赖的 skill/rule 且无迁移说明 |
23
+ | **MINOR**(`0.y.0`) | 新增 skill / rule / hook / command;既有规范**新增强制约束**或大段能力 |
24
+ | **PATCH**(`0.y.z`) | 文案修正、正反例补充、链接修复、不改变强制语义的小改 |
25
+
26
+ 0.x 阶段允许不稳定;仍用 minor / patch 区分「新增能力」与「修正」。
27
+
28
+ ### 版本段格式
29
+
30
+ ```markdown
31
+ ## x.y.z — YYYY-MM-DD
32
+
33
+ ### Added
34
+ - …
35
+
36
+ ### Changed
37
+ - …
38
+
39
+ ### Fixed
40
+ - …
41
+
42
+ ### Removed
43
+ - …
44
+ ```
45
+
46
+ 无某类变更可省略对应小节。日期用实际合并/提交日(`YYYY-MM-DD`)。
47
+
48
+ ### 与 create-dwy 的关系
49
+
50
+ - 改本目录 ≠ 自动发 npm;发布 `create-dwy` 时另走 CLI 发版流程,并在 `dwy-cli/CHANGELOG.md` 摘要用户可感变化
51
+ - 本文件是模板侧明细;CLI CHANGELOG 可只写要点并指向 ai-tools 版本号(如 `ai-tools 0.1.0`)
52
+
53
+ ---
54
+
55
+ ## 0.2.0 — 2026-07-23
56
+
57
+ ### Added
58
+
59
+ - `skills/运维发布/dwy-deploy-first/`:通用「首次部署」skill(分章)
60
+ - 第 1 章:镜像源强制配置 — apt → 阿里云;uv/pip → 阿里云 PyPI
61
+ - 禁止默认 `astral.sh/uv/install.sh`;与 `dwy-mirror-source`(本机工具源)、`dwy-docker`(工程规范)边界划清
62
+ - `references/chapter-01-mirrors.md`:通用 Dockerfile 片段(Debian/Ubuntu/Alpine + uv/pip)
63
+
64
+ ---
65
+
66
+ ## 0.1.0 — 2026-07-20
67
+
68
+ ### Added
69
+
70
+ - 建立 ai-tools 内部版本:`VERSION` + 本 `CHANGELOG.md` 及维护约定
71
+ - `skills/元工具/dwy-shared/SKILL.md`:入库流程增加步骤 6.5,改模板必须同批 bump `VERSION` + `CHANGELOG.md`
72
+ - `rules/Android/dwy-android-core.md`:新增「数据实体类封装(强制)」
73
+ - **Java**:字段 `private`,提供 `getXxx`/`setXxx` 与 `toString`;禁止 public 实例字段
74
+ - **Kotlin**:优先 `data class` + `val`(自带 `toString`);禁止 `@JvmField`;非 data class 必须手写 `toString`
75
+ - 不强制 `equals`/`hashCode`
76
+ - 联动 §2.6 注释、§八反模式、§九自检项 #22
@@ -54,6 +54,21 @@
54
54
  - 如果你写了 200 行但可以写成 50 行,重写它。
55
55
  - 问自己:"一个资深工程师会说这过于复杂吗?"如果是,简化。
56
56
 
57
+ ### 写代码前走决策阶梯
58
+
59
+ 写任何新代码前,从上往下逐级问,命中即停:
60
+
61
+ 1. 这任务真需要存在吗?(YAGNI——能不做就不做)
62
+ 2. 代码库里已有现成的吗?
63
+ 3. 标准库覆盖吗?
64
+ 4. 平台 / 框架原生特性有吗?
65
+ 5. 已安装的依赖能做吗?
66
+ 6. 能一行解决吗?
67
+ 7. 以上都不行 → 才写最小代码。
68
+
69
+ 刻意的简化用注释标注:跳过了什么、何时该加回。
70
+ 红线——以下永不简化掉:输入校验、错误处理、安全、可访问性。先理解问题,再谈优化。
71
+
57
72
  ### 3. 精准改动
58
73
 
59
74
  只动必须动的代码。只清理自己造成的混乱。
@@ -181,3 +196,15 @@
181
196
  - 后端用 dict / map / any 接收 body
182
197
  - 分页 `page_size` 无上限
183
198
  - 图片只校验文件大小不校验像素
199
+
200
+ ## 自成长项目规则(project-rules)
201
+
202
+ 写项目要自成长:同一类问题反复踩坑,主动沉淀成可复用规则,下次自动生效,不靠人事后总结。
203
+
204
+ - **触发**:写代码 / 排查问题时,同一类问题踩坑 **≥2 次**(重复的报错根因、踩同一个坑、被反复纠正同一处写法)。
205
+ - **动作**:把教训沉淀成一条可复用规则(写清「现象 / 根因 / 怎么避免」),追加到 `project-rules.md`;已有同类规则补充进去,不重复立条。
206
+ - **落盘位置**(按当前工具自动选):
207
+ - **Claude Code** → 项目 `.claude/rules/project-rules.md`(放进去即自动加载,无需注册)。
208
+ - **Codex** → 项目根 `AGENTS.md`,写在 `<!-- DWY-RULES:START/END -->` 托管块**之外**的 `## [自成长] 项目规则` 节下。托管块内会被 `dwy codex sync` 整块覆盖,自成长内容**必落块外**。
209
+ - **方式**:自动沉淀,事后输出一行告知「已沉淀 <规则> 到 project-rules」,不打断当前任务、不逐条问用户。
210
+ - **边界**:只沉淀**可复用、跨任务**的教训;一次性 / 本任务特有的问题不立规则,避免噪音堆积。
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -12,5 +12,12 @@
12
12
  "matcher": "Bash",
13
13
  "platforms": ["claude", "codex"],
14
14
  "timeout": 30
15
+ },
16
+ {
17
+ "name": "pre-git-commit-ai-signature-check.sh",
18
+ "event": "PreToolUse",
19
+ "matcher": "Bash",
20
+ "platforms": ["claude", "codex"],
21
+ "timeout": 30
15
22
  }
16
23
  ]
@@ -0,0 +1,57 @@
1
+ #!/bin/bash
2
+ # Claude Code PreToolUse Hook
3
+ # 拦截 git commit message 中的 AI 署名 / 生成声明
4
+ #
5
+ # 背景:AI agent 常在 commit message 中附加 Co-Authored-By: Claude / Generated with Claude Code 等署名。
6
+ # 本 hook 检出此类模式后硬拦截(exit 2),要求人工确认或清理后再提交。
7
+ #
8
+ # 覆盖模式(不区分大小写):
9
+ # Co-Authored-By: <ai-name> <noreply@...>
10
+ # Generated with Claude Code
11
+ # Generated with ...
12
+ # noreply@anthropic.com / noreply@openai.com
13
+ # Claude / ChatGPT / GPT / Copilot / Cursor / AI / LLM (在签名行上下文)
14
+
15
+ # 从 stdin 读 JSON 取待执行命令(Claude Code 与 Codex 同样通过 stdin 传 .tool_input.command)
16
+ CMD=$(python3 -c 'import sys,json; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
17
+
18
+ # 只在含 git commit 的命令上检查
19
+ printf '%s' "$CMD" | grep -q 'git commit' || exit 0
20
+
21
+ # AI 署名匹配模式
22
+ AI_PATTERNS=(
23
+ # Co-Authored-By trailer(任意 AI 名称 + 含 < 的邮箱)
24
+ "Co-Authored-By:"
25
+ # 生成声明
26
+ "Generated with"
27
+ # 常见 AI 署名邮箱
28
+ "noreply@anthropic\.com"
29
+ "noreply@openai\.com"
30
+ # AI 产品名(在签名行上下文,前面有 Co-Authored-By 或单独出现在行尾)
31
+ "Co-Authored-By:.*[Cc]laude"
32
+ "Co-Authored-By:.*[Cc]hatGPT"
33
+ "Co-Authored-By:.*GPT"
34
+ "Co-Authored-By:.*[Cc]opilot"
35
+ "Co-Authored-By:.*[Cc]ursor"
36
+ )
37
+
38
+ REGEX=$(IFS='|'; echo "${AI_PATTERNS[*]}")
39
+
40
+ HIT=$(printf '%s' "$CMD" | grep -inE "$REGEX" 2>/dev/null)
41
+
42
+ if [ -n "$HIT" ]; then
43
+ echo "========================================"
44
+ echo " WARNING: AI 署名 detected"
45
+ echo "========================================"
46
+ echo ""
47
+ echo "$HIT"
48
+ echo ""
49
+ echo "commit message 中禁止出现 Co-Authored-By / Generated with 等 AI 署名。"
50
+ echo "请清理 AI 署名后再提交。"
51
+ echo ""
52
+ echo "如果要强制提交,请手动执行 git commit(绕过 hook)。"
53
+ echo "========================================"
54
+ exit 2 # 暂停等待人工审批
55
+ fi
56
+
57
+ exit 0
@@ -336,7 +336,7 @@ fun computeDiscount(order: Order): BigDecimal {
336
336
 
337
337
  ### 2.6 字段 / 属性注释(强制)
338
338
 
339
- 公开字段、`data class` 字段、配置常量必须逐个注释。
339
+ 公开属性、`data class` 构造参数、配置常量必须逐个注释。**数据实体**除注释外还须遵守 §四「数据实体类封装」:Java 注释写在 **private 字段**上,Kotlin 用 `@property`。
340
340
 
341
341
  ```kotlin
342
342
  /**
@@ -356,6 +356,26 @@ data class User(
356
356
  ```
357
357
 
358
358
  ```java
359
+ /**
360
+ * 用户信息实体。字段私有,经 getter / setter 访问。
361
+ */
362
+ public class User {
363
+ /** 用户 ID,服务端分配,> 0 */
364
+ private long id;
365
+ /** 昵称,1-32 字符 */
366
+ private String name;
367
+
368
+ public long getId() {
369
+ return id;
370
+ }
371
+
372
+ public void setId(long id) {
373
+ this.id = id;
374
+ }
375
+
376
+ // ... 其余 get/set、toString 见 §四「数据实体类封装」
377
+ }
378
+
359
379
  public final class Constants {
360
380
  /** 登录态本地缓存的 SP key,迁移版本时不允许复用此 key,会读到旧数据。 */
361
381
  public static final String SP_KEY_LOGIN_TOKEN = "login_token_v2";
@@ -407,7 +427,7 @@ public final class Constants {
407
427
  | 有参数 / 返回值 / 异常 | 多行,含 `@param` / `@return` / `@throws` | ✓ |
408
428
  | Activity / Fragment | 必须注明入口、参数 key、返回结果 | ✓ |
409
429
  | 回调接口 | 必须注明触发线程 + 每个参数语义 | ✓ |
410
- | `data class` 字段 | Kotlin `@property`,Java 字段上 `/** */` | ✓ |
430
+ | `data class` / Java 实体字段 | Kotlin `@property`;Java **private 字段**上 `/** */` + get/set | ✓ |
411
431
  | 复杂业务规则 | 行内注释解释 why + 关联 PRD / 需求号 | ✓ |
412
432
  | 重写方法 | `@inheritDoc` 或重新写明覆写差异 | ✓ |
413
433
  | Gradle 脚本(Groovy) | 解释 why:锁版本/exclude 原因、变体用途、自定义 task/方法语义(详见 §2.10) | ✓ |
@@ -533,7 +553,7 @@ public List<Order> findOrders(long userId) {
533
553
 
534
554
  ### 强制规则
535
555
 
536
- - 公开方法 / 函数参数 ≤ **3 个**,超过则封装为 DTO / `data class`
556
+ - 公开方法 / 函数参数 ≤ **3 个**,超过则封装为 DTO / `data class`(封装出的 DTO **必须**遵守下文「数据实体类封装」)
537
557
  - Kotlin 优先**命名参数 + 默认参数**代替方法重载
538
558
  - 函数体不超过 **50 行**,超过则拆分
539
559
  - 单文件不超过 **500 行**,超过则拆分模块
@@ -636,6 +656,146 @@ for (Order o : orders) {
636
656
  }
637
657
  ```
638
658
 
659
+ ### 数据实体类封装(强制)
660
+
661
+ **动机**:Java 一旦暴露 `public` 字段,调用方直接依赖字段名与访问方式;后续改为 `private` + getter 会破坏源码 / 二进制兼容。实体从一开始就封装,避免后期改结构成本。Kotlin 编译属性本就是 private 字段 + 访问器,规则与 Java 分写,禁止用 `@JvmField` 退化成 public 字段。
662
+
663
+ #### 适用范围
664
+
665
+ | 覆盖 | 不覆盖 |
666
+ | ---- | ------ |
667
+ | Model / Entity / DTO / VO / Bean 及同类数据载体 | Activity / Fragment / Adapter / ViewHolder / Repository / ViewModel 等非数据载体 |
668
+ | 含功能包内私有、`internal` 实体 | `enum`;仅含常量的 `object` / `Constants` 类 |
669
+ | 网络响应体、跨层传递的数据对象 | 纯 UI 状态若仅为页面内部临时结构且不跨层,可豁免(跨层则按实体处理) |
670
+
671
+ #### 共同要求
672
+
673
+ 1. 字段 / 属性**不得**以 public 字段形式对外暴露(禁止 Java `public` 实例字段、禁止 Kotlin `@JvmField`)
674
+ 2. 必须提供读取能力:Java `getXxx()`,Kotlin property 访问器
675
+ 3. 必须有有意义的 `toString()`(能区分关键业务字段);禁止依赖 `Object` 默认 `ClassName@hash`
676
+ 4. **不强制** `equals` / `hashCode`(集合去重 / Map key 场景由调用方按需补)
677
+
678
+ ---
679
+
680
+ #### Java 规则
681
+
682
+ | 项 | 要求 |
683
+ | -- | ---- |
684
+ | 字段 | 必须 `private`;禁止 `public` / 包可见实例字段当实体 API |
685
+ | 访问器 | 提供 `getXxx()` / `setXxx()`;不可变实体可用构造注入 + 仅 getter |
686
+ | toString | `@Override public String toString()`,包含关键业务字段 |
687
+ | 注释 | 类级说明职责;每个 private 字段上 `/** */`(见 §2.6) |
688
+
689
+ ```java
690
+ /**
691
+ * 用户信息实体。
692
+ *
693
+ * <p>字段私有 + getter/setter,避免 public 字段导致后续无法安全改结构。
694
+ */
695
+ public class User {
696
+ /** 用户 ID,服务端分配,> 0 */
697
+ private long id;
698
+ /** 昵称,1-32 字符 */
699
+ private String name;
700
+
701
+ public long getId() {
702
+ return id;
703
+ }
704
+
705
+ public void setId(long id) {
706
+ this.id = id;
707
+ }
708
+
709
+ public String getName() {
710
+ return name;
711
+ }
712
+
713
+ public void setName(String name) {
714
+ this.name = name;
715
+ }
716
+
717
+ @Override
718
+ public String toString() {
719
+ return "User{id=" + id + ", name='" + name + "'}";
720
+ }
721
+ }
722
+ ```
723
+
724
+ ```java
725
+ // ❌ public 字段 — 调用方直接 .id,后期改 private 会大面积破坏
726
+ public class User {
727
+ public long id;
728
+ public String name;
729
+ }
730
+
731
+ // ❌ 有 private 字段但缺 toString / 缺访问器
732
+ public class User {
733
+ private long id;
734
+ // 无 getId,外部无法读;无 toString,日志只能看到 @hash
735
+ }
736
+ ```
737
+
738
+ ---
739
+
740
+ #### Kotlin 规则
741
+
742
+ | 项 | 要求 |
743
+ | -- | ---- |
744
+ | 优先写法 | **`data class` + `val`**(编译为 private 字段 + getter;`data class` 自带 `toString`) |
745
+ | 可变字段 | 确需可变用 `var`;仅允许类内改写时用 `private set` |
746
+ | 禁止 | `@JvmField`;Java 风格 public 字段;为 `data class` 再手写冗余 get/set(Java 互操作强制命名除外) |
747
+ | 非 data class | property 或 private 字段 + 访问器,**必须** `override fun toString()` |
748
+ | 注释 | 构造属性用 `@property`,与 §2.6 一致 |
749
+
750
+ ```kotlin
751
+ /**
752
+ * 用户信息实体。
753
+ *
754
+ * @property id 用户 ID,服务端分配,> 0
755
+ * @property name 昵称,1-32 字符
756
+ */
757
+ data class User(
758
+ val id: Long,
759
+ val name: String,
760
+ )
761
+ // data class 已生成 toString / componentN / copy;JVM 侧字段为 private + getter
762
+ ```
763
+
764
+ ```kotlin
765
+ /**
766
+ * 可编辑的草稿实体。
767
+ *
768
+ * 非 data class 时必须手写 toString;title 仅允许类内改写。
769
+ */
770
+ class Draft {
771
+ var title: String = ""
772
+ private set
773
+
774
+ /**
775
+ * 更新草稿标题。
776
+ *
777
+ * @param value 新标题,调用方已做长度校验
778
+ */
779
+ fun updateTitle(value: String) {
780
+ title = value
781
+ }
782
+
783
+ override fun toString(): String = "Draft(title=$title)"
784
+ }
785
+ ```
786
+
787
+ ```kotlin
788
+ // ❌ @JvmField 把属性暴露成 public 字段,与 Java public 字段同样难改
789
+ class User {
790
+ @JvmField
791
+ var id: Long = 0
792
+ }
793
+
794
+ // ❌ 普通 class 实体无 toString — 日志只有类名@hash
795
+ class User(val id: Long, val name: String)
796
+ // ✅ 改为 data class User(...) 或 override fun toString()
797
+ ```
798
+
639
799
  ### 包组织(按功能聚合)
640
800
 
641
801
  **按功能聚合(package-by-feature),不按技术分层(package-by-layer)。**
@@ -743,6 +903,51 @@ val result = calculate(x)
743
903
 
744
904
  ## 八、常见反模式(禁止)
745
905
 
906
+ ### 字符串空判断(强制)
907
+
908
+ 判断字符串是否为空、是否有有效内容时,**禁止**用 `length()` 比较,必须用语义明确的 `isEmpty()` / `isNotEmpty()`(Kotlin 意图为「非空白」时优先 `isNotBlank()`)。
909
+
910
+ **强制规则**:
911
+
912
+ - **禁止** `s.length() > 0`、`s.length() == 0`、`s.length() != 0`、`s.trim().length() > 0` 等 `length` 判空写法
913
+ - **必须**用 `isEmpty()` / `!isEmpty()`(Java) 或 `isEmpty()` / `isNotEmpty()`(Kotlin)
914
+ - 需先去首尾空白再判是否有内容:Java `!s.trim().isEmpty()`;Kotlin `s.trim().isNotEmpty()` 或等价的 `s.isNotBlank()`
915
+ - 可空字符串仍先做 null 判断,再判空:`s != null && !s.trim().isEmpty()`(与 §三可空性一致)
916
+
917
+ ```java
918
+ // ❌ 用 length 判「是否有有效内容」——语义弱、易被误读成「长度阈值」
919
+ public String resolveVideoType() {
920
+ if (videoType != null && videoType.trim().length() > 0) {
921
+ return videoType.trim();
922
+ }
923
+ if (type != null && type.trim().length() > 0) {
924
+ return type.trim();
925
+ }
926
+ return null;
927
+ }
928
+
929
+ // ✅ 用 isEmpty 表达「非空」
930
+ public String resolveVideoType() {
931
+ if (videoType != null && !videoType.trim().isEmpty()) {
932
+ return videoType.trim();
933
+ }
934
+ if (type != null && !type.trim().isEmpty()) {
935
+ return type.trim();
936
+ }
937
+ return null;
938
+ }
939
+ ```
940
+
941
+ ```kotlin
942
+ // ❌
943
+ if (videoType != null && videoType.trim().length > 0) { ... }
944
+
945
+ // ✅
946
+ if (!videoType.isNullOrBlank()) { ... }
947
+ // 或需保留 trim 后的值时:
948
+ val trimmed = videoType?.trim().orEmpty()
949
+ if (trimmed.isNotEmpty()) { ... }
950
+ ```
746
951
 
747
952
  | 反模式 | 正确做法 |
748
953
  | ----------------------------------------------------------------- | ------------------------------------------- |
@@ -761,6 +966,9 @@ val result = calculate(x)
761
966
  | 硬编码颜色值 `#FF0000` | `@color/color_primary` |
762
967
  | 用户可见文案硬编码 | `@string/...` |
763
968
  | 单行 `if` / `for` / `while` / `do-while` 无 `{}`(Kotlin 表达式 `if` 除外) | 即使只一行也写 `{}`;详见 §四「控制流大括号」 |
969
+ | Java 实体 `public` 实例字段 | `private` 字段 + `getXxx`/`setXxx` + `toString` |
970
+ | Kotlin 实体 `@JvmField` / 无 `toString` 的普通 class 实体 | 优先 `data class` + `val`;或手写访问器 + `toString` |
971
+ | `s.length() > 0` / `s.trim().length() > 0` 等用 length 判空 / 非空 | Java `!s.isEmpty()` / `!s.trim().isEmpty()`;Kotlin `s.isNotEmpty()` / `s.isNotBlank()`;详见本节「字符串空判断」 |
764
972
 
765
973
 
766
974
  ## 九、代码自检(写代码时强制执行)
@@ -774,7 +982,7 @@ val result = calculate(x)
774
982
  | 2 | 方法 / 函数(不分可见性)有 Javadoc / KDoc,含 `@param` / `@return` / `@throws`;一目了然的简单 getter / setter、测试方法可省 | ✓ |
775
983
  | 3 | Activity / Fragment 类注释包含入口、参数 key、返回结果 | ✓ |
776
984
  | 4 | 回调接口注明每个参数语义 + 触发线程 | ✓ |
777
- | 5 | `data class` / 公开字段 / 配置常量逐个 `@property` / 字段注释 | ✓ |
985
+ | 5 | `data class` 属性 / Java 实体 private 字段 / 配置常量逐个 `@property` 或字段注释 | ✓ |
778
986
  | 6 | 复杂业务逻辑有"why"行内注释,关联 PRD / 需求号 | ✓ |
779
987
  | 7 | 注释全部中文,无中英混用,无敷衍式"获取 / 设置 / 处理" | ✓ |
780
988
  | 8 | 注释与代码同步更新,无过时注释 | ✓ |
@@ -791,6 +999,9 @@ val result = calculate(x)
791
999
  | 19 | `if` / `else` / `else if` / `for` / `while` / `do-while` 语句即使单行也带 `{}`(Kotlin 表达式 `if` 除外) | ✓ |
792
1000
  | 20 | 新增类按**功能聚合**放对应功能包(`login/` / `order/`),不按 `activities/` / `models/` 分层横切;仅多功能共享才放 `common/` | ✓ |
793
1001
  | 21 | Gradle 脚本(Groovy / kts):锁版本 / `exclude` / 自定义 task / 变体 / 关键魔法值有 why 注释,无复述式、无注释掉的配置块 | ✓ |
1002
+ | 22 | 数据实体(Model/DTO/VO/Bean 等):Java 字段 `private` + get/set + `toString`;Kotlin 优先 `data class`+`val`(自带 toString),禁止 `@JvmField`,非 data class 必须手写 `toString` | ✓ |
1003
+ | 23 | 字符串空判断用 `isEmpty` / `isNotEmpty` / `isNotBlank`,禁止 `length() > 0` / `== 0` / `!= 0` 及 `trim().length() > 0` 判空;详见 §八「字符串空判断」 | ✓ |
794
1004
 
795
1005
 
796
1006
  **不执行自检就提交代码 = 违规。**
1007
+
@@ -0,0 +1,52 @@
1
+ ---
2
+ description: Android 开发避坑清单(Gradle / 签名 / Compose / 生命周期 / 真机部署踩坑),按分类持续追加
3
+ paths:
4
+ - "**/*.java"
5
+ - "**/*.kt"
6
+ - "**/*.kts"
7
+ - "**/*.gradle"
8
+ - "**/AndroidManifest.xml"
9
+ - "**/res/**/*.xml"
10
+ ---
11
+
12
+ # Android 开发坑清单
13
+
14
+ > 动手前先对照本清单避开已知坑;踩到新坑随手按四栏格式(现象 / 根因 / 正确做法 / 适用)追加到对应分类,没有合适分类就新建一个。
15
+ >
16
+ > 通用开发规范见 `dwy-android-core` rule,本文件只记**非显而易见的实战坑**。
17
+
18
+ ## 分类索引
19
+
20
+ - 工程与构建(Gradle / AGP / 依赖冲突)
21
+ - 签名与真机部署
22
+ - Compose / UI(重组、状态、生命周期)
23
+ - 生命周期 / 并发(协程、ViewModel、内存泄漏)
24
+ - 权限 / Manifest / Capability
25
+ - 排查方法论
26
+ - 其他分类遇到再加
27
+
28
+ ---
29
+
30
+ ## 工程与构建
31
+
32
+ <!-- 待填:踩到坑按四栏格式追加 -->
33
+
34
+ ## 签名与真机部署
35
+
36
+ <!-- 待填 -->
37
+
38
+ ## Compose / UI
39
+
40
+ <!-- 待填 -->
41
+
42
+ ## 生命周期 / 并发
43
+
44
+ <!-- 待填 -->
45
+
46
+ ## 权限 / Manifest / Capability
47
+
48
+ <!-- 待填 -->
49
+
50
+ ## 排查方法论
51
+
52
+ <!-- 待填 -->