duaer-spec 0.27.0 → 0.27.2

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.
Files changed (72) hide show
  1. package/.claude/rules/agents-workflow.md +2 -0
  2. package/.claude/rules/duaer-spec.md +1 -0
  3. package/.cursor/rules/agents-workflow.mdc +2 -0
  4. package/.cursor/rules/duaer-spec.mdc +1 -0
  5. package/.duaer/active-job.json +3 -3
  6. package/.duaer/specs/270-patch-version/delivery.json +7 -0
  7. package/.duaer/specs/270-patch-version/spec.md +19 -0
  8. package/.duaer/specs/270-patch-version/tasks.md +4 -0
  9. package/.duaer/specs/271-fde04-compat/delivery.json +9 -0
  10. package/.duaer/specs/271-fde04-compat/spec.md +25 -0
  11. package/.duaer/specs/271-fde04-compat/tasks.md +5 -0
  12. package/.duaer/specs/272-fde03-env-check/delivery.json +9 -0
  13. package/.duaer/specs/272-fde03-env-check/spec.md +34 -0
  14. package/.duaer/specs/272-fde03-env-check/tasks.md +5 -0
  15. package/.duaer/specs/273-fde06-data-precheck/delivery.json +9 -0
  16. package/.duaer/specs/273-fde06-data-precheck/spec.md +37 -0
  17. package/.duaer/specs/273-fde06-data-precheck/tasks.md +5 -0
  18. package/.duaer/specs/274-fde05-external-deps/delivery.json +9 -0
  19. package/.duaer/specs/274-fde05-external-deps/spec.md +37 -0
  20. package/.duaer/specs/274-fde05-external-deps/tasks.md +5 -0
  21. package/.duaer/specs/275-release-0271/delivery.json +9 -0
  22. package/.duaer/specs/275-release-0271/spec.md +13 -0
  23. package/.duaer/specs/275-release-0271/tasks.md +4 -0
  24. package/.duaer/specs/276-fde08-perf-budget/delivery.json +9 -0
  25. package/.duaer/specs/276-fde08-perf-budget/spec.md +38 -0
  26. package/.duaer/specs/276-fde08-perf-budget/tasks.md +5 -0
  27. package/.duaer/specs/277-project-dir-reuse/delivery.json +9 -0
  28. package/.duaer/specs/277-project-dir-reuse/spec.md +31 -0
  29. package/.duaer/specs/277-project-dir-reuse/tasks.md +5 -0
  30. package/.duaer/specs/278-project-create-unique/delivery.json +9 -0
  31. package/.duaer/specs/278-project-create-unique/spec.md +27 -0
  32. package/.duaer/specs/278-project-create-unique/tasks.md +5 -0
  33. package/.duaer/specs/279-arch-side-detour/delivery.json +8 -0
  34. package/.duaer/specs/279-arch-side-detour/spec.md +28 -0
  35. package/.duaer/specs/279-arch-side-detour/tasks.md +5 -0
  36. package/.duaer/specs/280-arch-bottom-via-label/delivery.json +8 -0
  37. package/.duaer/specs/280-arch-bottom-via-label/spec.md +22 -0
  38. package/.duaer/specs/280-arch-bottom-via-label/tasks.md +5 -0
  39. package/.duaer/specs/281-release-0272/spec.md +13 -0
  40. package/.duaer/specs/281-release-0272/tasks.md +4 -0
  41. package/AGENTS.md +5 -2
  42. package/CHANGELOG.md +69 -0
  43. package/bin/duaer-live.mjs +202 -23
  44. package/bin/live-archify.mjs +382 -26
  45. package/bin/live-deliverables.mjs +16 -0
  46. package/bin/live-modules.mjs +81 -1
  47. package/bin/live-project-chat.mjs +8 -0
  48. package/bin/live-repo-path.mjs +91 -2
  49. package/docs/agent/branching-and-release.md +16 -0
  50. package/docs/agent/e2e-test-plan.md +10 -0
  51. package/docs/agent/live-desk.md +34 -4
  52. package/docs/agent/workflow.md +2 -0
  53. package/package.json +1 -1
  54. package/web/live-dev/app.js +95 -2
  55. package/web/live-dev/baseline.mjs +4 -0
  56. package/web/live-dev/compat-matrix.mjs +38 -0
  57. package/web/live-dev/confirm-card.mjs +4 -0
  58. package/web/live-dev/data-precheck.mjs +35 -0
  59. package/web/live-dev/env-check.mjs +41 -0
  60. package/web/live-dev/external-deps.mjs +35 -0
  61. package/web/live-dev/i18n-de.js +25 -10
  62. package/web/live-dev/i18n-es.js +25 -10
  63. package/web/live-dev/i18n-fr.js +25 -10
  64. package/web/live-dev/i18n-ja.js +25 -10
  65. package/web/live-dev/i18n-ko.js +25 -10
  66. package/web/live-dev/i18n-pt-BR.js +25 -10
  67. package/web/live-dev/i18n-ru.js +25 -10
  68. package/web/live-dev/i18n-vi.js +25 -10
  69. package/web/live-dev/i18n-zh-TW.js +25 -10
  70. package/web/live-dev/i18n.js +50 -20
  71. package/web/live-dev/index.html +33 -9
  72. package/web/live-dev/perf-budget.mjs +38 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,75 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.27.2 — 2026-09-23
6
+
7
+ ### Fix: bottom-via edge labels stay under the floor
8
+
9
+ - Bottom/top via labels use `labelAt` on the via corridor instead of
10
+ `labelDy: -36`, which pulled「改稿」up onto mid-row boxes like `artifact`
11
+
12
+ ### Fix: architecture corridor detour when edges cross stacked nodes
13
+
14
+ - Auto-routing validates candidate corridors (bottom/top wrap, side, gutter)
15
+ so detours stay clear of unrelated boxes — not only avoid bottom exits
16
+ - Vertical edge labels use `labelAt` in the inter-box gap
17
+ - Fixes Archify `clean-flow/edge-through-node` and label-overlap failures on stacked layouts
18
+
19
+ ### Fix: create project uses a unique short name under the parent only
20
+
21
+ - Create form no longer accepts absolute paths; folder name is a short name under the product parent
22
+ - Duplicate on-disk folders and duplicate project titles are refused
23
+ - Open existing projects from the list or Browse to open; create and open are separate
24
+
25
+ ### Fix: creating a project no longer reuses an existing directory
26
+
27
+ - Create mode refuses when the target folder already exists on disk
28
+ - Open an existing project from the list or browse; do not create over it
29
+ - Hint copy tells the operator to rename or open the existing project
30
+
31
+ ### Feat: FDE-08 performance budget before confirm
32
+
33
+ - Feature confirm must name LCP, INP, bundle size, virtualized long lists, a weak-network check, and a large-data check
34
+ - A vague speed line such as "pretty fast" fails local validate
35
+ - Modules with no page performance requirement may write that opt-out
36
+ - Kickoff schedules the budget before implement, after the environment probe, data precheck, and external-dependency board when those exist
37
+ - The desk records the budget; it does not run a customer performance lab
38
+
39
+ ## 0.27.1 — 2026-09-21
40
+
41
+ ### Feat: FDE-05 external dependency board before kickoff
42
+
43
+ - Feature confirm requires a blocker, SLA, backup mock, and parallel path,
44
+ or an explicit no-dependency opt-out
45
+ - Kickoff adds that board before implement when a third party, ERP, or SSO
46
+ is declared
47
+
48
+ ### Feat: FDE-06 import precheck before kickoff
49
+
50
+ - Feature confirm requires a field mapping, an import failure list, and an
51
+ export for cleanup, or an explicit no-import opt-out
52
+ - Kickoff adds a data-precheck task before implement when an import is
53
+ declared
54
+
55
+ ### Feat: FDE-03 environment checklist before kickoff
56
+
57
+ - Feature confirm requires DNS, TLS, CORS, auth, and third-party reachability
58
+ recorded as passed, or an explicit no-customer opt-out
59
+ - A failed probe blocks confirm
60
+ - Kickoff adds an environment-probe task before implement when a customer
61
+ environment is declared
62
+
63
+ ### Feat: FDE-04 browser matrix must include evidence
64
+
65
+ - Device matrix needs two named browsers or domestic clients, screenshot or
66
+ cloud evidence, and a polyfill or fallback
67
+ - Kickoff adds a compat-evidence task per confirmed module
68
+
69
+ ### Docs: fast iteration uses patch versions
70
+
71
+ - Default ship bump is the last number (`0.27.1`, `0.27.2`, …)
72
+ - Next release after `0.27.0` is `0.27.1`; do not jump the minor on every ship
73
+
5
74
  ## 0.27.0 — 2026-09-21
6
75
 
7
76
  ### Feat: FDE-07 UAT pack on confirm
@@ -54,13 +54,33 @@ import {
54
54
  } from "../web/live-dev/baseline.mjs";
55
55
  import { apiContractIsFilled, apiContractDeclaresNoHttp } from "../web/live-dev/api-contract.mjs";
56
56
  import { missingUatCases, uatMentionsErrorCode } from "../web/live-dev/uat-pack.mjs";
57
+ import { missingCompatMatrix } from "../web/live-dev/compat-matrix.mjs";
58
+ import {
59
+ envChecklistDeclaresNoCustomer,
60
+ missingEnvChecklist,
61
+ } from "../web/live-dev/env-check.mjs";
62
+ import {
63
+ dataPrecheckDeclaresNoImport,
64
+ missingDataPrecheck,
65
+ } from "../web/live-dev/data-precheck.mjs";
66
+ import {
67
+ externalDepsDeclaresNone,
68
+ missingExternalDeps,
69
+ } from "../web/live-dev/external-deps.mjs";
70
+ import {
71
+ missingPerfBudget,
72
+ perfBudgetDeclaresNone,
73
+ } from "../web/live-dev/perf-budget.mjs";
57
74
  import { allocateUniqueBranch } from "./live-worktree-name.mjs";
58
75
  import {
59
76
  ensureGitInstalled,
60
77
  CURSOR_INSTALL_CMD as TOOLING_CURSOR_INSTALL,
61
78
  } from "./live-tooling.mjs";
62
79
  import {
80
+ assertProductDirFreeForCreate,
81
+ assertProjectIdentityFreeForCreate,
63
82
  ensureProductDir,
83
+ resolveCreateProductRepoPath,
64
84
  resolveProductRepoPath,
65
85
  } from "./live-repo-path.mjs";
66
86
  import {
@@ -738,10 +758,14 @@ const SYSTEM_PROMPT = `你是「Duaer-spec FDE」需求助手。通过多轮对
738
758
  1. 缺关键可执行信息时,每次只问 1 个卡点问题;信息够时不要用「请从多种风格/方向里选一个」代替可执行的验收标准。
739
759
  2. 系统可能很大:边聊边发现模块清单 modules(id/title/status)。对话可以乱跳模块;把内容写进对应模块的确认卡字段,不要强迫用户按顺序说完。
740
760
  3. 每个功能模块维护确认卡字段:goal、outOfScope、acceptance、assumptions,以及 FDE-01 基线三块与 FDE-02 契约:
741
- - deviceMatrix = 浏览器/设备矩阵(具体版本,如「Chrome 最近两版、手机 Safari」;禁止只写「主流浏览器」)
761
+ - deviceMatrix = 浏览器/设备矩阵:至少两个具体目标(如 Chrome 最近两版、手机 Safari、统信 UOS);禁止只写「主流浏览器」。必须写证据位置(截图/录屏或云测)和降级或 Polyfill。
742
762
  - criticalPaths = 联调必须覆盖的主路径(短列表)
743
763
  - exceptionCases = UAT 包,必须同时覆盖空态、失败、权限不足、超时、重试;有接口契约时写出对应错误码或 HTTP status(禁止只写一类)
744
764
  - apiContract = 接口契约路径(如 openapi/openapi.yaml 或 src/types/api.ts),或明确写「本模块无 HTTP API」
765
+ - envChecklist = 环境检查:DNS、TLS、CORS、鉴权、第三方可达须全部写明且通过;无客户联调则写「无客户联调环境」。任一未通过不得开工
766
+ - dataPrecheck = 数据预检:字段映射、导入预检失败清单、可导出给业务清洗;无导入则写「本模块无导入」
767
+ - externalDeps = 外部依赖:阻塞项、SLA、备用 Mock、并行路径;无第三方/ERP/SSO 则写「本模块无外部依赖」
768
+ - perfBudget = 性能预算:LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测;写「挺快的」不合格;无页面则写「本模块无页面性能要求」
745
769
  用户没提时:先根据产品形态填合理默认并写进 JSON,再在正文用一句话请用户改;不要长期留空。小项目可只有一个模块(id=main)。
746
770
  4. acceptance 必须可客观检查(打开何处、看到什么、哪条命令通过);禁止只写「更好用/更好看」。
747
771
  5. ready=true 只表示**当前 active 模块**可确认,且上述字段都已非空、验收可检查。不是整系统开工。不要催用户立刻派工。
@@ -751,7 +775,7 @@ const SYSTEM_PROMPT = `你是「Duaer-spec FDE」需求助手。通过多轮对
751
775
  - 先写对用户说的纯文本(可多行,不要 JSON;正文里不要再列一遍选项清单)
752
776
  - 然后单独一行:<<<JSON>>>
753
777
  - 再输出一个 JSON 对象(不要 markdown 围栏):
754
- {"modules":[{"id":"auth","title":"登录","status":"draft","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"..."}],"activeModuleId":"auth","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","ready":false,"options":["可选A","可选B"]}
778
+ {"modules":[{"id":"auth","title":"登录","status":"draft","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"..."}],"activeModuleId":"auth","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"...","ready":false,"options":["可选A","可选B"]}
755
779
  说明:顶层字段与 ready 对应 activeModuleId 那一模块;modules 为完整清单(可增删改名)。`;
756
780
 
757
781
  const BUG_CHAT_PROMPT = `你是「Duaer-spec FDE」缺陷助手。用户要修 bug,不是做新功能。通过多轮对话整理成一张可派工的缺陷卡,使数字员工能复现、修复并回归。
@@ -786,6 +810,10 @@ const REVISE_CHAT_PROMPT = `你是「Duaer-spec FDE」改进对话助手。用
786
810
  - assumptions = 用户不满意的原因 / 背景摘要
787
811
  - deviceMatrix / criticalPaths / exceptionCases = 若本轮改动触及联调范围则更新;否则可沿用或写「同基线」
788
812
  - apiContract = 若触及接口则更新路径,或写「本模块无 HTTP API」/「同基线」
813
+ - envChecklist = 若触及客户环境则更新探测结果,或写「无客户联调环境」/「同基线」
814
+ - dataPrecheck = 若触及导入则更新映射与失败清单,或写「本模块无导入」/「同基线」
815
+ - externalDeps = 若触及第三方/ERP/SSO 则更新阻塞项、SLA、备用 Mock、并行路径,或写「本模块无外部依赖」/「同基线」
816
+ - perfBudget = 若有页面则更新 LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测,或写「本模块无页面性能要求」/「同基线」
789
817
  3. 字段够清楚且可执行时 ready=true(确认前系统会自动校验)。缺基线/契约字段时先填合理默认或用 options 点选。
790
818
  4. 不要写代码。不要立刻派工。不要假设仓库路径。
791
819
  5. 只要问题是让用户做选择,必须在 options 填 2~5 个短选项(≤20字);界面可点选发送。禁止只让用户手打或「请回复数字」。
@@ -793,7 +821,7 @@ const REVISE_CHAT_PROMPT = `你是「Duaer-spec FDE」改进对话助手。用
793
821
  - 先写对用户说的纯文本(正文不要再列选项清单)
794
822
  - 然后单独一行:<<<JSON>>>
795
823
  - 再输出 JSON(不要 markdown 围栏):
796
- {"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","ready":false,"options":["可选A","可选B"]}`;
824
+ {"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"...","ready":false,"options":["可选A","可选B"]}`;
797
825
 
798
826
  const ARCHITECTURE_CHAT_PROMPT = `你是「Duaer-spec FDE」架构助手。需求已确认。通过多轮对话设计系统架构图,供数字员工按图开发。系统会把 JSON 自动渲染成图,用户看不到原始 JSON。
799
827
 
@@ -819,25 +847,29 @@ const ACCEPT_PROMPT = `你是「Duaer-spec FDE」需求验收官。用户即将
819
847
  2. acceptance 是否可客观检查:必须写清「打开/看到/点击/返回/命令通过/接口返回」等可核对结果;禁止仅「更好用 / 更好看 / nicer / looks better」这类空话
820
848
  3. outOfScope 是否划清边界(可简短)
821
849
  4. assumptions 是否合理、不偷换目标
822
- 5. deviceMatrix / criticalPaths / exceptionCases 是否已填且具体(禁止空;deviceMatrix 禁止只写「主流浏览器」;exceptionCases 必须覆盖空态、失败、权限不足、超时、重试;有接口时还要错误码或 HTTP status)
850
+ 5. deviceMatrix / criticalPaths / exceptionCases 是否已填且具体(禁止空;deviceMatrix 至少两个具体浏览器或国产终端,并写截图/云测证据和降级或 Polyfill,禁止只写「主流浏览器」;exceptionCases 必须覆盖空态、失败、权限不足、超时、重试;有接口时还要错误码或 HTTP status)
823
851
  6. apiContract 是否已填:OpenAPI/类型等路径,或明确「本模块无 HTTP API」(禁止空)
824
- 7. 按该验收标准做完后,用户是否有理由满意(成品可核对,而非过程叙事)
852
+ 7. envChecklist 是否已填:DNS、TLS、CORS、鉴权、第三方可达全部通过,或明确「无客户联调环境」;存在未通过项则 passed=false
853
+ 8. dataPrecheck 是否已填:字段映射、导入预检失败清单、可导出,或明确「本模块无导入」
854
+ 9. externalDeps 是否已填:阻塞项、SLA、备用 Mock、并行路径,或明确「本模块无外部依赖」
855
+ 10. perfBudget 是否已填:LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测,或明确「本模块无页面性能要求」;禁止只写「挺快的」
856
+ 11. 按该验收标准做完后,用户是否有理由满意(成品可核对,而非过程叙事)
825
857
 
826
858
  规则:
827
859
  - 若小改即可通过:修订确认卡字段(尤其把 acceptance 改成可检查句子,并补全基线与契约),passed=true
828
860
  - 若缺关键信息:passed=false,issues 列出缺什么(中文,短句)
829
861
  - 不要写代码。不要假设仓库路径。不要催派工。
830
862
  - 只输出一个 JSON,不要 markdown 围栏:
831
- {"passed":false,"summary":"一句话结论","issues":["问题1"],"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"..."}`;
863
+ {"passed":false,"summary":"一句话结论","issues":["问题1"],"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"..."}`;
832
864
 
833
- const FIX_ACCEPT_PROMPT = `你是「Duaer-spec FDE」需求修正助手。自动验收未通过,请根据 issues 修订确认卡。优先把 acceptance 改成可客观检查的句子(打开何处、看到什么、哪条命令通过),并补全 deviceMatrix / criticalPaths / exceptionCases / apiContract;不要编造用户没提过的大功能。
865
+ const FIX_ACCEPT_PROMPT = `你是「Duaer-spec FDE」需求修正助手。自动验收未通过,请根据 issues 修订确认卡。优先把 acceptance 改成可客观检查的句子(打开何处、看到什么、哪条命令通过),并补全 deviceMatrix / criticalPaths / exceptionCases / apiContract / envChecklist / dataPrecheck / externalDeps / perfBudget;不要编造用户没提过的大功能。
834
866
 
835
867
  规则:
836
- 1. 针对每条 issue 修改 goal / outOfScope / acceptance / assumptions / deviceMatrix / criticalPaths / exceptionCases / apiContract
837
- 2. 保持用户原意;缺信息时写合理、可检查的默认(如 Chrome 最近两版;主路径对齐 goal;异常态须同时写上空态、失败、权限不足、超时、重试,有接口则补错误码;无接口则写「本模块无 HTTP API」),并写进 assumptions
868
+ 1. 针对每条 issue 修改 goal / outOfScope / acceptance / assumptions / deviceMatrix / criticalPaths / exceptionCases / apiContract / envChecklist / dataPrecheck / externalDeps / perfBudget
869
+ 2. 保持用户原意;缺信息时写合理、可检查的默认(如 Chrome 最近两版、手机 Safari,云测截图放 compat/,旧壳降级提示升级;主路径对齐 goal;异常态须同时写上空态、失败、权限不足、超时、重试,有接口则补错误码;无接口则写「本模块无 HTTP API」;无客户联调则 envChecklist 写「无客户联调环境」,否则 DNS、TLS、CORS、鉴权、第三方可达全部写通过;无导入则 dataPrecheck 写「本模块无导入」,否则写字段映射、导入预检失败清单、可导出;无外部依赖则 externalDeps 写「本模块无外部依赖」,否则写阻塞项、SLA、备用 Mock、并行路径;无页面则 perfBudget 写「本模块无页面性能要求」,否则写 LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测,禁止只写「挺快的」),并写进 assumptions
838
870
  3. 不要写代码。不要假设仓库路径。
839
871
  4. 只输出一个 JSON,不要 markdown 围栏:
840
- {"summary":"一句话说明改了什么","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"..."}`;
872
+ {"summary":"一句话说明改了什么","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"..."}`;
841
873
 
842
874
  const BUG_ACCEPT_PROMPT = `你是「Duaer-spec FDE」缺陷验收官。用户即将锁定缺陷卡(尚未派工)。目标是:规范缺陷描述,使数字员工能复现、修复并回归。
843
875
 
@@ -1113,6 +1145,10 @@ function parseChatResult(content) {
1113
1145
  criticalPaths: flat(m.criticalPaths ?? m.card?.criticalPaths),
1114
1146
  exceptionCases: flat(m.exceptionCases ?? m.card?.exceptionCases),
1115
1147
  apiContract: flat(m.apiContract ?? m.card?.apiContract),
1148
+ envChecklist: flat(m.envChecklist ?? m.card?.envChecklist),
1149
+ dataPrecheck: flat(m.dataPrecheck ?? m.card?.dataPrecheck),
1150
+ externalDeps: flat(m.externalDeps ?? m.card?.externalDeps),
1151
+ perfBudget: flat(m.perfBudget ?? m.card?.perfBudget),
1116
1152
  dependsOn: Array.isArray(m.dependsOn)
1117
1153
  ? m.dependsOn.map((x) => flat(x)).filter(Boolean).slice(0, 20)
1118
1154
  : [],
@@ -1131,6 +1167,10 @@ function parseChatResult(content) {
1131
1167
  criticalPaths: flat(obj.criticalPaths),
1132
1168
  exceptionCases: flat(obj.exceptionCases),
1133
1169
  apiContract: flat(obj.apiContract),
1170
+ envChecklist: flat(obj.envChecklist),
1171
+ dataPrecheck: flat(obj.dataPrecheck),
1172
+ externalDeps: flat(obj.externalDeps),
1173
+ perfBudget: flat(obj.perfBudget),
1134
1174
  ready: Boolean(obj.ready),
1135
1175
  activeModuleId: flat(obj.activeModuleId) || undefined,
1136
1176
  modules,
@@ -1153,6 +1193,10 @@ function parseChatResult(content) {
1153
1193
  criticalPaths: "",
1154
1194
  exceptionCases: "",
1155
1195
  apiContract: "",
1196
+ envChecklist: "",
1197
+ dataPrecheck: "",
1198
+ externalDeps: "",
1199
+ perfBudget: "",
1156
1200
  ready: false,
1157
1201
  options: [],
1158
1202
  };
@@ -1172,6 +1216,10 @@ function chatDoneSsePayload(parsed, { includeJsonBlock = true } = {}) {
1172
1216
  criticalPaths: String(parsed?.criticalPaths || ""),
1173
1217
  exceptionCases: String(parsed?.exceptionCases || ""),
1174
1218
  apiContract: String(parsed?.apiContract || ""),
1219
+ envChecklist: String(parsed?.envChecklist || ""),
1220
+ dataPrecheck: String(parsed?.dataPrecheck || ""),
1221
+ externalDeps: String(parsed?.externalDeps || ""),
1222
+ perfBudget: String(parsed?.perfBudget || ""),
1175
1223
  ready: Boolean(parsed?.ready),
1176
1224
  activeModuleId: parsed?.activeModuleId
1177
1225
  ? String(parsed.activeModuleId).slice(0, 80)
@@ -1189,6 +1237,10 @@ function chatDoneSsePayload(parsed, { includeJsonBlock = true } = {}) {
1189
1237
  criticalPaths: String(m.criticalPaths || "").slice(0, 4000),
1190
1238
  exceptionCases: String(m.exceptionCases || "").slice(0, 4000),
1191
1239
  apiContract: String(m.apiContract || "").slice(0, 4000),
1240
+ envChecklist: String(m.envChecklist || "").slice(0, 4000),
1241
+ dataPrecheck: String(m.dataPrecheck || "").slice(0, 4000),
1242
+ externalDeps: String(m.externalDeps || "").slice(0, 4000),
1243
+ perfBudget: String(m.perfBudget || "").slice(0, 4000),
1192
1244
  dependsOn: Array.isArray(m.dependsOn)
1193
1245
  ? m.dependsOn.map(String).slice(0, 20)
1194
1246
  : [],
@@ -1352,6 +1404,10 @@ function parseAcceptResult(content, fallback) {
1352
1404
  criticalPaths: pick("criticalPaths"),
1353
1405
  exceptionCases: pick("exceptionCases"),
1354
1406
  apiContract: pick("apiContract"),
1407
+ envChecklist: pick("envChecklist"),
1408
+ dataPrecheck: pick("dataPrecheck"),
1409
+ externalDeps: pick("externalDeps"),
1410
+ perfBudget: pick("perfBudget"),
1355
1411
  };
1356
1412
  }
1357
1413
 
@@ -1396,8 +1452,13 @@ function localAcceptCheck(card, { kind = "feature" } = {}) {
1396
1452
  const deviceMatrix = String(card.deviceMatrix || "").trim();
1397
1453
  const criticalPaths = String(card.criticalPaths || "").trim();
1398
1454
  const exceptionCases = String(card.exceptionCases || "").trim();
1399
- if (deviceMatrix.length < 4) {
1400
- issues.push("「浏览器/设备矩阵」必填(如 Chrome 最近两版 / 手机 Safari)");
1455
+ if (deviceMatrix.length < 4 || missingCompatMatrix(deviceMatrix).length) {
1456
+ const missing = missingCompatMatrix(deviceMatrix);
1457
+ issues.push(
1458
+ missing.length
1459
+ ? `「浏览器/设备矩阵」未写成可核对兼容包:${missing.join("、")}`
1460
+ : "「浏览器/设备矩阵」必填(至少两个具体浏览器或国产终端,并写证据与降级)",
1461
+ );
1401
1462
  }
1402
1463
  if (criticalPaths.length < 4) {
1403
1464
  issues.push("「关键路径」必填(联调必须覆盖的主路径)");
@@ -1423,6 +1484,54 @@ function localAcceptCheck(card, { kind = "feature" } = {}) {
1423
1484
  "「接口契约」必填(OpenAPI/类型路径,或写「本模块无 HTTP API」)",
1424
1485
  );
1425
1486
  }
1487
+ const envChecklist = String(card.envChecklist || "").trim();
1488
+ if (
1489
+ !envChecklistDeclaresNoCustomer(envChecklist) &&
1490
+ (envChecklist.length < 4 || missingEnvChecklist(envChecklist).length)
1491
+ ) {
1492
+ const missing = missingEnvChecklist(envChecklist);
1493
+ issues.push(
1494
+ missing.length
1495
+ ? `「环境检查」未通过:${missing.join("、")}(须探测 DNS、TLS、CORS、鉴权、第三方可达且全部通过,或写「无客户联调环境」)`
1496
+ : "「环境检查」必填(DNS、TLS、CORS、鉴权、第三方可达均通过,或写「无客户联调环境」)",
1497
+ );
1498
+ }
1499
+ const dataPrecheck = String(card.dataPrecheck || "").trim();
1500
+ if (
1501
+ !dataPrecheckDeclaresNoImport(dataPrecheck) &&
1502
+ (dataPrecheck.length < 4 || missingDataPrecheck(dataPrecheck).length)
1503
+ ) {
1504
+ const missing = missingDataPrecheck(dataPrecheck);
1505
+ issues.push(
1506
+ missing.length
1507
+ ? `「数据预检」未写清:${missing.join("、")}(须含字段映射、导入预检失败清单、可导出,或写「本模块无导入」)`
1508
+ : "「数据预检」必填(字段映射、导入预检失败清单、可导出,或写「本模块无导入」)",
1509
+ );
1510
+ }
1511
+ const externalDeps = String(card.externalDeps || "").trim();
1512
+ if (
1513
+ !externalDepsDeclaresNone(externalDeps) &&
1514
+ (externalDeps.length < 4 || missingExternalDeps(externalDeps).length)
1515
+ ) {
1516
+ const missing = missingExternalDeps(externalDeps);
1517
+ issues.push(
1518
+ missing.length
1519
+ ? `「外部依赖」未写清:${missing.join("、")}(须含阻塞项、SLA、备用 Mock、并行路径,或写「本模块无外部依赖」)`
1520
+ : "「外部依赖」必填(阻塞项、SLA、备用 Mock、并行路径,或写「本模块无外部依赖」)",
1521
+ );
1522
+ }
1523
+ const perfBudget = String(card.perfBudget || "").trim();
1524
+ if (
1525
+ !perfBudgetDeclaresNone(perfBudget) &&
1526
+ (perfBudget.length < 4 || missingPerfBudget(perfBudget).length)
1527
+ ) {
1528
+ const missing = missingPerfBudget(perfBudget);
1529
+ issues.push(
1530
+ missing.length
1531
+ ? `「性能预算」未写清:${missing.join("、")}(须含 LCP、INP、包体、长列表、弱网、大数据压测,或写「本模块无页面性能要求」)`
1532
+ : "「性能预算」必填(LCP、INP、包体、长列表、弱网、大数据压测,或写「本模块无页面性能要求」)",
1533
+ );
1534
+ }
1426
1535
  }
1427
1536
  return issues;
1428
1537
  }
@@ -1442,6 +1551,10 @@ async function autoAcceptCard(cfg, card, { kind = "feature", projectContext = nu
1442
1551
  criticalPaths: card.criticalPaths,
1443
1552
  exceptionCases: card.exceptionCases,
1444
1553
  apiContract: card.apiContract,
1554
+ envChecklist: card.envChecklist,
1555
+ dataPrecheck: card.dataPrecheck,
1556
+ externalDeps: card.externalDeps,
1557
+ perfBudget: card.perfBudget,
1445
1558
  };
1446
1559
  }
1447
1560
  const ctxNote =
@@ -1472,6 +1585,10 @@ async function validateCardOnly(cfg, card, { kind = "feature", projectContext =
1472
1585
  criticalPaths: String(card.criticalPaths || "").trim(),
1473
1586
  exceptionCases: String(card.exceptionCases || "").trim(),
1474
1587
  apiContract: String(card.apiContract || "").trim(),
1588
+ envChecklist: String(card.envChecklist || "").trim(),
1589
+ dataPrecheck: String(card.dataPrecheck || "").trim(),
1590
+ externalDeps: String(card.externalDeps || "").trim(),
1591
+ perfBudget: String(card.perfBudget || "").trim(),
1475
1592
  };
1476
1593
  if (kind === "bug" && projectContext) {
1477
1594
  normalized = {
@@ -1530,6 +1647,10 @@ async function validateCardOnly(cfg, card, { kind = "feature", projectContext =
1530
1647
  criticalPaths: review.criticalPaths || normalized.criticalPaths,
1531
1648
  exceptionCases: review.exceptionCases || normalized.exceptionCases,
1532
1649
  apiContract: review.apiContract || normalized.apiContract,
1650
+ envChecklist: review.envChecklist || normalized.envChecklist,
1651
+ dataPrecheck: review.dataPrecheck || normalized.dataPrecheck,
1652
+ externalDeps: review.externalDeps || normalized.externalDeps,
1653
+ perfBudget: review.perfBudget || normalized.perfBudget,
1533
1654
  };
1534
1655
  }
1535
1656
 
@@ -1577,6 +1698,10 @@ async function autoFixConfirmCard(cfg, card, issues, { kind = "feature", project
1577
1698
  criticalPaths: String(obj.criticalPaths || card.criticalPaths || "").trim(),
1578
1699
  exceptionCases: String(obj.exceptionCases || card.exceptionCases || "").trim(),
1579
1700
  apiContract: String(obj.apiContract || card.apiContract || "").trim(),
1701
+ envChecklist: String(obj.envChecklist || card.envChecklist || "").trim(),
1702
+ dataPrecheck: String(obj.dataPrecheck || card.dataPrecheck || "").trim(),
1703
+ externalDeps: String(obj.externalDeps || card.externalDeps || "").trim(),
1704
+ perfBudget: String(obj.perfBudget || card.perfBudget || "").trim(),
1580
1705
  };
1581
1706
  }
1582
1707
 
@@ -2423,20 +2548,53 @@ function activateProject(body = {}) {
2423
2548
  const cfg = readConfig();
2424
2549
  let raw = String(body.path || body.name || "").trim();
2425
2550
  if (!raw) {
2426
- const e = new Error("请填写目录名或绝对路径");
2551
+ const e = new Error(
2552
+ body.create === true || body.mode === "create"
2553
+ ? "请填写要新建的目录短名"
2554
+ : "请填写目录名或绝对路径",
2555
+ );
2427
2556
  e.code = "EMPTY_PATH";
2428
2557
  throw e;
2429
2558
  }
2430
2559
  const titleIn = String(body.title || "").trim();
2431
2560
  const descriptionIn = String(body.description || "").trim();
2432
- const abs = resolveProductRepoPath(raw, {
2433
- projectsRoot: cfg.projectsRoot,
2434
- });
2561
+ const create = body.create === true || body.mode === "create";
2562
+ if (create) {
2563
+ if (!titleIn) {
2564
+ const e = new Error("请填写项目名称");
2565
+ e.code = "NEED_TITLE";
2566
+ throw e;
2567
+ }
2568
+ if (!descriptionIn) {
2569
+ const e = new Error("请填写简单的背景描述");
2570
+ e.code = "NEED_DESCRIPTION";
2571
+ throw e;
2572
+ }
2573
+ }
2574
+ const abs = create
2575
+ ? resolveCreateProductRepoPath(raw, {
2576
+ projectsRoot: cfg.projectsRoot,
2577
+ })
2578
+ : resolveProductRepoPath(raw, {
2579
+ projectsRoot: cfg.projectsRoot,
2580
+ });
2581
+ if (create) {
2582
+ assertProductDirFreeForCreate(abs);
2583
+ assertProjectIdentityFreeForCreate(
2584
+ { path: abs, title: titleIn },
2585
+ readRepos(),
2586
+ );
2587
+ } else if (!fs.existsSync(abs)) {
2588
+ const e = new Error(`项目目录不存在:${abs}`);
2589
+ e.code = "PATH_MISSING";
2590
+ e.path = abs;
2591
+ throw e;
2592
+ }
2435
2593
  const ensured = ensureProductDir(abs);
2436
2594
  const existing = readRepos().find(
2437
2595
  (r) => normalizeRepoPath(r.path) === normalizeRepoPath(abs),
2438
2596
  );
2439
- const isNew = Boolean(ensured.created) || !existing;
2597
+ const isNew = Boolean(ensured.created) || (create && !existing);
2440
2598
  const needMeta = Boolean(body.requireMeta) || isNew;
2441
2599
  if (needMeta && !titleIn) {
2442
2600
  const e = new Error("请填写项目名称");
@@ -2453,12 +2611,17 @@ function activateProject(body = {}) {
2453
2611
  exact: true,
2454
2612
  ensureDuaer: false,
2455
2613
  });
2456
- const savedTitle =
2457
- titleIn ||
2458
- existing?.title ||
2459
- existing?.name ||
2460
- path.basename(probe.path);
2461
- const savedDesc = descriptionIn || existing?.description || "";
2614
+ // Opening an existing project keeps prior title/description unless the
2615
+ // caller sends non-empty replacements. Create always writes the new meta.
2616
+ const savedTitle = create
2617
+ ? titleIn || path.basename(probe.path)
2618
+ : titleIn ||
2619
+ existing?.title ||
2620
+ existing?.name ||
2621
+ path.basename(probe.path);
2622
+ const savedDesc = create
2623
+ ? descriptionIn
2624
+ : descriptionIn || existing?.description || "";
2462
2625
  rememberRepo(probe.path, {
2463
2626
  title: savedTitle,
2464
2627
  name: savedTitle,
@@ -6389,6 +6552,10 @@ async function handleApi(req, res) {
6389
6552
  criticalPaths: body.criticalPaths,
6390
6553
  exceptionCases: body.exceptionCases,
6391
6554
  apiContract: body.apiContract,
6555
+ envChecklist: body.envChecklist,
6556
+ dataPrecheck: body.dataPrecheck,
6557
+ externalDeps: body.externalDeps,
6558
+ perfBudget: body.perfBudget,
6392
6559
  });
6393
6560
  const deskKind =
6394
6561
  String(body.deskKind || body.kind || "").trim() === "bug"
@@ -6442,6 +6609,10 @@ async function handleApi(req, res) {
6442
6609
  criticalPaths: body.criticalPaths,
6443
6610
  exceptionCases: body.exceptionCases,
6444
6611
  apiContract: body.apiContract,
6612
+ envChecklist: body.envChecklist,
6613
+ dataPrecheck: body.dataPrecheck,
6614
+ externalDeps: body.externalDeps,
6615
+ perfBudget: body.perfBudget,
6445
6616
  });
6446
6617
  const issues = Array.isArray(body.issues) ? body.issues : [];
6447
6618
  const deskKind =
@@ -6503,6 +6674,10 @@ async function handleApi(req, res) {
6503
6674
  criticalPaths: body.criticalPaths,
6504
6675
  exceptionCases: body.exceptionCases,
6505
6676
  apiContract: body.apiContract,
6677
+ envChecklist: body.envChecklist,
6678
+ dataPrecheck: body.dataPrecheck,
6679
+ externalDeps: body.externalDeps,
6680
+ perfBudget: body.perfBudget,
6506
6681
  });
6507
6682
  if (!card.goal || !card.acceptance) {
6508
6683
  send(res, 400, { error: "goal and acceptance are required" });
@@ -6612,6 +6787,10 @@ async function handleApi(req, res) {
6612
6787
  criticalPaths: body.criticalPaths,
6613
6788
  exceptionCases: body.exceptionCases,
6614
6789
  apiContract: body.apiContract,
6790
+ envChecklist: body.envChecklist,
6791
+ dataPrecheck: body.dataPrecheck,
6792
+ externalDeps: body.externalDeps,
6793
+ perfBudget: body.perfBudget,
6615
6794
  });
6616
6795
  const issues = Array.isArray(body.issues) ? body.issues : [];
6617
6796
  if (!card.goal && !card.acceptance) {