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
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { existsSync, mkdirSync, statSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
- import { basename, isAbsolute, join, parse, resolve } from "node:path";
7
+ import { basename, isAbsolute, parse, resolve } from "node:path";
8
8
 
9
9
  /**
10
10
  * @param {string} repoPath
@@ -53,9 +53,98 @@ export function resolveProductRepoPath(repoPath, opts = {}) {
53
53
  return resolve(parent, raw);
54
54
  }
55
55
 
56
+ /**
57
+ * Create mode: only a single short folder name under the product parent.
58
+ * Absolute paths and nested paths are refused so parent + name stay one story.
59
+ * @param {string} folderName
60
+ * @param {{ projectsRoot?: string, home?: string }} [opts]
61
+ */
62
+ export function resolveCreateProductRepoPath(folderName, opts = {}) {
63
+ const raw = String(folderName || "")
64
+ .trim()
65
+ .replace(/[\\/]+$/, "");
66
+ if (!raw) {
67
+ const e = new Error("请填写要新建的目录短名");
68
+ e.code = "EMPTY_PATH";
69
+ throw e;
70
+ }
71
+ if (isAbsolute(raw) || /[\\/]/.test(raw) || raw === "." || raw === "..") {
72
+ const e = new Error(
73
+ "新建只能填目录短名(如 my-app),不要填绝对路径。目录将建在产品父目录下;已有项目请从下方列表打开或点「浏览…」",
74
+ );
75
+ e.code = "NEED_SHORT_NAME";
76
+ throw e;
77
+ }
78
+ const projectsRoot = String(opts.projectsRoot || "").trim();
79
+ if (!projectsRoot) {
80
+ const e = new Error("新建前请先选择产品父目录");
81
+ e.code = "NEED_PROJECTS_ROOT";
82
+ throw e;
83
+ }
84
+ return resolveProductRepoPath(raw, opts);
85
+ }
86
+
87
+ /**
88
+ * Create mode must not reuse an on-disk folder (would clobber the prior project).
89
+ * @param {string} absPath
90
+ */
91
+ export function assertProductDirFreeForCreate(absPath) {
92
+ const abs = resolve(String(absPath || ""));
93
+ if (!abs) {
94
+ const e = new Error("请填写仓库绝对路径,或项目名(需先设置产品父目录)");
95
+ e.code = "EMPTY_PATH";
96
+ throw e;
97
+ }
98
+ if (existsSync(abs)) {
99
+ const e = new Error(
100
+ `项目目录已存在:${abs}。请换一个目录名,或从「已有项目」打开。`,
101
+ );
102
+ e.code = "PATH_EXISTS";
103
+ e.path = abs;
104
+ throw e;
105
+ }
106
+ return abs;
107
+ }
108
+
109
+ /**
110
+ * Create mode must not reuse a registered project path or display title.
111
+ * @param {{ path: string, title?: string }} want
112
+ * @param {Array<{ path?: string, title?: string, name?: string }>} repos
113
+ */
114
+ export function assertProjectIdentityFreeForCreate(want, repos = []) {
115
+ const abs = resolve(String(want?.path || ""));
116
+ const title = String(want?.title || "")
117
+ .trim()
118
+ .toLowerCase();
119
+ const list = Array.isArray(repos) ? repos : [];
120
+ for (const r of list) {
121
+ const p = resolve(String(r?.path || ""));
122
+ if (p && abs && p === abs) {
123
+ const e = new Error(
124
+ `项目已存在:${abs}。请换一个目录名,或从「已有项目」打开。`,
125
+ );
126
+ e.code = "PROJECT_EXISTS";
127
+ e.path = abs;
128
+ throw e;
129
+ }
130
+ const other = String(r?.title || r?.name || "")
131
+ .trim()
132
+ .toLowerCase();
133
+ if (title && other && title === other) {
134
+ const e = new Error(
135
+ `已有同名项目「${r.title || r.name}」。请换一个项目名称,或从「已有项目」打开。`,
136
+ );
137
+ e.code = "TITLE_EXISTS";
138
+ e.path = p || undefined;
139
+ throw e;
140
+ }
141
+ }
142
+ return abs;
143
+ }
144
+
56
145
  /**
57
146
  * Create missing directory (recursive). Refuses FS root and $HOME itself.
58
- * @returns {{ path: string, created: boolean }}
147
+ * @returns {{ path: string, created: boolean, name?: string }}
59
148
  */
60
149
  export function ensureProductDir(absPath, opts = {}) {
61
150
  const abs = resolve(String(absPath || ""));
@@ -140,6 +140,22 @@ develop is green / accepted for release
140
140
  Never merge a random `feat/*` straight into `main` for a normal feature.
141
141
  Features land on **`develop` first**, then ride a promotion to **`main`**.
142
142
 
143
+ ### Version number (fast iteration)
144
+
145
+ This repo ships often. The default bump is the **patch** (the last number):
146
+
147
+ ```text
148
+ 0.27.0 → 0.27.1 → 0.27.2 → …
149
+ ```
150
+
151
+ The next release after `0.27.1` is **`0.27.2`**. Do not jump to `0.28.0` just
152
+ because features landed. Bump the minor or major only when the user asks for
153
+ that cut, or when the change breaks adopters (`duaer init` / public CLI).
154
+
155
+ Set `package.json` `version`, move `CHANGELOG.md` `Unreleased` under that
156
+ version, tag `vX.Y.Z`, and publish the GitHub Release (npm Trusted Publishing
157
+ follows the release).
158
+
143
159
  ---
144
160
 
145
161
  ## 4. Flows by problem type
@@ -176,6 +176,15 @@ the table for their app.
176
176
  | E2E-204 | FDE-02: feature confirm requires apiContract; path injects pool tasks; no-HTTP opt-out skips pool tasks | `npm test` + `npm run test:live` |
177
177
  | E2E-205 | Desk refresh restores device matrix, critical paths, exception cases, and API contract on the confirm card | `npm test` + `npm run test:live` |
178
178
  | E2E-206 | FDE-07: feature exception cases must cover empty, failure, permission, timeout, and retry; a contract path also needs an error code; kickoff adds UAT tasks | `npm test` + `npm run test:live` |
179
+ | E2E-207 | FDE-04: device matrix needs two named targets, screenshot or cloud evidence, and a fallback; kickoff adds a compat task | `npm test` + `npm run test:live` |
180
+ | E2E-208 | FDE-03: environment checklist needs DNS, TLS, CORS, auth, and third-party reachability passed, or a no-customer opt-out; a failed probe blocks confirm; kickoff adds a probe task before implement | `npm test` + `npm run test:live` |
181
+ | E2E-209 | FDE-06: data precheck needs a field mapping, an import failure list, and an export, or a no-import opt-out; kickoff adds the precheck before implement | `npm test` + `npm run test:live` |
182
+ | E2E-210 | FDE-05: external dependencies need a blocker, SLA, backup mock, and parallel path, or a no-dependency opt-out; kickoff adds the board before implement | `npm test` + `npm run test:live` |
183
+ | E2E-211 | FDE-08: performance budget needs 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 adds the budget before implement | `npm test` + `npm run test:live` |
184
+ | E2E-212 | Creating a new project refuses when the target directory already exists; open from the project list still works | `npm test` + `npm run test:live` |
185
+ | E2E-213 | Create project accepts only a short folder name under the parent; absolute paths, duplicate folders, and duplicate titles are refused | `npm test` + `npm run test:live` |
186
+ | E2E-214 | Architecture edges that would cross unrelated boxes get a clear corridor via; vertical labels use gap `labelAt`; Archify render succeeds | `npm test` |
187
+ | E2E-215 | Bottom-via edge labels stay under the floor (`labelAt` on via) so they do not overlap mid-row boxes (e.g. 改稿 / artifact) | `npm test` |
179
188
 
180
189
  | E2E-156 | Accepted chat bubble preview URL is a clickable `<a>` (http(s) or /api/…) | `npm test` + `npm run test:live` |
181
190
  | E2E-157 | `http://host/api/result/…` chat links render as one clean `<a>` (no nested HTML garble) | `npm test` |
@@ -417,4 +426,5 @@ the table for their app.
417
426
  | E2E-204 | `266-fde02-api-contract` | API contract gate + pool tasks |
418
427
  | E2E-205 | `267-baseline-restore` | refresh keeps baseline confirm fields |
419
428
  | E2E-206 | `268-fde07-uat` | UAT pack on exception cases + kickoff |
429
+ | E2E-207 | `271-fde04-compat` | browser matrix evidence + kickoff |
420
430
 
@@ -80,7 +80,32 @@ registry linking into the requirements document (no duplicate full cards).
80
80
  Exception cases are an FDE-07 UAT pack (empty, failure, permission, timeout,
81
81
  retry). A declared contract path also requires an error code or HTTP status
82
82
  in that text. Kickoff adds a UAT task per confirmed module.
83
- Refreshing the desk restores those four fields from the saved session.
83
+ The device matrix is an FDE-04 compat pack: at least two named browsers or
84
+ domestic clients, screenshot or cloud evidence, and a polyfill or fallback.
85
+ Kickoff adds a compat-evidence task per confirmed module.
86
+ The environment checklist is an FDE-03 probe pack: DNS, TLS, CORS, auth,
87
+ and third-party reachability must all be recorded as passed, or the card
88
+ states that there is no customer environment. A failed probe blocks
89
+ confirm. Kickoff adds an environment-probe task before implement when a
90
+ customer environment is declared. The desk does not scan that network.
91
+ The data precheck is an FDE-06 import pack: a field mapping, an import
92
+ failure list, and an export the business can clean. Or the card states
93
+ that this module has no import. Kickoff adds that precheck before
94
+ implement, after the environment probe when both exist. The desk does
95
+ not scan the customer database.
96
+ External dependencies are an FDE-05 board: a blocker, an SLA, a backup
97
+ mock, and a parallel path. Or the card states that this module has no
98
+ external dependency. Kickoff adds that board before implement, after
99
+ the environment probe and data precheck when those exist. The desk does
100
+ not call the vendor.
101
+ The performance budget is an FDE-08 gate: LCP, INP, bundle size,
102
+ virtualized long lists, a weak-network check, and a large-data check.
103
+ A vague speed line such as "pretty fast" fails. Or the card states that
104
+ this module has no page performance requirement. Kickoff adds that
105
+ budget before implement, after the environment probe, data precheck, and
106
+ external-dependency board when those exist. The desk does not run a
107
+ customer performance lab.
108
+ Refreshing the desk restores those baseline fields from the saved session.
84
109
  3. Confirm locks **that module only**. It does **not** call `writeBrief` and does
85
110
  **not** start Terminal workers.
86
111
  4. When all relevant modules are confirmed, the desk unlocks architecture
@@ -93,9 +118,14 @@ registry linking into the requirements document (no duplicate full cards).
93
118
  Architecture chat may claim the diagram is ready before IR arrives; if the
94
119
  middle panel has no diagram, the **same** chat bubble is rewritten to offer
95
120
  Regenerate diagram (it must not leave a false “diagram ready” line).
96
- Before Archify deliver, `routeCrossingEdges` adds a bottom `via` detour for
97
- long same-row edges that would pass through unrelated components
98
- (`clean-flow/edge-through-node`).
121
+ Before Archify deliver, `routeCrossingEdges` picks a clear corridor
122
+ (`via` + sides) for edges that would pass through unrelated components:
123
+ same-row edges prefer a bottom/top wrap; stacked or diagonal edges try
124
+ gutters and outer sides, keeping every segment clear of other boxes
125
+ (`clean-flow/edge-through-node`). Edge labels on bottom/top vias use
126
+ `labelAt` on the via corridor (not a negative `labelDy` that pulls them
127
+ into mid-row boxes). Vertical stacked edges use `labelAt` in the gap
128
+ between boxes.
99
129
 
100
130
  ## Bug dispatch (defect path)
101
131
 
@@ -65,6 +65,8 @@ handling. They cannot be relaxed without explicit human override.
65
65
  delete the short branch.
66
66
  - Push only when the user explicitly requests remote publishing.
67
67
  - Promote **`develop` → `main`** only when the user explicitly asks to go online.
68
+ - Fast iteration: the default version bump is the patch. Next after `0.27.1`
69
+ is `0.27.2`. Do not bump the minor on every ship.
68
70
 
69
71
  Full matrix by issue type:
70
72
  [branching-and-release](branching-and-release.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "duaer-spec",
3
- "version": "0.27.0",
3
+ "version": "0.27.2",
4
4
  "description": "Norms → satisfactory delivery: digital employees for Cursor and Claude Code — talk in plain language, no Spec workflow required",
5
5
  "type": "module",
6
6
  "bin": {
@@ -257,6 +257,18 @@ const el = {
257
257
  apiContract: document.getElementById("apiContract"),
258
258
  apiContractView: document.getElementById("apiContractView"),
259
259
  apiContractField: document.getElementById("apiContractField"),
260
+ envChecklist: document.getElementById("envChecklist"),
261
+ envChecklistView: document.getElementById("envChecklistView"),
262
+ envChecklistField: document.getElementById("envChecklistField"),
263
+ dataPrecheck: document.getElementById("dataPrecheck"),
264
+ dataPrecheckView: document.getElementById("dataPrecheckView"),
265
+ dataPrecheckField: document.getElementById("dataPrecheckField"),
266
+ externalDeps: document.getElementById("externalDeps"),
267
+ externalDepsView: document.getElementById("externalDepsView"),
268
+ externalDepsField: document.getElementById("externalDepsField"),
269
+ perfBudget: document.getElementById("perfBudget"),
270
+ perfBudgetView: document.getElementById("perfBudgetView"),
271
+ perfBudgetField: document.getElementById("perfBudgetField"),
260
272
  lblDevice: document.getElementById("lblDevice"),
261
273
  lblPaths: document.getElementById("lblPaths"),
262
274
  lblExceptions: document.getElementById("lblExceptions"),
@@ -937,6 +949,10 @@ function cardValues() {
937
949
  criticalPaths: (el.criticalPaths?.value || "").trim(),
938
950
  exceptionCases: (el.exceptionCases?.value || "").trim(),
939
951
  apiContract: (el.apiContract?.value || "").trim(),
952
+ envChecklist: (el.envChecklist?.value || "").trim(),
953
+ dataPrecheck: (el.dataPrecheck?.value || "").trim(),
954
+ externalDeps: (el.externalDeps?.value || "").trim(),
955
+ perfBudget: (el.perfBudget?.value || "").trim(),
940
956
  };
941
957
  }
942
958
 
@@ -986,6 +1002,10 @@ function applyActiveModuleToFields() {
986
1002
  if (el.criticalPaths) el.criticalPaths.value = c.criticalPaths || "";
987
1003
  if (el.exceptionCases) el.exceptionCases.value = c.exceptionCases || "";
988
1004
  if (el.apiContract) el.apiContract.value = c.apiContract || "";
1005
+ if (el.envChecklist) el.envChecklist.value = c.envChecklist || "";
1006
+ if (el.dataPrecheck) el.dataPrecheck.value = c.dataPrecheck || "";
1007
+ if (el.externalDeps) el.externalDeps.value = c.externalDeps || "";
1008
+ if (el.perfBudget) el.perfBudget.value = c.perfBudget || "";
989
1009
  syncReqSections();
990
1010
  }
991
1011
 
@@ -1057,6 +1077,10 @@ function mergeModulesFromChatPayload(data) {
1057
1077
  criticalPaths: String(data.criticalPaths || data.modules?.[0]?.card?.criticalPaths || activeModule()?.card?.criticalPaths || ""),
1058
1078
  exceptionCases: String(data.exceptionCases || data.modules?.[0]?.card?.exceptionCases || activeModule()?.card?.exceptionCases || ""),
1059
1079
  apiContract: String(data.apiContract || data.modules?.[0]?.card?.apiContract || activeModule()?.card?.apiContract || ""),
1080
+ envChecklist: String(data.envChecklist || data.modules?.[0]?.card?.envChecklist || activeModule()?.card?.envChecklist || ""),
1081
+ dataPrecheck: String(data.dataPrecheck || data.modules?.[0]?.card?.dataPrecheck || activeModule()?.card?.dataPrecheck || ""),
1082
+ externalDeps: String(data.externalDeps || data.modules?.[0]?.card?.externalDeps || activeModule()?.card?.externalDeps || ""),
1083
+ perfBudget: String(data.perfBudget || data.modules?.[0]?.card?.perfBudget || activeModule()?.card?.perfBudget || ""),
1060
1084
  };
1061
1085
  const prev = activeModule();
1062
1086
  state.modules = [
@@ -1118,6 +1142,30 @@ function mergeModulesFromChatPayload(data) {
1118
1142
  old?.card?.apiContract ||
1119
1143
  "",
1120
1144
  ),
1145
+ envChecklist: String(
1146
+ raw.envChecklist ||
1147
+ raw.card?.envChecklist ||
1148
+ old?.card?.envChecklist ||
1149
+ "",
1150
+ ),
1151
+ dataPrecheck: String(
1152
+ raw.dataPrecheck ||
1153
+ raw.card?.dataPrecheck ||
1154
+ old?.card?.dataPrecheck ||
1155
+ "",
1156
+ ),
1157
+ externalDeps: String(
1158
+ raw.externalDeps ||
1159
+ raw.card?.externalDeps ||
1160
+ old?.card?.externalDeps ||
1161
+ "",
1162
+ ),
1163
+ perfBudget: String(
1164
+ raw.perfBudget ||
1165
+ raw.card?.perfBudget ||
1166
+ old?.card?.perfBudget ||
1167
+ "",
1168
+ ),
1121
1169
  };
1122
1170
  if (old?.status === "confirmed") {
1123
1171
  byId.set(id, {
@@ -1158,6 +1206,10 @@ function mergeModulesFromChatPayload(data) {
1158
1206
  if (data.criticalPaths) m.card.criticalPaths = data.criticalPaths;
1159
1207
  if (data.exceptionCases) m.card.exceptionCases = data.exceptionCases;
1160
1208
  if (data.apiContract) m.card.apiContract = data.apiContract;
1209
+ if (data.envChecklist) m.card.envChecklist = data.envChecklist;
1210
+ if (data.dataPrecheck) m.card.dataPrecheck = data.dataPrecheck;
1211
+ if (data.externalDeps) m.card.externalDeps = data.externalDeps;
1212
+ if (data.perfBudget) m.card.perfBudget = data.perfBudget;
1161
1213
  if (data.ready) m.status = "ready";
1162
1214
  }
1163
1215
  applyActiveModuleToFields();
@@ -1961,6 +2013,10 @@ function setConfirmFieldsReadonly(ro) {
1961
2013
  "criticalPaths",
1962
2014
  "exceptionCases",
1963
2015
  "apiContract",
2016
+ "envChecklist",
2017
+ "dataPrecheck",
2018
+ "externalDeps",
2019
+ "perfBudget",
1964
2020
  ]) {
1965
2021
  if (el[id]) el[id].readOnly = Boolean(ro);
1966
2022
  }
@@ -1985,6 +2041,10 @@ function restoreConfirmCardFromOriginal() {
1985
2041
  if (el.criticalPaths) el.criticalPaths.value = c.criticalPaths || "";
1986
2042
  if (el.exceptionCases) el.exceptionCases.value = c.exceptionCases || "";
1987
2043
  if (el.apiContract) el.apiContract.value = c.apiContract || "";
2044
+ if (el.envChecklist) el.envChecklist.value = c.envChecklist || "";
2045
+ if (el.dataPrecheck) el.dataPrecheck.value = c.dataPrecheck || "";
2046
+ if (el.externalDeps) el.externalDeps.value = c.externalDeps || "";
2047
+ if (el.perfBudget) el.perfBudget.value = c.perfBudget || "";
1988
2048
  syncReqSections();
1989
2049
  }
1990
2050
 
@@ -2002,6 +2062,10 @@ function applyConfirmCardChrome() {
2002
2062
  if (el.criticalPathsField) el.criticalPathsField.hidden = bug;
2003
2063
  if (el.exceptionCasesField) el.exceptionCasesField.hidden = bug;
2004
2064
  if (el.apiContractField) el.apiContractField.hidden = bug;
2065
+ if (el.envChecklistField) el.envChecklistField.hidden = bug;
2066
+ if (el.dataPrecheckField) el.dataPrecheckField.hidden = bug;
2067
+ if (el.externalDepsField) el.externalDepsField.hidden = bug;
2068
+ if (el.perfBudgetField) el.perfBudgetField.hidden = bug;
2005
2069
  if (el.confirm && !modulesAllConfirmedLocal()) {
2006
2070
  el.confirm.textContent = t(bug ? "card.bugConfirm" : "card.confirm");
2007
2071
  }
@@ -2212,6 +2276,10 @@ function cardFingerprint(v) {
2212
2276
  criticalPaths: String(v.criticalPaths || "").trim(),
2213
2277
  exceptionCases: String(v.exceptionCases || "").trim(),
2214
2278
  apiContract: String(v.apiContract || "").trim(),
2279
+ envChecklist: String(v.envChecklist || "").trim(),
2280
+ dataPrecheck: String(v.dataPrecheck || "").trim(),
2281
+ externalDeps: String(v.externalDeps || "").trim(),
2282
+ perfBudget: String(v.perfBudget || "").trim(),
2215
2283
  });
2216
2284
  }
2217
2285
 
@@ -2312,7 +2380,7 @@ function syncAutoHandleButtons() {
2312
2380
  function confirmFieldsOk(v = cardValues()) {
2313
2381
  if (!v.goal || !v.acceptance) return false;
2314
2382
  if (state.deskKind === "bug") return true;
2315
- return Boolean(v.deviceMatrix && v.criticalPaths && v.exceptionCases && v.apiContract);
2383
+ return Boolean(v.deviceMatrix && v.criticalPaths && v.exceptionCases && v.apiContract && v.envChecklist && v.dataPrecheck && v.externalDeps && v.perfBudget);
2316
2384
  }
2317
2385
 
2318
2386
  function validationAllowsSend(kind) {
@@ -2640,7 +2708,7 @@ function applyCardChrome() {
2640
2708
  syncConfirmEnabled();
2641
2709
  }
2642
2710
 
2643
- ["goal", "outOfScope", "acceptance", "assumptions", "deviceMatrix", "criticalPaths", "exceptionCases", "apiContract"].forEach((id) => {
2711
+ ["goal", "outOfScope", "acceptance", "assumptions", "deviceMatrix", "criticalPaths", "exceptionCases", "apiContract", "envChecklist", "dataPrecheck", "externalDeps", "perfBudget"].forEach((id) => {
2644
2712
  if (!el[id]) return;
2645
2713
  el[id].addEventListener("input", () => {
2646
2714
  autoGrowTextarea(el[id]);
@@ -2975,6 +3043,10 @@ function applySavedCardFields(card, reviseCard) {
2975
3043
  if (el.criticalPaths) el.criticalPaths.value = c.criticalPaths || "";
2976
3044
  if (el.exceptionCases) el.exceptionCases.value = c.exceptionCases || "";
2977
3045
  if (el.apiContract) el.apiContract.value = c.apiContract || "";
3046
+ if (el.envChecklist) el.envChecklist.value = c.envChecklist || "";
3047
+ if (el.dataPrecheck) el.dataPrecheck.value = c.dataPrecheck || "";
3048
+ if (el.externalDeps) el.externalDeps.value = c.externalDeps || "";
3049
+ if (el.perfBudget) el.perfBudget.value = c.perfBudget || "";
2978
3050
  const r = reviseCard || {};
2979
3051
  if (el.revGoal) el.revGoal.value = r.goal || "";
2980
3052
  if (el.revOut) el.revOut.value = r.outOfScope || "";
@@ -4816,6 +4888,10 @@ async function applyConfirmSuccess(data) {
4816
4888
  criticalPaths: String(m.card?.criticalPaths || m.criticalPaths || ""),
4817
4889
  exceptionCases: String(m.card?.exceptionCases || m.exceptionCases || ""),
4818
4890
  apiContract: String(m.card?.apiContract || m.apiContract || ""),
4891
+ envChecklist: String(m.card?.envChecklist || m.envChecklist || ""),
4892
+ dataPrecheck: String(m.card?.dataPrecheck || m.dataPrecheck || ""),
4893
+ externalDeps: String(m.card?.externalDeps || m.externalDeps || ""),
4894
+ perfBudget: String(m.card?.perfBudget || m.perfBudget || ""),
4819
4895
  },
4820
4896
  dependsOn: Array.isArray(m.dependsOn) ? m.dependsOn.map(String) : [],
4821
4897
  }));
@@ -4836,6 +4912,10 @@ async function applyConfirmSuccess(data) {
4836
4912
  criticalPaths: data.card.criticalPaths || "",
4837
4913
  exceptionCases: data.card.exceptionCases || "",
4838
4914
  apiContract: data.card.apiContract || "",
4915
+ envChecklist: data.card.envChecklist || "",
4916
+ dataPrecheck: data.card.dataPrecheck || "",
4917
+ externalDeps: data.card.externalDeps || "",
4918
+ perfBudget: data.card.perfBudget || "",
4839
4919
  };
4840
4920
  }
4841
4921
  applyActiveModuleToFields();
@@ -6237,6 +6317,10 @@ const REQ_FIELD_PAIRS = [
6237
6317
  { ta: "criticalPaths", view: "criticalPathsView", emptyKey: "card.pathsPh" },
6238
6318
  { ta: "exceptionCases", view: "exceptionCasesView", emptyKey: "card.exceptionsPh" },
6239
6319
  { ta: "apiContract", view: "apiContractView", emptyKey: "card.apiContractPh" },
6320
+ { ta: "envChecklist", view: "envChecklistView", emptyKey: "card.envChecklistPh" },
6321
+ { ta: "dataPrecheck", view: "dataPrecheckView", emptyKey: "card.dataPrecheckPh" },
6322
+ { ta: "externalDeps", view: "externalDepsView", emptyKey: "card.externalDepsPh" },
6323
+ { ta: "perfBudget", view: "perfBudgetView", emptyKey: "card.perfBudgetPh" },
6240
6324
  { ta: "revGoal", view: "revGoalView", emptyKey: "revise.goalPh" },
6241
6325
  { ta: "revOut", view: "revOutView", emptyKey: "revise.outPh" },
6242
6326
  { ta: "revAccept", view: "revAcceptView", emptyKey: "revise.acceptPh" },
@@ -6263,6 +6347,11 @@ function isReqReadonly(taId) {
6263
6347
  "deviceMatrix",
6264
6348
  "criticalPaths",
6265
6349
  "exceptionCases",
6350
+ "apiContract",
6351
+ "envChecklist",
6352
+ "dataPrecheck",
6353
+ "externalDeps",
6354
+ "perfBudget",
6266
6355
  ].includes(taId)
6267
6356
  ) {
6268
6357
  return Boolean(state.locked || el[taId]?.readOnly);
@@ -8820,6 +8909,7 @@ async function activateProjectPath(pathOrName, meta = {}) {
8820
8909
  return;
8821
8910
  }
8822
8911
  const requireMeta = meta.requireMeta !== false;
8912
+ const create = meta.create === true;
8823
8913
  const title = String(meta.title ?? "").trim();
8824
8914
  const description = String(meta.description ?? "").trim();
8825
8915
  if (requireMeta) {
@@ -8841,6 +8931,7 @@ async function activateProjectPath(pathOrName, meta = {}) {
8841
8931
  if (requireMeta || title) body.title = title;
8842
8932
  if (requireMeta || description) body.description = description;
8843
8933
  if (requireMeta) body.requireMeta = true;
8934
+ if (create) body.create = true;
8844
8935
  const res = await fetch("/api/projects/activate", {
8845
8936
  method: "POST",
8846
8937
  headers: { "content-type": "application/json" },
@@ -9085,6 +9176,7 @@ el.projectActivate?.addEventListener("click", () => {
9085
9176
  title: (el.projectTitle?.value || "").trim(),
9086
9177
  description: (el.projectDescription?.value || "").trim(),
9087
9178
  requireMeta: true,
9179
+ create: true,
9088
9180
  });
9089
9181
  });
9090
9182
 
@@ -9113,6 +9205,7 @@ el.projectBrowse?.addEventListener("click", async () => {
9113
9205
  title,
9114
9206
  description,
9115
9207
  requireMeta: true,
9208
+ create: false,
9116
9209
  });
9117
9210
  }
9118
9211
  } catch (err) {
@@ -54,6 +54,10 @@ export function baselineFingerprint(modules) {
54
54
  criticalPaths: String(m.card?.criticalPaths || "").trim(),
55
55
  exceptionCases: String(m.card?.exceptionCases || "").trim(),
56
56
  apiContract: String(m.card?.apiContract || "").trim(),
57
+ envChecklist: String(m.card?.envChecklist || "").trim(),
58
+ dataPrecheck: String(m.card?.dataPrecheck || "").trim(),
59
+ externalDeps: String(m.card?.externalDeps || "").trim(),
60
+ perfBudget: String(m.card?.perfBudget || "").trim(),
57
61
  }))
58
62
  .sort((a, b) => a.id.localeCompare(b.id));
59
63
  return JSON.stringify(confirmed);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * FDE-04: a browser matrix is not done until it names real targets,
3
+ * evidence, and a fallback.
4
+ */
5
+
6
+ const NAMED_TARGETS = [
7
+ /chrome/i,
8
+ /safari/i,
9
+ /firefox/i,
10
+ /\bedge\b/i,
11
+ /\bie\s*11\b|\bie11\b|\binternet explorer\b/i,
12
+ /麒麟|统信|\buos\b|360|qq\s*浏览器|微信|鸿蒙|android|\bios\b|iphone|国产/i,
13
+ /手机/,
14
+ ];
15
+
16
+ function namedTargetCount(text) {
17
+ const t = String(text || "");
18
+ return NAMED_TARGETS.filter((re) => re.test(t)).length;
19
+ }
20
+
21
+ export function missingCompatMatrix(text) {
22
+ const t = String(text || "").trim();
23
+ const missing = [];
24
+ const vagueOnly =
25
+ /^(主流浏览器|现代浏览器|主流终端|modern browsers|all browsers)[.。!!\s]*$/i.test(
26
+ t,
27
+ );
28
+ if (vagueOnly || namedTargetCount(t) < 2) {
29
+ missing.push("至少两个具体浏览器或国产终端");
30
+ }
31
+ if (!/截图|录屏|云测|真机|证据|screenshot|recording|browserstack|compat\//i.test(t)) {
32
+ missing.push("截图/录屏或云测证据");
33
+ }
34
+ if (!/降级|polyfill|不支持|fallback|垫片|提示升级|upgrade notice/i.test(t)) {
35
+ missing.push("降级或 Polyfill");
36
+ }
37
+ return missing;
38
+ }
@@ -15,5 +15,9 @@ export function restoreConfirmCard(source) {
15
15
  criticalPaths: pick("criticalPaths"),
16
16
  exceptionCases: pick("exceptionCases"),
17
17
  apiContract: pick("apiContract"),
18
+ envChecklist: pick("envChecklist"),
19
+ dataPrecheck: pick("dataPrecheck"),
20
+ externalDeps: pick("externalDeps"),
21
+ perfBudget: pick("perfBudget"),
18
22
  };
19
23
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * FDE-06: dirty rows and bad field maps must not reach the page.
3
+ * The confirm card records a mapping, an import precheck failure list,
4
+ * and that the list can be exported for cleanup. The desk does not scan
5
+ * the customer database.
6
+ */
7
+
8
+ const PARTS = [
9
+ { label: "字段映射", re: /字段映射|欄位映射|映射表|field mapping/i },
10
+ { label: "导入预检失败清单", re: /预检|預檢|precheck|失败清单|失敗清單|failure list/i },
11
+ { label: "可导出", re: /导出|導出|匯出|\bexport\b/i },
12
+ ];
13
+
14
+ export function dataPrecheckDeclaresNoImport(text) {
15
+ return /本模块无导入|本模組無導入|无数据导入|無資料匯入|無數據導入|no import/i.test(
16
+ String(text || ""),
17
+ );
18
+ }
19
+
20
+ export function missingDataPrecheck(text) {
21
+ const t = String(text || "").trim();
22
+ if (dataPrecheckDeclaresNoImport(t)) return [];
23
+ const missing = [];
24
+ for (const part of PARTS) {
25
+ if (!part.re.test(t)) missing.push(part.label);
26
+ }
27
+ return missing;
28
+ }
29
+
30
+ /** Kickoff schedules the precheck only when an import was declared. */
31
+ export function dataPrecheckNeedsTask(text) {
32
+ const t = String(text || "").trim();
33
+ if (!t || dataPrecheckDeclaresNoImport(t)) return false;
34
+ return true;
35
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * FDE-03: joint debug does not start until the customer environment
3
+ * checklist records DNS, TLS, CORS, auth, and third-party reachability
4
+ * as passed. The desk does not scan the customer network.
5
+ */
6
+
7
+ const PROBES = [
8
+ { label: "DNS", re: /\bdns\b|域名解析/i },
9
+ { label: "TLS", re: /\btls\b|证书|憑證|\bhttps\b/i },
10
+ { label: "CORS", re: /\bcors\b/i },
11
+ { label: "鉴权", re: /鉴权|鑑權|认证|認證|\bauth(?:entication|n|z)?\b|\bsso\b|登录态|登入態/i },
12
+ { label: "第三方可达", re: /第三方|third[- ]party/i },
13
+ ];
14
+
15
+ const FAIL_RE = /失败|失敗|不通|未通过|未通過|不可达|不可達|blocked|\bfail(?:ed|ure)?\b/i;
16
+ const PASS_RE = /通过|通過|已探测|已探測|全绿|全綠|\bpass(?:ed)?\b|reachable/i;
17
+
18
+ export function envChecklistDeclaresNoCustomer(text) {
19
+ return /无客户联调|無客戶聯調|无客户环境|無客戶環境|no customer environment/i.test(
20
+ String(text || ""),
21
+ );
22
+ }
23
+
24
+ export function missingEnvChecklist(text) {
25
+ const t = String(text || "").trim();
26
+ if (envChecklistDeclaresNoCustomer(t)) return [];
27
+ const missing = [];
28
+ for (const probe of PROBES) {
29
+ if (!probe.re.test(t)) missing.push(probe.label);
30
+ }
31
+ if (FAIL_RE.test(t)) missing.push("存在未通过项,不能开工");
32
+ else if (!PASS_RE.test(t)) missing.push("探测结果(通过)");
33
+ return missing;
34
+ }
35
+
36
+ /** Kickoff schedules a probe only when a customer environment was declared. */
37
+ export function envChecklistNeedsProbe(text) {
38
+ const t = String(text || "").trim();
39
+ if (!t || envChecklistDeclaresNoCustomer(t)) return false;
40
+ return true;
41
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * FDE-05: a third-party, ERP, or SSO dependency must not silently own
3
+ * the schedule. The confirm card records a blocker, an SLA, a backup
4
+ * mock, and a parallel path. The desk does not call the vendor.
5
+ */
6
+
7
+ const PARTS = [
8
+ { label: "阻塞项", re: /阻塞|blocker/i },
9
+ { label: "SLA", re: /\bsla\b/i },
10
+ { label: "备用 Mock", re: /\bmock\b|备用|備用/i },
11
+ { label: "并行路径", re: /并行|並行|parallel/i },
12
+ ];
13
+
14
+ export function externalDepsDeclaresNone(text) {
15
+ return /本模块无外部依赖|本模組無外部依賴|无第三方依赖|無第三方依賴|no external dependency/i.test(
16
+ String(text || ""),
17
+ );
18
+ }
19
+
20
+ export function missingExternalDeps(text) {
21
+ const t = String(text || "").trim();
22
+ if (externalDepsDeclaresNone(t)) return [];
23
+ const missing = [];
24
+ for (const part of PARTS) {
25
+ if (!part.re.test(t)) missing.push(part.label);
26
+ }
27
+ return missing;
28
+ }
29
+
30
+ /** Kickoff schedules the board only when an external dependency was declared. */
31
+ export function externalDepsNeedsTask(text) {
32
+ const t = String(text || "").trim();
33
+ if (!t || externalDepsDeclaresNone(t)) return false;
34
+ return true;
35
+ }