@marsken/dsh-studio 0.1.18 → 0.1.20

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.
@@ -6,7 +6,7 @@
6
6
 
7
7
  | 项 | 值 |
8
8
  | --- | --- |
9
- | dsh-studio | `package.json` `version: 0.1.18` |
9
+ | dsh-studio | `package.json` `version: 0.1.20` |
10
10
  | DeepSeek Harness | `0.2.2-rc.1`(`dsh --version`) |
11
11
  | Node | `v24.22.2`(Linux 目标机);本机 Windows `v24.19.0` |
12
12
  | 目标系统 | Ubuntu 24.04,`dsh web` 监听 `18080`,nginx `:80` 反代并注入 `dsh-auth-*` cookie |
package/docs/publish.md CHANGED
@@ -127,6 +127,8 @@ token 只写进临时 npmrc(放在仓库外或 `tmp/` 且不提交),发完
127
127
  | --- | --- | --- |
128
128
  | `0.1.0` | 2026-11-13 | 首个公开版:看板 / 需求 / 用例回归 / 效能 / 设计评审四期 |
129
129
  | `0.1.1` | 2026-11-13 | 补三张 README 截图(npm 页可看图);默认值与文档去掉公司内网信息 |
130
+ | `0.1.20` | 2026-11-15 | 拆卡时提示「有后端但功能点没覆盖后端」 |
131
+ | `0.1.19` | 2026-11-15 | 需求澄清三问 + 拆卡门禁;修补答保存不上的白名单漏字段 |
130
132
  | `0.1.17` | 2026-11-15 | 截图补到 10 张,且有脚本可重跑 |
131
133
  | `0.1.16` | 2026-11-15 | quickstart 命令修正 + GitLab 打桩验证配方 |
132
134
  | `0.1.15` | 2026-11-15 | 清掉三处过期的「已知限制」 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marsken/dsh-studio",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "DSH Studio: role-based multi-agent workspace (product / architect / developer / QA) with a live work board, GitLab + Feishu connection settings, all configurable from the web GUI.",
6
6
  "main": "index.js",
@@ -0,0 +1,41 @@
1
+ // 需求澄清的必答项:不问清楚就会出现「管理后台只交付了前端」这类返工。
2
+ // 纯函数,拆卡门禁和界面提示共用。
3
+
4
+ export const DISCOVERY_FIELDS = [
5
+ ['server', '有没有服务端?数据存在哪、刷新会不会丢'],
6
+ ['auth', '鉴权谁做:前端隐藏按钮,还是服务端拒绝'],
7
+ ['boundary', '交付边界:只交付界面,还是点开就能用'],
8
+ ]
9
+
10
+ export function sanitizeDiscovery(raw) {
11
+ const input = raw !== null && typeof raw === 'object' ? raw : {}
12
+ return {
13
+ server: String(input.server ?? '').trim().slice(0, 300),
14
+ auth: String(input.auth ?? '').trim().slice(0, 300),
15
+ boundary: String(input.boundary ?? '').trim().slice(0, 300),
16
+ }
17
+ }
18
+
19
+ const BACKEND_HINT = /接口|api|数据库|数据表|建表|后端|服务端|鉴权|sqlite|postgres|持久化|存储/i
20
+ const BACKEND_YES = /^(有|yes|是|需要|要)/i
21
+
22
+ /**
23
+ * 说了「有服务端」,功能点里却没有一条跟后端有关 —— 大概率又要只交付一半。
24
+ * 只提示、不拦:有的需求后端在别的仓库或别的需求里,硬拦会误伤。
25
+ */
26
+ export function backendCoverageWarning(requirement) {
27
+ const answer = String(requirement?.discovery?.server ?? '').trim()
28
+ if (answer === '' || BACKEND_YES.test(answer) !== true) return null
29
+ const points = Array.isArray(requirement?.points) ? requirement.points : []
30
+ const hit = points.some(point => BACKEND_HINT.test(String(point?.title ?? '') + ' ' + String(point?.description ?? '')))
31
+ if (hit) return null
32
+ return '澄清里说有服务端,但功能点没有一条讲接口 / 数据表 / 鉴权:后端在哪一块交付?'
33
+ }
34
+
35
+ /** 还缺哪几项澄清;空了才允许拆卡。 */
36
+ export function discoveryGaps(requirement) {
37
+ const discovery = requirement?.discovery ?? {}
38
+ return DISCOVERY_FIELDS
39
+ .filter(([key]) => String(discovery[key] ?? '').trim() === '')
40
+ .map(([, label]) => label)
41
+ }
@@ -1,6 +1,7 @@
1
1
  /** dsh-studio 的领域层:板列、卡片/需求/发布的数据形状与清洗、默认配置、编号生成。纯函数,可单测。 */
2
2
  import { createHash, randomBytes } from 'node:crypto'
3
3
  import { sanitizeDbCase } from './db-runner.js'
4
+ import { sanitizeDiscovery } from './discovery.js'
4
5
  import { sanitizePrices } from './usage.js'
5
6
  import { sanitizeWebhooks } from './webhooks.js'
6
7
  import { sanitizeCaseShell } from './shell-case.js'
@@ -979,8 +980,7 @@ export function sanitizeRequirement(raw) {
979
980
  const title = String(raw.title ?? '').trim().slice(0, 200)
980
981
  if (title.length === 0) return undefined
981
982
  const now = Date.now()
982
- const links = raw.links !== null && typeof raw.links === 'object' ? raw.links : {}
983
- const folderRaw = raw.folder !== null && typeof raw.folder === 'object' ? raw.folder : {}
983
+ const links = raw.links !== null && typeof raw.links === 'object' ? raw.links : {}, folderRaw = raw.folder !== null && typeof raw.folder === 'object' ? raw.folder : {}
984
984
  return {
985
985
  id: cleanId(raw.id ?? '', 'req'),
986
986
  code: String(raw.code ?? '').trim().toUpperCase().slice(0, 40),
@@ -992,6 +992,7 @@ export function sanitizeRequirement(raw) {
992
992
  /** 业务领域:卡没写领域时继承它,用来把卡路由给同领域专家。 */
993
993
  domain: String(raw.domain ?? '').trim().slice(0, 40),
994
994
  priority: PRIORITIES.includes(String(raw.priority)) ? String(raw.priority) : 'normal',
995
+ discovery: sanitizeDiscovery(raw.discovery), // 澄清三问,空的会在拆卡时被拦
995
996
  status: REQ_STATUSES.includes(String(raw.status)) ? String(raw.status) : 'draft',
996
997
  links: {
997
998
  prd: String(links.prd ?? '').trim().slice(0, 500),
@@ -1854,8 +1855,7 @@ export function sanitizeReleases(input) {
1854
1855
 
1855
1856
  export function sanitizeRelease(raw, index = 0) {
1856
1857
  if (raw === null || typeof raw !== 'object') return null
1857
- const version = String(raw.version ?? '').trim().slice(0, 40)
1858
- const name = String(raw.name ?? '').trim().slice(0, 120)
1858
+ const version = String(raw.version ?? '').trim().slice(0, 40), name = String(raw.name ?? '').trim().slice(0, 120)
1859
1859
  if (version === '' && name === '') return null
1860
1860
  return {
1861
1861
  id: cleanId(raw.id ?? (version === '' ? name : version), `rel-${index}`),
@@ -515,7 +515,8 @@ export async function saveRequirement(store, input) {
515
515
  points: requirementPointsFingerprint(existing.points),
516
516
  links: JSON.stringify(existing.links ?? {}),
517
517
  }
518
- for (const key of ['title', 'description', 'project', 'iteration', 'owner', 'domain', 'priority', 'status', 'links']) {
518
+ // 更新时的字段白名单:漏一个就会「保存成功但字段没变」(discovery 就是这样丢过一次)
519
+ for (const key of ['title', 'description', 'project', 'iteration', 'owner', 'domain', 'priority', 'status', 'links', 'discovery']) {
519
520
  existing[key] = draft[key]
520
521
  }
521
522
  // 排序与依赖单独处理:显式给了才覆盖,避免详情页保存别的字段时把它们清空。
@@ -7,6 +7,7 @@ import {
7
7
  sanitizeRequirementPool,
8
8
  text,
9
9
  } from '../domain.js'
10
+ import { backendCoverageWarning, discoveryGaps } from '../discovery.js'
10
11
  import { casesOfTasks, impactSummary } from '../impact.js'
11
12
  import {
12
13
  ensureReqFolder,
@@ -198,6 +199,8 @@ export async function handleRequirementRoute({ method, route, req, res, store, u
198
199
  const approved = (requirement.review ?? null)?.status === 'approved'
199
200
  const started = ['active', 'delivered', 'closed'].includes(requirement.status)
200
201
  if (!approved && !started) throw new Error('需求还没评审通过,先过评审再拆卡')
202
+ const gaps = discoveryGaps(requirement)
203
+ if (gaps.length > 0) throw new Error('拆卡前先把澄清答完:' + gaps.join(';'))
201
204
  const points = (requirement.points ?? []).filter(point => requested.length === 0 || requested.includes(point.id))
202
205
  const uncovered = points.filter(point => !before.tasks.some(task => task.requirementId === id && task.pointId === point.id))
203
206
  if (uncovered.length === 0) throw new Error('这些功能点都已经有卡了')
@@ -216,7 +219,9 @@ export async function handleRequirementRoute({ method, route, req, res, store, u
216
219
  })
217
220
  created.push(task)
218
221
  }
219
- host.sendJson(res, 200, { ok: true, created, state: host.publicState(store) })
222
+ // 说了有后端却没有后端功能点:给个提示,不拦(后端可能确实在别的需求里)
223
+ const warning = backendCoverageWarning(requirement)
224
+ host.sendJson(res, 200, { ok: true, created, warnings: warning === null ? [] : [warning], state: host.publicState(store) })
220
225
  return true
221
226
  }
222
227
  if (method === 'POST' && route === '/requirement/pool') {