dsh-card-dispatch 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +249 -2
- package/cordis.patch.yml +25 -0
- package/icon.svg +21 -0
- package/index.js +88 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
- package/src/catalog-source.js +122 -0
- package/src/client/index.js +155 -0
- package/src/config-store.js +49 -0
- package/src/config.js +229 -0
- package/src/console.html +964 -0
- package/src/criteria-llm.js +79 -0
- package/src/criteria.js +103 -0
- package/src/gates.js +123 -0
- package/src/human-channel.js +794 -0
- package/src/import.js +193 -0
- package/src/model-call.js +89 -0
- package/src/models.js +249 -0
- package/src/parse-llm.js +96 -0
- package/src/prompts-llm.js +104 -0
- package/src/selfcheck.js +117 -0
- package/src/split.js +397 -0
- package/src/verdict.js +133 -0
package/src/verdict.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verdict.js:验收结论的**文件契约**。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么是文件,不是看板状态
|
|
5
|
+
*
|
|
6
|
+
* 「打回之后谁重跑」原本看起来只能靠读看板状态,但两条路都不通:
|
|
7
|
+
*
|
|
8
|
+
* 1. 插件读不到看板:`GET /api/task-board/state` 走的是看板自己的信任栅栏,
|
|
9
|
+
* 插件进程没有那张 cookie,实测 403;
|
|
10
|
+
* 2. 就算读到了也没用:`ExecutionRecord.result` 只记「这次执行成没成」,
|
|
11
|
+
* 不记「验收判 PASS 还是 REJECT」——验收结论只活在会话记录里。
|
|
12
|
+
*
|
|
13
|
+
* 所以把结论**写进文件**:验收卡产出 `out/<根卡 id>/verdict.json`。
|
|
14
|
+
* 文件的好处是三方都能读:人、看板里的下一次执行、以及插件(人点一下时)。
|
|
15
|
+
* 这也顺带满足基线 P3——会话状态不可重建即不存在,落盘的才存在。
|
|
16
|
+
*
|
|
17
|
+
* 这个模块只做解析与判定,不读盘、不联网,所以能逐条单测。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** verdict.json 里 REJECT 的每一项必须带齐这四个字段。 */
|
|
21
|
+
const ITEM_KEYS = ['criteriaId', 'expected', 'actual', 'evidencePath']
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 解析 verdict.json。
|
|
25
|
+
*
|
|
26
|
+
* 故意严格:REJECT 不给定位信息 = 开发只能瞎改,所以「缺字段」本身就是错误,
|
|
27
|
+
* 而不是一条可以忽略的警告。
|
|
28
|
+
*
|
|
29
|
+
* @param {string|object} raw 文件内容或已解析对象
|
|
30
|
+
* @returns {{ok:boolean, verdict:string|null, items:Array, problems:Array<string>, summary:string, error:string|null}}
|
|
31
|
+
*/
|
|
32
|
+
export function parseVerdict(raw) {
|
|
33
|
+
const empty = { ok: false, verdict: null, items: [], problems: [], summary: '', error: null }
|
|
34
|
+
let data = raw
|
|
35
|
+
if (typeof raw === 'string') {
|
|
36
|
+
const text = raw.trim()
|
|
37
|
+
if (text === '') return { ...empty, error: 'verdict.json 是空文件' }
|
|
38
|
+
try {
|
|
39
|
+
data = JSON.parse(text)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { ...empty, error: `verdict.json 不是合法 JSON:${error.message}` }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
45
|
+
return { ...empty, error: 'verdict.json 的顶层必须是对象' }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const verdict = String(data.verdict ?? '').toUpperCase()
|
|
49
|
+
if (verdict !== 'PASS' && verdict !== 'REJECT') {
|
|
50
|
+
return { ...empty, error: `verdict 只能是 PASS 或 REJECT,收到 ${JSON.stringify(data.verdict)}` }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const items = Array.isArray(data.items) ? data.items : []
|
|
54
|
+
const problems = []
|
|
55
|
+
if (verdict === 'REJECT') {
|
|
56
|
+
if (items.length === 0) {
|
|
57
|
+
problems.push('REJECT 必须逐条列出 items:没有定位的「不行」等于没验收')
|
|
58
|
+
}
|
|
59
|
+
items.forEach((item, index) => {
|
|
60
|
+
const missing = ITEM_KEYS.filter((key) => String(item?.[key] ?? '').trim() === '')
|
|
61
|
+
if (missing.length > 0) problems.push(`items[${index}] 缺 ${missing.join('、')}`)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
ok: problems.length === 0,
|
|
67
|
+
verdict,
|
|
68
|
+
items,
|
|
69
|
+
problems,
|
|
70
|
+
summary: String(data.summary ?? '').trim(),
|
|
71
|
+
error: null,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 拿到 verdict 之后,下一步该干什么。
|
|
77
|
+
*
|
|
78
|
+
* 三种结局,正好对应基线的三条线:
|
|
79
|
+
* - `pass`:验收通过 → 父卡可以收尾;
|
|
80
|
+
* - `rework`:打回,且还没到上限 → 重跑开发卡(**这才是真正花钱的那一步**);
|
|
81
|
+
* - `escalate`:到上限 → 停下来交给人(基线 R3),不再让开发瞎改;
|
|
82
|
+
* - `invalid`:verdict 本身不合格 → 让验收卡重做,不要拿它当依据。
|
|
83
|
+
*
|
|
84
|
+
* @param {object} input
|
|
85
|
+
* @param {object} input.parsed parseVerdict 的结果
|
|
86
|
+
* @param {object} input.config 归一化配置(用 gates.maxReworkRounds)
|
|
87
|
+
* @param {number} [input.rejectedCount] 这一批已经打回过的次数
|
|
88
|
+
* @returns {{action:'pass'|'rework'|'escalate'|'invalid', rejectedCount:number, message:string, items:Array}}
|
|
89
|
+
*/
|
|
90
|
+
export function decideNext({ parsed, config, rejectedCount = 0 } = {}) {
|
|
91
|
+
const previous = Math.max(0, Math.trunc(Number(rejectedCount) || 0))
|
|
92
|
+
if (parsed === undefined || parsed === null || parsed.error !== null || parsed.verdict === null) {
|
|
93
|
+
return {
|
|
94
|
+
action: 'invalid',
|
|
95
|
+
rejectedCount: previous,
|
|
96
|
+
message: `验收结论不可用:${parsed?.error ?? '没有读到结论'};让验收卡重做一次,别拿它当依据`,
|
|
97
|
+
items: [],
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!parsed.ok) {
|
|
101
|
+
return {
|
|
102
|
+
action: 'invalid',
|
|
103
|
+
rejectedCount: previous,
|
|
104
|
+
message: `验收结论不合格:${parsed.problems.join(';')}`,
|
|
105
|
+
items: parsed.items,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (parsed.verdict === 'PASS') {
|
|
109
|
+
return { action: 'pass', rejectedCount: previous, message: '验收通过,这批可以收尾', items: [] }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const limit = config.gates.maxReworkRounds
|
|
113
|
+
const next = previous + 1
|
|
114
|
+
if (previous >= limit) {
|
|
115
|
+
return {
|
|
116
|
+
action: 'escalate',
|
|
117
|
+
rejectedCount: previous,
|
|
118
|
+
message: `这将是第 ${next} 次打回,超过上限 ${limit}:停下来交给人(基线 R3),不要再让开发改`,
|
|
119
|
+
items: parsed.items,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
action: 'rework',
|
|
124
|
+
rejectedCount: next,
|
|
125
|
+
message: `第 ${next} 次打回(上限 ${limit}):把这几条带着证据退回开发`,
|
|
126
|
+
items: parsed.items,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 验收卡该把结论写到哪里(相对工作区根目录)。 */
|
|
131
|
+
export function verdictPath(rootId) {
|
|
132
|
+
return `out/${rootId}/verdict.json`
|
|
133
|
+
}
|