create-dwy 0.17.0 → 0.18.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 (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/bin/index.js +24 -22
  3. package/package.json +1 -1
  4. package/src/cli-help.js +24 -0
  5. package/src/self-upgrade.js +158 -0
  6. package/src/sync-all.js +159 -5
  7. package/src/sync-packs.js +191 -0
  8. package/src/sync-scene.js +146 -0
  9. package/src/sync.js +9 -1
  10. package/templates/ai-tools/CHANGELOG.md +32 -0
  11. package/templates/ai-tools/VERSION +1 -1
  12. package/templates/ai-tools/rules/Android/dwy-android-core.md +67 -1
  13. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +26 -14
  14. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +90 -22
  15. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +1 -1
  16. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +1 -1
  17. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +1 -1
  18. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +1 -1
  19. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +1 -1
  20. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +1 -1
  21. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +3 -3
  22. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +1 -1
  23. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +1 -1
  24. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +1 -1
  25. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +1 -1
  26. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +1 -1
  27. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +39 -20
  28. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +28 -1
  29. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +31 -1
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/aso.md +52 -0
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +1 -1
  32. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-specs.yaml +98 -0
  33. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +62 -6
  34. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eui/references/eui-design-guide.md +3 -2
@@ -0,0 +1,191 @@
1
+ /**
2
+ * 按包勾选的包目录:技术栈包 + 场景包。
3
+ * 展开结果写入现有 skills/rules/hooks 选择,不改平台 sync 的按名增删。
4
+ * 条目按扫描结果的 category / name 匹配,模板里不存在的名字直接跳过。
5
+ */
6
+
7
+ /** 技术栈包(含通用)。第一屏只出这些。 */
8
+ export const STACK_PACKS = [
9
+ {
10
+ id: 'common',
11
+ label: '通用',
12
+ description: '开发流程 rules、Git hooks、跨栈 skill(dwy-shared / semver / 流程文档)',
13
+ ruleCategories: ['开发流程'],
14
+ hookCategories: ['Git'],
15
+ skillCategories: ['元工具', '开发流程'],
16
+ skillNames: ['dwy-semver'],
17
+ },
18
+ {
19
+ id: 'vue',
20
+ label: 'Vue',
21
+ description: 'Vue rules + dwy-eui / dwy-ekit / 全栈脚手架',
22
+ ruleCategories: ['Vue'],
23
+ skillNames: ['dwy-eui', 'dwy-ekit', 'dwy-fullstack-scaffold'],
24
+ },
25
+ {
26
+ id: 'python',
27
+ label: 'Python',
28
+ description: 'Python rules + dwy-eapi / 全栈脚手架',
29
+ ruleCategories: ['Python'],
30
+ skillNames: ['dwy-eapi', 'dwy-fullstack-scaffold'],
31
+ },
32
+ {
33
+ id: 'ios',
34
+ label: 'iOS',
35
+ description: 'iOS / Swift rules。上架 skill 在场景包「发布发版」',
36
+ ruleCategories: ['iOS'],
37
+ },
38
+ {
39
+ id: 'android',
40
+ label: 'Android',
41
+ description: 'Android rules',
42
+ ruleCategories: ['Android'],
43
+ },
44
+ {
45
+ id: 'flutter',
46
+ label: 'Flutter',
47
+ description: 'Flutter rules',
48
+ ruleCategories: ['Flutter'],
49
+ },
50
+ {
51
+ id: 'harmony',
52
+ label: 'HarmonyOS',
53
+ description: 'HarmonyOS / ArkTS rules',
54
+ ruleCategories: ['HarmonyOS'],
55
+ },
56
+ {
57
+ id: 'docker',
58
+ label: 'Docker / 运维',
59
+ description: 'Docker rule + 镜像/部署 skill',
60
+ ruleCategories: ['Docker'],
61
+ skillCategories: ['运维发布'],
62
+ },
63
+ {
64
+ id: 'database',
65
+ label: '数据库',
66
+ description: 'Postgres / Redis / 迁移 rules。DolphinDB skill 在场景包',
67
+ ruleCategories: ['数据库'],
68
+ },
69
+ ]
70
+
71
+ /** 场景包。第二屏单独问,允许全不选。 */
72
+ export const SCENE_PACKS = [
73
+ {
74
+ id: 'product-0to1',
75
+ label: '产品0到1',
76
+ description: '立项到上线的阶段 skill(launcher / PRD / 架构 / TDD…)',
77
+ skillCategories: ['产品0到1'],
78
+ },
79
+ {
80
+ id: 'media',
81
+ label: '自媒体',
82
+ description: '单位圆自媒体流水线 + 豆包配音',
83
+ skillCategories: ['自媒体'],
84
+ },
85
+ {
86
+ id: 'release',
87
+ label: '发布发版',
88
+ description: 'dwy-publish / App Store / SDK 检查 / semver / GitHub Action',
89
+ skillCategories: ['发布发版'],
90
+ },
91
+ {
92
+ id: 'security',
93
+ label: '安全',
94
+ description: '部署巡检与白帽渗透',
95
+ skillCategories: ['安全'],
96
+ },
97
+ {
98
+ id: 'article',
99
+ label: '内容创作',
100
+ description: '技术文章写作 skill',
101
+ skillCategories: ['内容创作'],
102
+ },
103
+ {
104
+ id: 'dolphindb',
105
+ label: 'DolphinDB',
106
+ description: 'DolphinDB 开发与审查 skill(体量大,不跟数据库栈绑定)',
107
+ skillNames: ['dwy-dolphindb'],
108
+ },
109
+ ]
110
+
111
+ const STACK_IDS = new Set(STACK_PACKS.map(pack => pack.id))
112
+ const SCENE_IDS = new Set(SCENE_PACKS.map(pack => pack.id))
113
+
114
+ /**
115
+ * 规范化 sync-state 里的 packs。未知 id 丢弃,避免旧状态撑爆交互默认勾选。
116
+ *
117
+ * @param {{ stacks?: string[], scenes?: string[] } | null | undefined} raw
118
+ */
119
+ export function normalizePacks(raw) {
120
+ return {
121
+ stacks: [...new Set((raw?.stacks || []).filter(id => STACK_IDS.has(id)))],
122
+ scenes: [...new Set((raw?.scenes || []).filter(id => SCENE_IDS.has(id)))],
123
+ }
124
+ }
125
+
126
+ /**
127
+ * 判断扫描项是否属于某个包。
128
+ * 约束:只看本类型对应的 category / 显式名字,避免 skill 目录名误伤 rules。
129
+ *
130
+ * @param {{ name: string, category?: string }} item
131
+ * @param {object} pack
132
+ * @param {'skills' | 'rules' | 'hooks'} type
133
+ */
134
+ function itemInPack(item, pack, type) {
135
+ if (type === 'skills') {
136
+ if ((pack.skillNames || []).includes(item.name)) return true
137
+ return (pack.skillCategories || []).includes(item.category)
138
+ }
139
+ if (type === 'rules') return (pack.ruleCategories || []).includes(item.category)
140
+ return (pack.hookCategories || []).includes(item.category)
141
+ }
142
+
143
+ /**
144
+ * 按名字去重合并,保持扫描原序。
145
+ *
146
+ * @param {Array<{ name: string }>} items
147
+ */
148
+ function uniqueByName(items) {
149
+ const seen = new Set()
150
+ const result = []
151
+ for (const item of items) {
152
+ if (seen.has(item.name)) continue
153
+ seen.add(item.name)
154
+ result.push(item)
155
+ }
156
+ return result
157
+ }
158
+
159
+ /**
160
+ * 把已选栈 + 场景包展开成 sync 用的条目。
161
+ * skillsOnly 时 rules/hooks 必须空数组,调用方不得据此删除项目已有项。
162
+ *
163
+ * @param {{ skills: object[], rules: object[], commands?: object[], hooks: object[] }} scans
164
+ * @param {{ stacks?: string[], scenes?: string[], skillsOnly?: boolean }} selection
165
+ */
166
+ export function expandPackSelection(scans, { stacks = [], scenes = [], skillsOnly = false } = {}) {
167
+ const packById = new Map([...STACK_PACKS, ...SCENE_PACKS].map(pack => [pack.id, pack]))
168
+ const selectedPacks = [...stacks, ...scenes]
169
+ .map(id => packById.get(id))
170
+ .filter(Boolean)
171
+
172
+ const pick = (type) => uniqueByName(
173
+ selectedPacks.flatMap(pack => (scans[type] || []).filter(item => itemInPack(item, pack, type))),
174
+ )
175
+
176
+ if (skillsOnly) {
177
+ return {
178
+ skills: pick('skills'),
179
+ rules: [],
180
+ commands: [],
181
+ hooks: [],
182
+ }
183
+ }
184
+
185
+ return {
186
+ skills: pick('skills'),
187
+ rules: pick('rules'),
188
+ commands: [],
189
+ hooks: pick('hooks'),
190
+ }
191
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * 按包勾选交互:技术栈包 → 场景包 → 确认/微调。
3
+ * 由 dwy / dwy sync 在「按技术栈 / 场景包」模式下调用。
4
+ */
5
+
6
+ import { isCancel } from '@clack/prompts'
7
+ import { SEARCH_PLACEHOLDER, searchableMultiselect, searchableSelect } from './searchable-select.js'
8
+ import { SCENE_PACKS, STACK_PACKS, expandPackSelection, normalizePacks } from './sync-packs.js'
9
+ import { promptSelection } from './sync.js'
10
+ import { chalk } from './utils.js'
11
+
12
+ /**
13
+ * 把包目录转成可搜索选项。
14
+ *
15
+ * @param {Array<{ id: string, label: string, description: string }>} packs
16
+ */
17
+ function packOptions(packs) {
18
+ return packs.map(pack => ({
19
+ value: pack.id,
20
+ label: pack.label,
21
+ description: pack.description,
22
+ }))
23
+ }
24
+
25
+ /**
26
+ * 打印包展开条数。确认步默认用这些预选项,微调再加减。
27
+ *
28
+ * @param {string[]} stacks
29
+ * @param {string[]} scenes
30
+ * @param {{ skills: object[], rules: object[], hooks: object[] }} selected
31
+ */
32
+ function printPackSummary(stacks, scenes, selected) {
33
+ const stackLabels = STACK_PACKS.filter(pack => stacks.includes(pack.id)).map(pack => pack.label)
34
+ const sceneLabels = SCENE_PACKS.filter(pack => scenes.includes(pack.id)).map(pack => pack.label)
35
+ const packText = [...stackLabels, ...sceneLabels].join(' + ') || '(未选包)'
36
+ console.log(chalk.gray('\n包展开:'))
37
+ console.log(chalk.gray(` ${packText}`))
38
+ console.log(chalk.gray(` Skills ${selected.skills.length} Rules ${selected.rules.length} Hooks ${selected.hooks.length}`))
39
+ }
40
+
41
+ /**
42
+ * 按包选择条目。
43
+ * 返回 { skills, rules, commands, hooks, packs } 或 null(取消)。
44
+ *
45
+ * @param {object} scans
46
+ * @param {{ packs?: { stacks?: string[], scenes?: string[] } }} syncState
47
+ * @param {{ skillsOnly?: boolean }} [opts]
48
+ */
49
+ export async function interactiveSelectByPacks(scans, syncState, { skillsOnly = false } = {}) {
50
+ const previous = normalizePacks(syncState?.packs)
51
+ let stacks = previous.stacks.length > 0 ? previous.stacks : ['common']
52
+ let scenes = previous.scenes
53
+ let step = 'stacks'
54
+ /** 微调后的展开结果;返回改包时丢弃,重新 expand。 */
55
+ let selected = null
56
+
57
+ while (true) {
58
+ if (step === 'stacks') {
59
+ const result = await searchableMultiselect({
60
+ message: '选择技术栈(通用默认勾选)',
61
+ options: packOptions(STACK_PACKS),
62
+ initialValues: stacks,
63
+ required: true,
64
+ maxItems: 12,
65
+ placeholder: SEARCH_PLACEHOLDER,
66
+ })
67
+ if (isCancel(result)) return null
68
+ stacks = result
69
+ selected = null
70
+
71
+ const nav = await searchableSelect({
72
+ message: `已选 ${stacks.length} 个技术栈。下一步:`,
73
+ options: [
74
+ { label: '继续选场景包', value: 'next' },
75
+ { label: '返回重选技术栈', value: 'back' },
76
+ { label: '取消同步', value: 'cancel' },
77
+ ],
78
+ initialValue: 'next',
79
+ placeholder: SEARCH_PLACEHOLDER,
80
+ })
81
+ if (isCancel(nav) || nav === 'cancel') return null
82
+ step = nav === 'back' ? 'stacks' : 'scenes'
83
+ continue
84
+ }
85
+
86
+ if (step === 'scenes') {
87
+ const result = await searchableMultiselect({
88
+ message: '选择场景包(可全不选)',
89
+ options: packOptions(SCENE_PACKS),
90
+ initialValues: scenes,
91
+ required: false,
92
+ maxItems: 10,
93
+ placeholder: SEARCH_PLACEHOLDER,
94
+ })
95
+ if (isCancel(result)) return null
96
+ scenes = result
97
+ selected = null
98
+ step = 'confirm'
99
+ continue
100
+ }
101
+
102
+ selected = selected || expandPackSelection(scans, { stacks, scenes, skillsOnly })
103
+ printPackSummary(stacks, scenes, selected)
104
+
105
+ const confirmOptions = [
106
+ { label: '确认提交(用上面勾好的)', value: 'confirm' },
107
+ { label: '微调 Skills(可加减)', value: 'skills' },
108
+ ...(!skillsOnly ? [
109
+ { label: '微调 Rules(可加减)', value: 'rules' },
110
+ { label: '微调 Hooks(可加减)', value: 'hooks' },
111
+ ] : []),
112
+ { label: '返回改技术栈', value: 'stacks' },
113
+ { label: '返回改场景包', value: 'scenes' },
114
+ { label: '取消同步', value: 'cancel' },
115
+ ]
116
+ const final = await searchableSelect({
117
+ message: '包已帮你勾好。确认提交,或微调加减条目:',
118
+ options: confirmOptions,
119
+ initialValue: 'confirm',
120
+ placeholder: SEARCH_PLACEHOLDER,
121
+ })
122
+ if (isCancel(final) || final === 'cancel') return null
123
+ if (final === 'confirm') {
124
+ return {
125
+ ...selected,
126
+ commands: [],
127
+ packs: { stacks, scenes },
128
+ }
129
+ }
130
+ if (final === 'stacks' || final === 'scenes') {
131
+ step = final
132
+ selected = null
133
+ continue
134
+ }
135
+
136
+ // 完整清单 + 包内项预勾:可减包内项,也可加包外项
137
+ const catalog = scans[final] || []
138
+ const tuned = await promptSelection(
139
+ catalog,
140
+ final === 'skills' ? 'Skills' : final === 'rules' ? 'Rules' : 'Hooks',
141
+ new Set(selected[final].map(item => item.name)),
142
+ )
143
+ if (tuned === null) return null
144
+ selected = { ...selected, [final]: tuned }
145
+ }
146
+ }
package/src/sync.js CHANGED
@@ -239,7 +239,15 @@ function buildSearchableOptions(items) {
239
239
  })
240
240
  }
241
241
 
242
- async function promptSelection(items, label, defaultNames) {
242
+ /**
243
+ * 可搜索多选一层。items 为空直接返回 [],不弹窗。
244
+ * 取消返回 null,调用方必须中止整次同步。
245
+ *
246
+ * @param {Array<{ name: string, category?: string, description?: string }>} items
247
+ * @param {string} label
248
+ * @param {Iterable<string>} defaultNames
249
+ */
250
+ export async function promptSelection(items, label, defaultNames) {
243
251
  if (items.length === 0) return []
244
252
  // 可搜索多选 + 底部说明区(聚焦项描述不跟在行尾)
245
253
  const selectedNames = await searchableMultiselect({
@@ -52,6 +52,38 @@
52
52
 
53
53
  ---
54
54
 
55
+ ## 0.13.1 — 2026-09-02
56
+
57
+ ### Changed
58
+
59
+ - 产品0到1 包装 skill:外部 skill 缺失时提示跑 `dwy` 选「刷新全局外部 skill」,不再写已删除的 `dwy skills install`
60
+
61
+ ---
62
+
63
+ ## 0.13.0 — 2026-09-02
64
+
65
+ ### Changed
66
+
67
+ - `skills/发布发版/dwy-ios-app-store-release`:送审后截图锁死,改图必须撤审 → 换图保序 → 新建 submission 再送;只补现网已有槽;What's New 能力现网图看不到则必须更新截图;凭据优先读仓库 `AGENTS.md` / `CLAUDE.md` 约定。`iphone_65` 补 `preferred: 1242×2688`
68
+
69
+ ---
70
+
71
+ ## 0.12.0 — 2026-09-02
72
+
73
+ ### Changed
74
+
75
+ - `rules/开发流程/dwy-dependency-freshness.md`:首次技术选型选版本必须只选正式版,禁止 beta / rc / alpha / preview / nightly 等预发布;AskUserQuestion 选项同样不列预发布号。7 天新鲜度约束仍在,两条同时满足
76
+
77
+ ---
78
+
79
+ ## 0.11.0 — 2026-08-18
80
+
81
+ ### Changed
82
+
83
+ - `rules/开发流程/dwy-git-commit.md`:提交前必须先 `git fetch` + `git pull --rebase --autostash`,冲突全部处理完才能 `git commit`(多人协作,禁止先提交再拉)
84
+
85
+ ---
86
+
55
87
  ## 0.10.0 — 2026-08-13
56
88
 
57
89
  ### Added
@@ -1 +1 @@
1
- 0.10.0
1
+ 0.13.1
@@ -23,6 +23,70 @@ paths:
23
23
  - 全屏背景、沉浸式遮罩可以延伸到系统栏区域,但前景可交互内容默认留在安全显示区内。
24
24
  - 禁止写死状态栏高度、导航栏高度、刘海高度,统一通过系统 inset 获取。
25
25
 
26
+ ## 控件选型(AppCompat)
27
+
28
+ XML 布局、代码 `new`、自定义 View:**只要存在 AppCompat 对应类,就必须用 AppCompat 版**,禁止 `android.widget` 平台控件。没有 AppCompat 对应类的布局容器 / 列表(LinearLayout、ConstraintLayout、RecyclerView 等)保持原样。
29
+
30
+ **理由**:平台控件在低版本没有矢量图 tint、自动夜间色、`?attr` 着色。`AppCompatActivity.setContentView` 虽会把 `<TextView>` 自动换成 `AppCompatTextView`,但 `LayoutInflater.from(context)`、`View.inflate`、非 AppCompat 上下文的 `<include>`、自定义 View 写 `extends TextView`,都会落到平台控件,静默丢兼容。写 AppCompat 类名,不依赖自动替换。
31
+
32
+ **包名禁止写死**:本规范只约束类名(`AppCompatTextView` / `SwitchCompat` 等)。XML 全限定包名、Kotlin/Java import 一律按当前项目已有控件推断,禁止把短类名当 XML 标签照抄,禁止未看项目写死 `androidx.*` 或 `android.support.*`。
33
+
34
+ **id 缩写不变**:`AppCompatTextView` 仍是 `tv_`,`SwitchCompat` 仍是 `sw_`,不要另造 `actv` / `sc`。页面基类用 `AppCompatActivity`,禁止 `android.app.Activity`。
35
+
36
+ ### 对照表(有则必须换)
37
+
38
+
39
+ | 禁止(平台) | 必须用 |
40
+ | ------------------------------ | ---------------------------------------------------------------------------------------- |
41
+ | TextView | AppCompatTextView |
42
+ | EditText | AppCompatEditText |
43
+ | Button | AppCompatButton |
44
+ | ImageView | AppCompatImageView |
45
+ | ImageButton | AppCompatImageButton |
46
+ | CheckBox | AppCompatCheckBox |
47
+ | RadioButton | AppCompatRadioButton |
48
+ | CheckedTextView | AppCompatCheckedTextView |
49
+ | AutoCompleteTextView | AppCompatAutoCompleteTextView |
50
+ | MultiAutoCompleteTextView | AppCompatMultiAutoCompleteTextView |
51
+ | Spinner | AppCompatSpinner |
52
+ | SeekBar | AppCompatSeekBar |
53
+ | RatingBar | AppCompatRatingBar |
54
+ | Switch | **SwitchCompat**(类名不是 AppCompatSwitch) |
55
+ | `android.widget.Toolbar` | appcompat 包下的 `Toolbar`(不是 `android.widget.Toolbar`) |
56
+
57
+
58
+ 表中未列、但依赖里能搜到 `AppCompatXxx` / `SwitchCompat` 的,同样必须用。搜不到才用平台类。
59
+
60
+ 无 AppCompat 对应、保持原样:LinearLayout / RelativeLayout / FrameLayout / TableLayout / ConstraintLayout / ScrollView / NestedScrollView / HorizontalScrollView / RecyclerView / ListView / GridView / ViewPager / WebView / CardView / View / Space。
61
+
62
+ ### 代码 `new` / 自定义 View
63
+
64
+ ```kotlin
65
+ // ❌
66
+ val btn = Button(context)
67
+ class PriceText(context: Context, attrs: AttributeSet?) : TextView(context, attrs)
68
+
69
+ // ✅
70
+ val btn = AppCompatButton(context)
71
+ class PriceText(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs)
72
+ ```
73
+
74
+ ### 例外(可观察条件)
75
+
76
+ - 依赖里搜不到 AppCompat 对应类 → 用原类
77
+ - 项目已在用 Material / Design 同功能控件 → 那些优先(`MaterialButton` 优于 `AppCompatButton`);无对应项仍用 AppCompat
78
+ - Jetpack Compose 组件不受本条约束
79
+
80
+ ### 禁止的借口
81
+
82
+
83
+ | 借口 | 事实 |
84
+ | -------------------------- | ----------------------------------------------------------- |
85
+ | XML 写 TextView 会被自动替换 | 仅 AppCompat 的 LayoutInflater Factory2 会替换;其它 inflate 路径不会 |
86
+ | 自定义 View 继承 TextView 即可 | 必须继承 AppCompatTextView,否则无 tint / 矢量 |
87
+ | Switch 没有 AppCompat 版 | 类名是 SwitchCompat |
88
+
89
+
26
90
  ## 一、命名规范
27
91
 
28
92
 
@@ -76,7 +140,7 @@ View id 规则:`<控件缩写>_<逻辑名>`,如 `tv_username`、`btn_submit`。
76
140
  | 容器 / 导航 | Toolbar=`toolbar`、TabLayout=`tab`、FloatingActionButton=`fab`、BottomNavigationView=`bnv`、AppBarLayout=`abl` |
77
141
 
78
142
 
79
- > 缩写冲突时取更长缩写消歧:`rbtn`(RadioButton) 区别于 `rb`(RatingBar)。表中无对应的控件,按"首字母 / 业务语义就近"自拟,组内保持一致。
143
+ > 缩写冲突时取更长缩写消歧:`rbtn`(RadioButton) 区别于 `rb`(RatingBar)。表中无对应的控件,按"首字母 / 业务语义就近"自拟,组内保持一致。AppCompat 控件 id 仍用对应平台缩写(`AppCompatTextView`=`tv`、`SwitchCompat`=`sw`),类名选型见上文「控件选型(AppCompat)」。
80
144
 
81
145
  #### drawable / selector 状态后缀
82
146
 
@@ -1033,6 +1097,7 @@ if (trimmed.isNotEmpty()) { ... }
1033
1097
  | Java 实体 `public` 实例字段 | `private` 字段 + `getXxx`/`setXxx` + `toString` |
1034
1098
  | Kotlin 实体 `@JvmField` / 无 `toString` 的普通 class 实体 | 优先 `data class` + `val`;或手写访问器 + `toString` |
1035
1099
  | `s.length() > 0` / `s.trim().length() > 0` 等用 length 判空 / 非空 | Java `!s.isEmpty()` / `!s.trim().isEmpty()`;Kotlin `s.isNotEmpty()` / `s.isNotBlank()`;详见本节「字符串空判断」 |
1100
+ | 平台 `TextView` / `Button` / `Switch` / 自定义 View 继承平台控件 | 改用 AppCompat 对应类(`SwitchCompat` 等),包名按项目推断;无对应类的布局容器保持原样;详见「控件选型(AppCompat)」 |
1036
1101
 
1037
1102
 
1038
1103
  ## 九、代码自检(写代码时强制执行)
@@ -1065,6 +1130,7 @@ if (trimmed.isNotEmpty()) { ... }
1065
1130
  | 21 | Gradle 脚本(Groovy / kts):锁版本 / `exclude` / 自定义 task / 变体 / 关键魔法值有 why 注释,无复述式、无注释掉的配置块 | ✓ |
1066
1131
  | 22 | 数据实体(Model/DTO/VO/Bean 等):Java 字段 `private` + get/set + `toString`;Kotlin 优先 `data class`+`val`(自带 toString),禁止 `@JvmField`,非 data class 必须手写 `toString` | ✓ |
1067
1132
  | 23 | 字符串空判断用 `isEmpty` / `isNotEmpty` / `isNotBlank`,禁止 `length() > 0` / `== 0` / `!= 0` 及 `trim().length() > 0` 判空;详见 §八「字符串空判断」 | ✓ |
1133
+ | 24 | 有 AppCompat 对应类的控件必须用 AppCompat 版(XML / `new` / 自定义 View 父类),包名按当前项目推断;Switch 用 `SwitchCompat`;无对应类的布局容器保持原样 | ✓ |
1068
1134
 
1069
1135
 
1070
1136
  **不执行自检就提交代码 = 违规。**
@@ -1,10 +1,10 @@
1
1
  ---
2
- description: 依赖版本新鲜度通用规则(首次技术选型下载依赖时,发布 <7 天的版本不采用;跨 npm/uv/Docker/Android/iOS/鸿蒙/Flutter 等所有栈)
2
+ description: 依赖版本选择通用规则(首次技术选型只选正式版、禁止 beta/rc 等预发布;发布 <7 天的版本不采用;跨 npm/uv/Docker/Android/iOS/鸿蒙/Flutter 等所有栈)
3
3
  ---
4
4
 
5
5
  # 依赖版本新鲜度规则
6
6
 
7
- > 首次技术选型下载依赖(引新包、选 base 镜像 / SDK 版本)时,刚发布的新版本常带回归 bug:构建失败、Docker 镜像缺层、API breaking change。本规则约束 AI 在装新依赖前主动查「版本发布时间」,避开 <7 天的版本。
7
+ > 首次技术选型下载依赖(引新包、选 base 镜像 / SDK 版本)时,预发布版(beta / rc / alpha)未冻结 API,刚发布的正式版也常带回归 bug。本规则约束 AI 在装新依赖前:先丢掉预发布,再查正式版「首次发布时间」,避开 <7 天的版本。
8
8
  >
9
9
  > 适用**所有技术栈**:npm/pnpm、uv/pip、Docker、Android(Gradle/Maven)、iOS(CocoaPods/SPM)、鸿蒙(ohpm)、Flutter(pub) 以及未来出现的任何包管理器。下面给出总约束 + 各栈示例,未列出的栈按「查官方 registry 发布时间」通用流程自行判断。
10
10
 
@@ -12,19 +12,24 @@ description: 依赖版本新鲜度通用规则(首次技术选型下载依赖
12
12
 
13
13
  - **仅约束首次技术选型下载依赖**:新项目引入新包、新搭脚手架选 base 镜像 / SDK 版本、原项目引入此前没用过的依赖。
14
14
  - **已安装的旧版本无需为此升级**。本规则只管「新装 / 新选」,不管「存量升级」。
15
- - 7 天是**下限排除条件**,不是「必须选最近一个 ≥7 天的版本」。最终选哪个稳定版由 AI 综合判断(成熟度、维护活跃度、依赖体积、与项目栈兼容性)。
15
+ - 7 天是**下限排除条件**,不是「必须选最近一个 ≥7 天的版本」。最终选哪个稳定正式版由 AI 综合判断(成熟度、维护活跃度、依赖体积、与项目栈兼容性)。
16
16
 
17
17
  ## 二、核心约束
18
18
 
19
- - **必须**在首次安装 / 选定版本前,查候选版本的「首次发布时间」
19
+ 候选版本必须**同时**满足下面两条,缺一不可。给用户的 AskUserQuestion 选项同样只列合格版本,禁止把预发布号摆上去让用户选。
20
+
21
+ - **必须只选正式版**
22
+ - **禁止**采用预发布:版本号或 tag 含 `beta` / `rc` / `alpha` / `preview` / `nightly` / `snapshot` / `canary` / `dev` / `experimental`,或 registry 标为 pre-release(npm dist-tag `next`/`beta`/`rc`、GitHub `prerelease: true`、PyPI 版本号带 `a`/`b`/`rc`)
23
+ - 先滤掉预发布,再进入 7 天判断;预发布即使发布超过 7 天也不用
24
+ - **必须**在首次安装 / 选定版本前,查候选**正式版**的「首次发布时间」
20
25
  - 用官方 registry 的发布时间,**不是**上传时间、**不是** `latest` / `stable` tag 时间
21
26
  - **禁止**采用发布时间距今 **<7 天**的版本
22
- - **禁止**无脑用 `@latest` / `latest` / `stable` / 浮动 tag / 不带版本号安装——这些随时可能落到刚发布的新版本上
27
+ - **禁止**无脑用 `@latest` / `latest` / `stable` / 浮动 tag / 不带版本号安装——这些随时可能落到刚发布的新版本或预发布通道上
23
28
  - 已安装版本**无需**为此规则升级
24
29
 
25
30
  ## 三、各栈查发布时间示例
26
31
 
27
- 未列出的栈,按「查官方 registry 元数据 → 取发布时间」自行处理。
32
+ 未列出的栈,按「查官方 registry 元数据 → 取发布时间」自行处理。查的对象必须是正式版,预发布条目直接跳过。
28
33
 
29
34
  | 栈 | 查发布时间方法 |
30
35
  |---|---|
@@ -40,20 +45,27 @@ description: 依赖版本新鲜度通用规则(首次技术选型下载依赖
40
45
  ## 四、正反例
41
46
 
42
47
  ```bash
43
- # 反例:直接装 latest,可能落到昨天才发的版本
48
+ # 反例:预发布,即使发布超过 7 天也不用
49
+ pnpm add some-pkg@1.3.0-beta.2
50
+ uv add some-pkg==2.0.0rc1
51
+ FROM node:23.0.0-rc
52
+ implementation 'com.example:lib:1.0.0-alpha'
53
+
54
+ # 反例:直接装 latest,可能落到昨天才发的正式版或预发布通道
44
55
  pnpm add some-pkg@latest
45
56
  uv add some-pkg # 不锁版本
46
57
  FROM node:latest # 随时被新镜像覆盖
47
58
  implementation 'com.example:lib:latest'
48
59
 
49
- # 正例:先查发布时间,选发布 ≥7 天的稳定版
50
- npm view some-pkg time --json # 看候选版本发布时间
51
- pnpm add some-pkg@1.2.3 # 1.2.3 发布已满 7 天
52
- FROM node:20.18.1-slim # 该 tag push 已满 7 天
60
+ # 正例:正式版,且发布 ≥7 天
61
+ npm view some-pkg time --json # 看候选正式版发布时间
62
+ pnpm add some-pkg@1.2.3 # 1.2.3 是正式版,发布已满 7 天
63
+ FROM node:20.18.1-slim # 该 tag 是正式版,push 已满 7 天
53
64
  ```
54
65
 
55
66
  ## 五、AI 自判提示
56
67
 
57
- - 栈未列全时,先找该栈的**官方 registry**(包管理器背后的中央仓库),查目标版本的发布时间字段。
58
- - 拿不到精确发布时间时,保守处理:选发布更早、更稳定的版本,不要选「最新」。
59
- - 7 天窗口内的新版本,**等待**而非采用,**无任何例外**(含安全补丁)。等满 7 天再用。
68
+ - 栈未列全时,先找该栈的**官方 registry**(包管理器背后的中央仓库),丢掉预发布条目,再查正式版发布时间字段。
69
+ - 拿不到精确发布时间时,保守处理:选发布更早、更稳定的正式版,不要选「最新」,更不要选 beta。
70
+ - 7 天窗口内的新正式版,**等待**而非采用,**无任何例外**(含安全补丁)。等满 7 天再用。
71
+ - 用户口头点名某个 beta / rc:**不要直接装**。说明本规则只选正式版,改给最近一个合格正式版让用户确认。