@openprd/cli 0.1.1 → 0.1.8
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/README.md +43 -69
- package/.openprd/README_EN.md +84 -0
- package/.openprd/benchmarks/index.md +7 -0
- package/.openprd/benchmarks/sources.yaml +25 -3
- package/.openprd/discovery/config.json +16 -2
- package/.openprd/engagements/active/flows.md +19 -14
- package/.openprd/engagements/active/handoff.md +11 -4
- package/.openprd/engagements/active/prd.md +99 -71
- package/.openprd/engagements/active/review.html +4 -4
- package/.openprd/engagements/active/roles.md +9 -8
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +4 -4
- package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +18 -0
- package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +18 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +78 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +129 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +41 -0
- package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +14 -0
- package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +49 -0
- package/.openprd/knowledge/index.json +44 -4
- package/.openprd/reviews/v0001.html +195 -129
- package/.openprd/reviews/v0002.html +1150 -0
- package/.openprd/reviews/v0003.html +1150 -0
- package/.openprd/reviews/v0004.html +1150 -0
- package/.openprd/reviews/v0005.html +1150 -0
- package/.openprd/standards/config.json +12 -9
- package/.openprd/state/changes.json +17 -2
- package/.openprd/state/current.json +399 -63
- package/.openprd/state/release-ledger.json +344 -0
- package/.openprd/state/version-index.json +52 -0
- package/.openprd/state/versions/v0002.json +264 -0
- package/.openprd/state/versions/v0002.md +183 -0
- package/.openprd/state/versions/v0003.json +269 -0
- package/.openprd/state/versions/v0003.md +188 -0
- package/.openprd/state/versions/v0004.json +274 -0
- package/.openprd/state/versions/v0004.md +193 -0
- package/.openprd/state/versions/v0005.json +299 -0
- package/.openprd/state/versions/v0005.md +189 -0
- package/.openprd/templates/agent/intake.md +5 -4
- package/.openprd/templates/b2b/intake.md +5 -4
- package/.openprd/templates/base/intake.md +10 -4
- package/.openprd/templates/company/README.md +9 -7
- package/.openprd/templates/company/README_EN.md +12 -0
- package/.openprd/templates/consumer/intake.md +5 -4
- package/.openprd/templates/industry/README.md +12 -10
- package/.openprd/templates/industry/README_EN.md +18 -0
- package/.openprd/templates/project/README.md +11 -9
- package/.openprd/templates/project/README_EN.md +16 -0
- package/.openprd/templates/session/README.md +11 -9
- package/.openprd/templates/session/README_EN.md +16 -0
- package/AGENTS.md +12 -8
- package/README.md +399 -438
- package/README_CN.md +4 -578
- package/README_EN.md +850 -0
- package/docs/assets/openprd-requirement-routing-en.png +0 -0
- package/docs/assets/openprd-requirement-routing-en.svg +102 -0
- package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.png +0 -0
- package/docs/assets/openprd-requirement-routing-zh.svg +102 -0
- package/package.json +6 -2
- package/scripts/dev-check-wrapup-copy.mjs +110 -0
- package/scripts/openprd-github-release-notes.mjs +99 -0
- package/scripts/quality-perf-check.mjs +203 -0
- package/skills/openprd-benchmark-router/SKILL.md +1 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +1 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +2 -0
- package/skills/openprd-discovery-loop/SKILL.md +2 -2
- package/skills/openprd-harness/SKILL.md +46 -24
- package/skills/openprd-harness/references/workflow-gates.md +15 -0
- package/skills/openprd-quality/SKILL.md +10 -4
- package/skills/openprd-requirement-intake/SKILL.md +31 -20
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -6
- package/skills/openprd-requirement-intake/references/routing-rubric.md +10 -2
- package/skills/openprd-router/SKILL.md +2 -2
- package/skills/openprd-shared/SKILL.md +51 -23
- package/skills/openprd-standards/SKILL.md +2 -1
- package/src/agent-integration.js +265 -65
- package/src/benchmark/constants.js +107 -0
- package/src/benchmark/operations.js +235 -0
- package/src/benchmark/registry.js +64 -0
- package/src/benchmark/render.js +115 -0
- package/src/benchmark/source.js +617 -0
- package/src/benchmark/storage.js +121 -0
- package/src/benchmark/verify.js +235 -0
- package/src/benchmark.js +50 -851
- package/src/change-summary.js +339 -0
- package/src/cli/args.js +67 -6
- package/src/cli/basic-print.js +365 -0
- package/src/cli/benchmark-print.js +91 -0
- package/src/cli/change-print.js +221 -0
- package/src/cli/doctor-print.js +268 -0
- package/src/cli/growth-print.js +176 -0
- package/src/cli/print.js +73 -1384
- package/src/cli/quality-print.js +284 -0
- package/src/cli/run-print.js +297 -0
- package/src/cli/shared-print.js +127 -0
- package/src/cli/workflow-print.js +195 -0
- package/src/codex-hook-runner-template.mjs +639 -117
- package/src/codex-runtime.js +324 -0
- package/src/dev-standards.js +178 -5
- package/src/diagram-core.js +5 -5
- package/src/discovery.js +2 -1
- package/src/execution-strategy.js +369 -0
- package/src/fleet.js +4 -0
- package/src/github-release.js +156 -0
- package/src/growth.js +311 -13
- package/src/html-artifact-utils.js +25 -0
- package/src/html-artifacts.js +157 -1596
- package/src/knowledge.js +1176 -75
- package/src/language-policy.js +2 -112
- package/src/learning-html-artifact.js +1031 -0
- package/src/learning-review.js +3 -2
- package/src/loop.js +280 -9
- package/src/openprd.js +341 -38
- package/src/openspec/change-validate.js +0 -9
- package/src/openspec/execute.js +79 -3
- package/src/openspec/generate.js +33 -20
- package/src/openspec/tasks.js +33 -2
- package/src/prd-core.js +10 -9
- package/src/product-type-copy.js +69 -0
- package/src/quality-html-artifact.js +108 -9
- package/src/quality-learning.js +30 -0
- package/src/quality-visual-review.js +237 -0
- package/src/quality.js +329 -43
- package/src/registry-hygiene.js +54 -0
- package/src/release-ledger.js +413 -0
- package/src/review-presentation.js +12 -6
- package/src/run-harness.js +722 -48
- package/src/session-binding.js +40 -3
- package/src/session-registry.js +159 -0
- package/src/standards.js +5 -3
- package/src/test-strategy.js +386 -0
- package/src/visual-compare.js +915 -34
- package/src/work-unit-migration.js +5 -1
- package/src/workspace-core.js +343 -19
- package/src/workspace-workflow.js +538 -134
|
@@ -2,16 +2,16 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { analyzePrdSnapshot, buildPrdSnapshot, diffSnapshots, formatVersionId, renderPrdMarkdown, summarizeSnapshot } from './prd-core.js';
|
|
5
|
+
import { formatProductTypeDisplay, formatProductTypeOptions, formatProductTypeQuestion, formatProductTypeSentence, formatTemplatePackDisplay } from './product-type-copy.js';
|
|
5
6
|
import { getDiagramReviewState } from './diagram-workspace.js';
|
|
6
7
|
import { exists, parseYamlText, readJson, readText, writeJson, writeText } from './fs-utils.js';
|
|
7
8
|
import { artifactBundlePaths, canonicalReviewPath, defaultReviewArtifactPath, openArtifactInBrowser, renderPlaygroundArtifact, renderPlaygroundMarkdown, renderPlaygroundPatch, renderReviewArtifact, renderReviewEntryHtml, writeHtmlArtifact } from './html-artifacts.js';
|
|
8
|
-
import {
|
|
9
|
-
import { buildSpec as buildOpenSpecSpec } from './openspec/generate.js';
|
|
9
|
+
import { buildReleaseLedgerSummary } from './release-ledger.js';
|
|
10
10
|
import { assertReviewPresentationReady, getReviewPresentationGate } from './review-presentation.js';
|
|
11
11
|
import { syncSessionBindingFromReview, syncSessionBindingFromSnapshot } from './session-binding.js';
|
|
12
12
|
import { timestamp } from './time.js';
|
|
13
13
|
import { generateWorkUnitId, normalizeWorkUnitId, readWorkUnitBinding, resolveTargetRoot, writeWorkUnitBinding } from './work-unit.js';
|
|
14
|
-
import { appendDecision, appendOpenQuestions, appendProgress, appendWorkflowEvent, buildClarificationPlan, buildClarificationState, buildWorkflowTaskGraph, CAPTURE_SOURCES, coerceCapturedValue, deriveGateLabels, detectWorkspaceScenario, extractMarkdownSection, FIELD_PATH_TO_STATE_KEY, isSupportedProductType, loadLatestVersionSnapshot, loadWorkspace, normalizeVersionId, readVersionIndex, readVersionSnapshot, renderFlowDoc, renderHandoffDoc, renderRolesDoc, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
|
|
14
|
+
import { appendDecision, appendOpenQuestions, appendProgress, appendWorkflowEvent, buildClarificationPlan, buildClarificationState, buildCurrentStateSnapshot, buildWorkflowTaskGraph, CAPTURE_SOURCES, coerceCapturedValue, deriveGateLabels, detectWorkspaceScenario, extractMarkdownSection, FIELD_PATH_TO_STATE_KEY, isSupportedProductType, loadCurrentLaneSnapshot, loadLatestVersionSnapshot, loadWorkspace, normalizeVersionId, persistWorkspaceCurrentState, readActiveRequirementLane, readVersionIndex, readVersionSnapshot, renderFlowDoc, renderHandoffDoc, renderRolesDoc, resolveActiveTemplatePack, resolveCurrentProductType, validateWorkspace, writeVersionIndex, writeVersionSnapshot } from './workspace-core.js';
|
|
15
15
|
|
|
16
16
|
function requirementGatePath(projectRoot) {
|
|
17
17
|
return path.join(projectRoot, '.openprd', 'harness', 'requirement-gate.json');
|
|
@@ -162,8 +162,8 @@ function normalizePrdReviewStatus(status) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
async function readActiveRequirementGate(projectRoot) {
|
|
165
|
-
const
|
|
166
|
-
return gate?.active ? gate : null;
|
|
165
|
+
const lane = await readActiveRequirementLane(projectRoot).catch(() => null);
|
|
166
|
+
return lane?.gate?.active ? lane.gate : null;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function meaningfulOverrideValue(value) {
|
|
@@ -231,7 +231,10 @@ function resolveReviewPaths(ws, snapshot) {
|
|
|
231
231
|
|
|
232
232
|
async function writeReviewFiles(ws, snapshot, { writeEntry = true } = {}) {
|
|
233
233
|
assertReviewPresentationReady(snapshot);
|
|
234
|
-
const reviewHtml = renderReviewArtifact({
|
|
234
|
+
const reviewHtml = renderReviewArtifact({
|
|
235
|
+
snapshot,
|
|
236
|
+
projectRelease: buildReleaseLedgerSummary(ws.data.releaseLedger),
|
|
237
|
+
});
|
|
235
238
|
const { canonicalReview, activeReviewEntry } = resolveReviewPaths(ws, snapshot);
|
|
236
239
|
await writeHtmlArtifact(canonicalReview, reviewHtml);
|
|
237
240
|
if (writeEntry) {
|
|
@@ -357,24 +360,6 @@ function requirementLooksLikeInterfaceWork(gate) {
|
|
|
357
360
|
return /界面|页面|菜单|入口|按钮|表单|弹窗|导航|布局|看板|列表|配置页|模块|组件|UI|tab/i.test(text);
|
|
358
361
|
}
|
|
359
362
|
|
|
360
|
-
function assertOpenSpecPreflightReady(snapshot) {
|
|
361
|
-
const specText = buildOpenSpecSpec({ snapshot });
|
|
362
|
-
const violations = findOpenPrdSpecLanguageViolations(specText);
|
|
363
|
-
if (violations.length === 0) {
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const examples = violations
|
|
367
|
-
.slice(0, 3)
|
|
368
|
-
.map((violation) => `第 ${violation.line} 行:${violation.reason}(${violation.text})`)
|
|
369
|
-
.join(';');
|
|
370
|
-
throw new Error([
|
|
371
|
-
'OpenPrd 已阻止 synthesize:按当前 PRD 生成的 spec.md 仍会触发简体中文预检,review.html 还不能进入确认。',
|
|
372
|
-
'请先把标题、问题陈述和场景文案整理成可直接产出 spec 的简体中文表达。',
|
|
373
|
-
'如果只是内部措辞规范化,请先用 openprd capture . --source agent-normalized 写回,再重新 synthesize。',
|
|
374
|
-
examples ? `示例:${examples}。` : null,
|
|
375
|
-
].filter(Boolean).join(' '));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
363
|
function requirementPrompt(gate) {
|
|
379
364
|
return String(gate?.promptPreview ?? '').trim();
|
|
380
365
|
}
|
|
@@ -450,6 +435,315 @@ function shortList(items, fallback = '待补充') {
|
|
|
450
435
|
return list.length > 0 ? list.slice(0, 3).join(';') : fallback;
|
|
451
436
|
}
|
|
452
437
|
|
|
438
|
+
function normalizeTextList(items) {
|
|
439
|
+
if (Array.isArray(items)) {
|
|
440
|
+
return items.map((item) => String(item || '').trim()).filter(Boolean);
|
|
441
|
+
}
|
|
442
|
+
const text = String(items || '').trim();
|
|
443
|
+
return text ? [text] : [];
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function summarizeProductShape(productType, gate) {
|
|
447
|
+
if (productType === 'consumer' || productType === 'b2b' || productType === 'agent' || productType === 'base') {
|
|
448
|
+
return formatProductTypeSentence(productType);
|
|
449
|
+
}
|
|
450
|
+
const text = requirementPrompt(gate);
|
|
451
|
+
if (/agent|自动化|workflow|MCP|tool|skill/i.test(text)) {
|
|
452
|
+
return formatProductTypeSentence('agent', { inferred: true });
|
|
453
|
+
}
|
|
454
|
+
if (/企业|团队|后台|审批|权限|客户|运营/i.test(text)) {
|
|
455
|
+
return formatProductTypeSentence('b2b', { inferred: true });
|
|
456
|
+
}
|
|
457
|
+
if (/个人|用户|社区|内容|创作者|消费|c端|to c/i.test(text)) {
|
|
458
|
+
return formatProductTypeSentence('consumer', { inferred: true });
|
|
459
|
+
}
|
|
460
|
+
return `产品场景仍待确认,可在 ${formatProductTypeOptions()} 之间进一步锁定。`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function summarizeArchitectureSignals(gate, snapshot) {
|
|
464
|
+
const sections = snapshot.sections ?? {};
|
|
465
|
+
const text = `${requirementPrompt(gate)} ${JSON.stringify({
|
|
466
|
+
technical: sections.constraints?.technical ?? [],
|
|
467
|
+
dependencies: sections.constraints?.dependencies ?? [],
|
|
468
|
+
})}`;
|
|
469
|
+
const hits = [];
|
|
470
|
+
if (/前端|页面|客户端|web|h5|移动端|ios|android|桌面|ui|交互|网站|站点|博客|内容|文章|落地页/i.test(text)) {
|
|
471
|
+
hits.push('用户入口与页面体验');
|
|
472
|
+
}
|
|
473
|
+
if (/后端|服务端|server|api|数据库|存储|队列|定时任务|cron|webhook/i.test(text)) {
|
|
474
|
+
hits.push('服务流程与数据处理');
|
|
475
|
+
}
|
|
476
|
+
if (/agent|workflow|tool|skill|MCP|自动化|编排/i.test(text)) {
|
|
477
|
+
hits.push('Agent 协作与自动执行');
|
|
478
|
+
}
|
|
479
|
+
return hits.length > 0
|
|
480
|
+
? hits.join(';')
|
|
481
|
+
: '影响环节仍待确认,可先按用户入口、服务流程或 Agent 协作补齐。';
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function collectProjectRiskProbes(gate, snapshot) {
|
|
485
|
+
const sections = snapshot.sections ?? {};
|
|
486
|
+
const text = `${requirementPrompt(gate)} ${JSON.stringify({
|
|
487
|
+
users: sections.users ?? {},
|
|
488
|
+
constraints: sections.constraints ?? {},
|
|
489
|
+
requirements: sections.requirements ?? {},
|
|
490
|
+
})}`;
|
|
491
|
+
const probes = [];
|
|
492
|
+
const probeMap = [
|
|
493
|
+
{ label: '账号身份与可见范围', pattern: /登录|账号|auth|oauth|权限|角色|rbac/i },
|
|
494
|
+
{ label: '用户数据与信息处理', pattern: /数据|数据库|存储|隐私|文件|上传|下载|同步/i },
|
|
495
|
+
{ label: '团队协作与审批流转', pattern: /团队|协作|审批|组织|成员|管理员|buyer|admin/i },
|
|
496
|
+
{ label: '外部系统与合作方对接', pattern: /第三方|外部|api|sdk|集成|webhook|支付|短信/i },
|
|
497
|
+
{ label: 'AI 结果可靠性与人工兜底', pattern: /ai|agent|模型|llm|生成|推理|prompt/i },
|
|
498
|
+
{ label: '收费模式、额度与成本', pattern: /收费|订阅|付费|价格|额度|点数|积分|成本|quota|billing/i },
|
|
499
|
+
];
|
|
500
|
+
for (const probe of probeMap) {
|
|
501
|
+
if (probe.pattern.test(text)) {
|
|
502
|
+
probes.push(probe.label);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return probes;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildProjectFraming({ gate, snapshot, scenario, productType }) {
|
|
509
|
+
const sections = snapshot.sections ?? {};
|
|
510
|
+
const guardrailHints = [
|
|
511
|
+
...normalizeTextList(sections.requirements?.businessRules),
|
|
512
|
+
...normalizeTextList(sections.constraints?.dependencies),
|
|
513
|
+
];
|
|
514
|
+
const riskProbes = collectProjectRiskProbes(gate, snapshot);
|
|
515
|
+
return {
|
|
516
|
+
audience: shortList(
|
|
517
|
+
sections.users?.primaryUsers,
|
|
518
|
+
scenario.id === 'cold-start-existing-project'
|
|
519
|
+
? '需要先确认这次改动主要服务谁。'
|
|
520
|
+
: '需要先确认目标用户或关键角色。'
|
|
521
|
+
),
|
|
522
|
+
productShape: summarizeProductShape(productType, gate),
|
|
523
|
+
firstSlice: shortList(sections.scope?.inScope, '需要先确认第一版最小可用切片。'),
|
|
524
|
+
nonGoals: shortList(sections.scope?.outOfScope, '需要先确认本轮先不做什么。'),
|
|
525
|
+
guardrails: shortList(guardrailHints, '需要先确认哪些现有能力、数据、流程或体验不能被破坏。'),
|
|
526
|
+
architectureSignals: summarizeArchitectureSignals(gate, snapshot),
|
|
527
|
+
riskProbes,
|
|
528
|
+
riskProbeSummary: shortList(riskProbes, '当前没有明显命中额外风险探针。'),
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function escapeMarkdownTableCell(value) {
|
|
533
|
+
return String(value ?? '').replace(/\|/g, '/');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function containsPendingClarifyMarker(value) {
|
|
537
|
+
return /(待确认|需要先确认|仍待确认|未分类)/.test(String(value ?? ''));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function describeProductLensFocus(productType) {
|
|
541
|
+
if (productType === 'consumer') {
|
|
542
|
+
return '这轮我会重点看首次使用场景、关键感受,以及用户愿不愿意继续回来。';
|
|
543
|
+
}
|
|
544
|
+
if (productType === 'b2b') {
|
|
545
|
+
return '这轮我会重点看谁拍板、谁使用、谁运营,以及上线协作会卡在哪里。';
|
|
546
|
+
}
|
|
547
|
+
if (productType === 'agent') {
|
|
548
|
+
return '这轮我会重点看哪些步骤让 Agent 自主完成、哪些节点必须人工拍板,以及失败时谁兜底。';
|
|
549
|
+
}
|
|
550
|
+
return '这轮我会优先把用户、场景、第一版切片、边界和风险讲清楚。';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function requirementTypeDisplay(gate) {
|
|
554
|
+
const tier = String(gate?.intent?.requirementTier ?? '').toLowerCase();
|
|
555
|
+
if (tier === 'l0') {
|
|
556
|
+
return '快速修正(L0)';
|
|
557
|
+
}
|
|
558
|
+
if (tier === 'l1') {
|
|
559
|
+
return '现有功能优化(L1)';
|
|
560
|
+
}
|
|
561
|
+
return '新功能/新流程方案(L2)';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function inferredProductTypeDisplay(productType, gate) {
|
|
565
|
+
if (productType === 'consumer' || productType === 'b2b' || productType === 'agent' || productType === 'base') {
|
|
566
|
+
return formatProductTypeDisplay(productType);
|
|
567
|
+
}
|
|
568
|
+
const text = requirementPrompt(gate);
|
|
569
|
+
if (/agent|自动化|workflow|MCP|tool|skill/i.test(text)) {
|
|
570
|
+
return formatProductTypeDisplay('agent');
|
|
571
|
+
}
|
|
572
|
+
if (/企业|团队|后台|审批|权限|客户|运营/i.test(text)) {
|
|
573
|
+
return formatProductTypeDisplay('b2b');
|
|
574
|
+
}
|
|
575
|
+
if (/个人|用户|社区|内容|创作者|消费|c端|to c/i.test(text)) {
|
|
576
|
+
return formatProductTypeDisplay('consumer');
|
|
577
|
+
}
|
|
578
|
+
return '待确认';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function fallbackScopeText(text, fallback) {
|
|
582
|
+
return containsPendingClarifyMarker(text) ? fallback : text;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function buildMarkdownTable(headers, rows) {
|
|
586
|
+
const normalizedRows = rows
|
|
587
|
+
.filter((row) => Array.isArray(row) && row.length > 0)
|
|
588
|
+
.map((row) => headers.map((_, index) => escapeMarkdownTableCell(row[index] ?? '待确认')));
|
|
589
|
+
return [
|
|
590
|
+
`| ${headers.map(escapeMarkdownTableCell).join(' | ')} |`,
|
|
591
|
+
`| ${headers.map(() => '---').join(' | ')} |`,
|
|
592
|
+
...normalizedRows.map((row) => `| ${row.join(' | ')} |`),
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function inferCoreScopeModule(prompt, productType) {
|
|
597
|
+
const text = String(prompt ?? '');
|
|
598
|
+
if (/页面|界面|布局|导航|按钮|表单|网站|站点|博客|内容|文章|落地页|UI/i.test(text)) {
|
|
599
|
+
return '用户入口与界面';
|
|
600
|
+
}
|
|
601
|
+
if (/后端|服务端|接口|api|数据库|存储|同步|队列|webhook|数据/i.test(text)) {
|
|
602
|
+
return '服务流程与数据处理';
|
|
603
|
+
}
|
|
604
|
+
if (/agent|自动化|workflow|tool|skill|MCP|编排/i.test(text) || productType === 'agent') {
|
|
605
|
+
return 'Agent 主流程';
|
|
606
|
+
}
|
|
607
|
+
return '核心主流程';
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function pushScopeRow(rows, seen, module, inScope, outOfScope) {
|
|
611
|
+
const key = String(module ?? '').trim();
|
|
612
|
+
if (!key || seen.has(key)) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
seen.add(key);
|
|
616
|
+
rows.push([module, inScope, outOfScope]);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function buildScopeTableRows({ projectFraming, prompt, productType }) {
|
|
620
|
+
const rows = [];
|
|
621
|
+
const seen = new Set();
|
|
622
|
+
const text = `${prompt ?? ''} ${projectFraming.architectureSignals ?? ''}`;
|
|
623
|
+
const firstSlice = fallbackScopeText(
|
|
624
|
+
projectFraming.firstSlice,
|
|
625
|
+
'先把第一版核心主流程收敛到一个最小可交付闭环。'
|
|
626
|
+
);
|
|
627
|
+
const nonGoals = fallbackScopeText(
|
|
628
|
+
projectFraming.nonGoals,
|
|
629
|
+
'暂不把与核心目标无关的大范围扩展一起塞进第一版。'
|
|
630
|
+
);
|
|
631
|
+
if (/用户入口与页面体验|页面|界面|布局|导航|按钮|表单|网站|站点|博客|内容|文章|落地页|UI/i.test(text)) {
|
|
632
|
+
pushScopeRow(
|
|
633
|
+
rows,
|
|
634
|
+
seen,
|
|
635
|
+
'用户入口与界面',
|
|
636
|
+
containsPendingClarifyMarker(projectFraming.firstSlice)
|
|
637
|
+
? '先把用户看到的入口、页面结构和关键交互讲清楚。'
|
|
638
|
+
: firstSlice,
|
|
639
|
+
nonGoals,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
if (/服务流程与数据处理|后端|服务端|接口|api|数据库|存储|同步|队列|webhook|数据/i.test(text)) {
|
|
643
|
+
pushScopeRow(
|
|
644
|
+
rows,
|
|
645
|
+
seen,
|
|
646
|
+
'服务流程与数据处理',
|
|
647
|
+
'先只覆盖支撑主流程所需的接口、状态处理或数据留痕。',
|
|
648
|
+
'暂不扩到与这次主流程无关的重构、清库或系统级迁移。',
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
if (/Agent 协作与自动执行|agent|自动化|workflow|tool|skill|MCP|编排/i.test(text) || productType === 'agent') {
|
|
652
|
+
pushScopeRow(
|
|
653
|
+
rows,
|
|
654
|
+
seen,
|
|
655
|
+
'Agent 主流程',
|
|
656
|
+
'先讲清 Agent 负责的步骤、人工确认点和失败恢复。',
|
|
657
|
+
'暂不把所有自动化场景一次性铺开,避免边界失控。',
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
if (rows.length === 0) {
|
|
661
|
+
pushScopeRow(rows, seen, inferCoreScopeModule(prompt, productType), firstSlice, nonGoals);
|
|
662
|
+
}
|
|
663
|
+
return rows;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function pushTechRow(rows, seen, area, approach, responsibility) {
|
|
667
|
+
const key = String(area ?? '').trim();
|
|
668
|
+
if (!key || seen.has(key)) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
seen.add(key);
|
|
672
|
+
rows.push([area, approach, responsibility]);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function buildTechnicalSolutionRows({ projectFraming, prompt, productType }) {
|
|
676
|
+
const rows = [];
|
|
677
|
+
const seen = new Set();
|
|
678
|
+
const text = `${prompt ?? ''} ${projectFraming.architectureSignals ?? ''}`;
|
|
679
|
+
if (/用户入口与页面体验|页面|界面|布局|导航|按钮|表单|网站|站点|博客|内容|文章|落地页|UI/i.test(text)) {
|
|
680
|
+
pushTechRow(
|
|
681
|
+
rows,
|
|
682
|
+
seen,
|
|
683
|
+
'前端 / 用户入口',
|
|
684
|
+
'先围绕当前入口、页面结构和关键交互做第一版,不提前铺开大范围视觉重做。',
|
|
685
|
+
'负责把用户真正看到的流程、反馈和操作路径讲清楚。',
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (/服务流程与数据处理|后端|服务端|接口|api|数据库|存储|同步|队列|webhook|数据/i.test(text)) {
|
|
689
|
+
pushTechRow(
|
|
690
|
+
rows,
|
|
691
|
+
seen,
|
|
692
|
+
'后端 / 服务逻辑',
|
|
693
|
+
'只补当前需求真正需要的接口、状态流转或数据写入,先保证主链路跑通。',
|
|
694
|
+
'负责把规则、状态和数据处理接住,并留下可验证证据。',
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (/Agent 协作与自动执行|agent|自动化|workflow|tool|skill|MCP|编排/i.test(text) || productType === 'agent') {
|
|
698
|
+
pushTechRow(
|
|
699
|
+
rows,
|
|
700
|
+
seen,
|
|
701
|
+
'Agent / 自动化编排',
|
|
702
|
+
'先定义 Agent 自主边界、人工拍板点和失败恢复,再决定要不要继续扩编排深度。',
|
|
703
|
+
'负责把多步流程串起来,并在失败时把用户安全带回人工兜底。',
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
if (rows.length === 0) {
|
|
707
|
+
pushTechRow(
|
|
708
|
+
rows,
|
|
709
|
+
seen,
|
|
710
|
+
'实现主链路',
|
|
711
|
+
'先按当前确认范围做最小可用闭环,再决定是否扩展到更多技术面。',
|
|
712
|
+
'负责把第一版能力真正落到可验证的主流程里。',
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return rows;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function buildClarifyDirectionChoices(productType) {
|
|
719
|
+
if (productType === 'consumer') {
|
|
720
|
+
return [
|
|
721
|
+
'先把第一次上手跑通:更快定第一版,但后续可能还要补留存动作。',
|
|
722
|
+
'先把关键转化做好:更贴近商业结果,但对场景边界要求更高。',
|
|
723
|
+
'先把愿意回来这件事做顺:更利于长期价值,但首版会慢一点。',
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
if (productType === 'b2b') {
|
|
727
|
+
return [
|
|
728
|
+
'先解决单角色提效:更容易落地,但跨团队价值暂时有限。',
|
|
729
|
+
'先打通跨角色流转:业务收益更明显,但协调成本更高。',
|
|
730
|
+
'先补管理与配置能力:便于后续扩张,但首版体感未必最强。',
|
|
731
|
+
];
|
|
732
|
+
}
|
|
733
|
+
if (productType === 'agent') {
|
|
734
|
+
return [
|
|
735
|
+
'先跑通单步自动化:最快看到结果,但协作链路还比较浅。',
|
|
736
|
+
'先做多步编排:更像完整方案,但边界和失败恢复要更早想清楚。',
|
|
737
|
+
'先把人工兜底做稳:风险更低,但自动化收益会慢一点体现。',
|
|
738
|
+
];
|
|
739
|
+
}
|
|
740
|
+
return [
|
|
741
|
+
'先把用户最痛的一步打通:更容易收敛首版,但覆盖面会窄一些。',
|
|
742
|
+
'先把主流程串起来:更完整,但第一版实现成本更高。',
|
|
743
|
+
'先把边界和风险控住:更稳,但用户感知收益会慢一点出现。',
|
|
744
|
+
];
|
|
745
|
+
}
|
|
746
|
+
|
|
453
747
|
function normalizeClarifyMode(mode) {
|
|
454
748
|
if (mode === 'artifact') {
|
|
455
749
|
return 'inline-with-checklist';
|
|
@@ -458,8 +752,15 @@ function normalizeClarifyMode(mode) {
|
|
|
458
752
|
}
|
|
459
753
|
|
|
460
754
|
function estimateInlineClarificationLines(clarification, reflection) {
|
|
755
|
+
const projectFraming = reflection?.projectContext?.projectFraming ?? null;
|
|
461
756
|
const activeChangeLines = reflection?.projectContext?.activeChange ? 1 : 0;
|
|
462
|
-
|
|
757
|
+
const framingLines = projectFraming
|
|
758
|
+
? 5
|
|
759
|
+
+ (projectFraming.architectureSignals ? 1 : 0)
|
|
760
|
+
+ (projectFraming.riskProbes?.length > 0 ? 1 : 0)
|
|
761
|
+
: 0;
|
|
762
|
+
return 2
|
|
763
|
+
+ framingLines
|
|
463
764
|
+ clarification.mustAskUser.length
|
|
464
765
|
+ Math.min(clarification.canInferLater.length, 2)
|
|
465
766
|
+ activeChangeLines;
|
|
@@ -498,23 +799,79 @@ function buildInlineClarification({ clarification, reflection, presentation }) {
|
|
|
498
799
|
}
|
|
499
800
|
const prompt = reflection?.promptPreview || '本轮需求';
|
|
500
801
|
const projectContext = reflection?.projectContext ?? {};
|
|
802
|
+
const projectFraming = projectContext.projectFraming ?? {};
|
|
803
|
+
const productType = projectContext.productType;
|
|
804
|
+
const primaryQuestion = clarification.mustAskUser[0] ?? null;
|
|
805
|
+
const followUpQuestions = clarification.mustAskUser.slice(1, presentation.mode === 'inline' ? 2 : 3);
|
|
806
|
+
const scopeTable = buildMarkdownTable(
|
|
807
|
+
['功能模块', '这次先做什么', '这次先不做什么'],
|
|
808
|
+
buildScopeTableRows({ projectFraming, prompt, productType }),
|
|
809
|
+
);
|
|
810
|
+
const technicalTable = buildMarkdownTable(
|
|
811
|
+
['技术部分', '初步方案', '主要负责什么'],
|
|
812
|
+
buildTechnicalSolutionRows({ projectFraming, prompt, productType }),
|
|
813
|
+
);
|
|
814
|
+
const directionChoices = (
|
|
815
|
+
containsPendingClarifyMarker(projectFraming.firstSlice)
|
|
816
|
+
|| containsPendingClarifyMarker(projectFraming.audience)
|
|
817
|
+
)
|
|
818
|
+
? buildClarifyDirectionChoices(productType)
|
|
819
|
+
: [];
|
|
501
820
|
const lines = [
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
'
|
|
505
|
-
|
|
821
|
+
`我先用产品和业务语言复述一下这次需求,并先按总分结构收一下:${prompt}`,
|
|
822
|
+
'',
|
|
823
|
+
'需求判断:',
|
|
824
|
+
`- 需求类型:${requirementTypeDisplay(reflection?.gate)}。`,
|
|
825
|
+
`- 产品类型:${inferredProductTypeDisplay(productType, reflection?.gate)}。`,
|
|
826
|
+
'',
|
|
827
|
+
'需求理解:',
|
|
828
|
+
`- 主要服务对象:${projectFraming.audience ?? '待确认'}。`,
|
|
829
|
+
`- 使用场景更像:${projectFraming.productShape ?? '待确认'}。`,
|
|
830
|
+
`- 第一版先让用户做到:${projectFraming.firstSlice ?? '待确认'}。`,
|
|
831
|
+
`- 这轮先不碰:${projectFraming.nonGoals ?? '待确认'}。`,
|
|
832
|
+
`- 必须守住:${projectFraming.guardrails ?? '待确认'}。`,
|
|
506
833
|
];
|
|
834
|
+
if (projectFraming.architectureSignals) {
|
|
835
|
+
lines.push(`- 这次更可能会影响:${projectFraming.architectureSignals}。`);
|
|
836
|
+
}
|
|
837
|
+
if (projectFraming.riskProbes?.length > 0) {
|
|
838
|
+
lines.push(`- 我先提醒的业务风险:${projectFraming.riskProbeSummary}。`);
|
|
839
|
+
}
|
|
840
|
+
lines.push(`- ${describeProductLensFocus(productType)}`);
|
|
841
|
+
lines.push('- 判断这轮是否值得做成:看用户是否真的更顺、更快、更稳地完成关键动作。');
|
|
842
|
+
lines.push('');
|
|
843
|
+
lines.push('功能范围:');
|
|
844
|
+
lines.push(...scopeTable);
|
|
845
|
+
lines.push('');
|
|
846
|
+
lines.push('技术方案:');
|
|
847
|
+
lines.push(...technicalTable);
|
|
507
848
|
if (projectContext.activeChange) {
|
|
849
|
+
lines.push('');
|
|
508
850
|
lines.push(`历史提醒:当前还有 ${projectContext.activeChange.activeChange},本轮先分开处理。`);
|
|
509
851
|
}
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
lines.push('
|
|
513
|
-
|
|
514
|
-
|
|
852
|
+
lines.push('');
|
|
853
|
+
if (primaryQuestion) {
|
|
854
|
+
lines.push('我建议这轮先确认这一点:');
|
|
855
|
+
lines.push(`- ${primaryQuestion.prompt}`);
|
|
856
|
+
if (followUpQuestions.length > 0) {
|
|
857
|
+
lines.push('这点定下来后,我再继续补下面这些:');
|
|
858
|
+
for (const item of followUpQuestions) {
|
|
859
|
+
lines.push(`- ${item.prompt}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
} else {
|
|
863
|
+
lines.push('我这边暂时没有新的关键追问了。');
|
|
864
|
+
}
|
|
865
|
+
if (directionChoices.length > 0) {
|
|
866
|
+
lines.push('如果你现在还不想展开细节,也可以先在这 3 个方向里选一个:');
|
|
867
|
+
for (const choice of directionChoices) {
|
|
868
|
+
lines.push(`- ${choice}`);
|
|
515
869
|
}
|
|
870
|
+
lines.push('你可以先回一个方向编号,或直接补一句你的倾向。');
|
|
871
|
+
lines.push('我收到后会先整理需求摘要给你确认;确认后再进入 PRD 和评审流程,不会直接跳到实现。');
|
|
516
872
|
} else {
|
|
517
|
-
lines.push('
|
|
873
|
+
lines.push('如果以上理解基本对,请先回复“可以”,或直接指出要调整的地方。');
|
|
874
|
+
lines.push('我收到后会先整理需求摘要给你确认;确认后再进入 PRD 和评审流程,不会直接跳到实现。');
|
|
518
875
|
}
|
|
519
876
|
return {
|
|
520
877
|
mode: presentation.mode,
|
|
@@ -547,33 +904,72 @@ function reflectionQuestion(id, label, prompt) {
|
|
|
547
904
|
};
|
|
548
905
|
}
|
|
549
906
|
|
|
907
|
+
function buildLensReflectionQuestion(productType) {
|
|
908
|
+
if (productType === 'consumer') {
|
|
909
|
+
return reflectionQuestion('product-lens', '用户场景与回访价值', '请确认这次主要服务哪类个人用户、他们第一次会在什么场景下想用它,以及什么结果会让他们愿意继续回来。');
|
|
910
|
+
}
|
|
911
|
+
if (productType === 'b2b') {
|
|
912
|
+
return reflectionQuestion('product-lens', '角色关系与上线阻力', '请确认谁拍板、谁使用、谁负责推进或运营,以及最可能拖慢上线的是哪段协作、审批或对接。');
|
|
913
|
+
}
|
|
914
|
+
if (productType === 'agent') {
|
|
915
|
+
return reflectionQuestion('product-lens', '自主边界与人工兜底', '请确认哪些步骤希望 Agent 自主完成、哪些节点必须人工拍板,以及失败时由谁接住。');
|
|
916
|
+
}
|
|
917
|
+
return reflectionQuestion('product-lens', '用户价值与取舍', '请确认这次最重要的用户价值是什么;如果只能先保一个方向,你更想先保效率、体验,还是风险可控。');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function shouldBuildRequirementIntakeReflection({ gate, scenario, analysis }) {
|
|
921
|
+
if (gate?.active) {
|
|
922
|
+
return true;
|
|
923
|
+
}
|
|
924
|
+
if (['cold-start-greenfield', 'cold-start-existing-project'].includes(scenario?.id)) {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
return Number(analysis?.missingRequiredFields ?? 0) > 0;
|
|
928
|
+
}
|
|
929
|
+
|
|
550
930
|
async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, analysis, scenario, gate }) {
|
|
551
|
-
if (!gate
|
|
931
|
+
if (!shouldBuildRequirementIntakeReflection({ gate, scenario, analysis })) {
|
|
552
932
|
return null;
|
|
553
933
|
}
|
|
554
934
|
|
|
935
|
+
const sections = snapshot.sections ?? {};
|
|
555
936
|
const text = requirementPrompt(gate);
|
|
937
|
+
const promptPreview = text || sections.problem?.problemStatement || snapshot.title || '当前需求待确认';
|
|
556
938
|
const complexity = detectRequirementIntakeComplexity(gate);
|
|
557
939
|
const activeChange = await readActiveChangeHint(projectRoot);
|
|
558
|
-
const sections = snapshot.sections ?? {};
|
|
559
940
|
const productName = snapshot.title || sections.meta?.title || '当前项目';
|
|
560
941
|
const productType = snapshot.productType ?? resolveCurrentProductType(ws) ?? '未分类';
|
|
561
942
|
const currentProblem = sections.problem?.problemStatement || '待补充';
|
|
562
943
|
const currentScope = shortList(sections.scope?.inScope, '当前范围还没有稳定记录');
|
|
563
944
|
const missing = analysis.missingFields.slice(0, 4).map((field) => field.label);
|
|
564
945
|
const needsInterfaceSketch = requirementLooksLikeInterfaceWork(gate);
|
|
946
|
+
const projectFraming = buildProjectFraming({
|
|
947
|
+
gate,
|
|
948
|
+
snapshot,
|
|
949
|
+
scenario,
|
|
950
|
+
productType: snapshot.productType ?? resolveCurrentProductType(ws),
|
|
951
|
+
});
|
|
952
|
+
const lensQuestion = buildLensReflectionQuestion(snapshot.productType ?? resolveCurrentProductType(ws));
|
|
953
|
+
const needsDeliveryShapeQuestion = !needsInterfaceSketch
|
|
954
|
+
&& (
|
|
955
|
+
projectFraming.riskProbes.length > 0
|
|
956
|
+
|| !containsPendingClarifyMarker(projectFraming.architectureSignals)
|
|
957
|
+
);
|
|
565
958
|
const mustConfirm = complexity.mode === 'deep'
|
|
566
959
|
? [
|
|
567
|
-
reflectionQuestion('intent', '意图与目标', '
|
|
568
|
-
|
|
569
|
-
reflectionQuestion('
|
|
960
|
+
reflectionQuestion('intent', '意图与目标', '请确认我理解得对不对:这次主要是谁在什么场景下遇到什么问题,第一版最想先改善什么结果?'),
|
|
961
|
+
lensQuestion,
|
|
962
|
+
reflectionQuestion('project-context', '项目影响范围', '结合当前项目,请确认第一版最小可用切片是什么;哪些已有模块、入口、流程或历史需求必须复用,哪些可以调整?'),
|
|
963
|
+
reflectionQuestion('scope-quality', '范围与验收', '请确认这次先做到哪一步就算有价值;哪些这轮先不动;哪些老用户习惯、现有业务结果或交付节奏不能被影响?'),
|
|
570
964
|
needsInterfaceSketch
|
|
571
965
|
? reflectionQuestion('interface-sketch', '界面或流程草图', '需求涉及界面或流程,请先确认主要区域、操作入口、预览/确认点和风险提示。')
|
|
572
|
-
:
|
|
966
|
+
: needsDeliveryShapeQuestion
|
|
967
|
+
? reflectionQuestion('delivery-shape', '影响环节与业务风险', '请确认这次大概会牵动哪些环节,例如用户入口、内部流程、账号与权限、外部对接、AI 自动化或成本控制;其中最大的业务风险是什么。')
|
|
968
|
+
: reflectionQuestion('details-boundary', '关键状态与验收细节', '请确认用户会看到的关键状态、重要字段、例外场景和最小验收标准。'),
|
|
573
969
|
]
|
|
574
970
|
: [
|
|
575
|
-
reflectionQuestion('project-context', '项目映射', '
|
|
576
|
-
reflectionQuestion('acceptance', '验收方式', '
|
|
971
|
+
reflectionQuestion('project-context', '项目映射', '请确认这个调整具体落在哪个页面、模块、入口或流程,以及第一版先做哪一小块最有价值。'),
|
|
972
|
+
reflectionQuestion('acceptance', '验收方式', '请确认完成后用户能明显感受到什么变化、哪些既有行为不能改变,以及最小验收标准是什么。'),
|
|
577
973
|
];
|
|
578
974
|
|
|
579
975
|
return {
|
|
@@ -583,7 +979,7 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
|
|
|
583
979
|
label: complexity.label,
|
|
584
980
|
minimumDepth: complexity.minimumDepth,
|
|
585
981
|
questionLimit: gateQuestionLimit(gate, complexity.questionLimit),
|
|
586
|
-
promptPreview
|
|
982
|
+
promptPreview,
|
|
587
983
|
reasons: complexity.reasons,
|
|
588
984
|
needsInterfaceSketch,
|
|
589
985
|
projectContext: {
|
|
@@ -595,15 +991,16 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
|
|
|
595
991
|
currentScope,
|
|
596
992
|
activeChange,
|
|
597
993
|
missingFields: missing,
|
|
994
|
+
projectFraming,
|
|
598
995
|
},
|
|
599
996
|
rounds: [
|
|
600
997
|
{
|
|
601
998
|
id: 'intent-normalization',
|
|
602
999
|
title: '第 1 轮:意图归一化',
|
|
603
1000
|
findings: [
|
|
604
|
-
`用户原始输入:${
|
|
1001
|
+
`用户原始输入:${promptPreview}`,
|
|
605
1002
|
`初步判断:${complexity.label}`,
|
|
606
|
-
|
|
1003
|
+
`需要先把表达收敛成用户、产品形态、场景、目标、动作和期望结果。`,
|
|
607
1004
|
],
|
|
608
1005
|
},
|
|
609
1006
|
{
|
|
@@ -611,8 +1008,9 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
|
|
|
611
1008
|
title: '第 2 轮:项目上下文映射',
|
|
612
1009
|
findings: [
|
|
613
1010
|
`工作区场景:${scenario.label},${scenario.reason}`,
|
|
614
|
-
`当前产品:${productName}
|
|
1011
|
+
`当前产品:${productName};当前产品场景:${formatProductTypeDisplay(productType, { fallback: '待确认' })};已记录问题:${currentProblem}`,
|
|
615
1012
|
`当前范围线索:${currentScope}`,
|
|
1013
|
+
`首轮画像:用户群体=${projectFraming.audience};产品形态=${projectFraming.productShape};第一版先做=${projectFraming.firstSlice}`,
|
|
616
1014
|
activeChange ? `仍有 active change:${activeChange.activeChange}(${activeChange.status}),需要和本轮需求分开评估。` : '当前没有检测到 active change 冲突。',
|
|
617
1015
|
],
|
|
618
1016
|
},
|
|
@@ -621,7 +1019,9 @@ async function buildRequirementIntakeReflection({ projectRoot, ws, snapshot, ana
|
|
|
621
1019
|
title: '第 3 轮:产品质量自检',
|
|
622
1020
|
findings: [
|
|
623
1021
|
`仍需确认的信息:${shortList(missing, '暂无明显缺口')}`,
|
|
624
|
-
|
|
1022
|
+
`边界与约束:先不做=${projectFraming.nonGoals};不能破坏=${projectFraming.guardrails}`,
|
|
1023
|
+
needsInterfaceSketch ? '需求看起来涉及界面或流程,需要先给用户确认草图或关键操作路径。' : `影响环节:${projectFraming.architectureSignals}`,
|
|
1024
|
+
`业务提醒:${projectFraming.riskProbeSummary}`,
|
|
625
1025
|
'进入实现前必须保留范围、非目标、异常路径和验收证据。',
|
|
626
1026
|
],
|
|
627
1027
|
},
|
|
@@ -644,11 +1044,23 @@ function renderRequirementIntakeReflection(reflection) {
|
|
|
644
1044
|
'## 项目上下文',
|
|
645
1045
|
'',
|
|
646
1046
|
`- 工作区场景: ${reflection.projectContext.scenario}`,
|
|
647
|
-
`- 当前产品: ${reflection.projectContext.productName}
|
|
1047
|
+
`- 当前产品: ${reflection.projectContext.productName};产品场景: ${formatProductTypeDisplay(reflection.projectContext.productType, { fallback: '待确认' })}`,
|
|
648
1048
|
`- 当前问题: ${reflection.projectContext.currentProblem}`,
|
|
649
1049
|
`- 当前范围: ${reflection.projectContext.currentScope}`,
|
|
650
1050
|
reflection.projectContext.activeChange ? `- 历史 active change: ${reflection.projectContext.activeChange.activeChange}` : '- 历史 active change: 无',
|
|
651
1051
|
'',
|
|
1052
|
+
'## 首轮项目画像',
|
|
1053
|
+
'',
|
|
1054
|
+
'| 模块 | 当前理解 |',
|
|
1055
|
+
'|---|---|',
|
|
1056
|
+
`| 用户群体 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.audience ?? '待补充')} |`,
|
|
1057
|
+
`| 产品形态 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.productShape ?? '待补充')} |`,
|
|
1058
|
+
`| 第一版先做 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.firstSlice ?? '待补充')} |`,
|
|
1059
|
+
`| 暂不处理 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.nonGoals ?? '待补充')} |`,
|
|
1060
|
+
`| 不能破坏 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.guardrails ?? '待补充')} |`,
|
|
1061
|
+
`| 影响环节 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.architectureSignals ?? '待补充')} |`,
|
|
1062
|
+
`| 业务提醒 | ${escapeMarkdownTableCell(reflection.projectContext.projectFraming?.riskProbeSummary ?? '待补充')} |`,
|
|
1063
|
+
'',
|
|
652
1064
|
];
|
|
653
1065
|
for (const round of reflection.rounds) {
|
|
654
1066
|
lines.push(`## ${round.title}`, '');
|
|
@@ -677,15 +1089,15 @@ async function writeRequirementIntakeReflection(ws, reflection) {
|
|
|
677
1089
|
function buildRequirementIntakeDepth(gate, reflection = null) {
|
|
678
1090
|
const needsInterfaceSketch = requirementLooksLikeInterfaceWork(gate);
|
|
679
1091
|
const fallbackLayers = [
|
|
680
|
-
reflectionQuestion('product-context', '用户 /
|
|
681
|
-
reflectionQuestion('product-outcome', '
|
|
682
|
-
reflectionQuestion('product-flow', '
|
|
1092
|
+
reflectionQuestion('product-context', '用户 / 产品形态 / 问题', '先确认:这是给谁用的、它更像个人产品 / 团队流程 / Agent 协作中的哪一种、为什么现在值得解决?'),
|
|
1093
|
+
reflectionQuestion('product-outcome', '第一版切片 / 目标 / 成功标准', '请确认第一版最小可用切片是什么;解决后用户先能完成什么,用什么业务结果或验收标准判断有效?'),
|
|
1094
|
+
reflectionQuestion('product-flow', '范围 / 非目标 / 异常路径', '请拆出本轮先做什么、不做什么、哪些既有行为不能被破坏,以及关键失败路径和恢复方式。'),
|
|
683
1095
|
reflectionQuestion(
|
|
684
1096
|
'product-detail',
|
|
685
1097
|
needsInterfaceSketch ? '界面草图 / 字段 / 状态' : '细节 / 状态 / 边界',
|
|
686
1098
|
needsInterfaceSketch
|
|
687
1099
|
? '这个需求涉及界面,请先给用户一版 ASCII 线框草图,标出主要区域、操作入口、预览/确认点和风险提示,让用户确认后再 synthesize。'
|
|
688
|
-
: '
|
|
1100
|
+
: '请补齐用户会看到的关键状态、重要字段、业务边界和可验收细节;如果后续发现涉及界面,也要先补 ASCII 线框草图。'
|
|
689
1101
|
),
|
|
690
1102
|
];
|
|
691
1103
|
const layers = reflection?.mustConfirm?.length > 0 ? reflection.mustConfirm : fallbackLayers;
|
|
@@ -850,7 +1262,6 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
|
|
|
850
1262
|
|
|
851
1263
|
snapshot.content = renderPrdMarkdown(snapshot);
|
|
852
1264
|
snapshot.digest = crypto.createHash('sha256').update(snapshot.content).digest('hex');
|
|
853
|
-
assertOpenSpecPreflightReady(snapshot);
|
|
854
1265
|
|
|
855
1266
|
await writeVersionSnapshot(ws, snapshot);
|
|
856
1267
|
|
|
@@ -890,14 +1301,13 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
|
|
|
890
1301
|
reviewPresentationRequired: !presentationGate.ok,
|
|
891
1302
|
});
|
|
892
1303
|
await appendDecision(ws, [
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
`Digest: ${snapshot.digest}.`,
|
|
1304
|
+
`已整理出一版可确认的需求稿。`,
|
|
1305
|
+
`产品场景: ${formatProductTypeDisplay(snapshot.productType, { fallback: '待确认' })}。`,
|
|
1306
|
+
`场景模板: ${formatTemplatePackDisplay(snapshot.templatePack, { fallback: '待确认' })}。`,
|
|
897
1307
|
]);
|
|
898
1308
|
await appendProgress(ws, [
|
|
899
|
-
|
|
900
|
-
|
|
1309
|
+
'已生成新的需求确认稿。',
|
|
1310
|
+
'已同步更新当前需求、流程、角色和交接说明。',
|
|
901
1311
|
presentationGate.ok
|
|
902
1312
|
? `已生成可确认评审面板: ${reviewFiles.canonicalReview}。`
|
|
903
1313
|
: '评审面板暂未生成:需要先通过 openprd review-presentation 写入展示文案。',
|
|
@@ -936,9 +1346,10 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
|
|
|
936
1346
|
templatePack: snapshot.templatePack,
|
|
937
1347
|
synthesizedAt: snapshot.createdAt,
|
|
938
1348
|
}, snapshot);
|
|
939
|
-
await
|
|
940
|
-
const nextWs = { ...ws, data: { ...ws.data, currentState } };
|
|
1349
|
+
const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
|
|
1350
|
+
const nextWs = { ...ws, data: { ...ws.data, currentState: storedCurrentState } };
|
|
941
1351
|
await syncSessionBindingFromSnapshot(projectRoot, snapshot, {
|
|
1352
|
+
sessionId: ws.data.currentSessionId ?? null,
|
|
942
1353
|
reviewStatus: 'pending-confirmation',
|
|
943
1354
|
reviewPath: reviewFiles.canonicalReview,
|
|
944
1355
|
activeReviewPath: reviewFiles.activeReviewEntry,
|
|
@@ -948,7 +1359,7 @@ async function synthesizeWorkspace(projectRoot, overrides = {}) {
|
|
|
948
1359
|
return {
|
|
949
1360
|
ws: nextWs,
|
|
950
1361
|
snapshot,
|
|
951
|
-
currentState,
|
|
1362
|
+
currentState: storedCurrentState,
|
|
952
1363
|
indexEntry,
|
|
953
1364
|
versionIndex: [...versionIndex, indexEntry],
|
|
954
1365
|
reviewArtifact: reviewFiles.activeReviewEntry,
|
|
@@ -1003,8 +1414,16 @@ async function reviewWorkspace(projectRoot, options = {}) {
|
|
|
1003
1414
|
if (!(await exists(ws.workspaceRoot))) {
|
|
1004
1415
|
throw new Error(`Missing workspace: ${ws.workspaceRoot}`);
|
|
1005
1416
|
}
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1417
|
+
const requestedVersion = normalizeVersionId(options.version);
|
|
1418
|
+
const latest = await loadCurrentLaneSnapshot(ws, {
|
|
1419
|
+
fallbackToLatest: !ws.data.currentSessionId || Boolean(requestedVersion),
|
|
1420
|
+
});
|
|
1421
|
+
const fallbackLatest = latest?.snapshot ? latest : (requestedVersion ? await loadLatestVersionSnapshot(ws) : null);
|
|
1422
|
+
const latestSnapshot = fallbackLatest?.snapshot ?? null;
|
|
1423
|
+
const snapshot = requestedVersion
|
|
1424
|
+
? await readVersionSnapshot(ws, requestedVersion)
|
|
1425
|
+
: latestSnapshot;
|
|
1426
|
+
if (!snapshot) {
|
|
1008
1427
|
return {
|
|
1009
1428
|
ok: false,
|
|
1010
1429
|
action: 'review',
|
|
@@ -1012,12 +1431,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
|
|
|
1012
1431
|
errors: ['No synthesized PRD version exists yet. Run openprd synthesize first.'],
|
|
1013
1432
|
};
|
|
1014
1433
|
}
|
|
1015
|
-
|
|
1016
|
-
const requestedVersion = normalizeVersionId(options.version);
|
|
1017
|
-
const snapshot = requestedVersion
|
|
1018
|
-
? await readVersionSnapshot(ws, requestedVersion)
|
|
1019
|
-
: latest.snapshot;
|
|
1020
|
-
if (!snapshot) {
|
|
1434
|
+
if (requestedVersion && normalizeVersionId(snapshot.versionId) !== requestedVersion) {
|
|
1021
1435
|
return {
|
|
1022
1436
|
ok: false,
|
|
1023
1437
|
action: 'review',
|
|
@@ -1041,10 +1455,10 @@ async function reviewWorkspace(projectRoot, options = {}) {
|
|
|
1041
1455
|
|
|
1042
1456
|
const validationErrors = [];
|
|
1043
1457
|
if (options.digest && options.digest !== snapshot.digest) {
|
|
1044
|
-
validationErrors.push(
|
|
1458
|
+
validationErrors.push(`确认指纹不匹配:当前稿件与传入参数不是同一版,请重新从确认页面复制这次确认命令。`);
|
|
1045
1459
|
}
|
|
1046
1460
|
if (requestedWorkUnitId && snapshot.workUnitId !== requestedWorkUnitId) {
|
|
1047
|
-
validationErrors.push(
|
|
1461
|
+
validationErrors.push('这次确认命令对应的稿件不一致,请重新从当前确认页面复制命令后再执行。');
|
|
1048
1462
|
}
|
|
1049
1463
|
if (validationErrors.length > 0) {
|
|
1050
1464
|
return {
|
|
@@ -1058,7 +1472,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
|
|
|
1058
1472
|
};
|
|
1059
1473
|
}
|
|
1060
1474
|
|
|
1061
|
-
const isLatest = normalizeVersionId(snapshot.versionId) === normalizeVersionId(
|
|
1475
|
+
const isLatest = normalizeVersionId(snapshot.versionId) === normalizeVersionId((latestSnapshot ?? snapshot).versionId);
|
|
1062
1476
|
const presentationGate = getReviewPresentationGate(snapshot);
|
|
1063
1477
|
if (!presentationGate.ok) {
|
|
1064
1478
|
return {
|
|
@@ -1106,7 +1520,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
|
|
|
1106
1520
|
notes: options.notes ?? null,
|
|
1107
1521
|
},
|
|
1108
1522
|
};
|
|
1109
|
-
await
|
|
1523
|
+
await persistWorkspaceCurrentState(ws, currentState);
|
|
1110
1524
|
}
|
|
1111
1525
|
workUnit = await writeWorkUnitBinding(ws, {
|
|
1112
1526
|
snapshot,
|
|
@@ -1126,6 +1540,7 @@ async function reviewWorkspace(projectRoot, options = {}) {
|
|
|
1126
1540
|
snapshot.workUnitId ? `工作单元: ${snapshot.workUnitId}。` : null,
|
|
1127
1541
|
]);
|
|
1128
1542
|
await syncSessionBindingFromReview(projectRoot, snapshot, {
|
|
1543
|
+
sessionId: ws.data.currentSessionId ?? null,
|
|
1129
1544
|
reviewStatus: status,
|
|
1130
1545
|
reviewPath: reviewFiles.canonicalReview,
|
|
1131
1546
|
activeReviewPath: reviewFiles.activeReviewEntry,
|
|
@@ -1174,16 +1589,8 @@ async function clarifyWorkspace(projectRoot, options = {}) {
|
|
|
1174
1589
|
|
|
1175
1590
|
const versionIndex = await readVersionIndex(ws);
|
|
1176
1591
|
const currentState = ws.data.currentState ?? {};
|
|
1177
|
-
const snapshot = (await
|
|
1178
|
-
|
|
1179
|
-
versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
|
|
1180
|
-
versionId: currentState.prdVersion > 0
|
|
1181
|
-
? formatVersionId(currentState.prdVersion)
|
|
1182
|
-
: (versionIndex.at(-1)?.versionId ?? 'v0000'),
|
|
1183
|
-
productType: resolveCurrentProductType(ws),
|
|
1184
|
-
templatePack: resolveActiveTemplatePack(ws),
|
|
1185
|
-
status: currentState.status ?? 'draft',
|
|
1186
|
-
});
|
|
1592
|
+
const snapshot = (await loadCurrentLaneSnapshot(ws, { fallbackToLatest: !ws.data.currentSessionId }))?.snapshot
|
|
1593
|
+
?? buildCurrentStateSnapshot(ws, currentState, versionIndex);
|
|
1187
1594
|
|
|
1188
1595
|
const analysis = analyzePrdSnapshot(snapshot);
|
|
1189
1596
|
const basePlan = buildClarificationPlan(snapshot, analysis);
|
|
@@ -1257,16 +1664,8 @@ async function playgroundWorkspace(projectRoot, options = {}) {
|
|
|
1257
1664
|
|
|
1258
1665
|
const versionIndex = await readVersionIndex(ws);
|
|
1259
1666
|
const currentState = ws.data.currentState ?? {};
|
|
1260
|
-
const snapshot = (await
|
|
1261
|
-
|
|
1262
|
-
versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
|
|
1263
|
-
versionId: currentState.prdVersion > 0
|
|
1264
|
-
? formatVersionId(currentState.prdVersion)
|
|
1265
|
-
: (versionIndex.at(-1)?.versionId ?? 'v0000'),
|
|
1266
|
-
productType: resolveCurrentProductType(ws),
|
|
1267
|
-
templatePack: resolveActiveTemplatePack(ws),
|
|
1268
|
-
status: currentState.status ?? 'draft',
|
|
1269
|
-
});
|
|
1667
|
+
const snapshot = (await loadCurrentLaneSnapshot(ws, { fallbackToLatest: !ws.data.currentSessionId }))?.snapshot
|
|
1668
|
+
?? buildCurrentStateSnapshot(ws, currentState, versionIndex);
|
|
1270
1669
|
|
|
1271
1670
|
const state = buildPlaygroundState(snapshot);
|
|
1272
1671
|
const bundle = artifactBundlePaths(ws, `${snapshot.versionId}-playground`);
|
|
@@ -1433,18 +1832,18 @@ async function captureWorkspace(projectRoot, options = {}) {
|
|
|
1433
1832
|
};
|
|
1434
1833
|
}
|
|
1435
1834
|
const staleReview = markReviewStateStaleAfterCapture(currentState, applied, currentState.lastCapturedAt);
|
|
1436
|
-
await
|
|
1437
|
-
|
|
1438
|
-
const snapshot = buildPrdSnapshot({ ...ws, data: { ...ws.data, currentState } }, {
|
|
1439
|
-
...
|
|
1440
|
-
versionNumber:
|
|
1441
|
-
versionId:
|
|
1442
|
-
productType:
|
|
1443
|
-
templatePack:
|
|
1835
|
+
const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
|
|
1836
|
+
|
|
1837
|
+
const snapshot = buildPrdSnapshot({ ...ws, data: { ...ws.data, currentState: storedCurrentState } }, {
|
|
1838
|
+
...storedCurrentState,
|
|
1839
|
+
versionNumber: storedCurrentState.prdVersion ?? 0,
|
|
1840
|
+
versionId: storedCurrentState.prdVersion > 0 ? formatVersionId(storedCurrentState.prdVersion) : 'v0000',
|
|
1841
|
+
productType: storedCurrentState.productType ?? resolveCurrentProductType(ws),
|
|
1842
|
+
templatePack: storedCurrentState.templatePack ?? resolveActiveTemplatePack(ws),
|
|
1444
1843
|
});
|
|
1445
1844
|
const analysis = analyzePrdSnapshot(snapshot);
|
|
1446
|
-
const diagramState = await getDiagramReviewState({ ...ws, data: { ...ws.data, currentState } }, snapshot);
|
|
1447
|
-
const updatedWs = { ...ws, data: { ...ws.data, currentState } };
|
|
1845
|
+
const diagramState = await getDiagramReviewState({ ...ws, data: { ...ws.data, currentState: storedCurrentState } }, snapshot);
|
|
1846
|
+
const updatedWs = { ...ws, data: { ...ws.data, currentState: storedCurrentState } };
|
|
1448
1847
|
const scenario = await detectWorkspaceScenario(projectRoot, updatedWs, await readVersionIndex(ws));
|
|
1449
1848
|
const requirementGate = await readActiveRequirementGate(projectRoot);
|
|
1450
1849
|
const intakeReflection = await buildRequirementIntakeReflection({
|
|
@@ -1462,7 +1861,7 @@ async function captureWorkspace(projectRoot, options = {}) {
|
|
|
1462
1861
|
analysis,
|
|
1463
1862
|
basePlan: buildClarificationPlan(snapshot, analysis),
|
|
1464
1863
|
scenario,
|
|
1465
|
-
captureMeta:
|
|
1864
|
+
captureMeta: storedCurrentState.captureMeta,
|
|
1466
1865
|
prdReviewState,
|
|
1467
1866
|
limit: 8,
|
|
1468
1867
|
}), requirementGate, intakeReflection);
|
|
@@ -1480,7 +1879,7 @@ async function captureWorkspace(projectRoot, options = {}) {
|
|
|
1480
1879
|
]);
|
|
1481
1880
|
|
|
1482
1881
|
return {
|
|
1483
|
-
ws: { ...ws, data: { ...ws.data, currentState } },
|
|
1882
|
+
ws: { ...ws, data: { ...ws.data, currentState: storedCurrentState } },
|
|
1484
1883
|
applied,
|
|
1485
1884
|
artifactMarkdown: options.artifactMarkdown ?? null,
|
|
1486
1885
|
field: applied[0]?.field ?? null,
|
|
@@ -1496,16 +1895,12 @@ async function computeWorkspaceGuidance(ws, options = {}) {
|
|
|
1496
1895
|
const currentState = ws.data.currentState ?? {};
|
|
1497
1896
|
const currentProductType = resolveCurrentProductType(ws);
|
|
1498
1897
|
const currentStatus = currentState.status ?? 'unknown';
|
|
1499
|
-
const latestVersion =
|
|
1500
|
-
const currentDraftSnapshot =
|
|
1898
|
+
const latestVersion = await loadCurrentLaneSnapshot(ws, { fallbackToLatest: !ws.data.currentSessionId });
|
|
1899
|
+
const currentDraftSnapshot = buildCurrentStateSnapshot(ws, {
|
|
1501
1900
|
...currentState,
|
|
1502
|
-
versionNumber: currentState.prdVersion ?? (versionIndex.at(-1)?.versionNumber ?? 0),
|
|
1503
|
-
versionId: currentState.prdVersion > 0
|
|
1504
|
-
? formatVersionId(currentState.prdVersion)
|
|
1505
|
-
: (versionIndex.at(-1)?.versionId ?? 'v0000'),
|
|
1506
1901
|
productType: currentProductType,
|
|
1507
1902
|
templatePack: resolveActiveTemplatePack(ws),
|
|
1508
|
-
});
|
|
1903
|
+
}, versionIndex);
|
|
1509
1904
|
const analysisSnapshot = shouldUseCurrentDraftForGuidance(currentState)
|
|
1510
1905
|
? currentDraftSnapshot
|
|
1511
1906
|
: (latestVersion?.snapshot ?? currentDraftSnapshot);
|
|
@@ -1546,9 +1941,9 @@ async function computeWorkspaceGuidance(ws, options = {}) {
|
|
|
1546
1941
|
suggestedQuestions = clarification.mustAskUser.map((item) => item.prompt);
|
|
1547
1942
|
} else if (!hasProductType) {
|
|
1548
1943
|
nextAction = 'classify';
|
|
1549
|
-
reason = '
|
|
1944
|
+
reason = '产品场景尚未锁定。';
|
|
1550
1945
|
suggestedCommand = 'openprd classify . <consumer|b2b|agent>';
|
|
1551
|
-
suggestedQuestions = [
|
|
1946
|
+
suggestedQuestions = [formatProductTypeQuestion()];
|
|
1552
1947
|
} else if (analysis.missingRequiredFields > 0) {
|
|
1553
1948
|
nextAction = 'interview';
|
|
1554
1949
|
reason = `仍缺少 ${analysis.missingRequiredFields} 个必填字段。`;
|
|
@@ -1708,18 +2103,18 @@ async function classifyWorkspace(projectRoot, productType) {
|
|
|
1708
2103
|
templatePack: productType,
|
|
1709
2104
|
classifiedAt: timestamp(),
|
|
1710
2105
|
};
|
|
1711
|
-
await
|
|
2106
|
+
const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
|
|
1712
2107
|
await appendWorkflowEvent(ws, 'classified', { productType });
|
|
1713
2108
|
await appendDecision(ws, [
|
|
1714
|
-
|
|
1715
|
-
|
|
2109
|
+
`已锁定产品场景为 ${formatProductTypeDisplay(productType, { fallback: productType })}。`,
|
|
2110
|
+
`场景模板已设置为 ${formatTemplatePackDisplay(productType, { fallback: productType })}。`,
|
|
1716
2111
|
]);
|
|
1717
2112
|
await appendProgress(ws, [
|
|
1718
|
-
|
|
2113
|
+
`已将工作区产品场景锁定为 ${formatProductTypeDisplay(productType, { fallback: productType })}。`,
|
|
1719
2114
|
]);
|
|
1720
|
-
await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(
|
|
2115
|
+
await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(storedCurrentState));
|
|
1721
2116
|
|
|
1722
|
-
return { ws, currentState };
|
|
2117
|
+
return { ws: { ...ws, data: { ...ws.data, currentState: storedCurrentState } }, currentState: storedCurrentState };
|
|
1723
2118
|
}
|
|
1724
2119
|
|
|
1725
2120
|
async function interviewWorkspace(projectRoot, requestedType = null) {
|
|
@@ -1754,30 +2149,39 @@ ${content}`);
|
|
|
1754
2149
|
templatePack: productType ?? resolveActiveTemplatePack(ws),
|
|
1755
2150
|
interviewStartedAt: timestamp(),
|
|
1756
2151
|
};
|
|
1757
|
-
await
|
|
2152
|
+
const storedCurrentState = await persistWorkspaceCurrentState(ws, currentState);
|
|
1758
2153
|
await appendWorkflowEvent(ws, 'interview_started', {
|
|
1759
2154
|
productType: currentState.productType,
|
|
1760
2155
|
sourceFiles: sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)),
|
|
1761
2156
|
});
|
|
1762
2157
|
await appendProgress(ws, [
|
|
1763
|
-
`已加载 ${productType
|
|
2158
|
+
`已加载 ${formatProductTypeDisplay(productType, { fallback: '待确认' })} 的访谈问题。`,
|
|
1764
2159
|
`来源文件: ${sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)).join(', ')}`,
|
|
1765
2160
|
]);
|
|
1766
|
-
|
|
1767
|
-
'
|
|
1768
|
-
'
|
|
1769
|
-
'
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2161
|
+
const openQuestions = [
|
|
2162
|
+
'这次主要是给谁用的,他们在什么场景下最卡?',
|
|
2163
|
+
'第一版最值得先让用户完成什么关键动作?',
|
|
2164
|
+
'这轮先不做什么,哪些既有体验、流程或业务结果不能被影响?',
|
|
2165
|
+
];
|
|
2166
|
+
if (productType === 'consumer') {
|
|
2167
|
+
openQuestions.push('什么结果会让用户愿意继续回来,甚至愿意推荐或付费?');
|
|
2168
|
+
} else if (productType === 'b2b') {
|
|
2169
|
+
openQuestions.push('谁拍板、谁使用、谁推进或运营,这几方最容易卡在哪里?');
|
|
2170
|
+
} else if (productType === 'agent') {
|
|
2171
|
+
openQuestions.push('哪些步骤让 Agent 自主做,哪些节点必须保留人工确认或兜底?');
|
|
2172
|
+
} else {
|
|
2173
|
+
openQuestions.push('如果现在还不想讲太细,先确认最重要的用户价值、边界和风险也可以。');
|
|
2174
|
+
}
|
|
2175
|
+
openQuestions.push('如果涉及账号、数据、外部对接、AI 或成本,最大的业务风险是什么?');
|
|
2176
|
+
await appendOpenQuestions(ws, openQuestions);
|
|
2177
|
+
await writeJson(ws.paths.taskGraph, buildWorkflowTaskGraph(storedCurrentState));
|
|
1774
2178
|
|
|
1775
2179
|
return {
|
|
1776
|
-
ws,
|
|
2180
|
+
ws: { ...ws, data: { ...ws.data, currentState: storedCurrentState } },
|
|
1777
2181
|
productType,
|
|
1778
2182
|
sourceFiles: sourceFiles.map((filePath) => path.relative(ws.workspaceRoot, filePath)),
|
|
1779
2183
|
transcript: sourceContent.join('\n\n---\n\n'),
|
|
1780
|
-
currentState,
|
|
2184
|
+
currentState: storedCurrentState,
|
|
1781
2185
|
};
|
|
1782
2186
|
}
|
|
1783
2187
|
|