@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
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT,
|
|
5
|
+
LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT,
|
|
6
|
+
LEGACY_OPENPRD_CHANGE_ROOT,
|
|
7
|
+
OPENPRD_ACCEPTED_SPEC_ROOT,
|
|
8
|
+
OPENPRD_ARCHIVE_CHANGE_ROOT,
|
|
9
|
+
OPENPRD_CHANGE_ROOT,
|
|
10
|
+
} from './constants.js';
|
|
11
|
+
|
|
12
|
+
function cjoin(...parts) {
|
|
13
|
+
return path.join(...parts);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function exists(filePath) {
|
|
17
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function relativeProjectPath(projectRoot, filePath) {
|
|
21
|
+
return path.relative(projectRoot, filePath) || '.';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function removeDirectoryIfEmpty(dirPath) {
|
|
25
|
+
const entries = await fs.readdir(dirPath).catch(() => null);
|
|
26
|
+
if (entries && entries.length === 0) {
|
|
27
|
+
await fs.rmdir(dirPath);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function sameFileContent(leftPath, rightPath) {
|
|
34
|
+
const [left, right] = await Promise.all([
|
|
35
|
+
fs.readFile(leftPath),
|
|
36
|
+
fs.readFile(rightPath),
|
|
37
|
+
]);
|
|
38
|
+
return left.equals(right);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function movePath(sourcePath, targetPath) {
|
|
42
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
43
|
+
try {
|
|
44
|
+
await fs.rename(sourcePath, targetPath);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code !== 'EXDEV') {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const stat = await fs.stat(sourcePath);
|
|
50
|
+
await fs.cp(sourcePath, targetPath, { recursive: stat.isDirectory(), force: false, errorOnExist: true });
|
|
51
|
+
await fs.rm(sourcePath, { recursive: stat.isDirectory(), force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function mergeLegacyTree(projectRoot, sourcePath, targetPath, changes) {
|
|
56
|
+
const sourceStat = await fs.stat(sourcePath).catch(() => null);
|
|
57
|
+
if (!sourceStat) {
|
|
58
|
+
return { migrated: 0, conflicts: 0 };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const targetStat = await fs.stat(targetPath).catch(() => null);
|
|
62
|
+
if (!targetStat) {
|
|
63
|
+
await movePath(sourcePath, targetPath);
|
|
64
|
+
changes.push({
|
|
65
|
+
path: relativeProjectPath(projectRoot, targetPath),
|
|
66
|
+
from: relativeProjectPath(projectRoot, sourcePath),
|
|
67
|
+
status: 'migrated',
|
|
68
|
+
});
|
|
69
|
+
return { migrated: 1, conflicts: 0 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (sourceStat.isDirectory() && targetStat.isDirectory()) {
|
|
73
|
+
let migrated = 0;
|
|
74
|
+
let conflicts = 0;
|
|
75
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const sourceChild = cjoin(sourcePath, entry.name);
|
|
78
|
+
const targetChild = cjoin(targetPath, entry.name);
|
|
79
|
+
const result = await mergeLegacyTree(projectRoot, sourceChild, targetChild, changes);
|
|
80
|
+
migrated += result.migrated;
|
|
81
|
+
conflicts += result.conflicts;
|
|
82
|
+
}
|
|
83
|
+
if (await removeDirectoryIfEmpty(sourcePath)) {
|
|
84
|
+
changes.push({
|
|
85
|
+
path: relativeProjectPath(projectRoot, sourcePath),
|
|
86
|
+
status: 'removed-empty-legacy-dir',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return { migrated, conflicts };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (sourceStat.isFile() && targetStat.isFile() && await sameFileContent(sourcePath, targetPath)) {
|
|
93
|
+
await fs.rm(sourcePath, { force: true });
|
|
94
|
+
changes.push({
|
|
95
|
+
path: relativeProjectPath(projectRoot, sourcePath),
|
|
96
|
+
target: relativeProjectPath(projectRoot, targetPath),
|
|
97
|
+
status: 'removed-duplicate-legacy-file',
|
|
98
|
+
});
|
|
99
|
+
return { migrated: 0, conflicts: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
changes.push({
|
|
103
|
+
path: relativeProjectPath(projectRoot, sourcePath),
|
|
104
|
+
target: relativeProjectPath(projectRoot, targetPath),
|
|
105
|
+
status: 'conflict-kept-legacy',
|
|
106
|
+
});
|
|
107
|
+
return { migrated: 0, conflicts: 1 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function hasLegacyOpenPrdArtifacts(projectRoot) {
|
|
111
|
+
const roots = [
|
|
112
|
+
cjoin(projectRoot, ...LEGACY_OPENPRD_CHANGE_ROOT),
|
|
113
|
+
cjoin(projectRoot, ...LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT),
|
|
114
|
+
cjoin(projectRoot, ...LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT),
|
|
115
|
+
];
|
|
116
|
+
for (const root of roots) {
|
|
117
|
+
if (await exists(root)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function migrateLegacyOpenPrdArtifacts(projectRoot) {
|
|
125
|
+
const legacyRoot = cjoin(projectRoot, 'openprd');
|
|
126
|
+
const changes = [];
|
|
127
|
+
let migrated = 0;
|
|
128
|
+
let conflicts = 0;
|
|
129
|
+
|
|
130
|
+
const pairs = [
|
|
131
|
+
[LEGACY_OPENPRD_CHANGE_ROOT, OPENPRD_CHANGE_ROOT],
|
|
132
|
+
[LEGACY_OPENPRD_ACCEPTED_SPEC_ROOT, OPENPRD_ACCEPTED_SPEC_ROOT],
|
|
133
|
+
[LEGACY_OPENPRD_ARCHIVE_CHANGE_ROOT, OPENPRD_ARCHIVE_CHANGE_ROOT],
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
for (const [legacyParts, targetParts] of pairs) {
|
|
137
|
+
const result = await mergeLegacyTree(
|
|
138
|
+
projectRoot,
|
|
139
|
+
cjoin(projectRoot, ...legacyParts),
|
|
140
|
+
cjoin(projectRoot, ...targetParts),
|
|
141
|
+
changes,
|
|
142
|
+
);
|
|
143
|
+
migrated += result.migrated;
|
|
144
|
+
conflicts += result.conflicts;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (await exists(legacyRoot)) {
|
|
148
|
+
await removeDirectoryIfEmpty(cjoin(legacyRoot, 'archive')).catch(() => false);
|
|
149
|
+
if (await removeDirectoryIfEmpty(legacyRoot).catch(() => false)) {
|
|
150
|
+
changes.push({ path: 'openprd', status: 'removed-empty-legacy-root' });
|
|
151
|
+
} else if (changes.length > 0) {
|
|
152
|
+
changes.push({ path: 'openprd', status: 'legacy-root-kept-nonempty' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
ok: conflicts === 0,
|
|
158
|
+
action: 'migrate-legacy-openprd-artifacts',
|
|
159
|
+
projectRoot,
|
|
160
|
+
migrated,
|
|
161
|
+
conflicts,
|
|
162
|
+
changed: changes.length > 0,
|
|
163
|
+
changes,
|
|
164
|
+
errors: conflicts > 0
|
|
165
|
+
? ['旧 openprd 目录中存在与 .openprd 目标路径同名但内容不同的文件,已保留旧文件等待人工处理。']
|
|
166
|
+
: [],
|
|
167
|
+
};
|
|
168
|
+
}
|
package/src/openspec/paths.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {
|
|
4
|
-
LEGACY_OPENSPEC_CHANGE_ROOT,
|
|
5
|
-
LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH,
|
|
6
|
-
LEGACY_OPENSPEC_DISCOVERY_DIR,
|
|
7
4
|
OPENPRD_ACCEPTED_SPEC_ROOT,
|
|
8
5
|
OPENPRD_ARCHIVE_CHANGE_ROOT,
|
|
9
6
|
OPENPRD_CHANGE_ROOT,
|
|
@@ -23,34 +20,18 @@ export function openPrdDiscoveryDir(projectRoot) {
|
|
|
23
20
|
return cjoin(projectRoot, ...OPENPRD_DISCOVERY_DIR);
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
export function legacyOpenSpecDiscoveryDir(projectRoot) {
|
|
27
|
-
return cjoin(projectRoot, ...LEGACY_OPENSPEC_DISCOVERY_DIR);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
23
|
export function openPrdDiscoveryConfigPath(projectRoot) {
|
|
31
24
|
return cjoin(projectRoot, ...OPENPRD_DISCOVERY_CONFIG_PATH);
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
export function legacyOpenSpecDiscoveryConfigPath(projectRoot) {
|
|
35
|
-
return cjoin(projectRoot, ...LEGACY_OPENSPEC_DISCOVERY_CONFIG_PATH);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
27
|
export async function readDiscoveryConfig(projectRoot, readJson) {
|
|
39
|
-
|
|
40
|
-
const legacy = legacyOpenSpecDiscoveryConfigPath(projectRoot);
|
|
41
|
-
return (await readJson(primary).catch(() => null))
|
|
42
|
-
?? (await readJson(legacy).catch(() => null))
|
|
43
|
-
?? {};
|
|
28
|
+
return (await readJson(openPrdDiscoveryConfigPath(projectRoot)).catch(() => null)) ?? {};
|
|
44
29
|
}
|
|
45
30
|
|
|
46
31
|
export function openPrdChangeRoot(projectRoot) {
|
|
47
32
|
return cjoin(projectRoot, ...OPENPRD_CHANGE_ROOT);
|
|
48
33
|
}
|
|
49
34
|
|
|
50
|
-
export function legacyOpenSpecChangeRoot(projectRoot) {
|
|
51
|
-
return cjoin(projectRoot, ...LEGACY_OPENSPEC_CHANGE_ROOT);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
35
|
export function openPrdAcceptedSpecRoot(projectRoot) {
|
|
55
36
|
return cjoin(projectRoot, ...OPENPRD_ACCEPTED_SPEC_ROOT);
|
|
56
37
|
}
|
|
@@ -62,7 +43,6 @@ export function openPrdArchiveChangeRoot(projectRoot) {
|
|
|
62
43
|
export async function resolveChangeDir(projectRoot, changeId) {
|
|
63
44
|
const candidates = [
|
|
64
45
|
cjoin(openPrdChangeRoot(projectRoot), changeId),
|
|
65
|
-
cjoin(legacyOpenSpecChangeRoot(projectRoot), changeId),
|
|
66
46
|
cjoin(openPrdArchiveChangeRoot(projectRoot), changeId),
|
|
67
47
|
];
|
|
68
48
|
for (const candidate of candidates) {
|
|
@@ -76,7 +56,6 @@ export async function resolveChangeDir(projectRoot, changeId) {
|
|
|
76
56
|
export async function listChangeDirs(projectRoot) {
|
|
77
57
|
const roots = [
|
|
78
58
|
{ root: openPrdChangeRoot(projectRoot), source: 'openprd', archived: false },
|
|
79
|
-
{ root: legacyOpenSpecChangeRoot(projectRoot), source: 'legacy-openspec', archived: false },
|
|
80
59
|
{ root: openPrdArchiveChangeRoot(projectRoot), source: 'openprd-archive', archived: true },
|
|
81
60
|
];
|
|
82
61
|
const changes = [];
|
package/src/prd-core.js
CHANGED
|
@@ -298,7 +298,7 @@ export function buildPrdSnapshot(ws, options = {}) {
|
|
|
298
298
|
handoff: {
|
|
299
299
|
owner: pickValue(options.handoffOwner, state.handoffOwner, owner),
|
|
300
300
|
nextStep: pickValue(options.nextStep, state.nextStep, '评审已生成的 PRD,并准备交接。'),
|
|
301
|
-
targetSystem: pickValue(options.targetSystem, state.targetSystem, '
|
|
301
|
+
targetSystem: pickValue(options.targetSystem, state.targetSystem, 'OpenPrd'),
|
|
302
302
|
},
|
|
303
303
|
typeSpecific: buildTypeSpecificSection(productType, state, options),
|
|
304
304
|
};
|
|
@@ -18,6 +18,15 @@ function inferVisualReviewMode(value) {
|
|
|
18
18
|
if (normalized.includes('visual-parallel-board') || normalized.includes('parallel-board') || normalized.includes('experiment-board')) {
|
|
19
19
|
return 'parallel-board';
|
|
20
20
|
}
|
|
21
|
+
if (normalized.includes('visual-verification-board') || normalized.includes('verification-board') || normalized.includes('screenshot-evidence-board') || normalized.includes('screenshot-board')) {
|
|
22
|
+
return 'verification-board';
|
|
23
|
+
}
|
|
24
|
+
if (normalized.includes('visual-alignment-board') || normalized.includes('alignment-board') || normalized.includes('grid-alignment')) {
|
|
25
|
+
return 'alignment-board';
|
|
26
|
+
}
|
|
27
|
+
if (normalized.includes('visual-centering-board') || normalized.includes('centering-board') || normalized.includes('center-board')) {
|
|
28
|
+
return 'centering-board';
|
|
29
|
+
}
|
|
21
30
|
if (normalized.includes('visual-before-after') || normalized.includes('before-after')) {
|
|
22
31
|
return 'before-after';
|
|
23
32
|
}
|
|
@@ -73,6 +82,9 @@ export async function listVisualReviewArtifacts(projectRoot) {
|
|
|
73
82
|
metadataPath: entry.relativePath,
|
|
74
83
|
mode: inferredMode,
|
|
75
84
|
labels: payload.labels ?? existing.labels ?? null,
|
|
85
|
+
alignmentScope: payload.alignmentScope ?? existing.alignmentScope ?? null,
|
|
86
|
+
centering: payload.centering ?? existing.centering ?? null,
|
|
87
|
+
groups: Array.isArray(payload.groups) ? payload.groups : (existing.groups ?? null),
|
|
76
88
|
generatedAt: payload.generatedAt ?? existing.generatedAt ?? null,
|
|
77
89
|
mtimeMs: stat?.mtimeMs ?? existing.mtimeMs ?? 0,
|
|
78
90
|
});
|
|
@@ -138,29 +150,186 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
|
|
|
138
150
|
'parallel board',
|
|
139
151
|
'parallel experiment',
|
|
140
152
|
'experiment board',
|
|
141
|
-
'evidence board',
|
|
142
153
|
'并行实验',
|
|
143
154
|
'并行方向',
|
|
144
155
|
'多方向实验',
|
|
145
156
|
'方案对比板',
|
|
146
|
-
|
|
157
|
+
];
|
|
158
|
+
const verificationTokens = [
|
|
159
|
+
'verification board',
|
|
160
|
+
'verification-board',
|
|
161
|
+
'screenshot board',
|
|
162
|
+
'screenshot-evidence-board',
|
|
163
|
+
'visual-verification-board',
|
|
164
|
+
'computer screenshot',
|
|
165
|
+
'computer use screenshot',
|
|
166
|
+
'browser screenshot',
|
|
167
|
+
'playwright screenshot',
|
|
168
|
+
'screencapture',
|
|
169
|
+
'普通截图',
|
|
170
|
+
'实测截图',
|
|
171
|
+
'截图实测',
|
|
172
|
+
'截图证据',
|
|
173
|
+
'截图验收',
|
|
174
|
+
'Computer 实测',
|
|
175
|
+
'Browser 实测',
|
|
176
|
+
'运行态截图',
|
|
177
|
+
];
|
|
178
|
+
const alignmentTokens = [
|
|
179
|
+
'alignment board',
|
|
180
|
+
'alignment-board',
|
|
181
|
+
'grid alignment',
|
|
182
|
+
'grid-alignment',
|
|
183
|
+
'baseline',
|
|
184
|
+
'baseline spread',
|
|
185
|
+
'same slot',
|
|
186
|
+
'repeated card',
|
|
187
|
+
'homogeneous card',
|
|
188
|
+
'list card',
|
|
189
|
+
'card grid',
|
|
190
|
+
'table layout',
|
|
191
|
+
'对齐辅助线',
|
|
192
|
+
'辅助线',
|
|
193
|
+
'网格对齐',
|
|
194
|
+
'基线',
|
|
195
|
+
'坐标偏差',
|
|
196
|
+
'同构',
|
|
197
|
+
'重复单元',
|
|
198
|
+
'相同槽位',
|
|
199
|
+
'相同文案类型',
|
|
200
|
+
'列表卡片',
|
|
201
|
+
'卡片列表',
|
|
202
|
+
'卡片网格',
|
|
203
|
+
'网格卡片',
|
|
204
|
+
'网格列表',
|
|
205
|
+
'列表网格',
|
|
206
|
+
'表格',
|
|
207
|
+
'排版对齐',
|
|
208
|
+
'排版漂移',
|
|
209
|
+
'布局漂移',
|
|
210
|
+
'对齐有问题',
|
|
211
|
+
'对齐问题',
|
|
212
|
+
'对齐不准',
|
|
213
|
+
'对不齐',
|
|
214
|
+
'不对齐',
|
|
215
|
+
'没有对齐',
|
|
216
|
+
'没对齐',
|
|
217
|
+
'左右偏差',
|
|
218
|
+
'上下偏差',
|
|
219
|
+
'横向偏差',
|
|
220
|
+
'竖向偏差',
|
|
221
|
+
];
|
|
222
|
+
const centeringTokens = [
|
|
223
|
+
'centering board',
|
|
224
|
+
'centering-board',
|
|
225
|
+
'center board',
|
|
226
|
+
'center-board',
|
|
227
|
+
'visual-centering-board',
|
|
228
|
+
'canvas center',
|
|
229
|
+
'visual centroid',
|
|
230
|
+
'bbox center',
|
|
231
|
+
'bounding box center',
|
|
232
|
+
'active bounds',
|
|
233
|
+
'single element',
|
|
234
|
+
'inside centered',
|
|
235
|
+
'internal centering',
|
|
236
|
+
'logo center',
|
|
237
|
+
'logo centered',
|
|
238
|
+
'logo internal',
|
|
239
|
+
'icon center',
|
|
240
|
+
'icon centered',
|
|
241
|
+
'icon internal',
|
|
242
|
+
'avatar center',
|
|
243
|
+
'badge center',
|
|
244
|
+
'button graphic',
|
|
245
|
+
'内部居中',
|
|
246
|
+
'内部不居中',
|
|
247
|
+
'居中判定',
|
|
248
|
+
'居中识别',
|
|
249
|
+
'居中评估',
|
|
250
|
+
'视觉重心',
|
|
251
|
+
'主体外接框',
|
|
252
|
+
'外接框中心',
|
|
253
|
+
'画布中心',
|
|
254
|
+
'单元素',
|
|
255
|
+
'单个元素',
|
|
256
|
+
'单一元素',
|
|
257
|
+
'图标内部',
|
|
258
|
+
'图片内部',
|
|
259
|
+
'素材内部',
|
|
260
|
+
'按钮图形',
|
|
261
|
+
'偏心',
|
|
262
|
+
'不居中',
|
|
263
|
+
'没居中',
|
|
264
|
+
'左偏',
|
|
265
|
+
'右偏',
|
|
266
|
+
'上飘',
|
|
267
|
+
'下沉',
|
|
268
|
+
];
|
|
269
|
+
const contentSlotAlignmentTokens = [
|
|
270
|
+
'content slot',
|
|
271
|
+
'internal slot',
|
|
272
|
+
'same slot',
|
|
273
|
+
'title slot',
|
|
274
|
+
'price slot',
|
|
275
|
+
'button slot',
|
|
276
|
+
'card content',
|
|
277
|
+
'repeated card',
|
|
278
|
+
'homogeneous card',
|
|
279
|
+
'list card',
|
|
280
|
+
'card grid',
|
|
281
|
+
'标题',
|
|
282
|
+
'副标题',
|
|
283
|
+
'描述',
|
|
284
|
+
'正文',
|
|
285
|
+
'标签',
|
|
286
|
+
'状态',
|
|
287
|
+
'价格',
|
|
288
|
+
'按钮',
|
|
289
|
+
'操作区',
|
|
290
|
+
'图标',
|
|
291
|
+
'内容槽位',
|
|
292
|
+
'内部槽位',
|
|
293
|
+
'相同槽位',
|
|
294
|
+
'相同文案类型',
|
|
295
|
+
'相同组件槽位',
|
|
296
|
+
'列表卡片',
|
|
297
|
+
'卡片列表',
|
|
298
|
+
'卡片网格',
|
|
299
|
+
'网格卡片',
|
|
300
|
+
'网格列表',
|
|
301
|
+
'列表网格',
|
|
147
302
|
];
|
|
148
303
|
const expectsReferenceCompare = includesAny(haystack, referenceTokens);
|
|
149
304
|
const expectsFocusBoard = includesAny(haystack, focusTokens);
|
|
150
305
|
const expectsParallelBoard = includesAny(haystack, parallelTokens);
|
|
306
|
+
const expectsVerificationBoard = includesAny(haystack, verificationTokens);
|
|
307
|
+
const expectsAlignmentBoard = includesAny(haystack, alignmentTokens);
|
|
308
|
+
const expectsCenteringBoard = includesAny(haystack, centeringTokens);
|
|
309
|
+
const expectsContentSlotAlignment = expectsAlignmentBoard && includesAny(haystack, contentSlotAlignmentTokens);
|
|
151
310
|
const referenceArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'reference-actual');
|
|
152
311
|
const beforeAfterArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'before-after');
|
|
153
312
|
const focusArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'focus-board');
|
|
154
313
|
const parallelArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'parallel-board');
|
|
314
|
+
const verificationArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'verification-board');
|
|
315
|
+
const alignmentArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'alignment-board');
|
|
316
|
+
const centeringArtifacts = visualArtifacts.filter((artifact) => artifact.mode === 'centering-board');
|
|
317
|
+
const contentSlotAlignmentArtifacts = alignmentArtifacts.filter(hasContentSlotAlignmentEvidence);
|
|
155
318
|
let matchingArtifacts;
|
|
156
|
-
if (
|
|
319
|
+
if (expectsCenteringBoard) {
|
|
320
|
+
matchingArtifacts = centeringArtifacts;
|
|
321
|
+
} else if (expectsAlignmentBoard) {
|
|
322
|
+
matchingArtifacts = alignmentArtifacts;
|
|
323
|
+
} else if (expectsVerificationBoard) {
|
|
324
|
+
matchingArtifacts = verificationArtifacts;
|
|
325
|
+
} else if (expectsParallelBoard) {
|
|
157
326
|
matchingArtifacts = parallelArtifacts;
|
|
158
327
|
} else if (expectsFocusBoard) {
|
|
159
328
|
matchingArtifacts = focusArtifacts;
|
|
160
329
|
} else if (expectsReferenceCompare) {
|
|
161
330
|
matchingArtifacts = referenceArtifacts;
|
|
162
331
|
} else {
|
|
163
|
-
matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts];
|
|
332
|
+
matchingArtifacts = [...referenceArtifacts, ...beforeAfterArtifacts, ...focusArtifacts, ...parallelArtifacts, ...verificationArtifacts, ...alignmentArtifacts, ...centeringArtifacts];
|
|
164
333
|
}
|
|
165
334
|
const evidenceSources = matchingArtifacts.slice(0, 12).map((artifact) => ({
|
|
166
335
|
path: artifact.path,
|
|
@@ -170,15 +339,31 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
|
|
|
170
339
|
? 'visual-review/before-after'
|
|
171
340
|
: artifact.mode === 'focus-board'
|
|
172
341
|
? 'visual-review/focus-board'
|
|
173
|
-
: '
|
|
342
|
+
: artifact.mode === 'parallel-board'
|
|
343
|
+
? 'visual-review/parallel-board'
|
|
344
|
+
: artifact.mode === 'verification-board'
|
|
345
|
+
? 'visual-review/verification-board'
|
|
346
|
+
: artifact.mode === 'alignment-board'
|
|
347
|
+
? 'visual-review/alignment-board'
|
|
348
|
+
: 'visual-review/centering-board',
|
|
174
349
|
}));
|
|
175
350
|
const warnings = [];
|
|
351
|
+
const missingContentSlotAlignment = relevant
|
|
352
|
+
&& expectsContentSlotAlignment
|
|
353
|
+
&& alignmentArtifacts.length > 0
|
|
354
|
+
&& contentSlotAlignmentArtifacts.length === 0;
|
|
176
355
|
|
|
177
356
|
if (relevant && matchingArtifacts.length === 0) {
|
|
178
357
|
warnings.push(
|
|
179
|
-
|
|
358
|
+
expectsCenteringBoard
|
|
359
|
+
? '检测到 logo、图标、头像、徽标、图片或按钮图形等单个元素的内部居中/视觉重心语义,但未看到本次内部居中证据板。单张截图或主观“看起来居中”不能替代画布中心、主体外接框和视觉重心偏移量测。'
|
|
360
|
+
: expectsAlignmentBoard
|
|
361
|
+
? '检测到同构列表、卡片、网格、表格或对齐反馈语义,但未看到本次对齐辅助线证据板。新功能开发中只要有重复同构单元,也应同时量测容器轨道和内部内容槽位的坐标偏差。'
|
|
362
|
+
: expectsParallelBoard
|
|
180
363
|
? '检测到界面视觉改动且用户在比较多方向实验,但未看到本次并行实验证据板。'
|
|
181
|
-
:
|
|
364
|
+
: expectsVerificationBoard
|
|
365
|
+
? '检测到普通截图、Computer/Browser 实测或运行态截图语义,但未看到本次截图实测证据板。普通截图只能作为原始素材,不能单独替代视觉收口拼图。'
|
|
366
|
+
: expectsFocusBoard
|
|
182
367
|
? '检测到界面视觉改动且用户在关注局部细节,但未看到本次局部焦点证据板。'
|
|
183
368
|
: expectsReferenceCompare
|
|
184
369
|
? '检测到界面视觉改动且已有参考图/设计稿语义,但未看到本次“效果图 / 实现截图”对比证据。'
|
|
@@ -190,24 +375,46 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
|
|
|
190
375
|
warnings.push('当前有视觉证据,但局部细节仍建议补一份局部焦点证据板,方便围绕编号区域复核。');
|
|
191
376
|
} else if (relevant && expectsParallelBoard && parallelArtifacts.length === 0 && matchingArtifacts.length > 0) {
|
|
192
377
|
warnings.push('当前有视觉证据,但多方向实验仍建议补一份并行实验证据板,把方案和指标放到同一板里审查。');
|
|
378
|
+
} else if (relevant && expectsVerificationBoard && verificationArtifacts.length === 0 && matchingArtifacts.length > 0) {
|
|
379
|
+
warnings.push('当前有视觉证据,但普通截图或 Computer 实测仍建议补一份截图实测证据板,把截图、实测路径和检查点拼到同一张图里。');
|
|
380
|
+
} else if (relevant && expectsAlignmentBoard && alignmentArtifacts.length === 0 && matchingArtifacts.length > 0) {
|
|
381
|
+
warnings.push('当前有视觉证据,但同构列表、卡片、网格或表格仍建议补一份对齐辅助线证据板,把相同槽位的 spread 放到同一张图里。');
|
|
382
|
+
} else if (relevant && expectsCenteringBoard && centeringArtifacts.length === 0 && matchingArtifacts.length > 0) {
|
|
383
|
+
warnings.push('当前有视觉证据,但单元素内部居中仍建议补一份内部居中证据板,把画布中心、主体外接框和视觉重心偏移放到同一张图里。');
|
|
384
|
+
} else if (missingContentSlotAlignment) {
|
|
385
|
+
warnings.push('当前只看到对齐辅助线证据板,但没有看到标题、副标题、描述、标签、状态、价格、按钮或图标等内部内容槽位量测。卡片/列表/网格不能只量外框、列宽或行顶。');
|
|
193
386
|
}
|
|
194
387
|
|
|
195
388
|
const summary = !relevant
|
|
196
389
|
? '当前场景未要求视觉评审证据'
|
|
197
390
|
: matchingArtifacts.length > 0
|
|
198
|
-
|
|
199
|
-
|
|
391
|
+
? (
|
|
392
|
+
missingContentSlotAlignment
|
|
393
|
+
? '已找到对齐辅助线证据板,但缺少内部内容槽位量测'
|
|
394
|
+
: expectsCenteringBoard
|
|
395
|
+
? `已找到 ${matchingArtifacts.length} 份内部居中证据板`
|
|
396
|
+
: expectsAlignmentBoard
|
|
397
|
+
? `已找到 ${matchingArtifacts.length} 份对齐辅助线证据板`
|
|
398
|
+
: expectsParallelBoard
|
|
200
399
|
? `已找到 ${matchingArtifacts.length} 份并行实验证据板`
|
|
201
|
-
:
|
|
400
|
+
: expectsVerificationBoard
|
|
401
|
+
? `已找到 ${matchingArtifacts.length} 份截图实测证据板`
|
|
402
|
+
: expectsFocusBoard
|
|
202
403
|
? `已找到 ${matchingArtifacts.length} 份局部焦点证据板`
|
|
203
404
|
: expectsReferenceCompare
|
|
204
405
|
? `已找到 ${matchingArtifacts.length} 份效果图 / 实现截图对比证据`
|
|
205
|
-
: `已找到 ${matchingArtifacts.length}
|
|
406
|
+
: `已找到 ${matchingArtifacts.length} 份视觉对比、局部焦点、截图实测或修改前后自检证据`
|
|
206
407
|
)
|
|
207
408
|
: (
|
|
208
|
-
|
|
409
|
+
expectsCenteringBoard
|
|
410
|
+
? '未找到本次内部居中证据板'
|
|
411
|
+
: expectsAlignmentBoard
|
|
412
|
+
? '未找到本次对齐辅助线证据板'
|
|
413
|
+
: expectsParallelBoard
|
|
209
414
|
? '未找到本次并行实验证据板'
|
|
210
|
-
:
|
|
415
|
+
: expectsVerificationBoard
|
|
416
|
+
? '未找到本次截图实测证据板'
|
|
417
|
+
: expectsFocusBoard
|
|
211
418
|
? '未找到本次局部焦点证据板'
|
|
212
419
|
: expectsReferenceCompare
|
|
213
420
|
? '未找到本次效果图 / 实现截图对比证据'
|
|
@@ -215,13 +422,19 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
|
|
|
215
422
|
);
|
|
216
423
|
|
|
217
424
|
return {
|
|
218
|
-
status: !relevant || matchingArtifacts.length > 0 ? 'pass' : 'needs-evidence',
|
|
425
|
+
status: !relevant || (matchingArtifacts.length > 0 && !missingContentSlotAlignment) ? 'pass' : 'needs-evidence',
|
|
219
426
|
relevant,
|
|
220
427
|
expectsReferenceCompare,
|
|
221
428
|
expectsFocusBoard,
|
|
222
429
|
expectsParallelBoard,
|
|
430
|
+
expectsVerificationBoard,
|
|
431
|
+
expectsAlignmentBoard,
|
|
432
|
+
expectsCenteringBoard,
|
|
433
|
+
expectsContentSlotAlignment,
|
|
223
434
|
artifacts: visualArtifacts,
|
|
224
435
|
matchingArtifacts,
|
|
436
|
+
contentSlotAlignmentArtifacts,
|
|
437
|
+
centeringArtifacts,
|
|
225
438
|
warnings,
|
|
226
439
|
evidence: {
|
|
227
440
|
present: matchingArtifacts.length > 0,
|
|
@@ -231,6 +444,36 @@ export function detectVisualReview({ policy, activeChangeContext, activeTasks, v
|
|
|
231
444
|
};
|
|
232
445
|
}
|
|
233
446
|
|
|
447
|
+
function hasContentSlotAlignmentEvidence(artifact) {
|
|
448
|
+
const haystack = JSON.stringify([
|
|
449
|
+
artifact.path,
|
|
450
|
+
artifact.metadataPath,
|
|
451
|
+
artifact.labels,
|
|
452
|
+
artifact.alignmentScope,
|
|
453
|
+
artifact.groups,
|
|
454
|
+
]).toLowerCase();
|
|
455
|
+
return [
|
|
456
|
+
'content-slot',
|
|
457
|
+
'content slot',
|
|
458
|
+
'internal slot',
|
|
459
|
+
'内容槽位',
|
|
460
|
+
'内部槽位',
|
|
461
|
+
'相同槽位',
|
|
462
|
+
'相同文案类型',
|
|
463
|
+
'相同组件槽位',
|
|
464
|
+
'标题',
|
|
465
|
+
'副标题',
|
|
466
|
+
'描述',
|
|
467
|
+
'正文',
|
|
468
|
+
'标签',
|
|
469
|
+
'状态',
|
|
470
|
+
'价格',
|
|
471
|
+
'按钮',
|
|
472
|
+
'操作区',
|
|
473
|
+
'图标',
|
|
474
|
+
].some((token) => haystack.includes(String(token).toLowerCase()));
|
|
475
|
+
}
|
|
476
|
+
|
|
234
477
|
export {
|
|
235
478
|
VISUAL_REVIEW_DIR,
|
|
236
479
|
VISUAL_REVIEW_SCHEMA,
|