duaer-spec 0.27.1 → 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 (45) hide show
  1. package/.duaer/active-job.json +3 -3
  2. package/.duaer/specs/275-release-0271/delivery.json +9 -0
  3. package/.duaer/specs/275-release-0271/tasks.md +1 -1
  4. package/.duaer/specs/276-fde08-perf-budget/delivery.json +9 -0
  5. package/.duaer/specs/276-fde08-perf-budget/spec.md +38 -0
  6. package/.duaer/specs/276-fde08-perf-budget/tasks.md +5 -0
  7. package/.duaer/specs/277-project-dir-reuse/delivery.json +9 -0
  8. package/.duaer/specs/277-project-dir-reuse/spec.md +31 -0
  9. package/.duaer/specs/277-project-dir-reuse/tasks.md +5 -0
  10. package/.duaer/specs/278-project-create-unique/delivery.json +9 -0
  11. package/.duaer/specs/278-project-create-unique/spec.md +27 -0
  12. package/.duaer/specs/278-project-create-unique/tasks.md +5 -0
  13. package/.duaer/specs/279-arch-side-detour/delivery.json +8 -0
  14. package/.duaer/specs/279-arch-side-detour/spec.md +28 -0
  15. package/.duaer/specs/279-arch-side-detour/tasks.md +5 -0
  16. package/.duaer/specs/280-arch-bottom-via-label/delivery.json +8 -0
  17. package/.duaer/specs/280-arch-bottom-via-label/spec.md +22 -0
  18. package/.duaer/specs/280-arch-bottom-via-label/tasks.md +5 -0
  19. package/.duaer/specs/281-release-0272/spec.md +13 -0
  20. package/.duaer/specs/281-release-0272/tasks.md +4 -0
  21. package/CHANGELOG.md +34 -0
  22. package/bin/duaer-live.mjs +93 -19
  23. package/bin/live-archify.mjs +382 -26
  24. package/bin/live-deliverables.mjs +4 -0
  25. package/bin/live-modules.mjs +17 -0
  26. package/bin/live-project-chat.mjs +2 -0
  27. package/bin/live-repo-path.mjs +91 -2
  28. package/docs/agent/e2e-test-plan.md +5 -0
  29. package/docs/agent/live-desk.md +15 -3
  30. package/package.json +1 -1
  31. package/web/live-dev/app.js +28 -2
  32. package/web/live-dev/baseline.mjs +1 -0
  33. package/web/live-dev/confirm-card.mjs +1 -0
  34. package/web/live-dev/i18n-de.js +14 -8
  35. package/web/live-dev/i18n-es.js +14 -8
  36. package/web/live-dev/i18n-fr.js +14 -8
  37. package/web/live-dev/i18n-ja.js +14 -8
  38. package/web/live-dev/i18n-ko.js +14 -8
  39. package/web/live-dev/i18n-pt-BR.js +14 -8
  40. package/web/live-dev/i18n-ru.js +14 -8
  41. package/web/live-dev/i18n-vi.js +14 -8
  42. package/web/live-dev/i18n-zh-TW.js +14 -8
  43. package/web/live-dev/i18n.js +28 -16
  44. package/web/live-dev/index.html +13 -7
  45. package/web/live-dev/perf-budget.mjs +38 -0
@@ -1,5 +1,5 @@
1
1
  {
2
- "feature": "275-release-0271",
3
- "branch": "feat/release-0271",
4
- "worktree": ".worktree/feat-release-0271"
2
+ "feature": "281-release-0272",
3
+ "branch": "feat/release-0272",
4
+ "worktree": ".worktree/feat-release-0272"
5
5
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "status": "accepted",
3
+ "summary": "0.27.1 is on main as v0.27.1, GitHub Release published, npm Trusted Publishing succeeded.",
4
+ "verification": {
5
+ "publish": "git push origin main develop v0.27.1 — fast-forward 1d6d164..26ede20",
6
+ "release": "https://github.com/Duaer/duaer-spec/releases/tag/v0.27.1",
7
+ "npm": "gh run 35558574069 success — + duaer-spec@0.27.1"
8
+ }
9
+ }
@@ -1,4 +1,4 @@
1
1
  # Tasks — 275-release-0271
2
2
 
3
3
  - [x] Bump 0.27.1 and move changelog notes
4
- - [ ] Tag v0.27.1 on main and publish the GitHub Release
4
+ - [x] Tag v0.27.1 on main and publish the GitHub Release
@@ -0,0 +1,9 @@
1
+ {
2
+ "status": "accepted",
3
+ "summary": "Feature confirm requires LCP, INP, bundle size, virtualized long lists, a weak-network check, and a large-data check, or a no-page-performance opt-out. A vague speed line fails. Kickoff schedules the budget before implement.",
4
+ "verification": {
5
+ "L0": "node --check bin/duaer-live.mjs bin/live-modules.mjs web/live-dev/app.js web/live-dev/perf-budget.mjs — pass",
6
+ "L1": "npm test — 254 pass",
7
+ "L3": "npm run test:live — pass"
8
+ }
9
+ }
@@ -0,0 +1,38 @@
1
+ # Feature Specification: FDE-08 performance budget
2
+
3
+ ## Goal
4
+
5
+ A feature confirm card cannot pass on a vague speed claim. It must record
6
+ LCP, INP, bundle size, virtualized long lists, a weak-network check, and a
7
+ large-data check. Kickoff schedules that budget before implementation.
8
+
9
+ ## In scope
10
+
11
+ - Local validate on perfBudget
12
+ - One FDE-08 task per confirmed module that declares page performance,
13
+ placed before implement and after the env probe, data precheck, and
14
+ external-dependency board when those exist
15
+ - Hint, placeholder, and chat/accept prompts
16
+ - Explicit opt-out: no page performance requirement
17
+
18
+ ## Out of scope
19
+
20
+ - Bug desk
21
+ - Running a customer performance lab from the desk
22
+
23
+ ## Assumptions
24
+
25
+ - The desk records the budget; the digital employee runs the weak-network
26
+ and large-data checks.
27
+ - 「本模块无页面性能要求」 / "no page performance" skips the six items
28
+ and the kickoff task.
29
+ - "挺快的" and other vague speed lines fail.
30
+
31
+ ## Acceptance
32
+
33
+ 1. A vague speed line, or a line missing any of the six items, fails
34
+ local validate
35
+ 2. All six present, or the no-page-performance opt-out, passes
36
+ 3. Kickoff adds an FDE-08 task before implement when page performance is
37
+ declared, and skips it on the opt-out
38
+ 4. npm test and npm run test:live pass
@@ -0,0 +1,5 @@
1
+ # Tasks — 276-fde08-perf-budget
2
+
3
+ - [x] Validate the performance budget (LCP, INP, bundle, long list, weak network, large data, or opt-out)
4
+ - [x] Inject an FDE-08 kickoff task before implement
5
+ - [x] Update hints, prompts, docs, and tests
@@ -0,0 +1,9 @@
1
+ {
2
+ "status": "accepted",
3
+ "summary": "Create mode refuses when the target project directory already exists. Open from the list or browse still works. Existing folder contents are left untouched.",
4
+ "verification": {
5
+ "L0": "node --check bin/live-repo-path.mjs bin/duaer-live.mjs web/live-dev/app.js — pass",
6
+ "L1": "npm test — pass",
7
+ "L3": "npm run test:live — pass"
8
+ }
9
+ }
@@ -0,0 +1,31 @@
1
+ # Feature Specification: Refuse reusing an existing project directory on create
2
+
3
+ ## Goal
4
+
5
+ Creating a new project must not reuse a directory that already exists.
6
+ An existing folder can only be opened from the project list or by browse-open.
7
+
8
+ ## In scope
9
+
10
+ - `/api/projects/activate` create mode refuses when the target path already exists
11
+ - Create form sends create mode; list click opens without create
12
+ - Clear Chinese error telling the user to rename or open the existing project
13
+ - Unit / smoke coverage
14
+
15
+ ## Out of scope
16
+
17
+ - Deleting or renaming old project folders
18
+ - Changing how desk sessions are keyed
19
+
20
+ ## Assumptions
21
+
22
+ - "选为当前项目" on the create form means create when the path is new
23
+ - Clicking an item under 「已有项目」 means open
24
+ - Browse into an existing folder opens it; it does not create over it
25
+
26
+ ## Acceptance
27
+
28
+ 1. Create with a folder name that already exists under the parent returns an error; disk contents are unchanged
29
+ 2. Opening the same path from the project list still works
30
+ 3. Create with a never-used name still creates the directory
31
+ 4. `npm test` and `npm run test:live` pass
@@ -0,0 +1,5 @@
1
+ # Tasks — 277-project-dir-reuse
2
+
3
+ - [x] Refuse create when the target project directory already exists
4
+ - [x] Keep open-from-list / browse-open working for existing paths
5
+ - [x] Add tests, copy, and E2E row
@@ -0,0 +1,9 @@
1
+ {
2
+ "status": "accepted",
3
+ "summary": "Create uses only a short folder name under the product parent. Absolute paths, existing folders, and duplicate project titles are refused. Open from the list or browse still works.",
4
+ "verification": {
5
+ "L0": "node --check bin/live-repo-path.mjs bin/duaer-live.mjs — pass",
6
+ "L1": "npm test — pass",
7
+ "L3": "npm run test:live — pass"
8
+ }
9
+ }
@@ -0,0 +1,27 @@
1
+ # Feature Specification: Unique create — short name under parent only
2
+
3
+ ## Goal
4
+
5
+ Create and open are separate. Creating a project only uses a short folder
6
+ name under the product parent. Absolute paths, duplicate folders, and
7
+ duplicate project titles are refused.
8
+
9
+ ## In scope
10
+
11
+ - Create mode: short name only, must have parent set, refuse if folder exists
12
+ - Create mode: refuse if an existing project already uses that path or title
13
+ - Open from list / browse still accepts absolute paths
14
+ - Copy and labels match the rule
15
+
16
+ ## Out of scope
17
+
18
+ - Moving or renaming existing project folders on disk
19
+ - Deleting stale list entries
20
+
21
+ ## Acceptance
22
+
23
+ 1. Create with `/Users/.../pcdemo` fails; asks for a short name under the parent
24
+ 2. Create with a short name that already exists on disk or in the project list fails
25
+ 3. Create with a title already used by another project fails
26
+ 4. Open from the list still works with absolute paths
27
+ 5. `npm test` and `npm run test:live` pass
@@ -0,0 +1,5 @@
1
+ # Tasks — 278-project-create-unique
2
+
3
+ - [x] Create resolves only a short name under the product parent
4
+ - [x] Refuse duplicate folder and duplicate project title on create
5
+ - [x] Update labels, hints, and tests
@@ -0,0 +1,8 @@
1
+ {
2
+ "status": "accepted",
3
+ "summary": "Corridor-aware Archify edge detours (validated bottom/top/side/gutter) plus vertical labelAt so stacked clean-flow IR renders.",
4
+ "verification": {
5
+ "commands": ["node --test test/live-archify.test.mjs"],
6
+ "result": "16 pass / 0 fail"
7
+ }
8
+ }
@@ -0,0 +1,28 @@
1
+ # Feature Specification: Architecture corridor detour for clean-flow
2
+
3
+ ## Goal
4
+
5
+ When Archify rejects edges that cross unrelated components, auto-routing
6
+ must pick a corridor whose segments stay clear of other boxes — not a
7
+ naive bottom exit through column neighbors, and not a side via whose
8
+ horizontal leg clips co-row nodes. Vertical edge labels must sit in the
9
+ gap between stacked boxes.
10
+
11
+ ## In scope
12
+
13
+ - `routeCrossingEdges` validates candidate routes (bottom/top wrap, side
14
+ corridor, gutter wrap) and applies the first clear detour
15
+ - Re-route edges that already have a bad bottom via
16
+ - Vertical edge labels get `labelAt` in the inter-box gap
17
+ - Regression test matching the speaker/editor/store failure
18
+
19
+ ## Out of scope
20
+
21
+ - Changing Archify itself
22
+ - Full force-directed relayout
23
+
24
+ ## Acceptance
25
+
26
+ 1. The reproduced IR (speaker→editor, store→gen, 初稿/定稿 labels) renders
27
+ 2. Existing same-row reverse-edge test still passes
28
+ 3. `npm test` live-archify tests pass
@@ -0,0 +1,5 @@
1
+ # Tasks — 279-arch-side-detour
2
+
3
+ - [x] Corridor-aware detour for edges that cross unrelated components
4
+ - [x] Place vertical edge labels in the gap (`labelAt`)
5
+ - [x] Regression test + docs
@@ -0,0 +1,8 @@
1
+ {
2
+ "status": "accepted",
3
+ "summary": "Bottom/top via edge labels pin labelAt on the via corridor so 改稿 no longer overlaps artifact.",
4
+ "verification": {
5
+ "commands": ["node --check bin/live-archify.mjs", "node --test test/live-archify.test.mjs"],
6
+ "result": "17 pass / 0 fail"
7
+ }
8
+ }
@@ -0,0 +1,22 @@
1
+ # Feature Specification: Bottom-via edge labels clear mid-row boxes
2
+
3
+ ## Goal
4
+
5
+ When a same-row edge detours under the floor, its label must stay on the
6
+ via (below) — not be pulled up with `labelDy: -36` onto mid-row components
7
+ such as `artifact`.
8
+
9
+ ## In scope
10
+
11
+ - `repairEdgeLabelPlacement` pins `labelAt` under bottom vias / above top vias
12
+ - Overlap nudge when an offset mid still hits a box
13
+ - Regression for speaker→revise「改稿」 overlapping artifact
14
+
15
+ ## Out of scope
16
+
17
+ - Changing Archify itself
18
+
19
+ ## Acceptance
20
+
21
+ 1. Reproduced IR renders via Archify
22
+ 2. `node --test test/live-archify.test.mjs` passes
@@ -0,0 +1,5 @@
1
+ # Tasks — 280-arch-bottom-via-label
2
+
3
+ - [x] Pin bottom/top via labels with labelAt on the via corridor
4
+ - [x] Regression test for 改稿 / artifact overlap
5
+ - [x] Docs + delivery stamp
@@ -0,0 +1,13 @@
1
+ # Feature Specification: Release 0.27.2
2
+
3
+ ## Goal
4
+
5
+ Ship 0.27.2: architecture clean-flow corridor detours, bottom-via label
6
+ placement, unique project create paths, and FDE-08 performance budget.
7
+ Promote develop to main, publish a GitHub Release, and let npm Trusted
8
+ Publishing run.
9
+
10
+ ## Acceptance
11
+
12
+ 1. `package.json` is `0.27.2`; CHANGELOG has `0.27.2` and an empty Unreleased section.
13
+ 2. Tag `v0.27.2` is on `main`; GitHub Release is published.
@@ -0,0 +1,4 @@
1
+ # Tasks — 281-release-0272
2
+
3
+ - [x] Bump package.json to 0.27.2 and move CHANGELOG Unreleased
4
+ - [ ] Promote develop → main, tag, GitHub Release
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
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
+
5
39
  ## 0.27.1 — 2026-09-21
6
40
 
7
41
  ### Feat: FDE-05 external dependency board before kickoff
@@ -67,13 +67,20 @@ import {
67
67
  externalDepsDeclaresNone,
68
68
  missingExternalDeps,
69
69
  } from "../web/live-dev/external-deps.mjs";
70
+ import {
71
+ missingPerfBudget,
72
+ perfBudgetDeclaresNone,
73
+ } from "../web/live-dev/perf-budget.mjs";
70
74
  import { allocateUniqueBranch } from "./live-worktree-name.mjs";
71
75
  import {
72
76
  ensureGitInstalled,
73
77
  CURSOR_INSTALL_CMD as TOOLING_CURSOR_INSTALL,
74
78
  } from "./live-tooling.mjs";
75
79
  import {
80
+ assertProductDirFreeForCreate,
81
+ assertProjectIdentityFreeForCreate,
76
82
  ensureProductDir,
83
+ resolveCreateProductRepoPath,
77
84
  resolveProductRepoPath,
78
85
  } from "./live-repo-path.mjs";
79
86
  import {
@@ -758,6 +765,7 @@ const SYSTEM_PROMPT = `你是「Duaer-spec FDE」需求助手。通过多轮对
758
765
  - envChecklist = 环境检查:DNS、TLS、CORS、鉴权、第三方可达须全部写明且通过;无客户联调则写「无客户联调环境」。任一未通过不得开工
759
766
  - dataPrecheck = 数据预检:字段映射、导入预检失败清单、可导出给业务清洗;无导入则写「本模块无导入」
760
767
  - externalDeps = 外部依赖:阻塞项、SLA、备用 Mock、并行路径;无第三方/ERP/SSO 则写「本模块无外部依赖」
768
+ - perfBudget = 性能预算:LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测;写「挺快的」不合格;无页面则写「本模块无页面性能要求」
761
769
  用户没提时:先根据产品形态填合理默认并写进 JSON,再在正文用一句话请用户改;不要长期留空。小项目可只有一个模块(id=main)。
762
770
  4. acceptance 必须可客观检查(打开何处、看到什么、哪条命令通过);禁止只写「更好用/更好看」。
763
771
  5. ready=true 只表示**当前 active 模块**可确认,且上述字段都已非空、验收可检查。不是整系统开工。不要催用户立刻派工。
@@ -767,7 +775,7 @@ const SYSTEM_PROMPT = `你是「Duaer-spec FDE」需求助手。通过多轮对
767
775
  - 先写对用户说的纯文本(可多行,不要 JSON;正文里不要再列一遍选项清单)
768
776
  - 然后单独一行:<<<JSON>>>
769
777
  - 再输出一个 JSON 对象(不要 markdown 围栏):
770
- {"modules":[{"id":"auth","title":"登录","status":"draft","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"..."}],"activeModuleId":"auth","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","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"]}
771
779
  说明:顶层字段与 ready 对应 activeModuleId 那一模块;modules 为完整清单(可增删改名)。`;
772
780
 
773
781
  const BUG_CHAT_PROMPT = `你是「Duaer-spec FDE」缺陷助手。用户要修 bug,不是做新功能。通过多轮对话整理成一张可派工的缺陷卡,使数字员工能复现、修复并回归。
@@ -805,6 +813,7 @@ const REVISE_CHAT_PROMPT = `你是「Duaer-spec FDE」改进对话助手。用
805
813
  - envChecklist = 若触及客户环境则更新探测结果,或写「无客户联调环境」/「同基线」
806
814
  - dataPrecheck = 若触及导入则更新映射与失败清单,或写「本模块无导入」/「同基线」
807
815
  - externalDeps = 若触及第三方/ERP/SSO 则更新阻塞项、SLA、备用 Mock、并行路径,或写「本模块无外部依赖」/「同基线」
816
+ - perfBudget = 若有页面则更新 LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测,或写「本模块无页面性能要求」/「同基线」
808
817
  3. 字段够清楚且可执行时 ready=true(确认前系统会自动校验)。缺基线/契约字段时先填合理默认或用 options 点选。
809
818
  4. 不要写代码。不要立刻派工。不要假设仓库路径。
810
819
  5. 只要问题是让用户做选择,必须在 options 填 2~5 个短选项(≤20字);界面可点选发送。禁止只让用户手打或「请回复数字」。
@@ -812,7 +821,7 @@ const REVISE_CHAT_PROMPT = `你是「Duaer-spec FDE」改进对话助手。用
812
821
  - 先写对用户说的纯文本(正文不要再列选项清单)
813
822
  - 然后单独一行:<<<JSON>>>
814
823
  - 再输出 JSON(不要 markdown 围栏):
815
- {"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","ready":false,"options":["可选A","可选B"]}`;
824
+ {"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"...","ready":false,"options":["可选A","可选B"]}`;
816
825
 
817
826
  const ARCHITECTURE_CHAT_PROMPT = `你是「Duaer-spec FDE」架构助手。需求已确认。通过多轮对话设计系统架构图,供数字员工按图开发。系统会把 JSON 自动渲染成图,用户看不到原始 JSON。
818
827
 
@@ -843,23 +852,24 @@ const ACCEPT_PROMPT = `你是「Duaer-spec FDE」需求验收官。用户即将
843
852
  7. envChecklist 是否已填:DNS、TLS、CORS、鉴权、第三方可达全部通过,或明确「无客户联调环境」;存在未通过项则 passed=false
844
853
  8. dataPrecheck 是否已填:字段映射、导入预检失败清单、可导出,或明确「本模块无导入」
845
854
  9. externalDeps 是否已填:阻塞项、SLA、备用 Mock、并行路径,或明确「本模块无外部依赖」
846
- 10. 按该验收标准做完后,用户是否有理由满意(成品可核对,而非过程叙事)
855
+ 10. perfBudget 是否已填:LCP、INP、包体、长列表虚拟滚动、弱网、大数据压测,或明确「本模块无页面性能要求」;禁止只写「挺快的」
856
+ 11. 按该验收标准做完后,用户是否有理由满意(成品可核对,而非过程叙事)
847
857
 
848
858
  规则:
849
859
  - 若小改即可通过:修订确认卡字段(尤其把 acceptance 改成可检查句子,并补全基线与契约),passed=true
850
860
  - 若缺关键信息:passed=false,issues 列出缺什么(中文,短句)
851
861
  - 不要写代码。不要假设仓库路径。不要催派工。
852
862
  - 只输出一个 JSON,不要 markdown 围栏:
853
- {"passed":false,"summary":"一句话结论","issues":["问题1"],"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"..."}`;
863
+ {"passed":false,"summary":"一句话结论","issues":["问题1"],"goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"..."}`;
854
864
 
855
- const FIX_ACCEPT_PROMPT = `你是「Duaer-spec FDE」需求修正助手。自动验收未通过,请根据 issues 修订确认卡。优先把 acceptance 改成可客观检查的句子(打开何处、看到什么、哪条命令通过),并补全 deviceMatrix / criticalPaths / exceptionCases / apiContract / envChecklist / dataPrecheck / externalDeps;不要编造用户没提过的大功能。
865
+ const FIX_ACCEPT_PROMPT = `你是「Duaer-spec FDE」需求修正助手。自动验收未通过,请根据 issues 修订确认卡。优先把 acceptance 改成可客观检查的句子(打开何处、看到什么、哪条命令通过),并补全 deviceMatrix / criticalPaths / exceptionCases / apiContract / envChecklist / dataPrecheck / externalDeps / perfBudget;不要编造用户没提过的大功能。
856
866
 
857
867
  规则:
858
- 1. 针对每条 issue 修改 goal / outOfScope / acceptance / assumptions / deviceMatrix / criticalPaths / exceptionCases / apiContract / envChecklist / dataPrecheck / externalDeps
859
- 2. 保持用户原意;缺信息时写合理、可检查的默认(如 Chrome 最近两版、手机 Safari,云测截图放 compat/,旧壳降级提示升级;主路径对齐 goal;异常态须同时写上空态、失败、权限不足、超时、重试,有接口则补错误码;无接口则写「本模块无 HTTP API」;无客户联调则 envChecklist 写「无客户联调环境」,否则 DNS、TLS、CORS、鉴权、第三方可达全部写通过;无导入则 dataPrecheck 写「本模块无导入」,否则写字段映射、导入预检失败清单、可导出;无外部依赖则 externalDeps 写「本模块无外部依赖」,否则写阻塞项、SLA、备用 Mock、并行路径),并写进 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
860
870
  3. 不要写代码。不要假设仓库路径。
861
871
  4. 只输出一个 JSON,不要 markdown 围栏:
862
- {"summary":"一句话说明改了什么","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"..."}`;
872
+ {"summary":"一句话说明改了什么","goal":"...","outOfScope":"...","acceptance":"...","assumptions":"...","deviceMatrix":"...","criticalPaths":"...","exceptionCases":"...","apiContract":"...","envChecklist":"...","dataPrecheck":"...","externalDeps":"...","perfBudget":"..."}`;
863
873
 
864
874
  const BUG_ACCEPT_PROMPT = `你是「Duaer-spec FDE」缺陷验收官。用户即将锁定缺陷卡(尚未派工)。目标是:规范缺陷描述,使数字员工能复现、修复并回归。
865
875
 
@@ -1138,6 +1148,7 @@ function parseChatResult(content) {
1138
1148
  envChecklist: flat(m.envChecklist ?? m.card?.envChecklist),
1139
1149
  dataPrecheck: flat(m.dataPrecheck ?? m.card?.dataPrecheck),
1140
1150
  externalDeps: flat(m.externalDeps ?? m.card?.externalDeps),
1151
+ perfBudget: flat(m.perfBudget ?? m.card?.perfBudget),
1141
1152
  dependsOn: Array.isArray(m.dependsOn)
1142
1153
  ? m.dependsOn.map((x) => flat(x)).filter(Boolean).slice(0, 20)
1143
1154
  : [],
@@ -1159,6 +1170,7 @@ function parseChatResult(content) {
1159
1170
  envChecklist: flat(obj.envChecklist),
1160
1171
  dataPrecheck: flat(obj.dataPrecheck),
1161
1172
  externalDeps: flat(obj.externalDeps),
1173
+ perfBudget: flat(obj.perfBudget),
1162
1174
  ready: Boolean(obj.ready),
1163
1175
  activeModuleId: flat(obj.activeModuleId) || undefined,
1164
1176
  modules,
@@ -1184,6 +1196,7 @@ function parseChatResult(content) {
1184
1196
  envChecklist: "",
1185
1197
  dataPrecheck: "",
1186
1198
  externalDeps: "",
1199
+ perfBudget: "",
1187
1200
  ready: false,
1188
1201
  options: [],
1189
1202
  };
@@ -1206,6 +1219,7 @@ function chatDoneSsePayload(parsed, { includeJsonBlock = true } = {}) {
1206
1219
  envChecklist: String(parsed?.envChecklist || ""),
1207
1220
  dataPrecheck: String(parsed?.dataPrecheck || ""),
1208
1221
  externalDeps: String(parsed?.externalDeps || ""),
1222
+ perfBudget: String(parsed?.perfBudget || ""),
1209
1223
  ready: Boolean(parsed?.ready),
1210
1224
  activeModuleId: parsed?.activeModuleId
1211
1225
  ? String(parsed.activeModuleId).slice(0, 80)
@@ -1226,6 +1240,7 @@ function chatDoneSsePayload(parsed, { includeJsonBlock = true } = {}) {
1226
1240
  envChecklist: String(m.envChecklist || "").slice(0, 4000),
1227
1241
  dataPrecheck: String(m.dataPrecheck || "").slice(0, 4000),
1228
1242
  externalDeps: String(m.externalDeps || "").slice(0, 4000),
1243
+ perfBudget: String(m.perfBudget || "").slice(0, 4000),
1229
1244
  dependsOn: Array.isArray(m.dependsOn)
1230
1245
  ? m.dependsOn.map(String).slice(0, 20)
1231
1246
  : [],
@@ -1392,6 +1407,7 @@ function parseAcceptResult(content, fallback) {
1392
1407
  envChecklist: pick("envChecklist"),
1393
1408
  dataPrecheck: pick("dataPrecheck"),
1394
1409
  externalDeps: pick("externalDeps"),
1410
+ perfBudget: pick("perfBudget"),
1395
1411
  };
1396
1412
  }
1397
1413
 
@@ -1504,6 +1520,18 @@ function localAcceptCheck(card, { kind = "feature" } = {}) {
1504
1520
  : "「外部依赖」必填(阻塞项、SLA、备用 Mock、并行路径,或写「本模块无外部依赖」)",
1505
1521
  );
1506
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
+ }
1507
1535
  }
1508
1536
  return issues;
1509
1537
  }
@@ -1526,6 +1554,7 @@ async function autoAcceptCard(cfg, card, { kind = "feature", projectContext = nu
1526
1554
  envChecklist: card.envChecklist,
1527
1555
  dataPrecheck: card.dataPrecheck,
1528
1556
  externalDeps: card.externalDeps,
1557
+ perfBudget: card.perfBudget,
1529
1558
  };
1530
1559
  }
1531
1560
  const ctxNote =
@@ -1559,6 +1588,7 @@ async function validateCardOnly(cfg, card, { kind = "feature", projectContext =
1559
1588
  envChecklist: String(card.envChecklist || "").trim(),
1560
1589
  dataPrecheck: String(card.dataPrecheck || "").trim(),
1561
1590
  externalDeps: String(card.externalDeps || "").trim(),
1591
+ perfBudget: String(card.perfBudget || "").trim(),
1562
1592
  };
1563
1593
  if (kind === "bug" && projectContext) {
1564
1594
  normalized = {
@@ -1620,6 +1650,7 @@ async function validateCardOnly(cfg, card, { kind = "feature", projectContext =
1620
1650
  envChecklist: review.envChecklist || normalized.envChecklist,
1621
1651
  dataPrecheck: review.dataPrecheck || normalized.dataPrecheck,
1622
1652
  externalDeps: review.externalDeps || normalized.externalDeps,
1653
+ perfBudget: review.perfBudget || normalized.perfBudget,
1623
1654
  };
1624
1655
  }
1625
1656
 
@@ -1670,6 +1701,7 @@ async function autoFixConfirmCard(cfg, card, issues, { kind = "feature", project
1670
1701
  envChecklist: String(obj.envChecklist || card.envChecklist || "").trim(),
1671
1702
  dataPrecheck: String(obj.dataPrecheck || card.dataPrecheck || "").trim(),
1672
1703
  externalDeps: String(obj.externalDeps || card.externalDeps || "").trim(),
1704
+ perfBudget: String(obj.perfBudget || card.perfBudget || "").trim(),
1673
1705
  };
1674
1706
  }
1675
1707
 
@@ -2516,20 +2548,53 @@ function activateProject(body = {}) {
2516
2548
  const cfg = readConfig();
2517
2549
  let raw = String(body.path || body.name || "").trim();
2518
2550
  if (!raw) {
2519
- const e = new Error("请填写目录名或绝对路径");
2551
+ const e = new Error(
2552
+ body.create === true || body.mode === "create"
2553
+ ? "请填写要新建的目录短名"
2554
+ : "请填写目录名或绝对路径",
2555
+ );
2520
2556
  e.code = "EMPTY_PATH";
2521
2557
  throw e;
2522
2558
  }
2523
2559
  const titleIn = String(body.title || "").trim();
2524
2560
  const descriptionIn = String(body.description || "").trim();
2525
- const abs = resolveProductRepoPath(raw, {
2526
- projectsRoot: cfg.projectsRoot,
2527
- });
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
+ }
2528
2593
  const ensured = ensureProductDir(abs);
2529
2594
  const existing = readRepos().find(
2530
2595
  (r) => normalizeRepoPath(r.path) === normalizeRepoPath(abs),
2531
2596
  );
2532
- const isNew = Boolean(ensured.created) || !existing;
2597
+ const isNew = Boolean(ensured.created) || (create && !existing);
2533
2598
  const needMeta = Boolean(body.requireMeta) || isNew;
2534
2599
  if (needMeta && !titleIn) {
2535
2600
  const e = new Error("请填写项目名称");
@@ -2546,12 +2611,17 @@ function activateProject(body = {}) {
2546
2611
  exact: true,
2547
2612
  ensureDuaer: false,
2548
2613
  });
2549
- const savedTitle =
2550
- titleIn ||
2551
- existing?.title ||
2552
- existing?.name ||
2553
- path.basename(probe.path);
2554
- 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 || "";
2555
2625
  rememberRepo(probe.path, {
2556
2626
  title: savedTitle,
2557
2627
  name: savedTitle,
@@ -6485,6 +6555,7 @@ async function handleApi(req, res) {
6485
6555
  envChecklist: body.envChecklist,
6486
6556
  dataPrecheck: body.dataPrecheck,
6487
6557
  externalDeps: body.externalDeps,
6558
+ perfBudget: body.perfBudget,
6488
6559
  });
6489
6560
  const deskKind =
6490
6561
  String(body.deskKind || body.kind || "").trim() === "bug"
@@ -6541,6 +6612,7 @@ async function handleApi(req, res) {
6541
6612
  envChecklist: body.envChecklist,
6542
6613
  dataPrecheck: body.dataPrecheck,
6543
6614
  externalDeps: body.externalDeps,
6615
+ perfBudget: body.perfBudget,
6544
6616
  });
6545
6617
  const issues = Array.isArray(body.issues) ? body.issues : [];
6546
6618
  const deskKind =
@@ -6605,6 +6677,7 @@ async function handleApi(req, res) {
6605
6677
  envChecklist: body.envChecklist,
6606
6678
  dataPrecheck: body.dataPrecheck,
6607
6679
  externalDeps: body.externalDeps,
6680
+ perfBudget: body.perfBudget,
6608
6681
  });
6609
6682
  if (!card.goal || !card.acceptance) {
6610
6683
  send(res, 400, { error: "goal and acceptance are required" });
@@ -6717,6 +6790,7 @@ async function handleApi(req, res) {
6717
6790
  envChecklist: body.envChecklist,
6718
6791
  dataPrecheck: body.dataPrecheck,
6719
6792
  externalDeps: body.externalDeps,
6793
+ perfBudget: body.perfBudget,
6720
6794
  });
6721
6795
  const issues = Array.isArray(body.issues) ? body.issues : [];
6722
6796
  if (!card.goal && !card.acceptance) {