@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
|
@@ -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
|
@@ -242,11 +242,26 @@ export function buildPrdSnapshot(ws, options = {}) {
|
|
|
242
242
|
validation: {
|
|
243
243
|
community: normalizeArray(pickValue(options.community, state.community)),
|
|
244
244
|
seedUsers: normalizeArray(pickValue(options.seedUsers, state.seedUsers)),
|
|
245
|
+
communityFit: normalizeArray(pickValue(options.communityFit, state.communityFit)),
|
|
245
246
|
currentAlternative: pickValue(options.currentAlternative, state.currentAlternative, state.asIs),
|
|
247
|
+
painEvidence: normalizeArray(pickValue(options.painEvidence, state.painEvidence)),
|
|
246
248
|
manualPath: normalizeArray(pickValue(options.manualPath, state.manualPath)),
|
|
249
|
+
manualPlaybook: normalizeArray(pickValue(options.manualPlaybook, state.manualPlaybook)),
|
|
247
250
|
commitmentSignals: normalizeArray(pickValue(options.commitmentSignals, state.commitmentSignals)),
|
|
248
251
|
firstValidationStep: pickValue(options.firstValidationStep, state.firstValidationStep, state.nextStep),
|
|
249
252
|
defaultAlivePlan: normalizeArray(pickValue(options.defaultAlivePlan, state.defaultAlivePlan)),
|
|
253
|
+
paymentProof: normalizeArray(pickValue(options.paymentProof, state.paymentProof)),
|
|
254
|
+
mvpSlice: pickValue(options.mvpSlice, state.mvpSlice),
|
|
255
|
+
weekendTest: pickValue(options.weekendTest, state.weekendTest),
|
|
256
|
+
smallestExecution: normalizeArray(pickValue(options.smallestExecution, state.smallestExecution)),
|
|
257
|
+
productizeGate: normalizeArray(pickValue(options.productizeGate, state.productizeGate)),
|
|
258
|
+
firstCustomerPath: normalizeArray(pickValue(options.firstCustomerPath, state.firstCustomerPath)),
|
|
259
|
+
pricingHypothesis: pickValue(options.pricingHypothesis, state.pricingHypothesis),
|
|
260
|
+
customerOneProfitability: pickValue(options.customerOneProfitability, state.customerOneProfitability),
|
|
261
|
+
growthDiscipline: normalizeArray(pickValue(options.growthDiscipline, state.growthDiscipline)),
|
|
262
|
+
reversibility: pickValue(options.reversibility, state.reversibility),
|
|
263
|
+
customerTruth: pickValue(options.customerTruth, state.customerTruth),
|
|
264
|
+
valuesFit: pickValue(options.valuesFit, state.valuesFit),
|
|
250
265
|
},
|
|
251
266
|
scope: {
|
|
252
267
|
inScope: normalizeArray(pickValue(options.inScope, state.inScope)),
|
|
@@ -283,7 +298,7 @@ export function buildPrdSnapshot(ws, options = {}) {
|
|
|
283
298
|
handoff: {
|
|
284
299
|
owner: pickValue(options.handoffOwner, state.handoffOwner, owner),
|
|
285
300
|
nextStep: pickValue(options.nextStep, state.nextStep, '评审已生成的 PRD,并准备交接。'),
|
|
286
|
-
targetSystem: pickValue(options.targetSystem, state.targetSystem, '
|
|
301
|
+
targetSystem: pickValue(options.targetSystem, state.targetSystem, 'OpenPrd'),
|
|
287
302
|
},
|
|
288
303
|
typeSpecific: buildTypeSpecificSection(productType, state, options),
|
|
289
304
|
};
|
|
@@ -379,11 +394,26 @@ export function renderPrdMarkdown(snapshot) {
|
|
|
379
394
|
renderSection('验证与创业闭环', [
|
|
380
395
|
['可触达社区', sections.validation.community],
|
|
381
396
|
['第一批种子用户', sections.validation.seedUsers],
|
|
397
|
+
['社区契合与触达依据', sections.validation.communityFit],
|
|
382
398
|
['当前替代方案', sections.validation.currentAlternative],
|
|
399
|
+
['痛点与替代证据', sections.validation.painEvidence],
|
|
383
400
|
['手工交付路径', sections.validation.manualPath],
|
|
401
|
+
['手工作战卡', sections.validation.manualPlaybook],
|
|
384
402
|
['承诺信号', sections.validation.commitmentSignals],
|
|
385
403
|
['首个低成本验证', sections.validation.firstValidationStep],
|
|
386
404
|
['先活下来方案', sections.validation.defaultAlivePlan],
|
|
405
|
+
['付费验证信号', sections.validation.paymentProof],
|
|
406
|
+
['第一版只做一件事', sections.validation.mvpSlice],
|
|
407
|
+
['周末级验证', sections.validation.weekendTest],
|
|
408
|
+
['最小工具桥接', sections.validation.smallestExecution],
|
|
409
|
+
['产品化门槛', sections.validation.productizeGate],
|
|
410
|
+
['第一批客户路径', sections.validation.firstCustomerPath],
|
|
411
|
+
['初始收费假设', sections.validation.pricingHypothesis],
|
|
412
|
+
['客户 1 盈利路径', sections.validation.customerOneProfitability],
|
|
413
|
+
['销售与增长纪律', sections.validation.growthDiscipline],
|
|
414
|
+
['可逆性判断', sections.validation.reversibility],
|
|
415
|
+
['客户真问题校验', sections.validation.customerTruth],
|
|
416
|
+
['价值观一致性', sections.validation.valuesFit],
|
|
387
417
|
]),
|
|
388
418
|
renderSection('范围与非目标', [
|
|
389
419
|
['范围内', sections.scope.inScope],
|
|
@@ -477,6 +507,32 @@ const BUSINESS_GUARDRAIL_FIELD_DESCRIPTORS = [
|
|
|
477
507
|
{ section: 'businessGuardrails', path: 'businessGuardrails.stopLossActions', label: '止损动作', prompt: '触发异常后,应该降级、暂停、关闭哪些能力,谁来处理?' },
|
|
478
508
|
];
|
|
479
509
|
|
|
510
|
+
|
|
511
|
+
const VALIDATION_REQUIRED_FIELD_DESCRIPTORS = [
|
|
512
|
+
{ section: 'validation', path: 'validation.community', label: '第一批可触达人群', prompt: '第一批最容易触达的社区、渠道或人群是谁?' },
|
|
513
|
+
{ section: 'validation', path: 'validation.seedUsers', label: '种子用户', prompt: '至少 3 到 10 个最该先找的人是谁?' },
|
|
514
|
+
{ section: 'validation', path: 'validation.communityFit', label: '社区契合', prompt: '你为什么算这个社区里的自己人?你现在是否已经在真实参与或贡献,他们为什么愿意先相信你?' },
|
|
515
|
+
{ section: 'validation', path: 'validation.currentAlternative', label: '当前替代方案', prompt: '用户现在主要靠什么替代方案、流程或人工方式在解决?' },
|
|
516
|
+
{ section: 'validation', path: 'validation.painEvidence', label: '痛点证据', prompt: '这个问题到底有多痛,用户已经在为更差的办法花什么时间或金钱?' },
|
|
517
|
+
{ section: 'validation', path: 'validation.manualPath', label: '手工交付路径', prompt: '如果先不做完整产品,怎么靠手工服务或半自动流程把价值跑出来?' },
|
|
518
|
+
{ section: 'validation', path: 'validation.manualPlaybook', label: '手工作战卡', prompt: '触发条件、步骤、工具、耗时和交接点分别是什么?' },
|
|
519
|
+
{ section: 'validation', path: 'validation.commitmentSignals', label: '承诺信号', prompt: '什么真实承诺最能证明这不是口头兴趣?' },
|
|
520
|
+
{ section: 'validation', path: 'validation.firstValidationStep', label: '最低成本验证', prompt: '最低成本先验证哪一步最关键?' },
|
|
521
|
+
{ section: 'validation', path: 'validation.defaultAlivePlan', label: '先活下来方案', prompt: '验证阶段怎样不把成本、复杂度和维护负担一下子做爆?' },
|
|
522
|
+
{ section: 'validation', path: 'validation.paymentProof', label: '付费验证信号', prompt: '有没有 10 个样本、3/10 付费意愿或更强交易信号?' },
|
|
523
|
+
{ section: 'validation', path: 'validation.mvpSlice', label: '一件事 MVP', prompt: '第一版到底只做哪一件事?' },
|
|
524
|
+
{ section: 'validation', path: 'validation.weekendTest', label: '周末级验证', prompt: '能不能压成周末级 MVP、顾问式试跑或更轻验证?' },
|
|
525
|
+
{ section: 'validation', path: 'validation.smallestExecution', label: '最小工具桥接', prompt: '能不能先用 spreadsheet、表单或 no-code 工具把第一版跑起来?如果必须开始做产品,也只自动化最重复的一步并先压成 forms / lists / CRUD 骨架;不要先为假想中的未来客户造复杂能力。' },
|
|
526
|
+
{ section: 'validation', path: 'validation.productizeGate', label: '产品化门槛', prompt: '达到什么条件才允许继续产品化或加功能?先服务今天已经存在的重复需求,不要提前为未来规模开壳。' },
|
|
527
|
+
{ section: 'validation', path: 'validation.firstCustomerPath', label: '第一批客户路径', prompt: '第一批客户最现实的触达顺序是什么?' },
|
|
528
|
+
{ section: 'validation', path: 'validation.pricingHypothesis', label: '初始收费假设', prompt: '从第一个客户开始准备怎么收费?' },
|
|
529
|
+
{ section: 'validation', path: 'validation.customerOneProfitability', label: '客户 1 盈利路径', prompt: '第一个客户如何覆盖时间和交付成本?' },
|
|
530
|
+
{ section: 'validation', path: 'validation.growthDiscipline', label: '增长纪律', prompt: '销售、launch 和增长阶段准备守哪些纪律?' },
|
|
531
|
+
{ section: 'validation', path: 'validation.reversibility', label: '可逆性', prompt: '如果结果一般,这条路是否容易回退?是否在逼你做重招聘、长期绑定或重平台化这类不可逆决策?' },
|
|
532
|
+
{ section: 'validation', path: 'validation.customerTruth', label: '客户真问题', prompt: '这更像在解决客户真问题,还是满足内部冲动?' },
|
|
533
|
+
{ section: 'validation', path: 'validation.valuesFit', label: '价值观一致性', prompt: '这条路是否符合团队想坚持的价值观和长期经营方式?如果连续这样做 3 到 5 年,你还愿意住在这套业务里吗?' },
|
|
534
|
+
];
|
|
535
|
+
|
|
480
536
|
const TYPE_REQUIRED_FIELD_DESCRIPTORS = {
|
|
481
537
|
consumer: [
|
|
482
538
|
{ section: 'consumer', path: 'typeSpecific.fields.persona', label: '用户画像', prompt: '目标用户画像是什么?' },
|
|
@@ -535,8 +591,49 @@ function isMissingPrdValue(value) {
|
|
|
535
591
|
return false;
|
|
536
592
|
}
|
|
537
593
|
|
|
538
|
-
|
|
594
|
+
function needsValidationLoop(snapshot) {
|
|
595
|
+
const sections = snapshot?.sections ?? {};
|
|
596
|
+
const validation = sections.validation ?? {};
|
|
597
|
+
const explicitValidationSignals = [
|
|
598
|
+
validation.community,
|
|
599
|
+
validation.seedUsers,
|
|
600
|
+
validation.communityFit,
|
|
601
|
+
validation.painEvidence,
|
|
602
|
+
validation.manualPath,
|
|
603
|
+
validation.manualPlaybook,
|
|
604
|
+
validation.commitmentSignals,
|
|
605
|
+
validation.defaultAlivePlan,
|
|
606
|
+
validation.paymentProof,
|
|
607
|
+
validation.mvpSlice,
|
|
608
|
+
validation.weekendTest,
|
|
609
|
+
validation.smallestExecution,
|
|
610
|
+
validation.productizeGate,
|
|
611
|
+
validation.firstCustomerPath,
|
|
612
|
+
validation.pricingHypothesis,
|
|
613
|
+
validation.customerOneProfitability,
|
|
614
|
+
validation.growthDiscipline,
|
|
615
|
+
validation.reversibility,
|
|
616
|
+
validation.customerTruth,
|
|
617
|
+
validation.valuesFit,
|
|
618
|
+
];
|
|
619
|
+
if (explicitValidationSignals.some((value) => !isMissingPrdValue(value))) {
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
const text = flattenForSearch({
|
|
623
|
+
problem: sections.problem,
|
|
624
|
+
goals: sections.goals,
|
|
625
|
+
scope: sections.scope,
|
|
626
|
+
requirements: sections.requirements,
|
|
627
|
+
risks: sections.risks,
|
|
628
|
+
});
|
|
629
|
+
return /(创业|脑暴|brainstorm|0\s*(到|to)\s*1|值不值得|worth doing|种子用户|第一批(客户|用户)|社区|workaround|手工交付|顾问式|一件事\s*mvp|周末级|首批成交|default alive|productize|no-code|spreadsheet|first customer)/iu.test(text);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function getRequiredFieldDescriptors(productType, options = {}) {
|
|
539
633
|
const descriptors = [...BASE_REQUIRED_FIELD_DESCRIPTORS];
|
|
634
|
+
if (options.includeValidation) {
|
|
635
|
+
descriptors.push(...VALIDATION_REQUIRED_FIELD_DESCRIPTORS);
|
|
636
|
+
}
|
|
540
637
|
if (TYPE_REQUIRED_FIELD_DESCRIPTORS[productType]) {
|
|
541
638
|
descriptors.push(...TYPE_REQUIRED_FIELD_DESCRIPTORS[productType]);
|
|
542
639
|
}
|
|
@@ -568,7 +665,8 @@ export function needsBusinessGuardrails(snapshot) {
|
|
|
568
665
|
|
|
569
666
|
export function analyzePrdSnapshot(snapshot) {
|
|
570
667
|
const productType = snapshot.productType ?? null;
|
|
571
|
-
const
|
|
668
|
+
const includeValidation = needsValidationLoop(snapshot);
|
|
669
|
+
const descriptors = getRequiredFieldDescriptors(productType, { includeValidation });
|
|
572
670
|
if (needsBusinessGuardrails(snapshot)) {
|
|
573
671
|
descriptors.push(...BUSINESS_GUARDRAIL_FIELD_DESCRIPTORS);
|
|
574
672
|
}
|
|
@@ -596,6 +694,7 @@ export function analyzePrdSnapshot(snapshot) {
|
|
|
596
694
|
|
|
597
695
|
return {
|
|
598
696
|
productType,
|
|
697
|
+
includeValidation,
|
|
599
698
|
totalRequiredFields,
|
|
600
699
|
completedRequiredFields,
|
|
601
700
|
missingRequiredFields: missingFields.length,
|
|
@@ -517,13 +517,38 @@ function exceptionItems(report) {
|
|
|
517
517
|
}));
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
const EVIDENCE_SOURCE_LABELS = {
|
|
521
|
+
'openprd-tasks': '任务清单',
|
|
522
|
+
'project-scan': '项目扫描',
|
|
523
|
+
'openprd-knowledge': '项目经验库',
|
|
524
|
+
'openprd-knowledge-candidate': '待确认经验草案',
|
|
525
|
+
'openprd-growth-ledger': '成长记录',
|
|
526
|
+
'openprd-test-report': '测试报告',
|
|
527
|
+
'openprd-visual-review': '视觉对比记录',
|
|
528
|
+
evidence: '证据文件',
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const EVIDENCE_PATH_LABELS = {
|
|
532
|
+
'no-active-change': '当前没有进行中的需求改动',
|
|
533
|
+
'no-cost-risk-detected': '本次没有发现成本相关风险',
|
|
534
|
+
'business-guardrails-evidence': '成本与滥用护栏证据',
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
function evidenceSourceLabel(source) {
|
|
538
|
+
return EVIDENCE_SOURCE_LABELS[source] ?? source;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function evidencePathLabel(path) {
|
|
542
|
+
return EVIDENCE_PATH_LABELS[path] ?? path;
|
|
543
|
+
}
|
|
544
|
+
|
|
520
545
|
function evidenceItems(report) {
|
|
521
546
|
return evidenceRows(report)
|
|
522
547
|
.filter((row) => !row.empty)
|
|
523
548
|
.slice(0, 5)
|
|
524
549
|
.map((row) => ({
|
|
525
550
|
summary: gateDisplay(row.gate),
|
|
526
|
-
detail: `${row.source},${row.path}`,
|
|
551
|
+
detail: `${evidenceSourceLabel(row.source)},${evidencePathLabel(row.path)}`,
|
|
527
552
|
}));
|
|
528
553
|
}
|
|
529
554
|
|
|
@@ -573,8 +598,8 @@ function environmentItems(report) {
|
|
|
573
598
|
{
|
|
574
599
|
summary: '成长账本',
|
|
575
600
|
detail: Number(growth.summary?.eventCount ?? 0) > 0
|
|
576
|
-
? `已记录 ${growth.summary.eventCount}
|
|
577
|
-
: '
|
|
601
|
+
? `已记录 ${growth.summary.eventCount} 条成长事件,其中收尾检查记录 ${growth.summary.completionCheckpoints ?? 0} 条`
|
|
602
|
+
: '当前还没有成长记录;收尾前至少应留下一条收尾检查记录或经验草案',
|
|
578
603
|
},
|
|
579
604
|
];
|
|
580
605
|
}
|
|
@@ -692,8 +717,8 @@ function tableRowsForEvidence(report) {
|
|
|
692
717
|
return evidenceRows(report).map((row) => `
|
|
693
718
|
<tr>
|
|
694
719
|
<td>${escapeHtml(gateDisplay(row.gate))}</td>
|
|
695
|
-
<td>${escapeHtml(row.source)}</td>
|
|
696
|
-
<td><code>${escapeHtml(row.path)}</code></td>
|
|
720
|
+
<td>${escapeHtml(evidenceSourceLabel(row.source))}</td>
|
|
721
|
+
<td><code>${escapeHtml(evidencePathLabel(row.path))}</code></td>
|
|
697
722
|
<td>${row.gate.required ? '本期必测' : '按风险确认'}</td>
|
|
698
723
|
</tr>
|
|
699
724
|
`).join('\n');
|