@openprd/cli 0.1.10 → 0.1.18
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/benchmarks/index.md +17 -15
- package/.openprd/benchmarks/sources.yaml +53 -1
- 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/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 +103 -100
- package/.openprd/schema/prd.schema.yaml +15 -0
- package/.openprd/standards/config.json +7 -1
- package/.openprd/templates/base/intake.md +13 -0
- package/.openprd/templates/base/prd.md +15 -0
- package/AGENTS.md +14 -4
- package/README.md +107 -14
- package/README_EN.md +131 -13
- package/package.json +1 -1
- package/scripts/dev-check-wrapup-copy.mjs +46 -21
- package/skills/openprd-benchmark-router/SKILL.md +2 -1
- 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 +5 -3
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +2 -2
- package/skills/openprd-requirement-intake/references/startup-validation-lens.md +98 -13
- package/skills/openprd-router/SKILL.md +2 -1
- package/skills/openprd-shared/SKILL.md +27 -17
- package/skills/openprd-standards/SKILL.md +4 -2
- package/src/agent-canonical-content.js +808 -0
- package/src/agent-integration.js +259 -778
- package/src/brainstorm-artifacts.js +156 -8
- package/src/brainstorm-presentation.js +14 -4
- package/src/brainstorm.js +165 -2
- package/src/canvas-app.html.js +2910 -0
- package/src/canvas-i18n.js +274 -0
- package/src/canvas-workspace.js +2271 -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 +37 -5
- 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 +963 -65
- 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 +390 -171
- 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 +825 -51
- package/src/knowledge.js +101 -20
- 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 +102 -3
- package/src/quality-html-artifact.js +30 -5
- package/src/quality-visual-review.js +257 -14
- package/src/quality.js +66 -18
- 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 +132 -4
- package/src/workspace-workflow.js +172 -10
package/src/run-harness.js
CHANGED
|
@@ -19,6 +19,9 @@ const OPENPRD_HARNESS_RUN_STATE = cjoin(OPENPRD_HARNESS_DIR, 'run-state.json');
|
|
|
19
19
|
const OPENPRD_HARNESS_ITERATIONS = cjoin(OPENPRD_HARNESS_DIR, 'iterations.jsonl');
|
|
20
20
|
const OPENPRD_HARNESS_LEARNINGS = cjoin(OPENPRD_HARNESS_DIR, 'learnings.md');
|
|
21
21
|
const OPENPRD_HARNESS_LOOP_FEATURE_LIST = cjoin(OPENPRD_HARNESS_DIR, 'feature-list.json');
|
|
22
|
+
const OPENPRD_HARNESS_INSTALL_MANIFEST = cjoin(OPENPRD_HARNESS_DIR, 'install-manifest.json');
|
|
23
|
+
const OPENPRD_HARNESS_HOOK_STATE = cjoin(OPENPRD_HARNESS_DIR, 'hook-state.json');
|
|
24
|
+
const OPENPRD_HARNESS_RUNTIME_ENVIRONMENT = cjoin(OPENPRD_HARNESS_DIR, 'runtime-environment.json');
|
|
22
25
|
const OPENPRD_HARNESS_REQUIREMENT_GATE = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gate.json');
|
|
23
26
|
const OPENPRD_HARNESS_REQUIREMENT_GATES_DIR = cjoin(OPENPRD_HARNESS_DIR, 'requirement-gates');
|
|
24
27
|
const OPENPRD_HARNESS_SESSION_BINDINGS_DIR = cjoin(OPENPRD_HARNESS_DIR, 'session-bindings');
|
|
@@ -82,6 +85,34 @@ async function ensureRunHarness(projectRoot) {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
async function readRuntimeEnvironmentContext(projectRoot) {
|
|
89
|
+
const [runtimeEnvironment, installManifest, hookState] = await Promise.all([
|
|
90
|
+
readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUNTIME_ENVIRONMENT)).catch(() => null),
|
|
91
|
+
readJson(harnessFile(projectRoot, OPENPRD_HARNESS_INSTALL_MANIFEST)).catch(() => null),
|
|
92
|
+
readJson(harnessFile(projectRoot, OPENPRD_HARNESS_HOOK_STATE)).catch(() => null),
|
|
93
|
+
]);
|
|
94
|
+
const active = runtimeEnvironment?.active
|
|
95
|
+
?? hookState?.runtimeEnvironment
|
|
96
|
+
?? {
|
|
97
|
+
activeClient: 'unknown',
|
|
98
|
+
surface: 'unknown',
|
|
99
|
+
confidence: 0,
|
|
100
|
+
sessionId: null,
|
|
101
|
+
evidence: [],
|
|
102
|
+
observedCapabilities: [],
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
active,
|
|
106
|
+
runtimeDetection: installManifest?.runtimeDetection ?? runtimeEnvironment?.runtimeDetection ?? null,
|
|
107
|
+
platformCapabilityPacks: installManifest?.platformCapabilityPacks ?? runtimeEnvironment?.platformCapabilityPacks ?? [],
|
|
108
|
+
files: {
|
|
109
|
+
runtimeEnvironment: OPENPRD_HARNESS_RUNTIME_ENVIRONMENT,
|
|
110
|
+
installManifest: OPENPRD_HARNESS_INSTALL_MANIFEST,
|
|
111
|
+
hookState: OPENPRD_HARNESS_HOOK_STATE,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
async function readRunState(projectRoot) {
|
|
86
117
|
await ensureRunHarness(projectRoot);
|
|
87
118
|
return readJson(harnessFile(projectRoot, OPENPRD_HARNESS_RUN_STATE)).catch(() => ({
|
|
@@ -225,7 +256,7 @@ function executionGate() {
|
|
|
225
256
|
confirmationChecklistRequired: true,
|
|
226
257
|
allowedIntents: ['开发', '实现', '修复', '继续任务', '落地执行', '深度调研', '深度对标', '复刻落地', '提交'],
|
|
227
258
|
readOnlyIntents: ['看看', '规划', '梳理', '分析', '评估', '预计动哪些文件', '怎么改', '代码审查'],
|
|
228
|
-
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact
|
|
259
|
+
rule: '只有当用户当前明确要求实现、继续、深度调研、对标或提交时,才运行 executionCommand。单纯的“请帮我实现/继续实现”只表示有执行意图,不表示可以跳过 requirement 摘要确认、`capture/classify/synthesize` 写入路径或 review;只有用户明确表示“不需要进行任何确认”时,才允许静默走完整 requirement write path,并对当前精确匹配的稳定 review artifact 记录确认。即使用户授权实现,也不等于授权云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行;这些动作必须由当前用户明确批准,并说明如何同步回本地源码、配置或迁移。若还需要向用户索取执行授权,先展示 executionConfirmationChecklist,再请求明确确认;规划、分析、文件影响范围和审查类请求保持只读,并基于证据回答。',
|
|
229
260
|
};
|
|
230
261
|
}
|
|
231
262
|
|
|
@@ -251,6 +282,7 @@ function buildExecutionConfirmationChecklist(recommendation) {
|
|
|
251
282
|
outOfScope: [
|
|
252
283
|
'不会默认处理这次范围以外的历史问题。',
|
|
253
284
|
'不会默认顺带做提交、发布或额外的全局调整。',
|
|
285
|
+
'不会默认做云端热修复、生产远端写入、业务兜底、写死逻辑或缓存补行。',
|
|
254
286
|
'如果真的需要扩大范围,我会先单独说明。',
|
|
255
287
|
].filter(Boolean),
|
|
256
288
|
verification: [
|
|
@@ -261,6 +293,7 @@ function buildExecutionConfirmationChecklist(recommendation) {
|
|
|
261
293
|
risks: [
|
|
262
294
|
'这一步会正式写入当前工作区。',
|
|
263
295
|
'如果牵出和本次无关的历史遗留问题,我会单列说明,不把它混成本次失败。',
|
|
296
|
+
'如果确实需要临时兜底或线上热修复,我会先单独说明影响、回滚和本地同步计划,等你明确批准。',
|
|
264
297
|
parallelPlan?.eligible ? '如果多人同时推进,我会避免范围互相踩踏。' : null,
|
|
265
298
|
],
|
|
266
299
|
confirmationPrompt: '如果你希望我现在就按这次范围继续,我就直接往下做。',
|
|
@@ -1816,6 +1849,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1816
1849
|
await ensureRunHarness(projectRoot);
|
|
1817
1850
|
const runState = await readRunState(projectRoot);
|
|
1818
1851
|
const currentTurn = await readJson(harnessFile(projectRoot, OPENPRD_HARNESS_TURN_STATE)).catch(() => null);
|
|
1852
|
+
const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
|
|
1819
1853
|
const currentTurnPrompt = String(currentTurn?.prompt ?? '').trim();
|
|
1820
1854
|
const fallbackPrompt = !options.hookInject
|
|
1821
1855
|
? (inferPromptDrivenLightweightRecommendation(currentTurnPrompt) ? currentTurnPrompt : null)
|
|
@@ -1942,8 +1976,8 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
1942
1976
|
activeChange,
|
|
1943
1977
|
nextTaskTitle: nextTask?.title,
|
|
1944
1978
|
relatedFiles: [
|
|
1945
|
-
focusedChangeId ?
|
|
1946
|
-
activeChange ?
|
|
1979
|
+
focusedChangeId ? `.openprd/changes/${focusedChangeId}/tasks.md` : null,
|
|
1980
|
+
activeChange ? `.openprd/changes/${activeChange}` : null,
|
|
1947
1981
|
].filter(Boolean),
|
|
1948
1982
|
limit: options.hookInject ? 4 : 3,
|
|
1949
1983
|
}).catch(() => ({ matched: [], summary: { matched: 0 } }));
|
|
@@ -2035,6 +2069,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
2035
2069
|
: null,
|
|
2036
2070
|
lane,
|
|
2037
2071
|
recommendation: effectiveRecommendation,
|
|
2072
|
+
runtimeEnvironment,
|
|
2038
2073
|
knowledgeSkills: {
|
|
2039
2074
|
matched: renderedKnowledgeSkills,
|
|
2040
2075
|
mandatoryCheck: knowledgeSkillMatches.mandatoryCheck ?? null,
|
|
@@ -2050,6 +2085,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
2050
2085
|
runState: OPENPRD_HARNESS_RUN_STATE,
|
|
2051
2086
|
iterations: OPENPRD_HARNESS_ITERATIONS,
|
|
2052
2087
|
learnings: OPENPRD_HARNESS_LEARNINGS,
|
|
2088
|
+
runtimeEnvironment: OPENPRD_HARNESS_RUNTIME_ENVIRONMENT,
|
|
2053
2089
|
},
|
|
2054
2090
|
};
|
|
2055
2091
|
|
|
@@ -2065,6 +2101,7 @@ async function buildRunContext(projectRoot, dependencies, options = {}) {
|
|
|
2065
2101
|
async function recordRunHook(projectRoot, options = {}) {
|
|
2066
2102
|
await ensureRunHarness(projectRoot);
|
|
2067
2103
|
const state = await readRunState(projectRoot);
|
|
2104
|
+
const runtimeEnvironment = await readRuntimeEnvironmentContext(projectRoot);
|
|
2068
2105
|
const currentIteration = Number(state.currentIteration ?? 0) + 1;
|
|
2069
2106
|
const event = {
|
|
2070
2107
|
version: 1,
|
|
@@ -2075,6 +2112,7 @@ async function recordRunHook(projectRoot, options = {}) {
|
|
|
2075
2112
|
risk: options.risk ?? 'unknown',
|
|
2076
2113
|
outcome: options.outcome ?? 'unknown',
|
|
2077
2114
|
preview: options.preview ?? null,
|
|
2115
|
+
runtimeEnvironment: runtimeEnvironment.active,
|
|
2078
2116
|
};
|
|
2079
2117
|
await appendJsonl(harnessFile(projectRoot, OPENPRD_HARNESS_ITERATIONS), event);
|
|
2080
2118
|
await writeRunState(projectRoot, {
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 核心功能
|
|
3
|
+
* 从 Codex 原始会话事件中抽取可审查证据,并隔离系统提示、AGENTS 和工具 schema 噪声。
|
|
4
|
+
*
|
|
5
|
+
* 输入
|
|
6
|
+
* 接收 JSONL 事件对象、消息对象或文本片段。
|
|
7
|
+
*
|
|
8
|
+
* 输出
|
|
9
|
+
* 输出带 sourceKind 的 evidence records,以及供 knowledge matching 使用的清理后文本。
|
|
10
|
+
*
|
|
11
|
+
* 定位
|
|
12
|
+
* 位于会话证据层,服务 daily review、knowledge review 和项目经验召回,不直接读写工作区。
|
|
13
|
+
*
|
|
14
|
+
* 依赖
|
|
15
|
+
* 仅依赖标准 JavaScript,不依赖 CLI、文件系统或 OpenPrd workspace 状态。
|
|
16
|
+
*
|
|
17
|
+
* 维护规则
|
|
18
|
+
* 这里负责证据卫生,不负责判断需求类型;新增来源时先补 sourceKind 和污染样例测试。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const CONTEXT_ONLY_PATTERNS = [
|
|
22
|
+
/\bAGENTS\.md instructions\b/i,
|
|
23
|
+
/<INSTRUCTIONS>/i,
|
|
24
|
+
/<\/INSTRUCTIONS>/i,
|
|
25
|
+
/<environment_context>/i,
|
|
26
|
+
/# Tools\b/i,
|
|
27
|
+
/## Skills\b/i,
|
|
28
|
+
/### Available skills\b/i,
|
|
29
|
+
/\bnamespace\s+\w+\s*\{/i,
|
|
30
|
+
/\btool schema\b/i,
|
|
31
|
+
/\bYou are Codex\b/i,
|
|
32
|
+
/\bOpenPrd Harness\b/i,
|
|
33
|
+
/\bproject-doc\b/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const CONTEXT_ROLE_SET = new Set(['system', 'developer']);
|
|
37
|
+
|
|
38
|
+
const GENERIC_FRAMEWORK_TERMS = new Set([
|
|
39
|
+
'agent',
|
|
40
|
+
'agents',
|
|
41
|
+
'agentruntime',
|
|
42
|
+
'codex',
|
|
43
|
+
'context',
|
|
44
|
+
'developer',
|
|
45
|
+
'hook',
|
|
46
|
+
'hooks',
|
|
47
|
+
'harness',
|
|
48
|
+
'instruction',
|
|
49
|
+
'instructions',
|
|
50
|
+
'openprd',
|
|
51
|
+
'schema',
|
|
52
|
+
'skill',
|
|
53
|
+
'skills',
|
|
54
|
+
'system',
|
|
55
|
+
'tool',
|
|
56
|
+
'tools',
|
|
57
|
+
'workflow',
|
|
58
|
+
'workflows',
|
|
59
|
+
'项目级',
|
|
60
|
+
'项目经验',
|
|
61
|
+
'沉淀',
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function normalizeWhitespace(value) {
|
|
65
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function trimEvidenceText(value, max = 1200) {
|
|
69
|
+
const text = normalizeWhitespace(value);
|
|
70
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeEventType(event = {}) {
|
|
74
|
+
return String(
|
|
75
|
+
event.type
|
|
76
|
+
?? event.event
|
|
77
|
+
?? event.kind
|
|
78
|
+
?? event.eventType
|
|
79
|
+
?? event.name
|
|
80
|
+
?? event.item?.type
|
|
81
|
+
?? event.event_msg?.type
|
|
82
|
+
?? '',
|
|
83
|
+
).trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function normalizeRole(event = {}) {
|
|
87
|
+
return String(
|
|
88
|
+
event.role
|
|
89
|
+
?? event.message?.role
|
|
90
|
+
?? event.item?.role
|
|
91
|
+
?? event.event_msg?.role
|
|
92
|
+
?? '',
|
|
93
|
+
).trim().toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function firstString(...values) {
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
if (typeof value === 'string' && value.trim()) {
|
|
99
|
+
return value.trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractTextFromContent(content) {
|
|
106
|
+
if (typeof content === 'string') return content;
|
|
107
|
+
if (Array.isArray(content)) {
|
|
108
|
+
return content
|
|
109
|
+
.map((part) => firstString(part?.text, part?.content, part?.input, part?.output))
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join('\n');
|
|
112
|
+
}
|
|
113
|
+
if (content && typeof content === 'object') {
|
|
114
|
+
return firstString(content.text, content.content, content.input, content.output) ?? '';
|
|
115
|
+
}
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function extractEventText(event = {}) {
|
|
120
|
+
if (typeof event === 'string') return event;
|
|
121
|
+
const eventMsg = event.event_msg ?? {};
|
|
122
|
+
const item = event.item ?? {};
|
|
123
|
+
const message = event.message ?? {};
|
|
124
|
+
return firstString(
|
|
125
|
+
event.text,
|
|
126
|
+
event.content,
|
|
127
|
+
event.output,
|
|
128
|
+
event.final_answer,
|
|
129
|
+
event.summary,
|
|
130
|
+
event.prompt,
|
|
131
|
+
event.user_message,
|
|
132
|
+
event.agent_message,
|
|
133
|
+
eventMsg.user_message,
|
|
134
|
+
eventMsg.agent_message,
|
|
135
|
+
eventMsg.final_answer,
|
|
136
|
+
eventMsg.summary,
|
|
137
|
+
eventMsg.message,
|
|
138
|
+
item.final_answer,
|
|
139
|
+
item.summary,
|
|
140
|
+
item.text,
|
|
141
|
+
item.output,
|
|
142
|
+
message.text,
|
|
143
|
+
extractTextFromContent(message.content),
|
|
144
|
+
extractTextFromContent(item.content),
|
|
145
|
+
) ?? '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function looksLikeContextOnly(text, event = {}) {
|
|
149
|
+
const role = normalizeRole(event);
|
|
150
|
+
if (CONTEXT_ROLE_SET.has(role)) return true;
|
|
151
|
+
return CONTEXT_ONLY_PATTERNS.some((pattern) => pattern.test(text));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function classifyEvent(event = {}, text = '') {
|
|
155
|
+
const role = normalizeRole(event);
|
|
156
|
+
const type = normalizeEventType(event).toLowerCase();
|
|
157
|
+
const eventMsg = event.event_msg ?? {};
|
|
158
|
+
const item = event.item ?? {};
|
|
159
|
+
if (looksLikeContextOnly(text, event)) {
|
|
160
|
+
return { sourceKind: 'context-only', reason: 'system-context' };
|
|
161
|
+
}
|
|
162
|
+
if (role === 'user' || eventMsg.user_message) {
|
|
163
|
+
return { sourceKind: 'user-direct', reason: 'user-message' };
|
|
164
|
+
}
|
|
165
|
+
if (
|
|
166
|
+
role === 'assistant'
|
|
167
|
+
|| eventMsg.agent_message
|
|
168
|
+
|| eventMsg.final_answer
|
|
169
|
+
|| item.final_answer
|
|
170
|
+
|| type.includes('final')
|
|
171
|
+
) {
|
|
172
|
+
return { sourceKind: 'assistant-final', reason: 'assistant-message' };
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
type.includes('task_complete')
|
|
176
|
+
|| event.task_complete
|
|
177
|
+
|| eventMsg.task_complete
|
|
178
|
+
|| item.type === 'task_complete'
|
|
179
|
+
) {
|
|
180
|
+
return { sourceKind: 'assistant-final', reason: 'task-complete' };
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
type.includes('function_call')
|
|
184
|
+
|| type.includes('tool')
|
|
185
|
+
|| event.tool
|
|
186
|
+
|| event.name
|
|
187
|
+
|| item.name
|
|
188
|
+
) {
|
|
189
|
+
return { sourceKind: 'tool-action', reason: 'tool-event' };
|
|
190
|
+
}
|
|
191
|
+
if (/\b(git status|npm test|node --test|openprd .*--verify|doctor)\b/i.test(text)) {
|
|
192
|
+
return { sourceKind: 'tool-action', reason: 'command-evidence' };
|
|
193
|
+
}
|
|
194
|
+
return { sourceKind: 'context-only', reason: 'unclassified' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeEvidenceRecord(record = {}, index = 0) {
|
|
198
|
+
const text = trimEvidenceText(record.text);
|
|
199
|
+
if (!text) return null;
|
|
200
|
+
const sourceKind = String(record.sourceKind ?? 'context-only').trim() || 'context-only';
|
|
201
|
+
return {
|
|
202
|
+
sourceKind,
|
|
203
|
+
text,
|
|
204
|
+
reason: String(record.reason ?? '').trim() || null,
|
|
205
|
+
role: String(record.role ?? '').trim() || null,
|
|
206
|
+
eventType: String(record.eventType ?? '').trim() || null,
|
|
207
|
+
eventIndex: Number.isFinite(Number(record.eventIndex)) ? Number(record.eventIndex) : index,
|
|
208
|
+
confidence: Number.isFinite(Number(record.confidence)) ? Number(record.confidence) : (sourceKind === 'context-only' ? 0.2 : 0.8),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractSessionEvidenceRecords(events = [], options = {}) {
|
|
213
|
+
const maxRecords = Math.max(1, Number(options.maxRecords ?? 80));
|
|
214
|
+
return (Array.isArray(events) ? events : [events])
|
|
215
|
+
.map((event, index) => {
|
|
216
|
+
const text = extractEventText(event);
|
|
217
|
+
if (!normalizeWhitespace(text)) return null;
|
|
218
|
+
const classification = classifyEvent(event, text);
|
|
219
|
+
return normalizeEvidenceRecord({
|
|
220
|
+
...classification,
|
|
221
|
+
text,
|
|
222
|
+
role: normalizeRole(event),
|
|
223
|
+
eventType: normalizeEventType(event),
|
|
224
|
+
eventIndex: index,
|
|
225
|
+
}, index);
|
|
226
|
+
})
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.slice(0, maxRecords);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function normalizeSessionEvidenceRecords(records = []) {
|
|
232
|
+
return (Array.isArray(records) ? records : [records])
|
|
233
|
+
.map((record, index) => normalizeEvidenceRecord(record, index))
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function cleanEvidenceRecords(records = [], options = {}) {
|
|
238
|
+
const includeContextOnly = options.includeContextOnly === true;
|
|
239
|
+
return normalizeSessionEvidenceRecords(records)
|
|
240
|
+
.filter((record) => includeContextOnly || record.sourceKind !== 'context-only');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildCleanEvidenceText(records = [], options = {}) {
|
|
244
|
+
const maxRecords = Math.max(1, Number(options.maxRecords ?? 24));
|
|
245
|
+
return cleanEvidenceRecords(records, options)
|
|
246
|
+
.slice(0, maxRecords)
|
|
247
|
+
.map((record) => `[${record.sourceKind}] ${record.text}`)
|
|
248
|
+
.join('\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeSearchText(value) {
|
|
252
|
+
return String(value ?? '')
|
|
253
|
+
.toLowerCase()
|
|
254
|
+
.replace(/[`"'()[\]{}:;,!?]/g, ' ')
|
|
255
|
+
.replace(/[_/\\.-]+/g, ' ')
|
|
256
|
+
.replace(/\s+/g, ' ')
|
|
257
|
+
.trim();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function sanitizeEvidenceText(value) {
|
|
261
|
+
const text = String(value ?? '');
|
|
262
|
+
if (!text.trim()) return '';
|
|
263
|
+
const lines = text.split(/\r?\n/);
|
|
264
|
+
const kept = [];
|
|
265
|
+
let skipBlock = false;
|
|
266
|
+
for (const line of lines) {
|
|
267
|
+
const startsContextBlock = /<INSTRUCTIONS>|<environment_context>|# Tools\b|### Available skills\b/i.test(line);
|
|
268
|
+
if (startsContextBlock) {
|
|
269
|
+
skipBlock = true;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (skipBlock && /<\/INSTRUCTIONS>|<\/environment_context>/i.test(line)) {
|
|
273
|
+
skipBlock = false;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (skipBlock) continue;
|
|
277
|
+
if (CONTEXT_ONLY_PATTERNS.some((pattern) => pattern.test(line))) continue;
|
|
278
|
+
kept.push(line);
|
|
279
|
+
}
|
|
280
|
+
return normalizeWhitespace(kept.join('\n'));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isGenericFrameworkTerm(token) {
|
|
284
|
+
const normalized = normalizeSearchText(token);
|
|
285
|
+
return !normalized || GENERIC_FRAMEWORK_TERMS.has(normalized);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function filterGenericFrameworkTokens(tokens = []) {
|
|
289
|
+
return [...new Set(tokens.filter((token) => !isGenericFrameworkTerm(token)))];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function buildSessionEvidenceReviewSource(options = {}) {
|
|
293
|
+
const records = cleanEvidenceRecords(options.records ?? []);
|
|
294
|
+
const title = firstString(options.title, records.find((record) => record.sourceKind === 'user-direct')?.text, '只读复盘证据候选');
|
|
295
|
+
const sourceId = firstString(options.sourceId, title, 'session-evidence-review');
|
|
296
|
+
const sourcePath = firstString(options.sourcePath, 'session-evidence');
|
|
297
|
+
const userRecords = records.filter((record) => record.sourceKind === 'user-direct');
|
|
298
|
+
const assistantRecords = records.filter((record) => record.sourceKind === 'assistant-final');
|
|
299
|
+
const toolRecords = records.filter((record) => record.sourceKind === 'tool-action');
|
|
300
|
+
const symptoms = [...userRecords, ...assistantRecords]
|
|
301
|
+
.map((record) => record.text)
|
|
302
|
+
.slice(0, 8);
|
|
303
|
+
const rootCauseCandidates = records
|
|
304
|
+
.filter((record) => ['assistant-final', 'tool-action', 'repo-state'].includes(record.sourceKind))
|
|
305
|
+
.slice(0, 6)
|
|
306
|
+
.map((record) => ({
|
|
307
|
+
title: record.text,
|
|
308
|
+
nextSteps: ['把这条证据和用户目标重新核对,只在确认可复用时推荐项目级沉淀。'],
|
|
309
|
+
sourceKind: record.sourceKind,
|
|
310
|
+
}));
|
|
311
|
+
return {
|
|
312
|
+
kind: 'session-evidence-review',
|
|
313
|
+
evidenceOnly: true,
|
|
314
|
+
sourceId,
|
|
315
|
+
sourcePath,
|
|
316
|
+
primaryPath: sourcePath,
|
|
317
|
+
sourcePaths: [sourcePath],
|
|
318
|
+
title,
|
|
319
|
+
status: 'needs-review',
|
|
320
|
+
symptoms,
|
|
321
|
+
attentionGates: [],
|
|
322
|
+
correlationFields: [],
|
|
323
|
+
extraContextFields: [],
|
|
324
|
+
missingCorrelationFields: [],
|
|
325
|
+
eventNames: [...new Set(records.map((record) => record.sourceKind))],
|
|
326
|
+
rootCauseCandidates,
|
|
327
|
+
evidenceSources: records.map((record) => ({
|
|
328
|
+
kind: record.sourceKind,
|
|
329
|
+
path: `${sourcePath}#${record.eventIndex}`,
|
|
330
|
+
reason: record.reason,
|
|
331
|
+
})),
|
|
332
|
+
queryExamples: [
|
|
333
|
+
'先只看 user-direct、assistant-final、tool-action 证据,不把 AGENTS 或系统提示当成用户诉求。',
|
|
334
|
+
'如果复盘证据跨多轮重复出现,再推荐用户把它保留为当前项目经验。',
|
|
335
|
+
],
|
|
336
|
+
abstractPattern: '从只读复盘证据中先隔离系统上下文噪声,再把重复摩擦或可复用判断沉淀为项目级候选。',
|
|
337
|
+
triggers: records
|
|
338
|
+
.filter((record) => record.sourceKind !== 'context-only')
|
|
339
|
+
.map((record) => `${record.sourceKind}: ${record.text}`)
|
|
340
|
+
.slice(0, 8),
|
|
341
|
+
prevention: [
|
|
342
|
+
'raw session 先抽取 clean evidence,再进入候选判断。',
|
|
343
|
+
'AGENTS、system、developer、tool schema 和 skill list 只能作为 context-only 背景,不触发用户诉求或项目经验候选。',
|
|
344
|
+
'项目级沉淀先作为候选推荐给用户确认,不自动 promote。',
|
|
345
|
+
],
|
|
346
|
+
verificationSteps: [
|
|
347
|
+
'确认每条证据都有 sourceKind。',
|
|
348
|
+
'确认候选推荐没有依赖 context-only 命中。',
|
|
349
|
+
'确认用户确认前只生成候选,不修改正式项目 skill。',
|
|
350
|
+
],
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export {
|
|
355
|
+
buildCleanEvidenceText,
|
|
356
|
+
buildSessionEvidenceReviewSource,
|
|
357
|
+
cleanEvidenceRecords,
|
|
358
|
+
extractSessionEvidenceRecords,
|
|
359
|
+
filterGenericFrameworkTokens,
|
|
360
|
+
isGenericFrameworkTerm,
|
|
361
|
+
normalizeSessionEvidenceRecords,
|
|
362
|
+
sanitizeEvidenceText,
|
|
363
|
+
};
|
package/src/standards.js
CHANGED
|
@@ -773,6 +773,42 @@ export async function initStandardsWorkspace(projectRoot, options = {}) {
|
|
|
773
773
|
};
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
+
export const DOC_MEGA_LINE_CHARS = 300;
|
|
777
|
+
export const DOC_AVG_LINE_CHARS = 200;
|
|
778
|
+
export const DOC_STALE_VERSION_THRESHOLD = 20;
|
|
779
|
+
|
|
780
|
+
export function analyzeDocQuality(relativePath, text) {
|
|
781
|
+
const lines = text.split('\n');
|
|
782
|
+
const contentLines = lines.filter((line) => line.trim().length > 0);
|
|
783
|
+
const megaLines = contentLines.filter((line) => line.length > DOC_MEGA_LINE_CHARS);
|
|
784
|
+
const totalChars = contentLines.reduce((sum, line) => sum + line.length, 0);
|
|
785
|
+
const avgLineChars = contentLines.length > 0 ? Math.round(totalChars / contentLines.length) : 0;
|
|
786
|
+
const maxLineChars = contentLines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
787
|
+
return {
|
|
788
|
+
path: relativePath,
|
|
789
|
+
lines: lines.length,
|
|
790
|
+
contentLines: contentLines.length,
|
|
791
|
+
megaLines: megaLines.length,
|
|
792
|
+
avgLineChars,
|
|
793
|
+
maxLineChars,
|
|
794
|
+
bytes: Buffer.byteLength(text, 'utf8'),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export async function analyzeDocStaleness(projectRoot, docMtimeMs) {
|
|
799
|
+
const versionsDir = cjoin(projectRoot, '.openprd', 'state', 'versions');
|
|
800
|
+
const entries = await fs.readdir(versionsDir).catch(() => []);
|
|
801
|
+
const versionFiles = entries.filter((name) => /^v\d+\.json$/.test(name)).sort();
|
|
802
|
+
let versionsBehind = 0;
|
|
803
|
+
for (const name of versionFiles) {
|
|
804
|
+
const stat = await fs.stat(cjoin(versionsDir, name)).catch(() => null);
|
|
805
|
+
if (stat && stat.mtimeMs > docMtimeMs) {
|
|
806
|
+
versionsBehind += 1;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return { totalVersions: versionFiles.length, versionsBehind };
|
|
810
|
+
}
|
|
811
|
+
|
|
776
812
|
export async function checkStandardsWorkspace(projectRoot, options = {}) {
|
|
777
813
|
const errors = [];
|
|
778
814
|
const warnings = [];
|
|
@@ -838,6 +874,7 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
|
|
|
838
874
|
...externalReferenceIgnorePatterns(projectRoot, config ?? {}),
|
|
839
875
|
];
|
|
840
876
|
const requiredDocs = [];
|
|
877
|
+
const docsQuality = [];
|
|
841
878
|
const sourceFiles = await collectSourceFiles(projectRoot, projectRoot, [], ignorePatterns);
|
|
842
879
|
const hasProjectSource = sourceFiles.length > 0;
|
|
843
880
|
const openPrdToolProject = await isOpenPrdToolProject(projectRoot);
|
|
@@ -860,6 +897,28 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
|
|
|
860
897
|
if (hasProjectSource && !openPrdToolProject && options.docsContent !== false && DOC_PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
861
898
|
errors.push(`${relativePath} 仍包含模板占位内容,必须更新为当前项目事实。`);
|
|
862
899
|
}
|
|
900
|
+
|
|
901
|
+
if (options.docsQuality !== false) {
|
|
902
|
+
const quality = analyzeDocQuality(relativePath, text);
|
|
903
|
+
if (quality.megaLines > 0) {
|
|
904
|
+
warnings.push(`${relativePath} 存在 ${quality.megaLines} 行超过 ${DOC_MEGA_LINE_CHARS} 字符的巨行(最长 ${quality.maxLineChars} 字符),疑似 append-only 污染,建议运行 openprd docs-compact . 查看压缩建议。`);
|
|
905
|
+
}
|
|
906
|
+
if (quality.contentLines >= 10 && quality.avgLineChars > DOC_AVG_LINE_CHARS) {
|
|
907
|
+
warnings.push(`${relativePath} 平均行长 ${quality.avgLineChars} 字符(阈值 ${DOC_AVG_LINE_CHARS}),文档结构疑似退化为长段落堆叠,建议拆分为结构化小节。`);
|
|
908
|
+
}
|
|
909
|
+
if (doc.fileName === 'prd.md') {
|
|
910
|
+
const docStat = await fs.stat(docPath).catch(() => null);
|
|
911
|
+
if (docStat) {
|
|
912
|
+
const staleness = await analyzeDocStaleness(projectRoot, docStat.mtimeMs);
|
|
913
|
+
quality.versionsBehind = staleness.versionsBehind;
|
|
914
|
+
quality.totalVersions = staleness.totalVersions;
|
|
915
|
+
if (staleness.versionsBehind > DOC_STALE_VERSION_THRESHOLD) {
|
|
916
|
+
warnings.push(`${relativePath} 自上次更新后已产生 ${staleness.versionsBehind} 个 requirement 版本(阈值 ${DOC_STALE_VERSION_THRESHOLD}),基线 PRD 已陈旧,需要回填最新已确认需求。`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
docsQuality.push(quality);
|
|
921
|
+
}
|
|
863
922
|
}
|
|
864
923
|
|
|
865
924
|
const templateFiles = [];
|
|
@@ -901,6 +960,10 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
|
|
|
901
960
|
|
|
902
961
|
checks.push(`Standards docs root: ${STANDARD_DOCS_ROOT.replaceAll(path.sep, '/')}`);
|
|
903
962
|
checks.push(`Required docs: ${requiredDocs.filter((doc) => doc.exists).length}/${STANDARD_DOCS.length}.`);
|
|
963
|
+
if (docsQuality.length > 0) {
|
|
964
|
+
const pollutedDocs = docsQuality.filter((doc) => doc.megaLines > 0).length;
|
|
965
|
+
checks.push(`Docs quality: ${docsQuality.length} docs analyzed, ${pollutedDocs} with mega-lines > ${DOC_MEGA_LINE_CHARS} chars.`);
|
|
966
|
+
}
|
|
904
967
|
checks.push(`Manual templates: ${templateFiles.filter((file) => file.exists).length}/${STANDARD_TEMPLATE_FILES.length}.`);
|
|
905
968
|
const lineConfig = config?.developmentStandards?.codeFileLines ?? DEFAULT_DEVELOPMENT_STANDARDS.codeFileLines;
|
|
906
969
|
checks.push(`Development standards: code files ok <= ${lineConfig.okMax} lines, attention <= ${lineConfig.attentionMax} lines.`);
|
|
@@ -916,6 +979,7 @@ export async function checkStandardsWorkspace(projectRoot, options = {}) {
|
|
|
916
979
|
warnings,
|
|
917
980
|
checks,
|
|
918
981
|
requiredDocs,
|
|
982
|
+
docsQuality,
|
|
919
983
|
templateFiles,
|
|
920
984
|
manualReport,
|
|
921
985
|
};
|
package/src/test-strategy.js
CHANGED
|
@@ -145,13 +145,16 @@ export function inferTestStrategyForTask(task = {}) {
|
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
const singleElementCentering = includesAny(text, [/centering-board|center-board|visual centroid|bbox center|canvas center|inside centered|internal centering|内部居中|视觉重心|主体外接框|外接框中心|画布中心|单元素|单个元素|偏心|不居中|没居中|图标内部|图片内部|素材内部|按钮图形|logo center|icon center/]);
|
|
149
|
+
if (includesAny(text, [/visual|视觉|截图|界面|页面|组件|样式|间距|留白|边距|宽度|高度|卡片|对齐|内容槽位|内部槽位|标题|副标题|标签|状态|价格|按钮|密度|圆角|颜色|字号|图标|按钮|padding|margin|spacing|gap|browser|playwright|cypress|e2e|端到端|用户主路径|主流程|内部居中|视觉重心|主体外接框|画布中心|偏心|不居中|没居中/])) {
|
|
149
150
|
return {
|
|
150
151
|
layers: ['integration', 'e2e'],
|
|
151
152
|
size: 'large',
|
|
152
|
-
scope: includesAny(text, [/visual
|
|
153
|
-
evidencePlan:
|
|
154
|
-
|
|
153
|
+
scope: includesAny(text, [/visual|视觉|截图|样式|间距|留白|边距|宽度|高度|卡片|对齐|内容槽位|内部槽位|标题|副标题|标签|状态|价格|按钮|密度|圆角|颜色|字号|图标|按钮|padding|margin|spacing|gap|内部居中|视觉重心|主体外接框|画布中心|偏心|不居中|没居中/]) ? 'visual-flow' : 'user-flow',
|
|
154
|
+
evidencePlan: singleElementCentering
|
|
155
|
+
? '主流程自动化、截图或 visual-compare 证据 + 本任务 verify 命令;单元素内部居中要补 centering-board,量测画布中心、主体外接框中心和视觉重心偏移'
|
|
156
|
+
: '主流程自动化、截图或 visual-compare 证据 + 本任务 verify 命令;轻量 UI 可视优化也需要 before/after、verification-board 或 alignment-board 证据,同构卡片/列表要覆盖内部内容槽位',
|
|
157
|
+
upgradeReason: '触达用户可见路径,需要端到端或视觉级证据;轻量 UI 可视优化不能只用构建或 dev-check 收口',
|
|
155
158
|
inferred: true,
|
|
156
159
|
};
|
|
157
160
|
}
|