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.
- package/.claude/rules/agents-workflow.md +2 -0
- package/.claude/rules/duaer-spec.md +1 -0
- package/.cursor/rules/agents-workflow.mdc +2 -0
- package/.cursor/rules/duaer-spec.mdc +1 -0
- package/.duaer/active-job.json +3 -3
- package/.duaer/specs/270-patch-version/delivery.json +7 -0
- package/.duaer/specs/270-patch-version/spec.md +19 -0
- package/.duaer/specs/270-patch-version/tasks.md +4 -0
- package/.duaer/specs/271-fde04-compat/delivery.json +9 -0
- package/.duaer/specs/271-fde04-compat/spec.md +25 -0
- package/.duaer/specs/271-fde04-compat/tasks.md +5 -0
- package/.duaer/specs/272-fde03-env-check/delivery.json +9 -0
- package/.duaer/specs/272-fde03-env-check/spec.md +34 -0
- package/.duaer/specs/272-fde03-env-check/tasks.md +5 -0
- package/.duaer/specs/273-fde06-data-precheck/delivery.json +9 -0
- package/.duaer/specs/273-fde06-data-precheck/spec.md +37 -0
- package/.duaer/specs/273-fde06-data-precheck/tasks.md +5 -0
- package/.duaer/specs/274-fde05-external-deps/delivery.json +9 -0
- package/.duaer/specs/274-fde05-external-deps/spec.md +37 -0
- package/.duaer/specs/274-fde05-external-deps/tasks.md +5 -0
- package/.duaer/specs/275-release-0271/delivery.json +9 -0
- package/.duaer/specs/275-release-0271/spec.md +13 -0
- package/.duaer/specs/275-release-0271/tasks.md +4 -0
- package/.duaer/specs/276-fde08-perf-budget/delivery.json +9 -0
- package/.duaer/specs/276-fde08-perf-budget/spec.md +38 -0
- package/.duaer/specs/276-fde08-perf-budget/tasks.md +5 -0
- package/.duaer/specs/277-project-dir-reuse/delivery.json +9 -0
- package/.duaer/specs/277-project-dir-reuse/spec.md +31 -0
- package/.duaer/specs/277-project-dir-reuse/tasks.md +5 -0
- package/.duaer/specs/278-project-create-unique/delivery.json +9 -0
- package/.duaer/specs/278-project-create-unique/spec.md +27 -0
- package/.duaer/specs/278-project-create-unique/tasks.md +5 -0
- package/.duaer/specs/279-arch-side-detour/delivery.json +8 -0
- package/.duaer/specs/279-arch-side-detour/spec.md +28 -0
- package/.duaer/specs/279-arch-side-detour/tasks.md +5 -0
- package/.duaer/specs/280-arch-bottom-via-label/delivery.json +8 -0
- package/.duaer/specs/280-arch-bottom-via-label/spec.md +22 -0
- package/.duaer/specs/280-arch-bottom-via-label/tasks.md +5 -0
- package/.duaer/specs/281-release-0272/spec.md +13 -0
- package/.duaer/specs/281-release-0272/tasks.md +4 -0
- package/AGENTS.md +5 -2
- package/CHANGELOG.md +69 -0
- package/bin/duaer-live.mjs +202 -23
- package/bin/live-archify.mjs +382 -26
- package/bin/live-deliverables.mjs +16 -0
- package/bin/live-modules.mjs +81 -1
- package/bin/live-project-chat.mjs +8 -0
- package/bin/live-repo-path.mjs +91 -2
- package/docs/agent/branching-and-release.md +16 -0
- package/docs/agent/e2e-test-plan.md +10 -0
- package/docs/agent/live-desk.md +34 -4
- package/docs/agent/workflow.md +2 -0
- package/package.json +1 -1
- package/web/live-dev/app.js +95 -2
- package/web/live-dev/baseline.mjs +4 -0
- package/web/live-dev/compat-matrix.mjs +38 -0
- package/web/live-dev/confirm-card.mjs +4 -0
- package/web/live-dev/data-precheck.mjs +35 -0
- package/web/live-dev/env-check.mjs +41 -0
- package/web/live-dev/external-deps.mjs +35 -0
- package/web/live-dev/i18n-de.js +25 -10
- package/web/live-dev/i18n-es.js +25 -10
- package/web/live-dev/i18n-fr.js +25 -10
- package/web/live-dev/i18n-ja.js +25 -10
- package/web/live-dev/i18n-ko.js +25 -10
- package/web/live-dev/i18n-pt-BR.js +25 -10
- package/web/live-dev/i18n-ru.js +25 -10
- package/web/live-dev/i18n-vi.js +25 -10
- package/web/live-dev/i18n-zh-TW.js +25 -10
- package/web/live-dev/i18n.js +50 -20
- package/web/live-dev/index.html +33 -9
- package/web/live-dev/perf-budget.mjs +38 -0
package/bin/live-repo-path.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
package/docs/agent/live-desk.md
CHANGED
|
@@ -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
|
-
|
|
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`
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
package/docs/agent/workflow.md
CHANGED
|
@@ -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
package/web/live-dev/app.js
CHANGED
|
@@ -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
|
+
}
|