@openprd/cli 0.1.11 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.openprd/changes/adaptive-project-framing/.openprd.yaml +2 -0
- package/.openprd/changes/adaptive-project-framing/design.md +47 -0
- package/.openprd/changes/adaptive-project-framing/proposal.md +35 -0
- package/.openprd/changes/adaptive-project-framing/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/adaptive-project-framing/task-events.jsonl +20 -0
- package/.openprd/changes/adaptive-project-framing/tasks.md +340 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/.openprd.yaml +2 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/design.md +34 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/proposal.md +23 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/task-events.jsonl +14 -0
- package/.openprd/changes/docs-anti-pollution-and-design-direction-review/tasks.md +238 -0
- package/.openprd/changes/loop-isolated-worktree-commit/.openprd.yaml +2 -0
- package/.openprd/changes/loop-isolated-worktree-commit/design.md +53 -0
- package/.openprd/changes/loop-isolated-worktree-commit/proposal.md +38 -0
- package/.openprd/changes/loop-isolated-worktree-commit/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/loop-isolated-worktree-commit/task-events.jsonl +22 -0
- package/.openprd/changes/loop-isolated-worktree-commit/tasks.md +357 -0
- package/.openprd/changes/openprd/.openprd.yaml +2 -0
- package/.openprd/changes/openprd/design.md +46 -0
- package/.openprd/changes/openprd/proposal.md +36 -0
- package/.openprd/changes/openprd/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd/task-events.jsonl +23 -0
- package/.openprd/changes/openprd/tasks.md +242 -0
- package/.openprd/changes/openprd-generated-change/.openprd.yaml +2 -0
- package/.openprd/changes/openprd-generated-change/design.md +50 -0
- package/.openprd/changes/openprd-generated-change/proposal.md +37 -0
- package/.openprd/changes/openprd-generated-change/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/openprd-generated-change/task-events.jsonl +21 -0
- package/.openprd/changes/openprd-generated-change/tasks.md +357 -0
- package/.openprd/changes/pm-human-in-the-loop/.openprd.yaml +2 -0
- package/.openprd/changes/pm-human-in-the-loop/design.md +38 -0
- package/.openprd/changes/pm-human-in-the-loop/proposal.md +29 -0
- package/.openprd/changes/pm-human-in-the-loop/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/pm-human-in-the-loop/task-events.jsonl +18 -0
- package/.openprd/changes/pm-human-in-the-loop/tasks.md +306 -0
- package/.openprd/changes/test-strategy-router/.openprd.yaml +2 -0
- package/.openprd/changes/test-strategy-router/design.md +65 -0
- package/.openprd/changes/test-strategy-router/proposal.md +45 -0
- package/.openprd/changes/test-strategy-router/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/test-strategy-router/task-events.jsonl +1 -0
- package/.openprd/changes/test-strategy-router/tasks.md +132 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/.openprd.yaml +2 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/design.md +28 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/proposal.md +23 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/task-events.jsonl +11 -0
- package/.openprd/changes/verify-boundaries-and-historical-refresh/tasks.md +66 -0
- package/.openprd/changes/vibe-coding-dev-check/.openprd.yaml +2 -0
- package/.openprd/changes/vibe-coding-dev-check/design.md +37 -0
- package/.openprd/changes/vibe-coding-dev-check/proposal.md +28 -0
- package/.openprd/changes/vibe-coding-dev-check/specs/agent-requirements/spec.md +16 -0
- package/.openprd/changes/vibe-coding-dev-check/task-events.jsonl +18 -0
- package/.openprd/changes/vibe-coding-dev-check/tasks.md +306 -0
- package/.openprd/design/README.md +11 -6
- package/.openprd/design/README_EN.md +4 -1
- package/.openprd/design/active/asset-spec.md +6 -0
- package/.openprd/design/active/direction-plan.md +5 -5
- package/.openprd/design/active/selected-direction.md +2 -0
- package/.openprd/design/anti-slop.md +26 -1
- package/.openprd/design/assets/surface-presets.md +6 -0
- package/.openprd/design/checklists/ui-quality-gate.md +7 -0
- package/.openprd/design/lenses/frontend-lenses.md +28 -0
- package/.openprd/design/recipes/content-experience.md +8 -1
- package/.openprd/design/recipes/operational-tool.md +7 -0
- package/.openprd/design/recipes/product-launch.md +8 -1
- package/.openprd/design/templates/README.md +6 -3
- package/.openprd/design/templates/README_EN.md +6 -3
- package/.openprd/design/templates/ops-dashboard.html +8 -1
- package/.openprd/design/templates/product-launch.html +2 -1
- package/.openprd/design/themes/theme-catalog.json +23 -5
- package/.openprd/engagements/active/prd.md +137 -112
- package/.openprd/standards/config.json +7 -1
- package/AGENTS.md +13 -3
- package/README.md +127 -14
- package/README_EN.md +156 -13
- package/package.json +1 -1
- package/scripts/dev-check-wrapup-copy.mjs +46 -21
- package/skills/openprd-diagram-review/SKILL.md +27 -2
- package/skills/openprd-diagram-review/references/explanation-svg-patterns.md +131 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-frontend-design/SKILL.md +44 -20
- package/skills/openprd-frontend-design/references/design-asset-contract.md +6 -0
- package/skills/openprd-frontend-design/references/direction-engine.md +29 -4
- package/skills/openprd-harness/SKILL.md +22 -16
- package/skills/openprd-quality/SKILL.md +10 -8
- package/skills/openprd-requirement-intake/SKILL.md +2 -0
- package/skills/openprd-router/SKILL.md +2 -1
- package/skills/openprd-shared/SKILL.md +26 -16
- package/skills/openprd-standards/SKILL.md +4 -2
- package/src/agent-canonical-content.js +810 -0
- package/src/agent-integration.js +258 -777
- package/src/canvas-app-client-app.js +838 -0
- package/src/canvas-app-client-helpers.js +1138 -0
- package/src/canvas-app-styles.js +898 -0
- package/src/canvas-app.html.js +135 -0
- package/src/canvas-binding.js +414 -0
- package/src/canvas-codex-relay.js +430 -0
- package/src/canvas-i18n.js +282 -0
- package/src/canvas-scene.js +377 -0
- package/src/canvas-session-store.js +148 -0
- package/src/canvas-workspace.js +1264 -0
- package/src/cli/args.js +72 -4
- package/src/cli/basic-print.js +3 -0
- package/src/cli/canvas-print.js +106 -0
- package/src/cli/doctor-print.js +20 -0
- package/src/cli/print.js +2 -0
- package/src/cli/quality-commands.js +222 -0
- package/src/cli/quality-print.js +28 -2
- package/src/cli/run-print.js +5 -0
- package/src/cli/workflow-print.js +3 -0
- package/src/codex-app-server-relay.js +251 -0
- package/src/codex-hook-runner-template.mjs +978 -58
- package/src/design-starter-support.js +2 -2
- package/src/design-starter.js +39 -3
- package/src/dev-standards.js +125 -10
- package/src/diagram-core.js +6 -20
- package/src/discovery.js +2 -8
- package/src/docs-compact.js +125 -0
- package/src/execution-strategy.js +1 -1
- package/src/fleet.js +9 -3
- package/src/guide.js +227 -0
- package/src/html-artifacts.js +428 -7
- package/src/knowledge.js +45 -11
- package/src/language-policy.js +63 -3
- package/src/learning-review.js +1 -1
- package/src/loop.js +1 -1
- package/src/openprd.js +68 -131
- package/src/openspec/change-validate.js +2 -2
- package/src/openspec/constants.js +6 -7
- package/src/openspec/execute.js +2 -2
- package/src/openspec/generate.js +318 -102
- package/src/openspec/migration.js +168 -0
- package/src/openspec/paths.js +1 -22
- package/src/prd-core.js +1 -1
- package/src/quality-visual-review.js +257 -14
- package/src/quality.js +46 -8
- package/src/review-presentation.js +60 -0
- package/src/run-harness.js +41 -3
- package/src/session-evidence.js +363 -0
- package/src/standards.js +64 -0
- package/src/test-strategy.js +7 -4
- package/src/visual-compare-centering.js +622 -0
- package/src/visual-compare-core.js +1123 -0
- package/src/visual-compare.js +620 -698
- package/src/workspace-core.js +94 -4
- package/src/workspace-workflow.js +46 -1
|
@@ -157,8 +157,8 @@ function normalizeTheme(theme) {
|
|
|
157
157
|
}
|
|
158
158
|
return {
|
|
159
159
|
...theme,
|
|
160
|
-
bodyFont: String(theme.bodyFont ?? '"
|
|
161
|
-
headlineFont: String(theme.headlineFont ?? '"
|
|
160
|
+
bodyFont: String(theme.bodyFont ?? '"Avenir Next", "Noto Sans SC", "PingFang SC", sans-serif'),
|
|
161
|
+
headlineFont: String(theme.headlineFont ?? '"Avenir Next", "Noto Sans SC", "PingFang SC", sans-serif'),
|
|
162
162
|
surfacePreset: String(theme.surfacePreset ?? 'paper-grid'),
|
|
163
163
|
};
|
|
164
164
|
}
|
package/src/design-starter.js
CHANGED
|
@@ -41,6 +41,8 @@ const STARTER_CATALOG = [
|
|
|
41
41
|
lens: 'editorial-contrast',
|
|
42
42
|
theme: 'warm-editorial',
|
|
43
43
|
layout: 'story-map',
|
|
44
|
+
aesthetic: '温暖编辑式内容首页,用标题字体、图文节奏和章节停顿建立可长期阅读的气质。',
|
|
45
|
+
memoryPoint: '第一眼记住清楚的主题入口和像杂志目录一样的内容节奏。',
|
|
44
46
|
components: ['editorial-hero', 'stat-row', 'story-section', 'feature-grid', 'timeline', 'cta-banner'],
|
|
45
47
|
},
|
|
46
48
|
directions: [
|
|
@@ -50,6 +52,8 @@ const STARTER_CATALOG = [
|
|
|
50
52
|
lens: 'editorial-contrast',
|
|
51
53
|
theme: 'warm-editorial',
|
|
52
54
|
layout: 'story-map',
|
|
55
|
+
aesthetic: '温暖编辑感,靠标题字体和章节节奏组织内容。',
|
|
56
|
+
memoryPoint: '像一本打开的专题目录,用户知道从哪里开始读。',
|
|
53
57
|
suitable: '长期写作首页、专题内容页、个人博客首页',
|
|
54
58
|
risk: '如果信息密度控制不好,首屏会显得偏满。',
|
|
55
59
|
},
|
|
@@ -59,6 +63,8 @@ const STARTER_CATALOG = [
|
|
|
59
63
|
lens: 'catalog-clarity',
|
|
60
64
|
theme: 'quiet-sand',
|
|
61
65
|
layout: 'editorial-rail',
|
|
66
|
+
aesthetic: '克制资料库感,靠清楚分类和可比较信息单元建立秩序。',
|
|
67
|
+
memoryPoint: '用户能快速回到分类、筛选和上一次浏览位置。',
|
|
62
68
|
suitable: '知识整理页、文章归档页、资源导航页',
|
|
63
69
|
risk: '如果目录层级太重,会削弱阅读感。',
|
|
64
70
|
},
|
|
@@ -68,6 +74,8 @@ const STARTER_CATALOG = [
|
|
|
68
74
|
lens: 'immersive-showcase',
|
|
69
75
|
theme: 'deep-launch',
|
|
70
76
|
layout: 'story-map',
|
|
77
|
+
aesthetic: '沉浸展示感,靠强主视觉和少量文字信号制造记忆。',
|
|
78
|
+
memoryPoint: '首屏真实场景或关键视觉成为页面锚点。',
|
|
71
79
|
suitable: '需要更强个人风格的内容展示页',
|
|
72
80
|
risk: '容易更像展示页而不是稳定阅读首页。',
|
|
73
81
|
},
|
|
@@ -84,6 +92,8 @@ const STARTER_CATALOG = [
|
|
|
84
92
|
lens: 'product-launch',
|
|
85
93
|
theme: 'deep-launch',
|
|
86
94
|
layout: 'product-spec-runway',
|
|
95
|
+
aesthetic: '暗色发布叙事,用产品状态、规格跑道和强对比动作建立发布感。',
|
|
96
|
+
memoryPoint: '用户记住一个明确承诺和一个可验证的产品状态。',
|
|
87
97
|
components: ['spec-hero', 'stat-row', 'feature-grid', 'logo-wall', 'cta-banner'],
|
|
88
98
|
},
|
|
89
99
|
directions: [
|
|
@@ -93,6 +103,8 @@ const STARTER_CATALOG = [
|
|
|
93
103
|
lens: 'product-launch',
|
|
94
104
|
theme: 'deep-launch',
|
|
95
105
|
layout: 'product-spec-runway',
|
|
106
|
+
aesthetic: '深色电影感发布页,强调新能力、证明材料和决策动作。',
|
|
107
|
+
memoryPoint: '一个强主视觉承载发布承诺,而不是一组泛泛功能卡片。',
|
|
96
108
|
suitable: '正式发布页、版本更新页、关键能力介绍页',
|
|
97
109
|
risk: '如果证据不足,会只剩氛围没有事实。',
|
|
98
110
|
},
|
|
@@ -102,6 +114,8 @@ const STARTER_CATALOG = [
|
|
|
102
114
|
lens: 'editorial-contrast',
|
|
103
115
|
theme: 'warm-editorial',
|
|
104
116
|
layout: 'pricing-and-proof',
|
|
117
|
+
aesthetic: '编辑型解释页,降低销售感,用证据、价格或适用对象帮助决策。',
|
|
118
|
+
memoryPoint: '用户记住“为什么现在值得用”和“适合谁”。',
|
|
105
119
|
suitable: '要兼顾解释与决策的介绍页',
|
|
106
120
|
risk: '可能不够强势,发布感会偏弱。',
|
|
107
121
|
},
|
|
@@ -111,6 +125,8 @@ const STARTER_CATALOG = [
|
|
|
111
125
|
lens: 'immersive-showcase',
|
|
112
126
|
theme: 'deep-launch',
|
|
113
127
|
layout: 'immersive-hero-stack',
|
|
128
|
+
aesthetic: '沉浸式产品故事,靠媒体层级和少量强文本拉开情绪。',
|
|
129
|
+
memoryPoint: '首屏像一段产品片头,但仍保留下一步动作。',
|
|
114
130
|
suitable: '视觉驱动型发布叙事',
|
|
115
131
|
risk: '容易把页面做成纯展示而削弱可扫描性。',
|
|
116
132
|
},
|
|
@@ -127,6 +143,8 @@ const STARTER_CATALOG = [
|
|
|
127
143
|
lens: 'operational-density',
|
|
128
144
|
theme: 'tool-neutral',
|
|
129
145
|
layout: 'ops-density-grid',
|
|
146
|
+
aesthetic: '安静工作台,用稳定密度、清楚状态和短路径体现专业感。',
|
|
147
|
+
memoryPoint: '用户记住这是一个能反复使用、快速扫描的控制台。',
|
|
130
148
|
components: ['filter-rail', 'summary-panel', 'dense-table', 'settings-group'],
|
|
131
149
|
},
|
|
132
150
|
directions: [
|
|
@@ -136,6 +154,8 @@ const STARTER_CATALOG = [
|
|
|
136
154
|
lens: 'operational-density',
|
|
137
155
|
theme: 'tool-neutral',
|
|
138
156
|
layout: 'ops-density-grid',
|
|
157
|
+
aesthetic: '克制工具感,靠密度、对齐、状态和控件层级让用户快速完成工作。',
|
|
158
|
+
memoryPoint: '首屏一眼知道待处理、关键指标和下一步动作。',
|
|
139
159
|
suitable: '后台首页、运营看板、任务工作台',
|
|
140
160
|
risk: '如果主次不清,扫描效率会下降。',
|
|
141
161
|
},
|
|
@@ -145,6 +165,8 @@ const STARTER_CATALOG = [
|
|
|
145
165
|
lens: 'catalog-clarity',
|
|
146
166
|
theme: 'quiet-sand',
|
|
147
167
|
layout: 'catalog-browser',
|
|
168
|
+
aesthetic: '资料浏览器感,靠分类、筛选和可比较对象减少重复判断。',
|
|
169
|
+
memoryPoint: '用户记住清楚的浏览结构和稳定的回看路径。',
|
|
148
170
|
suitable: '对象浏览与筛选更重的系统首页',
|
|
149
171
|
risk: '容易太像列表页,动作感不足。',
|
|
150
172
|
},
|
|
@@ -154,6 +176,8 @@ const STARTER_CATALOG = [
|
|
|
154
176
|
lens: 'operational-density',
|
|
155
177
|
theme: 'deep-launch',
|
|
156
178
|
layout: 'settings-split',
|
|
179
|
+
aesthetic: '深色控制中心感,适合高风险配置和集中监控。',
|
|
180
|
+
memoryPoint: '关键状态像仪表盘一样醒目,但操作仍克制。',
|
|
157
181
|
suitable: '偏控制中心与配置中枢的场景',
|
|
158
182
|
risk: '暗色高密度更考验层级控制。',
|
|
159
183
|
},
|
|
@@ -269,6 +293,7 @@ function buildStarterContext(starter, defaults, options = {}) {
|
|
|
269
293
|
noRealImages: Boolean(options.noRealImages),
|
|
270
294
|
themeConfig: null,
|
|
271
295
|
imageBundle: null,
|
|
296
|
+
selectedDirection: starter.selectedDirection,
|
|
272
297
|
};
|
|
273
298
|
}
|
|
274
299
|
|
|
@@ -383,8 +408,12 @@ function buildGenericAssetSpec(context) {
|
|
|
383
408
|
| logo | ${normalizePathForMarkdown(context.noBrandAssets ? '无外部 logo,优先用文字字标' : '待补品牌字标或官方 logo')} | ${context.noBrandAssets ? 'n/a' : '待确认'} | ${context.noBrandAssets ? 'not-needed' : 'defined'} | ${normalizePathForMarkdown(assetLine)} |
|
|
384
409
|
| 产品图 | ${normalizePathForMarkdown(context.noBrandAssets ? '无' : '按任务需要补产品图')} | ${context.noBrandAssets ? 'n/a' : '待确认'} | ${context.noBrandAssets ? 'not-needed' : 'defined'} | ${normalizePathForMarkdown(imageLine)} |
|
|
385
410
|
| UI 图 | ${normalizePathForMarkdown(context.starterId === 'ops-dashboard' ? '如需现有界面截图再补' : '无必需 UI 截图')} | ${context.starterId === 'ops-dashboard' ? '待确认' : 'n/a'} | ${context.starterId === 'ops-dashboard' ? 'defined' : 'not-needed'} | |
|
|
411
|
+
| 功能图标 | ${normalizePathForMarkdown('新入口/按钮/空态默认配图标;先复用项目已有图标体系,缺失时按最佳实践路由自动补')} | ${normalizePathForMarkdown('UI 图标 Phosphor;实现库 Lucide/Tabler/React Icons;AI 品牌 LobeHub;技术栈 Tech Icons')} | defined | 接入后把图标名与用途登记到本行备注,不等用户要求 |
|
|
386
412
|
| 摄影 / 插图 | ${normalizePathForMarkdown(context.noRealImages ? '无' : '按任务需要补真实图片或插图')} | ${context.noRealImages ? 'n/a' : '待确认'} | ${context.noRealImages ? 'not-needed' : 'defined'} | ${normalizePathForMarkdown(imageLine)} |
|
|
387
413
|
| 色板 / 字体 | ${normalizePathForMarkdown(`${context.defaults.theme ?? 'starter-default'} 对应的默认 tokens`)} | implementation | defined | 先锁定 starter 默认组合,再按页面细化 |
|
|
414
|
+
| 动效节奏 | ${normalizePathForMarkdown('一段关键 reveal / 状态反馈 / 滚动节奏,按审美主张控制复杂度')} | implementation | defined | 不到处散落无意义 hover |
|
|
415
|
+
| 背景 / 表面 | ${normalizePathForMarkdown(`${context.defaults.theme ?? 'starter-default'} 对应表面预设`)} | implementation | defined | 背景和纹理服务内容,不替代真实素材 |
|
|
416
|
+
| 构图记忆点 | ${normalizePathForMarkdown(context.selectedDirection?.memoryPoint ?? '按当前方向继续补记忆点')} | implementation | defined | 进入 Patch Mode 时优先保住 |
|
|
388
417
|
|
|
389
418
|
${fetchedImageRows.length > 0 ? `
|
|
390
419
|
## 已补真实图片
|
|
@@ -399,6 +428,8 @@ ${fetchedImageRows.join('\n')}
|
|
|
399
428
|
- lens: ${context.defaults.lens ?? context.starterId}
|
|
400
429
|
- theme: ${context.defaults.theme ?? '待补'}
|
|
401
430
|
- layout: ${context.defaults.layout ?? '待补'}
|
|
431
|
+
- aesthetic: ${normalizePathForMarkdown(context.selectedDirection?.aesthetic ?? '待补')}
|
|
432
|
+
- memory-point: ${normalizePathForMarkdown(context.selectedDirection?.memoryPoint ?? '待补')}
|
|
402
433
|
${imageBundle?.succeeded ? `- image-query: ${normalizePathForMarkdown(imageBundle.query)}\n- image-manifest: ${normalizePathForMarkdown(imageBundle.relativeManifestPath ?? '待补')}` : ''}
|
|
403
434
|
`;
|
|
404
435
|
}
|
|
@@ -456,11 +487,11 @@ function buildGenericImagePreflight(context) {
|
|
|
456
487
|
}
|
|
457
488
|
|
|
458
489
|
function buildGenericDirectionPlan(starter, context) {
|
|
459
|
-
const lines = starter.directions.map((direction) => `| ${direction.label} | ${direction.logic} | ${direction.lens} | ${direction.theme} | ${direction.layout} | ${normalizePathForMarkdown(`${context.brief} 下刻意拉开气质和信息组织方式`)} | ${normalizePathForMarkdown(direction.suitable)} | ${normalizePathForMarkdown(direction.risk)} |`);
|
|
490
|
+
const lines = starter.directions.map((direction) => `| ${direction.label} | ${direction.logic} | ${direction.lens} | ${direction.theme} | ${direction.layout} | ${normalizePathForMarkdown(direction.aesthetic ?? `${context.brief} 下刻意拉开气质和信息组织方式`)} | ${normalizePathForMarkdown(direction.memoryPoint ?? '当前方向的首屏视觉锚点和任务路径')} | ${normalizePathForMarkdown(`${context.brief} 下刻意拉开气质和信息组织方式`)} | ${normalizePathForMarkdown(direction.suitable)} | ${normalizePathForMarkdown(direction.risk)} |`);
|
|
460
491
|
return `# Direction Plan
|
|
461
492
|
|
|
462
|
-
| 方向 | 生成逻辑 | lens | theme | layout | 为什么不同 | 适用场景 | 主要风险 |
|
|
463
|
-
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
493
|
+
| 方向 | 生成逻辑 | lens | theme | layout | 审美主张 | 记忆点 | 为什么不同 | 适用场景 | 主要风险 |
|
|
494
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
464
495
|
${lines.join('\n')}
|
|
465
496
|
`;
|
|
466
497
|
}
|
|
@@ -474,11 +505,14 @@ function buildGenericSelectedDirection(starter) {
|
|
|
474
505
|
- lens: ${selected.lens}
|
|
475
506
|
- theme: ${selected.theme}
|
|
476
507
|
- layout: ${selected.layout}
|
|
508
|
+
- aesthetic: ${normalizePathForMarkdown(selected.aesthetic ?? '当前方向需要在实现前补充审美主张')}
|
|
509
|
+
- memory-point: ${normalizePathForMarkdown(selected.memoryPoint ?? '当前方向需要在实现前补充记忆点')}
|
|
477
510
|
- components:
|
|
478
511
|
${selected.components.map((component) => ` - ${component}`).join('\n')}
|
|
479
512
|
- follow-up risks:
|
|
480
513
|
- 进入实现时继续控制信息密度,避免局部突然换轨
|
|
481
514
|
- 页面文案要保留当前任务语气,不要把模板示例话术原样带入交付
|
|
515
|
+
- 进入 Patch Mode 前用 anti-slop 自检,避免回到通用字体、紫白渐变或均匀卡片堆叠
|
|
482
516
|
`;
|
|
483
517
|
}
|
|
484
518
|
|
|
@@ -586,6 +620,8 @@ function buildBasicDocMap(projectRoot, starter, context, relativeOutputPath) {
|
|
|
586
620
|
- 把最后一批必要的查事实、查图、读模板动作放在口头宣布之前做完;一旦已经说“开始覆盖入口文件”或“开始整页重写”,下一步必须是对 \`${relativeOutputPath}\` 或 sibling draft 的实际写入,不要继续只读扫描、压图或停在口头承诺;必要时 hook 会把这类非写入动作挡回去。
|
|
587
621
|
- 主要区块固定为: ${sections.join('、')}。
|
|
588
622
|
- 当前视觉方向锁定为 \`${context.defaults.lens ?? starter.id}\`,主题采用 \`${context.defaults.theme ?? 'starter-default'}\`,页面骨架采用 \`${context.defaults.layout ?? 'starter-layout'}\`。
|
|
623
|
+
- 当前审美主张和记忆点记录在 \`.openprd/design/active/selected-direction.md\`;后续样式细化必须保住这个气质,而不是回到通用模板。
|
|
624
|
+
- 进入样式细化前,用 \`.openprd/design/anti-slop.md\` 排除默认紫白渐变、通用字体栈、白底卡片堆叠和无语境装饰。
|
|
589
625
|
- 优先使用稳定的信息层级、卡片、标签、导航锚点和纯文字表达;不通过花哨装饰替代真实结构。
|
|
590
626
|
|
|
591
627
|
## 交互规范
|
package/src/dev-standards.js
CHANGED
|
@@ -8,7 +8,8 @@ import { recordKnowledgeReviewSignal, reviewKnowledgeWorkspace } from './knowled
|
|
|
8
8
|
|
|
9
9
|
const DEVELOPMENT_STANDARDS_CONFIG = cjoin('.openprd', 'standards', 'config.json');
|
|
10
10
|
const DEV_CHECK_WRAPUP_COPY_SCRIPT = fileURLToPath(new URL('../scripts/dev-check-wrapup-copy.mjs', import.meta.url));
|
|
11
|
-
const
|
|
11
|
+
const DEV_CHECK_WRAPUP_FIELDS_AUTO = ['源文件规模', '优化原因', '本次处理结果', '后续建议'];
|
|
12
|
+
const DEV_CHECK_WRAPUP_FIELDS_RECOMMEND = ['规模信号', '预警原因', '本次处理结果', '后续建议'];
|
|
12
13
|
const CODE_FILE_EXTENSIONS = new Set([
|
|
13
14
|
'.c',
|
|
14
15
|
'.cc',
|
|
@@ -68,8 +69,25 @@ export const DEFAULT_DEVELOPMENT_STANDARDS = {
|
|
|
68
69
|
attentionMax: 1500,
|
|
69
70
|
appliesTo: 'agent-touched-code-files',
|
|
70
71
|
},
|
|
72
|
+
autoRefactor: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
},
|
|
71
75
|
};
|
|
72
76
|
|
|
77
|
+
const UI_FILE_EXTENSIONS = new Set([
|
|
78
|
+
'.css',
|
|
79
|
+
'.html',
|
|
80
|
+
'.jsx',
|
|
81
|
+
'.scss',
|
|
82
|
+
'.svelte',
|
|
83
|
+
'.tsx',
|
|
84
|
+
'.vue',
|
|
85
|
+
'.wxml',
|
|
86
|
+
'.wxss',
|
|
87
|
+
]);
|
|
88
|
+
const VISUAL_REVIEWS_DIR = cjoin('.openprd', 'harness', 'visual-reviews');
|
|
89
|
+
const VISUAL_EVIDENCE_FRESH_MS = 24 * 60 * 60 * 1000;
|
|
90
|
+
|
|
73
91
|
function normalizePathForReport(value) {
|
|
74
92
|
return String(value ?? '').split(path.sep).join('/');
|
|
75
93
|
}
|
|
@@ -167,6 +185,7 @@ function normalizeLineConfig(config = {}) {
|
|
|
167
185
|
exemptFilePatterns: [...EXEMPT_FILE_PATTERNS, ...customPatterns],
|
|
168
186
|
growthEnabled: config?.growth?.enabled !== false,
|
|
169
187
|
growthAutoApply: config?.growth?.autoApply,
|
|
188
|
+
autoRefactorEnabled: (config?.developmentStandards?.autoRefactor ?? config?.autoRefactor)?.enabled !== false,
|
|
170
189
|
};
|
|
171
190
|
}
|
|
172
191
|
|
|
@@ -212,13 +231,20 @@ function fileStatus(lineCount, lineConfig) {
|
|
|
212
231
|
}
|
|
213
232
|
|
|
214
233
|
function nextActionForStatus(status, lineConfig) {
|
|
234
|
+
const autoMode = lineConfig?.autoRefactorEnabled !== false;
|
|
215
235
|
if (status === 'ok') {
|
|
216
236
|
return `本轮没有显著维护风险;最终回复可简要说明已回顾本次改动文件。`;
|
|
217
237
|
}
|
|
218
238
|
if (status === 'attention') {
|
|
239
|
+
if (autoMode) {
|
|
240
|
+
return `本轮若为该文件新增了职责,直接按入口、状态、渲染或数据边界完成小步拆分(保持行为不变、测试通过);若只是小修,说明没有继续扩展职责即可。`;
|
|
241
|
+
}
|
|
219
242
|
return `本轮只做当前目标相关的小范围改动,并说明没有继续扩展该文件职责。`;
|
|
220
243
|
}
|
|
221
244
|
if (status === 'warning') {
|
|
245
|
+
if (autoMode) {
|
|
246
|
+
return `本轮直接执行拆分:先确认现有测试可保住行为,再按职责边界拆出独立模块(高内聚低耦合、不丢任何功能),拆完重跑测试与 dev-check,并把拆分产出写进最终回复的“OpenPRD 自动优化报告”。`;
|
|
247
|
+
}
|
|
222
248
|
return `先判断这次是否新增职责;如果新增了,优先拆分或解耦后再收尾;如果只是小修,说明暂不拆的原因和后续拆分建议。`;
|
|
223
249
|
}
|
|
224
250
|
if (status === 'exempt') {
|
|
@@ -321,9 +347,10 @@ function countDisplayChars(value) {
|
|
|
321
347
|
return Array.from(String(value ?? '')).length;
|
|
322
348
|
}
|
|
323
349
|
|
|
324
|
-
function buildCompactWrapUpRows(files) {
|
|
350
|
+
function buildCompactWrapUpRows(files, options = {}) {
|
|
325
351
|
const result = spawnSync(process.execPath, [DEV_CHECK_WRAPUP_COPY_SCRIPT], {
|
|
326
352
|
input: JSON.stringify({
|
|
353
|
+
autoRefactor: Boolean(options.autoRefactor),
|
|
327
354
|
files: files.map((file) => ({
|
|
328
355
|
status: file.status,
|
|
329
356
|
lineCount: file.lineCount,
|
|
@@ -351,8 +378,9 @@ function buildCompactWrapUpRows(files) {
|
|
|
351
378
|
if (!rows || rows.length !== files.length) {
|
|
352
379
|
throw new Error('Compact dev-check wrap-up copy returned an unexpected row count.');
|
|
353
380
|
}
|
|
381
|
+
const expectedFields = options.autoRefactor ? DEV_CHECK_WRAPUP_FIELDS_AUTO : DEV_CHECK_WRAPUP_FIELDS_RECOMMEND;
|
|
354
382
|
for (const row of rows) {
|
|
355
|
-
for (const field of
|
|
383
|
+
for (const field of expectedFields) {
|
|
356
384
|
if (typeof row[field] !== 'string') {
|
|
357
385
|
throw new Error(`Compact dev-check wrap-up copy is missing field: ${field}`);
|
|
358
386
|
}
|
|
@@ -364,29 +392,78 @@ function buildCompactWrapUpRows(files) {
|
|
|
364
392
|
return rows;
|
|
365
393
|
}
|
|
366
394
|
|
|
367
|
-
function buildDevCheckWrapUp(files) {
|
|
368
|
-
const
|
|
369
|
-
const
|
|
395
|
+
function buildDevCheckWrapUp(files, lineConfig = {}) {
|
|
396
|
+
const autoMode = lineConfig?.autoRefactorEnabled !== false;
|
|
397
|
+
const title = autoMode ? 'OpenPRD 自动优化报告' : '后续建议';
|
|
398
|
+
const statusColumn = autoMode ? '问题级别' : '关注程度';
|
|
399
|
+
const columns = autoMode
|
|
400
|
+
? ['影响对象', '问题级别', '源文件规模', '优化原因', '本次处理结果', '后续建议']
|
|
401
|
+
: ['影响对象', '关注程度', '规模信号', '预警原因', '本次处理结果', '后续建议'];
|
|
370
402
|
const attentionFiles = files
|
|
371
403
|
.filter((file) => ['attention', 'warning', 'error'].includes(file.status))
|
|
372
404
|
.sort((left, right) => devCheckConcernRank(right) - devCheckConcernRank(left));
|
|
373
|
-
const compactRows = attentionFiles.length > 0
|
|
405
|
+
const compactRows = attentionFiles.length > 0
|
|
406
|
+
? buildCompactWrapUpRows(attentionFiles, { autoRefactor: autoMode })
|
|
407
|
+
: [];
|
|
374
408
|
const rows = attentionFiles.map((file, index) => ({
|
|
375
409
|
影响对象: file.path,
|
|
376
|
-
|
|
410
|
+
[statusColumn]: devCheckStatusLabel(file.status),
|
|
377
411
|
...compactRows[index],
|
|
378
412
|
}));
|
|
379
413
|
const markdownTable = renderMarkdownTable(columns, rows);
|
|
414
|
+
const switchHint = autoMode
|
|
415
|
+
? '自动代码结构优化当前默认开启。如果下次不需要 Agent 自动优化,回复“关闭自动优化”,Agent 会运行 `openprd dev-check . --auto-refactor off` 转为仅推荐。'
|
|
416
|
+
: '自动代码结构优化当前已关闭(仅推荐模式)。要恢复 Agent 自动优化,运行 `openprd dev-check . --auto-refactor on`。';
|
|
380
417
|
return {
|
|
381
418
|
required: rows.length > 0,
|
|
419
|
+
mode: autoMode ? 'auto-refactor' : 'recommend',
|
|
382
420
|
reason: rows.length > 0
|
|
383
|
-
?
|
|
421
|
+
? (autoMode
|
|
422
|
+
? '存在超行影响对象;本轮 Agent 应先按“本次处理结果”完成高内聚低耦合拆分(不丢功能、测试保住行为),再在最终回复用“OpenPRD 自动优化报告”呈现优化完成的结果,并附一句自动优化开关确认。'
|
|
423
|
+
: '存在需要用户关注的影响对象;最终回复需要用“后续建议”说明本次处理结果和后续建议。')
|
|
384
424
|
: '本轮改动文件未触发影响范围提醒;最终回复可简要说明已完成改动对象回顾。',
|
|
385
425
|
title,
|
|
386
426
|
columns,
|
|
387
427
|
rows,
|
|
388
428
|
markdownTable,
|
|
389
429
|
markdownBlock: markdownTable ? `**${title}**\n\n${markdownTable}` : '',
|
|
430
|
+
switchHint: rows.length > 0 ? switchHint : null,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function buildVisualEvidenceReminder(projectRoot, files) {
|
|
435
|
+
const uiFiles = files
|
|
436
|
+
.filter((file) => file.status !== 'error' && UI_FILE_EXTENSIONS.has(path.extname(file.path).toLowerCase()))
|
|
437
|
+
.map((file) => file.path);
|
|
438
|
+
if (uiFiles.length === 0) {
|
|
439
|
+
return { required: false, uiFiles: [] };
|
|
440
|
+
}
|
|
441
|
+
const reviewsDir = cjoin(projectRoot, VISUAL_REVIEWS_DIR);
|
|
442
|
+
let freshEvidence = [];
|
|
443
|
+
const entries = await fs.readdir(reviewsDir).catch(() => []);
|
|
444
|
+
const now = Date.now();
|
|
445
|
+
for (const entry of entries) {
|
|
446
|
+
if (!/\.(jpe?g|png|webp)$/i.test(entry)) continue;
|
|
447
|
+
const stat = await fs.stat(path.join(reviewsDir, entry)).catch(() => null);
|
|
448
|
+
if (stat && now - stat.mtimeMs <= VISUAL_EVIDENCE_FRESH_MS) {
|
|
449
|
+
freshEvidence.push(normalizePathForReport(path.join(VISUAL_REVIEWS_DIR, entry)));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
freshEvidence = freshEvidence.slice(-5);
|
|
453
|
+
const required = freshEvidence.length === 0;
|
|
454
|
+
return {
|
|
455
|
+
required,
|
|
456
|
+
uiFiles,
|
|
457
|
+
freshEvidence,
|
|
458
|
+
reason: required
|
|
459
|
+
? '本轮触达界面文件,但最近 24 小时没有任何拼图证据板产出;用户可见变化需要视觉证据,不能只靠构建通过或单张截图。'
|
|
460
|
+
: '本轮触达界面文件,最近已有拼图证据板产出;确认它覆盖了本轮改动,并把证据图嵌入对话流。',
|
|
461
|
+
suggestedCommands: [
|
|
462
|
+
'openprd visual-compare . --before <改前截图> --after <改后截图>',
|
|
463
|
+
'openprd visual-compare . --board <alignment-board.json>(同构列表/卡片/网格/表格对齐,含容器轨道与内容槽位)',
|
|
464
|
+
'openprd visual-compare . --board <centering-board.json>(单素材内部居中/视觉重心)',
|
|
465
|
+
],
|
|
466
|
+
chatEmbedHint: '生成后把证据图用 `` 直接嵌入最终回复,让用户在对话流里看到对齐效果。',
|
|
390
467
|
};
|
|
391
468
|
}
|
|
392
469
|
|
|
@@ -526,6 +603,10 @@ export async function checkDevelopmentStandardsWorkspace(projectRoot, options =
|
|
|
526
603
|
action: 'dev-check',
|
|
527
604
|
projectRoot,
|
|
528
605
|
enabled: lineConfig.enabled,
|
|
606
|
+
autoRefactor: {
|
|
607
|
+
enabled: lineConfig.autoRefactorEnabled,
|
|
608
|
+
mode: lineConfig.autoRefactorEnabled ? 'auto-refactor' : 'recommend',
|
|
609
|
+
},
|
|
529
610
|
thresholds: {
|
|
530
611
|
okMax: lineConfig.okMax,
|
|
531
612
|
attentionMax: lineConfig.attentionMax,
|
|
@@ -538,8 +619,42 @@ export async function checkDevelopmentStandardsWorkspace(projectRoot, options =
|
|
|
538
619
|
warning: statusCounts.warning ?? 0,
|
|
539
620
|
},
|
|
540
621
|
files,
|
|
541
|
-
wrapUp: buildDevCheckWrapUp(files),
|
|
622
|
+
wrapUp: buildDevCheckWrapUp(files, lineConfig),
|
|
623
|
+
visualEvidence: await buildVisualEvidenceReminder(projectRoot, files),
|
|
542
624
|
knowledgeReview,
|
|
543
625
|
errors,
|
|
544
626
|
};
|
|
545
627
|
}
|
|
628
|
+
|
|
629
|
+
export async function setDevCheckAutoRefactor(projectRoot, enabled) {
|
|
630
|
+
const configPath = cjoin(projectRoot, DEVELOPMENT_STANDARDS_CONFIG);
|
|
631
|
+
const config = (await exists(configPath)) ? await readJson(configPath).catch(() => ({})) : {};
|
|
632
|
+
const developmentStandards = config.developmentStandards && typeof config.developmentStandards === 'object'
|
|
633
|
+
? config.developmentStandards
|
|
634
|
+
: {};
|
|
635
|
+
const autoRefactor = developmentStandards.autoRefactor && typeof developmentStandards.autoRefactor === 'object'
|
|
636
|
+
? developmentStandards.autoRefactor
|
|
637
|
+
: {};
|
|
638
|
+
const next = {
|
|
639
|
+
...config,
|
|
640
|
+
developmentStandards: {
|
|
641
|
+
...developmentStandards,
|
|
642
|
+
autoRefactor: {
|
|
643
|
+
...autoRefactor,
|
|
644
|
+
enabled: Boolean(enabled),
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
649
|
+
await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
650
|
+
return {
|
|
651
|
+
ok: true,
|
|
652
|
+
action: 'dev-check-auto-refactor',
|
|
653
|
+
projectRoot,
|
|
654
|
+
configPath: normalizePathForReport(path.relative(projectRoot, configPath)),
|
|
655
|
+
autoRefactor: {
|
|
656
|
+
enabled: Boolean(enabled),
|
|
657
|
+
mode: enabled ? 'auto-refactor' : 'recommend',
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
}
|
package/src/diagram-core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { timestamp } from './time.js';
|
|
2
|
+
import { isEnglishHeavyText, normalizeOutputLocale } from './language-policy.js';
|
|
2
3
|
|
|
3
4
|
function escapeHtml(value) {
|
|
4
5
|
return `${value ?? ''}`
|
|
@@ -120,28 +121,13 @@ function normalizePanel(panel, fallbackTitle = '评审备注', fallbackColor = '
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
function normalizeLocale(contract) {
|
|
123
|
-
return pickValue(contract?.locale, contract?.lang ?? 'zh-CN');
|
|
124
|
+
return normalizeOutputLocale(pickValue(contract?.locale, contract?.lang ?? 'zh-CN'));
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
function normalizeReviewStatus(value) {
|
|
127
128
|
return pickValue(value, 'pending-confirmation');
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function hasCjk(text) {
|
|
131
|
-
return /[\u3400-\u9fff]/.test(text);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function englishWords(text) {
|
|
135
|
-
return `${text ?? ''}`.match(/[A-Za-z][A-Za-z0-9+_.-]*/g) ?? [];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function isEnglishHeavyText(text) {
|
|
139
|
-
const value = `${text ?? ''}`.trim();
|
|
140
|
-
if (!value || hasCjk(value)) return false;
|
|
141
|
-
const words = englishWords(value);
|
|
142
|
-
return words.length >= 4;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
131
|
function collectDiagramTexts(model) {
|
|
146
132
|
const entries = [];
|
|
147
133
|
const push = (path, value) => {
|
|
@@ -181,7 +167,7 @@ function collectDiagramTexts(model) {
|
|
|
181
167
|
}
|
|
182
168
|
|
|
183
169
|
export function validateDiagramLanguage(model) {
|
|
184
|
-
const locale =
|
|
170
|
+
const locale = normalizeOutputLocale(model?.locale ?? 'zh-CN').toLowerCase();
|
|
185
171
|
if (!locale.startsWith('zh')) {
|
|
186
172
|
return { valid: true, errors: [] };
|
|
187
173
|
}
|
|
@@ -609,7 +595,7 @@ function resolveProductLayerTitle(productType) {
|
|
|
609
595
|
|
|
610
596
|
function buildArchitectureComponents(snapshot) {
|
|
611
597
|
const { sections } = snapshot;
|
|
612
|
-
const reviewTarget = sections.handoff.targetSystem ?? '
|
|
598
|
+
const reviewTarget = sections.handoff.targetSystem ?? 'OpenPrd';
|
|
613
599
|
return [
|
|
614
600
|
{
|
|
615
601
|
id: 'users',
|
|
@@ -733,7 +719,7 @@ export function buildArchitectureDiagramModel(snapshot) {
|
|
|
733
719
|
productType: snapshot.productType ?? '未分类',
|
|
734
720
|
owner: snapshot.owner,
|
|
735
721
|
versionId: snapshot.versionId,
|
|
736
|
-
targetSystem: snapshot.sections.handoff.targetSystem ?? '
|
|
722
|
+
targetSystem: snapshot.sections.handoff.targetSystem ?? 'OpenPrd',
|
|
737
723
|
reviewStatus: normalizeReviewStatus(snapshot?.reviewStatus),
|
|
738
724
|
},
|
|
739
725
|
};
|
|
@@ -926,7 +912,7 @@ export function buildProductFlowDiagramModel(snapshot) {
|
|
|
926
912
|
productType: snapshot.productType ?? '未分类',
|
|
927
913
|
owner: snapshot.owner,
|
|
928
914
|
versionId: snapshot.versionId,
|
|
929
|
-
targetSystem: snapshot.sections.handoff.targetSystem ?? '
|
|
915
|
+
targetSystem: snapshot.sections.handoff.targetSystem ?? 'OpenPrd',
|
|
930
916
|
reviewStatus: normalizeReviewStatus(snapshot?.reviewStatus),
|
|
931
917
|
},
|
|
932
918
|
};
|
package/src/discovery.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { analyzePrdSnapshot, buildPrdSnapshot, formatVersionId } from './prd-core.js';
|
|
4
4
|
import { formatProductTypeDisplay } from './product-type-copy.js';
|
|
5
5
|
import { analyzeOpenSpecTaskVolumes } from './openspec/tasks.js';
|
|
6
|
-
import {
|
|
6
|
+
import { openPrdDiscoveryDir, readDiscoveryConfig } from './openspec/paths.js';
|
|
7
7
|
import { appendJsonl, cjoin, exists, readJson, readJsonl, writeJson, writeText } from './fs-utils.js';
|
|
8
8
|
import { collectSourceInventory, shouldIgnoreSourceDirectory } from './source-inventory.js';
|
|
9
9
|
import { compactTimestamp, timestamp } from './time.js';
|
|
@@ -391,13 +391,7 @@ async function openspecDiscoveryWorkspaceImpl(projectRoot, options = {}, depende
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
async function loadOpenSpecDiscoveryRun(projectRoot) {
|
|
394
|
-
|
|
395
|
-
if (!(await exists(cjoin(discoveryRoot, 'current.json')))) {
|
|
396
|
-
const legacyRoot = legacyOpenSpecDiscoveryDir(projectRoot);
|
|
397
|
-
if (await exists(cjoin(legacyRoot, 'current.json'))) {
|
|
398
|
-
discoveryRoot = legacyRoot;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
394
|
+
const discoveryRoot = openPrdDiscoveryDir(projectRoot);
|
|
401
395
|
const currentPath = cjoin(discoveryRoot, 'current.json');
|
|
402
396
|
if (!(await exists(currentPath))) {
|
|
403
397
|
throw new Error(`Missing OpenPrd discovery state: ${currentPath}`);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
DOC_MEGA_LINE_CHARS,
|
|
5
|
+
DOC_STALE_VERSION_THRESHOLD,
|
|
6
|
+
STANDARD_DOCS,
|
|
7
|
+
STANDARD_DOCS_ROOT,
|
|
8
|
+
analyzeDocQuality,
|
|
9
|
+
analyzeDocStaleness,
|
|
10
|
+
} from './standards.js';
|
|
11
|
+
|
|
12
|
+
const PREVIEW_CHARS = 80;
|
|
13
|
+
const MAX_LINE_SUGGESTIONS_PER_DOC = 8;
|
|
14
|
+
|
|
15
|
+
function lineSuggestion(lineNumber, line) {
|
|
16
|
+
const preview = Array.from(line.trim()).slice(0, PREVIEW_CHARS).join('');
|
|
17
|
+
const separators = (line.match(/[;;。]/gu) ?? []).length;
|
|
18
|
+
const hint = separators >= 2
|
|
19
|
+
? `含 ${separators + 1} 个句段,建议拆成分点列表(每点一行)`
|
|
20
|
+
: '建议拆成小节或分点,删除重复叙述';
|
|
21
|
+
return {
|
|
22
|
+
line: lineNumber,
|
|
23
|
+
chars: Array.from(line).length,
|
|
24
|
+
preview: `${preview}…`,
|
|
25
|
+
hint,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function docsCompactWorkspace(projectRoot, options = {}) {
|
|
30
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
31
|
+
const docs = [];
|
|
32
|
+
let totalMegaLines = 0;
|
|
33
|
+
|
|
34
|
+
for (const doc of STANDARD_DOCS) {
|
|
35
|
+
const docPath = path.join(resolvedRoot, STANDARD_DOCS_ROOT, doc.fileName);
|
|
36
|
+
const text = await fs.readFile(docPath, 'utf8').catch(() => null);
|
|
37
|
+
if (text === null) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const relativePath = path.relative(resolvedRoot, docPath);
|
|
41
|
+
const quality = analyzeDocQuality(relativePath, text);
|
|
42
|
+
const lines = text.split('\n');
|
|
43
|
+
const suggestions = [];
|
|
44
|
+
lines.forEach((line, index) => {
|
|
45
|
+
if (Array.from(line).length > DOC_MEGA_LINE_CHARS && suggestions.length < MAX_LINE_SUGGESTIONS_PER_DOC) {
|
|
46
|
+
suggestions.push(lineSuggestion(index + 1, line));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
totalMegaLines += quality.megaLines;
|
|
50
|
+
|
|
51
|
+
const entry = {
|
|
52
|
+
...quality,
|
|
53
|
+
suggestions,
|
|
54
|
+
status: quality.megaLines > 0 ? 'needs-compaction' : 'ok',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (doc.fileName === 'prd.md') {
|
|
58
|
+
const stat = await fs.stat(docPath).catch(() => null);
|
|
59
|
+
if (stat) {
|
|
60
|
+
const staleness = await analyzeDocStaleness(resolvedRoot, stat.mtimeMs);
|
|
61
|
+
entry.versionsBehind = staleness.versionsBehind;
|
|
62
|
+
entry.totalVersions = staleness.totalVersions;
|
|
63
|
+
if (staleness.versionsBehind > DOC_STALE_VERSION_THRESHOLD) {
|
|
64
|
+
entry.status = entry.status === 'needs-compaction' ? 'needs-compaction-and-backfill' : 'needs-backfill';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
docs.push(entry);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const needsAttention = docs.filter((doc) => doc.status !== 'ok');
|
|
73
|
+
const recommendations = [];
|
|
74
|
+
if (needsAttention.length > 0) {
|
|
75
|
+
recommendations.push('压缩原则:同一事实只保留一处最新版本;把巨行拆成分点;过时段落直接删除而不是追加“更新:”。');
|
|
76
|
+
recommendations.push('压缩后运行 openprd standards . --verify 确认警告消除,再运行 openprd dev-check . 收口。');
|
|
77
|
+
}
|
|
78
|
+
for (const doc of needsAttention) {
|
|
79
|
+
if (doc.status.includes('backfill')) {
|
|
80
|
+
recommendations.push(`${doc.path}: 已落后 ${doc.versionsBehind} 个 requirement 版本,需要按最新已确认需求回填基线,再删除已失效内容。`);
|
|
81
|
+
}
|
|
82
|
+
if (doc.megaLines > 0) {
|
|
83
|
+
recommendations.push(`${doc.path}: ${doc.megaLines} 行巨行(阈值 ${DOC_MEGA_LINE_CHARS} 字符),按 suggestions 列出的行号拆分。`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
action: 'docs-compact',
|
|
90
|
+
projectRoot: resolvedRoot,
|
|
91
|
+
docsRoot: STANDARD_DOCS_ROOT,
|
|
92
|
+
analyzedDocs: docs.length,
|
|
93
|
+
docsNeedingAttention: needsAttention.length,
|
|
94
|
+
totalMegaLines,
|
|
95
|
+
docs,
|
|
96
|
+
recommendations,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function printDocsCompactResult(result, json) {
|
|
101
|
+
if (json) {
|
|
102
|
+
console.log(JSON.stringify(result, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`docs/basic 压缩体检: ${result.docsNeedingAttention === 0 ? '健康' : `${result.docsNeedingAttention} 个文档需要处理`}`);
|
|
106
|
+
console.log(`- 已分析文档: ${result.analyzedDocs}`);
|
|
107
|
+
console.log(`- 巨行总数(> ${DOC_MEGA_LINE_CHARS} 字符): ${result.totalMegaLines}`);
|
|
108
|
+
for (const doc of result.docs) {
|
|
109
|
+
if (doc.status === 'ok') {
|
|
110
|
+
console.log(`- ${doc.path}: 健康(${doc.lines} 行,平均 ${doc.avgLineChars} 字符/行)`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
console.log(`- ${doc.path}: ${doc.status}(${doc.lines} 行,巨行 ${doc.megaLines},最长 ${doc.maxLineChars} 字符${doc.versionsBehind != null ? `,落后 ${doc.versionsBehind} 版本` : ''})`);
|
|
114
|
+
for (const suggestion of doc.suggestions) {
|
|
115
|
+
console.log(` · L${suggestion.line}(${suggestion.chars} 字符): ${suggestion.preview}`);
|
|
116
|
+
console.log(` ${suggestion.hint}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (result.recommendations.length > 0) {
|
|
120
|
+
console.log('建议:');
|
|
121
|
+
for (const item of result.recommendations) {
|
|
122
|
+
console.log(`- ${item}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -85,7 +85,7 @@ function inferParallelGroup(text, type, phase) {
|
|
|
85
85
|
|
|
86
86
|
function inferWriteScope(parallelGroup) {
|
|
87
87
|
const scopeByGroup = {
|
|
88
|
-
governance: ['openprd/changes/**', '.openprd/**'],
|
|
88
|
+
governance: ['.openprd/changes/**', '.openprd/**'],
|
|
89
89
|
contracts: ['src/**', 'test/**', 'docs/basic/backend-structure.md'],
|
|
90
90
|
domain: ['src/**', 'test/**'],
|
|
91
91
|
surface: ['src/cli/**', 'src/**', 'test/**'],
|