@marsken/dsh-studio 0.1.7 → 0.1.9

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.7` |
9
+ | dsh-studio | `package.json` `version: 0.1.9` |
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.9` | 2026-11-14 | 准出门禁提示「代码事实核对不通过」(只提醒,不拦) |
131
+ | `0.1.8` | 2026-11-14 | 卡进「待评审」时自动核对代码事实,对不上就写进 history 并记 gate/blocked |
130
132
  | `0.1.7` | 2026-11-14 | webhook 投递结果进「集成健康」(地址写错了看得见) |
131
133
  | `0.1.6` | 2026-11-14 | 回归跑完也发事件(regression/finished);数据库用例可指定断言的列 |
132
134
  | `0.1.5` | 2026-11-14 | agent/failed 事件、控制面板(执行开关 / 上限 / webhook 按事件订阅)、抽屉里加数据库用例入口 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marsken/dsh-studio",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",
@@ -274,7 +274,7 @@ export function runSummary() {
274
274
  current.ms += run.ms
275
275
  map.set(key, current)
276
276
  }
277
- bump(byExpert, run.expertName || run.expert || '未记录')
277
+ bump(byExpert, run.expertId || run.expert || run.expertName || '未记录')
278
278
  bump(byModel, run.model || '跟随默认')
279
279
  bump(byPrompt, run.promptVersion || '未记录')
280
280
  const day = new Date(run.at).toISOString().slice(0, 10)
@@ -660,6 +660,9 @@ export class Dispatcher {
660
660
  taskCode: task.code || task.id,
661
661
  sessionId,
662
662
  expert: expert.id,
663
+ // 卡上真正指派的专家 id:列负责人是「角色位」时,expert.id 是角色(dev),
664
+ // 只按它统计会把同一个人的记录拆成两行(dev 有派单、expert-xxx 有卡)。
665
+ expertId: String(task.expert ?? ''),
663
666
  expertName: expert.name,
664
667
  model: String(this.modelFor(expert)?.model ?? ''),
665
668
  promptVersion: String(expert.promptVersion ?? ''),
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { CODE_PREFIX, ROLE_IDS, STATUS_IDS, STATUSES, nextRequirementCode, nextTaskCode, normalizePatch, sanitizeBaseline, sanitizeReqDoc, sanitizeRequirement, sanitizeTestRun, text,
5
5
  canonicalAuthor,
6
+ sanitizeCoding,
6
7
  } from './domain.js'
7
8
  import { casesOfTasks, impactSummary } from './impact.js'
8
9
  import { designGate } from './design.js'
@@ -30,6 +31,35 @@ export function configureExecutor(services) {
30
31
  * 核对执行记录和代码事实:agent 自报的提交/文件,分支上到底有没有。
31
32
  * 没有 worktree(隔离没开)或还没写执行记录时返回 skipped,不拦流程。
32
33
  */
34
+ /** 自动核对:进「待评审」时跑一次,把结果写回 coding.verified 并在 history 留痕。 */
35
+ export async function autoVerifyCoding(store, taskId) {
36
+ const state = store.snapshot()
37
+ const task = state.tasks.find(entry => entry.id === taskId)
38
+ if (task === undefined || task.coding === null || task.coding === undefined) return null
39
+ const report = await verifyCodingRecord(state, task)
40
+ await store.mutate(draft => {
41
+ const target = draft.tasks.find(entry => entry.id === taskId)
42
+ if (target === undefined || target.coding === null || target.coding === undefined) return
43
+ const skipped = report.skipped !== undefined
44
+ target.coding = sanitizeCoding({ ...target.coding, verified: skipped ? null : report })
45
+ const missingCommits = (report.missingCommits ?? []).length
46
+ const missingFiles = (report.missingFiles ?? []).length
47
+ target.history = [...(target.history ?? []), {
48
+ at: Date.now(),
49
+ by: '系统',
50
+ status: target.status,
51
+ note: skipped
52
+ ? '代码事实没法核对:' + report.skipped
53
+ : report.ok === true
54
+ ? '代码事实核对通过'
55
+ : '代码事实核对不通过:' + missingCommits + ' 个提交 / ' + missingFiles + ' 个文件在仓库里找不到',
56
+ }]
57
+ })
58
+ if (report.ok !== true && report.skipped === undefined) {
59
+ emitEvent('gate/blocked', { taskId, gate: 'coding', blockers: ['代码事实核对不通过'] })
60
+ }
61
+ return report
62
+ }
33
63
  export async function verifyCodingRecord(state, task) {
34
64
  const coding = task?.coding ?? null
35
65
  if (coding === null || coding === undefined) return { ok: false, skipped: '这张卡还没有执行记录' }
@@ -794,6 +824,11 @@ export function taskExitGate(state, task) {
794
824
  if (pending > 0) warnings.push(`${pending} 条用例还没跑`)
795
825
  }
796
826
  const artifacts = task.artifacts ?? []
827
+ // 核对过但没通过:给一句提醒(不拦准出:拦不拦是产品决定)。
828
+ const coding = task.coding ?? null
829
+ if (coding?.verified !== null && coding?.verified !== undefined && coding.verified.ok !== true) {
830
+ warnings.push('代码事实核对不通过:' + String((coding.verified.missingCommits ?? []).length) + ' 个提交 / ' + String((coding.verified.missingFiles ?? []).length) + ' 个文件在仓库里找不到')
831
+ }
797
832
  if (artifacts.length === 0) warnings.push('没有登记任何交付物')
798
833
  else {
799
834
  const empty = artifacts.filter(item => String(item.content ?? '').trim() === '').length
@@ -1396,6 +1431,10 @@ export async function updateTask(store, id, patch) {
1396
1431
  host.INTEGRATION.lastPushError = String(error?.message ?? error).slice(0, 300)
1397
1432
  }
1398
1433
  }
1434
+ // 卡进「待评审」时自动核对一次代码事实:AI 说改了什么,仓库里得真有。
1435
+ if (moved !== null && moved.to === 'review') {
1436
+ await autoVerifyCoding(store, id).catch(() => {})
1437
+ }
1399
1438
  return result
1400
1439
  }
1401
1440
 
@@ -58,10 +58,12 @@ export function buildQuality(state, runs) {
58
58
  }
59
59
  const runRows = (runs?.byExpert ?? []).map(item => ({ name: String(item.key ?? ''), runs: Number(item.runs) || 0, ms: Number(item.ms) || 0 }))
60
60
  const runByName = new Map(runRows.map(item => [item.name.toLowerCase(), item]))
61
+ const runById = new Map(runRows.map(item => [item.name, item]))
61
62
  const experts = [...perExpert.values()].map(row => {
62
63
  const info = (state?.config?.experts ?? []).find(item => item.id === row.key)
63
64
  const label = info === undefined ? row.key : `${info.emoji ?? ''} ${info.name}`.trim()
64
- const run = runByName.get(String(info?.name ?? row.key).toLowerCase())
65
+ // 先按 id 认领(派单日志里带了 expertId),再退回名字。
66
+ const run = runById.get(row.key) ?? runByName.get(String(info?.name ?? row.key).toLowerCase())
65
67
  return {
66
68
  expert: row.key,
67
69
  label,