sillyspec 3.26.13 → 3.26.15
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/skills/sillyspec-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/docs-check.js +48 -12
- package/src/doctor-diagnostics.js +49 -0
- package/src/hooks/worktree-guard.js +20 -1
- package/src/index.js +547 -21
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +146 -0
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress.js +18 -3
- package/src/quicklog.js +106 -1
- package/src/run/command.js +151 -38
- package/src/run/complete-handlers.js +62 -4
- package/src/run/complete.js +10 -8
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +86 -4
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +28 -6
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +28 -7
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +55 -7
- package/src/stages/archive.js +1 -1
- package/src/stages/brainstorm.js +26 -12
- package/src/stages/doctor.js +2 -4
- package/src/stages/execute.js +110 -26
- package/src/stages/knowledge.js +21 -5
- package/src/stages/plan-postcheck.js +24 -10
- package/src/stages/plan.js +14 -9
- package/src/stages/quick.js +14 -6
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +7 -49
- package/src/sync.js +19 -7
- package/src/task-review.js +256 -10
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +15 -1
- package/src/worktree.js +58 -8
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- package/templates/prompts/verify-probes.md +13 -47
package/src/sync.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync, chmodSync } from 'fs';
|
|
12
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
12
13
|
import { join } from 'path';
|
|
13
14
|
import { resolvePlatformSpecDir } from './progress.js';
|
|
14
15
|
import { safeGit } from './git-helper.js';
|
|
@@ -131,12 +132,14 @@ function replaceTopLevelSection(text, name, body) {
|
|
|
131
132
|
return `${stripped}\n\n${name}:\n${body}\n`;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
/** 文本级写 local.yaml(保留传入 text 的注释/结构);确保 .sillyspec 目录存在。
|
|
135
|
+
/** 文本级写 local.yaml(保留传入 text 的注释/结构);确保 .sillyspec 目录存在。
|
|
136
|
+
* 原子写(fs-atomic 契约:local.yaml 被 hook/probe 等其他进程并发读,裸 writeFileSync 的
|
|
137
|
+
* 写入窗口可读到半截文件 → mcp 段解析失败回退 env → 误判 no-config 降级)。 */
|
|
135
138
|
function writeLocalYamlRaw(cwd, text) {
|
|
136
139
|
const dir = join(cwd, '.sillyspec');
|
|
137
140
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
138
141
|
const yamlPath = join(cwd, LOCAL_YAML);
|
|
139
|
-
|
|
142
|
+
writeAtomicSync(yamlPath, text, 'utf8');
|
|
140
143
|
// 体检 SEC-04:local.yaml 含明文 token,收紧为仅属主可读写。POSIX chmod 0600 生效;
|
|
141
144
|
// Windows 上 Node chmod 只映射只读位(近似 no-op),属可接受 best-effort
|
|
142
145
|
try { chmodSync(yamlPath, 0o600); } catch { /* chmod 失败不阻断连接流程 */ }
|
|
@@ -364,7 +367,7 @@ export class SyncManager {
|
|
|
364
367
|
// 删段后纯空白(无任何段也无注释)→ 删除整个文件;注释算内容,有注释则保留
|
|
365
368
|
try { unlinkSync(p); } catch { /* best effort */ }
|
|
366
369
|
} else {
|
|
367
|
-
|
|
370
|
+
writeAtomicSync(p, newText, 'utf8');
|
|
368
371
|
}
|
|
369
372
|
}
|
|
370
373
|
}
|
|
@@ -609,7 +612,10 @@ export class SyncManager {
|
|
|
609
612
|
return { synced: 0, errors: ['未指定变更名称'] };
|
|
610
613
|
}
|
|
611
614
|
|
|
612
|
-
|
|
615
|
+
// 树根与进度/spec 树同源(safePlatformSpecDir,BUG-01 同族):此前硬编码 cwd/.sillyspec/changes,
|
|
616
|
+
// 平台模式(specRoot 指向外部)下路径不存在 → syncDocuments 恒报「变更不存在」且调用方不查
|
|
617
|
+
// 返回值 → 文档在平台模式下静默永不同步。
|
|
618
|
+
const changeDir = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), 'changes', changeName);
|
|
613
619
|
if (!existsSync(changeDir)) {
|
|
614
620
|
console.warn(`[sync] 变更不存在: ${changeName}`);
|
|
615
621
|
return { synced: 0, errors: [`变更不存在: ${changeName}`] };
|
|
@@ -997,7 +1003,10 @@ export class SyncManager {
|
|
|
997
1003
|
const { ProgressManager } = await import('./progress.js');
|
|
998
1004
|
const pm = new ProgressManager({ specDir: safePlatformSpecDir(this.cwd) });
|
|
999
1005
|
const db = pm._ensureDB(this.cwd).getDb();
|
|
1000
|
-
|
|
1006
|
+
// 参数侧同样 COALESCE(坑 keep-local-base-ts-null-param):SQLite 标量 MAX() 任一参数
|
|
1007
|
+
// 为 NULL 即返回 NULL——冲突文件 platform_last_pushed_at 为 null 时 MAX(NULL, 旧值)
|
|
1008
|
+
// 会把已回填的 base_ts 清空,恰好违背本注释的单调防回退意图。
|
|
1009
|
+
db.prepare('UPDATE changes SET last_synced_platform_ts = MAX(COALESCE(?, last_synced_platform_ts), COALESCE(last_synced_platform_ts, ?)) WHERE name = ?')
|
|
1001
1010
|
.run(platformPushedAt, platformPushedAt, changeName);
|
|
1002
1011
|
} catch (err) {
|
|
1003
1012
|
return { ok: false, resolved: false, reason: `keep-local 更新 base_ts 失败: ${err.message}` };
|
|
@@ -1024,9 +1033,12 @@ export class SyncManager {
|
|
|
1024
1033
|
}
|
|
1025
1034
|
|
|
1026
1035
|
if (mode === 'take-platform' && cf) {
|
|
1027
|
-
// 用冲突文件的 platform_progress 调 import 覆盖本地(保隔离:import 不覆盖 isolation_
|
|
1036
|
+
// 用冲突文件的 platform_progress 调 import 覆盖本地(保隔离:import 不覆盖 isolation_*)。
|
|
1037
|
+
// fail-closed(坑 take-platform-empty-import):缺 platform_progress 时必须直接返回——
|
|
1038
|
+
// { ...null } 展开为 {},import 会照常跑完整事务(DELETE stages 后按空 JSON 重建 =
|
|
1039
|
+
// 清空本地进度),返回值却报 ok:false「未执行」,用户以为无事发生实则数据已丢。
|
|
1028
1040
|
if (!cf.platform_progress) {
|
|
1029
|
-
|
|
1041
|
+
return { ok: false, resolved: false, reason: '冲突文件缺 platform_progress,无法 take-platform(建议先 platform pull)' };
|
|
1030
1042
|
}
|
|
1031
1043
|
try {
|
|
1032
1044
|
const { ProgressManager } = await import('./progress.js');
|
package/src/task-review.js
CHANGED
|
@@ -525,12 +525,15 @@ export function validateTaskReviews(opts) {
|
|
|
525
525
|
|
|
526
526
|
// QUAL-01 收口:原为本地 execFileSync('git')(无 safe.directory、与 git-helper 口径分裂),
|
|
527
527
|
// 现走统一入口;抛错/trim 语义不变,timeout 15000 保留(rev-parse/diff 大仓余量)
|
|
528
|
-
function runGit(gitDir, args) {
|
|
529
|
-
return git(gitDir, args, { timeout: 15000 })
|
|
528
|
+
function runGit(gitDir, args, opts = {}) {
|
|
529
|
+
return git(gitDir, args, { timeout: 15000, ...opts })
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
// porcelain 状态行 → 变更文件路径(对齐 shared.js:343 parsePorcelainPath:`XY path`、引号包裹、
|
|
533
533
|
// rename `R old -> new` 取箭头后)。未 commit 的 working-tree 改动对账用。
|
|
534
|
+
// ⚠️ 入参必须是【未 trim】的原始输出(git(..., {trim:false}))——porcelain 行首空格是状态码
|
|
535
|
+
// 一部分,git() 默认 trim 会把 ' M feature.js' 削成 'M feature.js',slice(3) 再咬掉路径首字符
|
|
536
|
+
// (实测 'eature.js')→ 未提交文件对账全错位(verifyReviewGitEvidence 相交比对误判「完全不相交」)。
|
|
534
537
|
function parsePorcelainFiles(statusOut) {
|
|
535
538
|
const files = []
|
|
536
539
|
for (const line of String(statusOut).split('\n')) {
|
|
@@ -639,7 +642,7 @@ export function verifyReviewGitEvidence(review, gitDir, cache = null) {
|
|
|
639
642
|
try {
|
|
640
643
|
let wtStatus = c.wtStatus.get(gitDir)
|
|
641
644
|
if (wtStatus === undefined) {
|
|
642
|
-
wtStatus = runGit(gitDir, ['status', '--porcelain'])
|
|
645
|
+
wtStatus = runGit(gitDir, ['status', '--porcelain'], { trim: false })
|
|
643
646
|
c.wtStatus.set(gitDir, wtStatus)
|
|
644
647
|
}
|
|
645
648
|
if (wtStatus && wtStatus.trim().length > 0) {
|
|
@@ -983,7 +986,30 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
983
986
|
}
|
|
984
987
|
|
|
985
988
|
// base..head diff 文件集(worktree-aware;null=git 不可用,[]=无 commit diff)
|
|
986
|
-
|
|
989
|
+
let diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
990
|
+
// ⚠️ 并入 worktree 未提交改动(坑 draft-attribution-uncommitted-worktree,2026-08-21 实证):
|
|
991
|
+
// 子代理默认不 commit(execute 复盘 a 同源事实),真实改动全在 worktree working-tree——只看
|
|
992
|
+
// base..HEAD commit diff 时归属恒空,9/9 草稿全成「无归属」靠主代理手写升级。与
|
|
993
|
+
// verifyReviewGitEvidence 的 working-tree 并入口径同源:status --porcelain 文件(排除
|
|
994
|
+
// .sillyspec/ 运行时产物)并入归属集,按 allowed_paths 正常路径归属;review.head 仍为 HEAD
|
|
995
|
+
// commit——evidence 校验对「commit diff 空 + working-tree 有改动」明示不判伪造,语义一致。
|
|
996
|
+
try {
|
|
997
|
+
const wm = new WorktreeManager({ cwd })
|
|
998
|
+
const meta = wm.getMeta(changeName)
|
|
999
|
+
if (meta) {
|
|
1000
|
+
const wtGitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
1001
|
+
? meta.worktreePath
|
|
1002
|
+
: cwd
|
|
1003
|
+
const wtStatus = runGit(wtGitDir, ['status', '--porcelain'], { trim: false })
|
|
1004
|
+
const wtFiles = parsePorcelainFiles(wtStatus)
|
|
1005
|
+
.map(p => String(p).replace(/\\/g, '/'))
|
|
1006
|
+
.filter(p => p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1007
|
+
if (wtFiles.length > 0) {
|
|
1008
|
+
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles])]
|
|
1009
|
+
console.log(`[sillyspec] 草稿归属并入 ${wtFiles.length} 个 worktree 未提交文件(子代理未 commit 的改动按 allowed_paths 归属)`)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
|
|
987
1013
|
// 单仓模式(无 ctx):主仓无 diff 即无任何 task 可生成 → 提前返回(原逻辑零回归)。
|
|
988
1014
|
// 有 ctx:主仓无 diff 不阻断——跨仓 task 的 diff 在跨仓仓根独立取(per-task),主仓 task 自然跳过(空 changedFiles)。
|
|
989
1015
|
if (!ctx && (!diffFiles || diffFiles.length === 0)) {
|
|
@@ -1023,6 +1049,7 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1023
1049
|
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
1024
1050
|
let generated = 0
|
|
1025
1051
|
let skipped = 0
|
|
1052
|
+
let noAttributionCount = 0
|
|
1026
1053
|
const attributed = new Set()
|
|
1027
1054
|
|
|
1028
1055
|
for (const tf of taskFiles) {
|
|
@@ -1081,11 +1108,6 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1081
1108
|
: []
|
|
1082
1109
|
}
|
|
1083
1110
|
|
|
1084
|
-
// 空 changedFiles 的 task 不生成(verifyReviewGitEvidence 判空 diff 伪造,留给 agent 手补)
|
|
1085
|
-
if (taskChangedFiles.length === 0) {
|
|
1086
|
-
skipped++
|
|
1087
|
-
continue
|
|
1088
|
-
}
|
|
1089
1111
|
// 主仓 task 缺 base/head(meta 缺失 / HEAD 解析失败,见上方 :968-981 注释承诺的"后续跳过"):
|
|
1090
1112
|
// 跳过留给 agent 手补。此前 null.slice(0,8) 直接 TypeError 崩掉整个草稿生成(体检 BUG-06)
|
|
1091
1113
|
if (!crossEntry && (!taskBase || !taskHead)) {
|
|
@@ -1097,6 +1119,42 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1097
1119
|
taskChangedFiles.forEach(f => attributed.add(f))
|
|
1098
1120
|
}
|
|
1099
1121
|
|
|
1122
|
+
// 空 changedFiles 的 task 也生成「无归属」草稿(坑 task-review-draft-skip-leak,2026-08-21
|
|
1123
|
+
// 实证:task-08/task-10 两次都因 allowed_paths 未命中 diff 被静默跳过,--done gate 拦截后
|
|
1124
|
+
// agent 只能从零手写 review.json)。cannot_verify 草稿本就待复核(verify 阶段兑现
|
|
1125
|
+
// requiredEvidence),空 changedFiles 不触发 emptyDiff 伪造误判(该判定看真实 git diff 而非
|
|
1126
|
+
// review.changedFiles),shouldAutoCheckTask 零 diff 守卫也保证不会被自动勾选——草稿从
|
|
1127
|
+
// 「漏了让 agent 从零写」变成「给了骨架让 agent 升级 verdict」。
|
|
1128
|
+
if (taskChangedFiles.length === 0) {
|
|
1129
|
+
const draft = {
|
|
1130
|
+
schemaVersion: REVIEW_SCHEMA_VERSION,
|
|
1131
|
+
task: taskId,
|
|
1132
|
+
base: taskBase,
|
|
1133
|
+
head: taskHead,
|
|
1134
|
+
changedFiles: [],
|
|
1135
|
+
specVerdict: 'cannot_verify',
|
|
1136
|
+
qualityVerdict: 'cannot_verify',
|
|
1137
|
+
requiredEvidence: ['auto-generated draft (no-attributed-diff): ' + taskId +
|
|
1138
|
+
' 的 allowed_paths 未命中本次 diff 任何文件(task 未实现 / 改动与他人同文件 / 路径不匹配)——' +
|
|
1139
|
+
'需人工确认实际改动后升级 verdict,确属未实现则回 fail'],
|
|
1140
|
+
reviewerNotes: 'auto-generated draft from git diff ' + taskBase.slice(0, 8) + '..' + taskHead.slice(0, 8) +
|
|
1141
|
+
';verdict=未评审(无归属 diff 兜底草稿,坑 task-review-draft-skip-leak)',
|
|
1142
|
+
}
|
|
1143
|
+
if (draftRepo) draft.repo = draftRepo
|
|
1144
|
+
// fail-open(对齐函数头契约 + execute-run-dir-fail-loud 测试锁定的「写失败返回统计不抛」):
|
|
1145
|
+
// 无归属草稿写入失败(如 tasks/ 被普通文件占位 ENOTDIR)降级 skipped,不崩整个草稿生成
|
|
1146
|
+
try {
|
|
1147
|
+
mkdirSync(reviewDir, { recursive: true })
|
|
1148
|
+
writeFileSync(reviewPath, JSON.stringify(draft, null, 2) + '\n')
|
|
1149
|
+
generated++
|
|
1150
|
+
noAttributionCount++
|
|
1151
|
+
} catch (e) {
|
|
1152
|
+
console.error(`[sillyspec] task ${taskId} 无归属草稿落盘失败(降级留给 agent 手补): ${e.message}`)
|
|
1153
|
+
skipped++
|
|
1154
|
+
}
|
|
1155
|
+
continue
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1100
1158
|
const draft = {
|
|
1101
1159
|
schemaVersion: REVIEW_SCHEMA_VERSION,
|
|
1102
1160
|
task: taskId,
|
|
@@ -1115,7 +1173,195 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1115
1173
|
}
|
|
1116
1174
|
|
|
1117
1175
|
const unattributed = (Array.isArray(diffFiles) ? diffFiles : []).filter(f => !attributed.has(f))
|
|
1118
|
-
return { generated, skipped, unattributed, executeRunId }
|
|
1176
|
+
return { generated, skipped, noAttribution: noAttributionCount, unattributed, executeRunId }
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* adoptTaskReviewMechanics —— 已存在 review.json 的 mechanics 字段一键重算代填(保 verdict)。
|
|
1181
|
+
*
|
|
1182
|
+
* 背景(2026-08-21 agent-手工产出审计项①):review.json 里 schemaVersion/task/base/head/
|
|
1183
|
+
* changedFiles(/repo) 全部可由 CLI 从 git + task 卡机械推导(generateTaskReviewDrafts 已证明),
|
|
1184
|
+
* agent/子代理手算 hash、手抄 diff 清单是纯错误面——而 gate(verifyReviewGitEvidence)校验的
|
|
1185
|
+
* 恰是这些字段。与草稿生成互补:草稿只补「缺失」,adopt 修「已存在但 mechanics 错/缺」——
|
|
1186
|
+
* 保留人工语义字段(specVerdict/qualityVerdict/requiredEvidence/reviewerNotes),只重写 mechanics。
|
|
1187
|
+
*
|
|
1188
|
+
* verdict 保护策略(不猜 agent 本意):
|
|
1189
|
+
* - 合法 verdict(VALID_VERDICTS)原样保留;
|
|
1190
|
+
* - 非法/缺失 verdict 降级 cannot_verify,reviewerNotes 追加标记,requiredEvidence 缺失补占位
|
|
1191
|
+
* (schema 要求 cannot_verify 必带非空 requiredEvidence);
|
|
1192
|
+
* - JSON 解析失败的 review 不救(无法可信保留任何字段),报出来留给 agent 修 JSON 后重跑。
|
|
1193
|
+
*
|
|
1194
|
+
* fail-open:与草稿生成同契约——缺数据返回统计与 reasons,不抛。
|
|
1195
|
+
*
|
|
1196
|
+
* @param {{ changeName: string, cwd: string, platformOpts?: object, ctx?: object|null }} opts
|
|
1197
|
+
* @returns {Promise<{ adopted: number, unchanged: number, skipped: number, missing: number,
|
|
1198
|
+
* downgraded: string[], reasons: string[], executeRunId?: string }>}
|
|
1199
|
+
*/
|
|
1200
|
+
export async function adoptTaskReviewMechanics({ changeName, cwd, platformOpts = {}, ctx = null }) {
|
|
1201
|
+
const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
|
|
1202
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
1203
|
+
const tasksDir = join(specBase, 'changes', changeName, 'tasks')
|
|
1204
|
+
const reasons = []
|
|
1205
|
+
|
|
1206
|
+
if (!changeName) {
|
|
1207
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 changeName'] }
|
|
1208
|
+
}
|
|
1209
|
+
if (!existsSync(tasksDir)) {
|
|
1210
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 tasks/ 目录'] }
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// executeRunId:与草稿/gate 同源 marker。缺 marker 不新建 run——adopt 修的是当前 run 里
|
|
1214
|
+
// 已存在的 review,marker 丢失说明无「当前 run」语义,无对象可修(gate 对 marker 缺失放行,
|
|
1215
|
+
// 下游 Task Review Gate 兜底,这里同口径不造 run)。
|
|
1216
|
+
const runIdFile = join(runtimeRoot, 'current-execute-run-id-' + changeName)
|
|
1217
|
+
let executeRunId = ''
|
|
1218
|
+
try {
|
|
1219
|
+
if (existsSync(runIdFile)) {
|
|
1220
|
+
const c = readFileSync(runIdFile, 'utf8').trim()
|
|
1221
|
+
if (isValidExecuteRunId(c)) executeRunId = c
|
|
1222
|
+
}
|
|
1223
|
+
} catch {}
|
|
1224
|
+
if (!executeRunId) {
|
|
1225
|
+
return { adopted: 0, unchanged: 0, skipped: 0, missing: 0, downgraded: [], reasons: ['无 execute run marker——无当前 run,adopt 无对象'] }
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// base/head + diff:与 generateTaskReviewDrafts 同源口径(meta 锡点 + worktree rev-parse +
|
|
1229
|
+
// resolveVerifyChangedFiles),保证 adopt 产物与 gate 校验输入一致。
|
|
1230
|
+
const diffFiles = resolveVerifyChangedFiles(cwd, changeName)
|
|
1231
|
+
let base = null
|
|
1232
|
+
let head = null
|
|
1233
|
+
let reviewGitDir = cwd
|
|
1234
|
+
try {
|
|
1235
|
+
const wm = new WorktreeManager({ cwd })
|
|
1236
|
+
const meta = wm.getMeta(changeName)
|
|
1237
|
+
base = (meta && (meta.baselineCommit || meta.baseHash)) || null
|
|
1238
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
1239
|
+
reviewGitDir = meta.worktreePath
|
|
1240
|
+
}
|
|
1241
|
+
} catch {}
|
|
1242
|
+
if (base) {
|
|
1243
|
+
try {
|
|
1244
|
+
head = runGit(reviewGitDir, ['rev-parse', 'HEAD'])
|
|
1245
|
+
} catch {
|
|
1246
|
+
head = null
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const taskFiles = readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f)).sort()
|
|
1251
|
+
let adopted = 0
|
|
1252
|
+
let unchanged = 0
|
|
1253
|
+
let skipped = 0
|
|
1254
|
+
let missing = 0
|
|
1255
|
+
const downgraded = []
|
|
1256
|
+
|
|
1257
|
+
for (const tf of taskFiles) {
|
|
1258
|
+
const taskId = tf.replace(/\.md$/, '')
|
|
1259
|
+
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', taskId, 'review.json')
|
|
1260
|
+
if (!existsSync(reviewPath)) {
|
|
1261
|
+
missing++
|
|
1262
|
+
continue
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const existing = readReview(reviewPath)
|
|
1266
|
+
if (existing.parseError) {
|
|
1267
|
+
skipped++
|
|
1268
|
+
reasons.push(`${taskId}: JSON 解析失败,adopt 不猜字段——先修 JSON 再重跑`)
|
|
1269
|
+
continue
|
|
1270
|
+
}
|
|
1271
|
+
const prev = existing.review || {}
|
|
1272
|
+
|
|
1273
|
+
const content = readFileSync(join(tasksDir, tf), 'utf8')
|
|
1274
|
+
const allowedPaths = parseAllowedPaths(content)
|
|
1275
|
+
const taskRepo = normalizeRepoKey(parseRepo(content))
|
|
1276
|
+
const crossEntry = (ctx && taskRepo !== DEFAULT_REPO_KEY) ? ctx.resolve(taskRepo) : null
|
|
1277
|
+
let taskBase = base
|
|
1278
|
+
let taskHead = head
|
|
1279
|
+
let taskChangedFiles
|
|
1280
|
+
let draftRepo
|
|
1281
|
+
|
|
1282
|
+
if (crossEntry) {
|
|
1283
|
+
const baseCommit = parseBaseCommit(content)
|
|
1284
|
+
const headCommit = parseHeadCommit(content)
|
|
1285
|
+
if (!baseCommit || !headCommit) {
|
|
1286
|
+
skipped++
|
|
1287
|
+
reasons.push(`${taskId}: 跨仓 task 卡缺 base/head 锡点,无法 adopt`)
|
|
1288
|
+
continue
|
|
1289
|
+
}
|
|
1290
|
+
taskBase = baseCommit
|
|
1291
|
+
taskHead = headCommit
|
|
1292
|
+
draftRepo = taskRepo
|
|
1293
|
+
let crossDiffFiles = []
|
|
1294
|
+
try {
|
|
1295
|
+
const out = runGit(crossEntry.gitDir, ['diff', '--name-only', `${baseCommit}..${headCommit}`])
|
|
1296
|
+
crossDiffFiles = out ? out.split('\n').filter(Boolean).map(p => p.replace(/\\/g, '/')) : []
|
|
1297
|
+
} catch {
|
|
1298
|
+
skipped++
|
|
1299
|
+
reasons.push(`${taskId}: 跨仓 git diff 失败(${crossEntry.gitDir})`)
|
|
1300
|
+
continue
|
|
1301
|
+
}
|
|
1302
|
+
taskChangedFiles = allowedPaths.length > 0
|
|
1303
|
+
? crossDiffFiles.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
1304
|
+
: crossDiffFiles
|
|
1305
|
+
} else {
|
|
1306
|
+
const mainDiff = Array.isArray(diffFiles) ? diffFiles : []
|
|
1307
|
+
taskChangedFiles = allowedPaths.length > 0
|
|
1308
|
+
? mainDiff.filter(f => allowedPaths.some(p => pathMatches(f, p)))
|
|
1309
|
+
: []
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (!crossEntry && (!taskBase || !taskHead)) {
|
|
1313
|
+
skipped++
|
|
1314
|
+
reasons.push(`${taskId}: 主仓缺 worktree meta base/head,无法 adopt`)
|
|
1315
|
+
continue
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const merged = { ...prev }
|
|
1319
|
+
merged.schemaVersion = REVIEW_SCHEMA_VERSION
|
|
1320
|
+
merged.task = taskId
|
|
1321
|
+
merged.base = taskBase
|
|
1322
|
+
merged.head = taskHead
|
|
1323
|
+
merged.changedFiles = taskChangedFiles
|
|
1324
|
+
if (draftRepo) merged.repo = draftRepo
|
|
1325
|
+
|
|
1326
|
+
let verdictTouched = false
|
|
1327
|
+
for (const v of ['specVerdict', 'qualityVerdict']) {
|
|
1328
|
+
if (!VALID_VERDICTS.includes(merged[v])) {
|
|
1329
|
+
merged[v] = 'cannot_verify'
|
|
1330
|
+
verdictTouched = true
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (verdictTouched) downgraded.push(taskId)
|
|
1334
|
+
let evidenceFilled = false
|
|
1335
|
+
if ((merged.specVerdict === 'cannot_verify' || merged.qualityVerdict === 'cannot_verify')
|
|
1336
|
+
&& (!Array.isArray(merged.requiredEvidence) || merged.requiredEvidence.length === 0)) {
|
|
1337
|
+
merged.requiredEvidence = [`adopt 代填:原 review 缺 requiredEvidence(verdict=${merged.specVerdict}/${merged.qualityVerdict}),待人工复核补证据`]
|
|
1338
|
+
evidenceFilled = true
|
|
1339
|
+
}
|
|
1340
|
+
if (verdictTouched) {
|
|
1341
|
+
merged.reviewerNotes = String(merged.reviewerNotes || '') + (merged.reviewerNotes ? '; ' : '')
|
|
1342
|
+
+ 'adopt: 原 verdict 非法/缺失已降级 cannot_verify(不猜本意)'
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const mechanicsChanged = prev.schemaVersion !== merged.schemaVersion
|
|
1346
|
+
|| prev.base !== merged.base
|
|
1347
|
+
|| prev.head !== merged.head
|
|
1348
|
+
|| JSON.stringify(prev.changedFiles ?? null) !== JSON.stringify(merged.changedFiles)
|
|
1349
|
+
|| prev.repo !== merged.repo
|
|
1350
|
+
|
|
1351
|
+
try {
|
|
1352
|
+
if (mechanicsChanged || verdictTouched || evidenceFilled) {
|
|
1353
|
+
writeFileSync(reviewPath, JSON.stringify(merged, null, 2) + '\n')
|
|
1354
|
+
adopted++
|
|
1355
|
+
} else {
|
|
1356
|
+
unchanged++
|
|
1357
|
+
}
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
skipped++
|
|
1360
|
+
reasons.push(`${taskId}: 写回失败 ${e.message}`)
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
return { adopted, unchanged, skipped, missing, downgraded, reasons, executeRunId }
|
|
1119
1365
|
}
|
|
1120
1366
|
|
|
1121
1367
|
/**
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-probes.js — `sillyspec verify-probes` 的机械探针实现 + verify-result.md 骨架生成
|
|
3
|
+
* (2026-08-21 agent-手工产出审计第三批 H1/H3/H5/H7/F9)。
|
|
4
|
+
*
|
|
5
|
+
* verify-probes.md 模板定义六个探针,agent 此前逐条手跑 grep/递归查找/git 对账再手工拼表格。
|
|
6
|
+
* 本模块把纯机械的四个探针命令化(语义判断的留 agent,输出里显式标注):
|
|
7
|
+
* 探针1 未实现标记扫描:design §6 清单的具体文件逐行 grep TODO/FIXME/尚未实现 等
|
|
8
|
+
* 探针3 测试覆盖:逐 task 按 allowed_paths 定位模块目录,递归找测试文件(co-located tests/ 陷阱)
|
|
9
|
+
* 探针5 API 契约对账:复用 contract-matrix.verifyApiParity(endpoints.json × 前端调用)+ 表格渲染
|
|
10
|
+
* 探针6 删除对账:git diff --name-status HEAD 的 D/R × design 声明操作三态判定
|
|
11
|
+
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
|
+
*
|
|
13
|
+
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
14
|
+
* 结论章节留「待填」——extractVerifyConclusion 找不到 PASS/FAIL 关键词即判不过,骨架不能
|
|
15
|
+
* 直接过门(与 symbol-impact 骨架同款防偷懒语义)。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
18
|
+
import { join, dirname, basename } from 'path'
|
|
19
|
+
import { gitQuiet } from './git-helper.js'
|
|
20
|
+
import { parseFileChangeListDetailed } from './change-list.js'
|
|
21
|
+
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
22
|
+
import { verifyApiParity } from './contract-matrix.js'
|
|
23
|
+
import { resolveSpecDir, resolveRuntimeRoot } from './run/shared.js'
|
|
24
|
+
|
|
25
|
+
const TODO_MARKER_RE = /尚未实现|TODO|FIXME|HACK|XXX/
|
|
26
|
+
const TEST_FILE_RE = /test|spec/i
|
|
27
|
+
const PROBE1_MAX_MATCHES = 200
|
|
28
|
+
|
|
29
|
+
function listTaskIds(tasksPath) {
|
|
30
|
+
if (!existsSync(tasksPath)) return []
|
|
31
|
+
const ids = []
|
|
32
|
+
for (const line of readFileSync(tasksPath, 'utf8').split('\n')) {
|
|
33
|
+
const m = line.match(/^[-*]\s*\[[ xX]\]\s*(task-\d+)\b/)
|
|
34
|
+
if (m) ids.push(m[1])
|
|
35
|
+
}
|
|
36
|
+
return ids
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 递归找测试文件(排除 node_modules/.git/dist 等;返回 posix 相对 cwd 路径,封顶展示) */
|
|
40
|
+
function findTestFiles(rootDir, cwd, cap = 10) {
|
|
41
|
+
const found = []
|
|
42
|
+
const skip = new Set(['node_modules', '.git', '.gradle', '__pycache__', '.venv', 'dist', 'build', 'target', 'out'])
|
|
43
|
+
const walk = (d) => {
|
|
44
|
+
let entries
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(d, { withFileTypes: true })
|
|
47
|
+
} catch {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const full = join(d, e.name)
|
|
52
|
+
if (e.isDirectory()) {
|
|
53
|
+
if (!skip.has(e.name)) walk(full)
|
|
54
|
+
} else if (e.isFile() && TEST_FILE_RE.test(basename(e.name))) {
|
|
55
|
+
found.push(full.split('\\').join('/').replace(cwd.split('\\').join('/').replace(/\/$/, '') + '/', ''))
|
|
56
|
+
if (found.length >= cap) return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(rootDir)) walk(rootDir)
|
|
61
|
+
return found
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 跑四个机械探针。
|
|
66
|
+
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
67
|
+
* @returns {{ probe1: object, probe3: object, probe5: object, probe6: object }}
|
|
68
|
+
*/
|
|
69
|
+
export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
70
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
71
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
72
|
+
const designPath = join(changeDir, 'design.md')
|
|
73
|
+
const detailed = existsSync(designPath) ? parseFileChangeListDetailed(designPath) : []
|
|
74
|
+
const designOps = new Map(detailed.map(e => [e.path, e.operation]))
|
|
75
|
+
|
|
76
|
+
// ── 探针 1:未实现标记扫描(design 清单具体文件;glob 项列出让 agent 展开)──
|
|
77
|
+
const probe1 = { matches: [], globEntries: [], skippedFiles: [] }
|
|
78
|
+
for (const e of detailed) {
|
|
79
|
+
if (e.path.startsWith('.sillyspec/')) continue
|
|
80
|
+
if (/[*?[\]]/.test(e.path)) {
|
|
81
|
+
probe1.globEntries.push(e.path)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const abs = join(cwd, e.path)
|
|
85
|
+
if (!existsSync(abs)) {
|
|
86
|
+
probe1.skippedFiles.push(e.path)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const lines = readFileSync(abs, 'utf8').split('\n')
|
|
91
|
+
lines.forEach((line, i) => {
|
|
92
|
+
if (probe1.matches.length >= PROBE1_MAX_MATCHES) return
|
|
93
|
+
if (TODO_MARKER_RE.test(line)) probe1.matches.push({ file: e.path, line: i + 1, content: line.trim().slice(0, 160) })
|
|
94
|
+
})
|
|
95
|
+
} catch {
|
|
96
|
+
probe1.skippedFiles.push(e.path)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 探针 3:验收标准测试覆盖(task 卡 allowed_paths → 模块目录递归找测试文件)──
|
|
101
|
+
const tasksPath = join(changeDir, 'tasks.md')
|
|
102
|
+
const probe3 = { tasks: [], note: '集成盲区(路由/跨模块装配)与断言有效性抽查是语义判断,留给 agent 逐 task 标注 ⚠️' }
|
|
103
|
+
for (const taskId of listTaskIds(tasksPath)) {
|
|
104
|
+
const cardPath = join(changeDir, 'tasks', `${taskId}.md`)
|
|
105
|
+
let allowed = []
|
|
106
|
+
if (existsSync(cardPath)) allowed = parseAllowedPaths(readFileSync(cardPath, 'utf8'))
|
|
107
|
+
const moduleDirs = [...new Set(allowed.map(p => dirname(p.split('\\').join('/'))).filter(d => d && d !== '.'))]
|
|
108
|
+
const testFiles = []
|
|
109
|
+
for (const d of moduleDirs) {
|
|
110
|
+
for (const f of findTestFiles(join(cwd, d), cwd)) {
|
|
111
|
+
if (!testFiles.includes(f)) testFiles.push(f)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
probe3.tasks.push({
|
|
115
|
+
task: taskId,
|
|
116
|
+
moduleDirs,
|
|
117
|
+
testFiles: testFiles.slice(0, 10),
|
|
118
|
+
testFileCount: testFiles.length,
|
|
119
|
+
hasTest: testFiles.length > 0,
|
|
120
|
+
located: moduleDirs.length > 0,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── 探针 5:API 契约对账(复用 verifyApiParity:endpoints.json × 前端调用)──
|
|
125
|
+
const runtimeRoot = resolveRuntimeRoot({ specRoot: specDir }, specBase)
|
|
126
|
+
const probe5 = verifyApiParity(specBase, cwd, runtimeRoot, changeName)
|
|
127
|
+
|
|
128
|
+
// ── 探针 6:代码删除对账(git diff --name-status HEAD 的 D/R × design 声明三态)──
|
|
129
|
+
const probe6 = { deletions: [], note: '以 git 事实为准(真实 > 声明);是否 FAIL blocker 由 agent 诚实判定' }
|
|
130
|
+
const nsOut = gitQuiet(cwd, ['diff', '--name-status', 'HEAD']) || ''
|
|
131
|
+
for (const row of nsOut.split('\n').filter(Boolean)) {
|
|
132
|
+
const [status, ...paths] = row.split('\t')
|
|
133
|
+
const st = (status || '').trim().toUpperCase()
|
|
134
|
+
if (!/^[DRC]/.test(st)) continue
|
|
135
|
+
// R/C 的旧路径等价删除(paths[0]);D 的唯一路径
|
|
136
|
+
const target = (st.startsWith('D') ? paths[0] : paths[0]) || ''
|
|
137
|
+
const posix = target.split('\\').join('/')
|
|
138
|
+
if (!posix || posix.startsWith('.sillyspec/') || posix === 'meta.json' || basename(posix) === 'meta.json') continue
|
|
139
|
+
const op = designOps.get(posix)
|
|
140
|
+
let verdict
|
|
141
|
+
if (op && /删除|delete|del/i.test(op)) verdict = '✅ 合规(design 声明删除)'
|
|
142
|
+
else if (op) verdict = `❌ 高风险(design 声明「${op}」却整文件删除)`
|
|
143
|
+
else verdict = '⚠️ 未声明删除(design 清单未列出)'
|
|
144
|
+
probe6.deletions.push({ path: posix, status: st, designOp: op || null, verdict })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { probe1, probe3, probe5, probe6 }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 渲染探针结果为 markdown(可直接粘进 verify-result.md「探针结果」章节)。
|
|
152
|
+
*/
|
|
153
|
+
export function renderVerifyProbesReport(result) {
|
|
154
|
+
const L = []
|
|
155
|
+
const { probe1, probe3, probe5, probe6 } = result
|
|
156
|
+
|
|
157
|
+
L.push('#### 探针 1:未实现标记扫描(design 清单文件)')
|
|
158
|
+
if (probe1.matches.length === 0) {
|
|
159
|
+
L.push('- ✅ 无 TODO/FIXME/尚未实现 标记命中')
|
|
160
|
+
} else {
|
|
161
|
+
for (const m of probe1.matches) L.push(`- ⚠️ \`${m.file}:${m.line}\` ${m.content}`)
|
|
162
|
+
}
|
|
163
|
+
if (probe1.globEntries.length > 0) L.push(`- ℹ️ glob 项未展开(agent 手动展开扫描):${probe1.globEntries.join('、')}`)
|
|
164
|
+
if (probe1.skippedFiles.length > 0) L.push(`- ℹ️ 清单文件不存在(跳过):${probe1.skippedFiles.join('、')}`)
|
|
165
|
+
L.push('')
|
|
166
|
+
|
|
167
|
+
L.push('#### 探针 2:设计关键词覆盖')
|
|
168
|
+
L.push('<!--TODO: 半语义探针——从 design 提取能力关键词逐个 grep 确认实现(agent 执行)-->')
|
|
169
|
+
L.push('')
|
|
170
|
+
|
|
171
|
+
L.push('#### 探针 3:验收标准测试覆盖')
|
|
172
|
+
if (probe3.tasks.length === 0) {
|
|
173
|
+
L.push('- ℹ️ tasks.md 无 checkbox 任务')
|
|
174
|
+
} else {
|
|
175
|
+
for (const t of probe3.tasks) {
|
|
176
|
+
if (!t.located) {
|
|
177
|
+
L.push(`- ⚠️ ${t.task}: 无 task 卡/allowed_paths,无法定位模块目录(agent 手查)`)
|
|
178
|
+
} else if (t.hasTest) {
|
|
179
|
+
L.push(`- ✅ ${t.task}: 模块目录(${t.moduleDirs.join('、')})找到 ${t.testFileCount} 个测试文件(${t.testFiles.slice(0, 5).join('、')}${t.testFileCount > 5 ? ' …' : ''})`)
|
|
180
|
+
} else {
|
|
181
|
+
L.push(`- ⚠️ ${t.task}: 模块目录(${t.moduleDirs.join('、')})递归未找到测试文件(含 co-located tests/)`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
L.push(`- ℹ️ ${probe3.note}`)
|
|
186
|
+
L.push('')
|
|
187
|
+
|
|
188
|
+
L.push('#### 探针 4:决策追踪覆盖')
|
|
189
|
+
L.push('<!--TODO: 语义探针——D-xxx@vN → FR-xxx → plan/task 引用 → 证据回指闭环(agent 执行)-->')
|
|
190
|
+
L.push('')
|
|
191
|
+
|
|
192
|
+
L.push('#### 探针 5:API Contract Parity')
|
|
193
|
+
L.push(`- ${probe5.summary || `backend ${probe5.backendCount ?? 0} 端点 / frontend ${probe5.frontendCount ?? 0} 调用`}`)
|
|
194
|
+
if ((probe5.missingBackend || []).length > 0) {
|
|
195
|
+
L.push('')
|
|
196
|
+
L.push('| 状态 | 前端调用 | 后端端点 | 文件 |')
|
|
197
|
+
L.push('|---|---|---|---|')
|
|
198
|
+
for (const m of probe5.missingBackend) {
|
|
199
|
+
L.push(`| ❌ missing | ${m.method} ${m.path} | — | ${m.consumerFile || '?'}${m.consumerLine ? ':' + m.consumerLine : ''} |`)
|
|
200
|
+
}
|
|
201
|
+
L.push('')
|
|
202
|
+
L.push('- ❌ contract gap 是真实集成缺陷——诚实判 FAIL 并回 execute 补端点(CLI 仅 advisory 不硬阻断)')
|
|
203
|
+
}
|
|
204
|
+
if ((probe5.unusedBackend || []).length > 0) {
|
|
205
|
+
L.push(`- ⚠️ ${probe5.unusedBackend.length} 个后端端点前端未调用(warning 不阻断):${probe5.unusedBackend.slice(0, 5).map(u => `${u.method} ${u.path}`).join('、')}${probe5.unusedBackend.length > 5 ? ' …' : ''}`)
|
|
206
|
+
}
|
|
207
|
+
L.push('')
|
|
208
|
+
|
|
209
|
+
L.push('#### 探针 6:代码删除对账')
|
|
210
|
+
if (probe6.deletions.length === 0) {
|
|
211
|
+
L.push('- ✅ git diff 无整文件删除(D/R/C)记录')
|
|
212
|
+
} else {
|
|
213
|
+
for (const d of probe6.deletions) {
|
|
214
|
+
L.push(`- ${d.verdict} \`${d.path}\`(git 状态 ${d.status})`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
L.push(`- ℹ️ ${probe6.note}`)
|
|
218
|
+
return L.join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
|
|
223
|
+
* 结论章节留「待填」——extractVerifyConclusion 无 PASS/FAIL 关键词即判不过,骨架不能直接过门。
|
|
224
|
+
* @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
|
|
225
|
+
*/
|
|
226
|
+
export function generateVerifyResultSkeleton(result) {
|
|
227
|
+
const L = [
|
|
228
|
+
'# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
|
|
229
|
+
'',
|
|
230
|
+
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论必须写明 PASS / FAIL**——',
|
|
231
|
+
'> 留「待填」会被 gate 判不过(fail-closed)。',
|
|
232
|
+
'',
|
|
233
|
+
'## 结论:<待填:PASS 或 FAIL(+一句话理由)>',
|
|
234
|
+
'',
|
|
235
|
+
'## 任务完成度',
|
|
236
|
+
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
|
|
237
|
+
'',
|
|
238
|
+
'## 设计一致性',
|
|
239
|
+
'<!--TODO: 实现与 design.md 的偏差(无偏差也显式写「一致」)-->',
|
|
240
|
+
'',
|
|
241
|
+
'## 探针结果(CLI 机械预填)',
|
|
242
|
+
renderVerifyProbesReport(result),
|
|
243
|
+
'',
|
|
244
|
+
'## 测试结果',
|
|
245
|
+
'<!--TODO: 测试命令 + 结果(通过数/失败数;known_failures 豁免逐条注明)-->',
|
|
246
|
+
'',
|
|
247
|
+
'## 决策追踪矩阵(如存在 decisions.md;无则删本节)',
|
|
248
|
+
'<!--TODO: | 决策 ID | FR | Task | Evidence | 状态 |(D-xxx@vN → FR-xxx → task → 证据回指闭环)-->',
|
|
249
|
+
'',
|
|
250
|
+
'## 技术债务',
|
|
251
|
+
'<!--TODO: TODO/FIXME/HACK 统计(探针 1 的命中已预填在上方探针结果)-->',
|
|
252
|
+
'',
|
|
253
|
+
'## 变更风险等级',
|
|
254
|
+
'<!--TODO: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,写明「显式声明 = <等级>」+ 理由(豁免可审计)-->',
|
|
255
|
+
'',
|
|
256
|
+
'## Runtime Evidence',
|
|
257
|
+
'<!--TODO: 关键命令输出/时间戳/commit hash 证据链;integration/deployment-critical 必填,按实际触碰的运行时组件写(启动命令/端点/请求响应/日志片段/生命周期终态断言/失败模式排除),未涉及的行写「不涉及」-->',
|
|
258
|
+
'',
|
|
259
|
+
'## 代码审查',
|
|
260
|
+
'<!--TODO: 问题列表 + 总体评价-->',
|
|
261
|
+
'',
|
|
262
|
+
]
|
|
263
|
+
return L.join('\n')
|
|
264
|
+
}
|