sillyspec 3.26.2 → 3.26.4
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-execute/SKILL.md +38 -0
- package/.claude/skills/sillyspec-plan/SKILL.md +48 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +13 -1
- package/package.json +1 -1
- package/src/db.js +11 -0
- package/src/dispatch/backends/local-agent.js +3 -0
- package/src/dispatch/backends/sillyhub-mcp.js +3 -0
- package/src/index.js +55 -3
- package/src/machine-interface.js +11 -4
- package/src/run/command.js +9 -0
- package/src/run/complete.js +27 -4
- package/src/run/gates.js +26 -14
- package/src/run/multi-repo-context.js +206 -0
- package/src/run/prompt.js +6 -0
- package/src/run/shared.js +170 -2
- package/src/stage-review.js +1 -0
- package/src/stages/brainstorm.js +22 -20
- package/src/stages/execute.js +188 -7
- package/src/stages/plan-postcheck.js +266 -11
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +25 -2
- package/src/task-review.js +135 -25
- package/src/verify-postcheck.js +242 -11
- package/src/worktree-apply.js +137 -9
- package/templates/prompts/testcase-design.md +12 -0
- package/templates/prompts/verify-probes.md +1 -0
- package/templates/skills/sillyspec-onboard/SKILL.md +92 -0
- package/.sillyspec-platform-cleaned +0 -1
- package/.sillyspec-platform.json +0 -7
package/src/stages/execute.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { buildContractMatrix, buildConsumerInjection, buildContractFieldInjection } from '../contract-matrix.js'
|
|
4
4
|
import { getRule } from '../stage-contract-spec.js'
|
|
5
5
|
import { renderDispatchInstruction } from '../dispatch/strategy.js'
|
|
6
6
|
import { isPathASupported } from '../dispatch/backends/sillyhub-mcp.js'
|
|
7
7
|
import { readMcpConfig } from '../sillyhub-mcp/config.js'
|
|
8
|
+
import { gitQuiet } from '../git-helper.js'
|
|
9
|
+
import { parseRepo } from './plan-postcheck.js'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* 校验 plan.md 是否满足 execute 执行契约
|
|
@@ -460,10 +462,92 @@ export function getDispatchMode() {
|
|
|
460
462
|
if (!hasConfig) return 'local'
|
|
461
463
|
return isPathASupported() ? 'sillyhub' : 'local-fallback'
|
|
462
464
|
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 解析 task 卡的 repo 归属(W3 task-08,D-012)。
|
|
468
|
+
* 读 changeDir/tasks/task-<num>.md 的 frontmatter `repo:` 字段;缺省/无卡/'main' → 'main'。
|
|
469
|
+
* 单仓 change(无 repo: 字段)恒返 'main',零回归。
|
|
470
|
+
*
|
|
471
|
+
* @param {{ index?: number, name?: string }} task - wave.tasks 元素
|
|
472
|
+
* @param {string|null} changeDir - change 目录绝对路径
|
|
473
|
+
* @returns {string} repoKey,'main' 或 parseRepo 返回值
|
|
474
|
+
*/
|
|
475
|
+
function resolveTaskRepo(task, changeDir) {
|
|
476
|
+
if (!changeDir || !task || task.index == null) return 'main'
|
|
477
|
+
const num = String(task.index).padStart(2, '0')
|
|
478
|
+
const taskFile = path.join(changeDir, 'tasks', `task-${num}.md`)
|
|
479
|
+
if (!existsSync(taskFile)) return 'main'
|
|
480
|
+
try {
|
|
481
|
+
const content = readFileSync(taskFile, 'utf8')
|
|
482
|
+
const repo = parseRepo(content)
|
|
483
|
+
return repo || 'main'
|
|
484
|
+
} catch {
|
|
485
|
+
return 'main'
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* 把 base_commit 锡点写入 task 卡 frontmatter(W3 task-08,D-010)。
|
|
491
|
+
* 派发跨仓 task 前 CLI 实时 git rev-parse HEAD 落盘——锁定子代理起改的 base 锚点,
|
|
492
|
+
* 防止同 Wave 多 task 改同一跨仓仓时 HEAD 推进致 diff 范围漂移(design §5.3 约束① / R-01)。
|
|
493
|
+
*
|
|
494
|
+
* 策略:幂等写——frontmatter 有 base_commit 行则就地替换,无则紧跟 repo 行后插入(
|
|
495
|
+
* 无 frontmatter 时整段创建)。仅改 frontmatter 不动正文。已存在相同值不重复写。
|
|
496
|
+
*
|
|
497
|
+
* @param {string} taskFilePath - task-NN.md 绝对路径
|
|
498
|
+
* @param {string} baseCommit - 跨仓仓 HEAD sha
|
|
499
|
+
* @returns {boolean} 是否实际写入(false = 文件不存在 / 写失败 / 值未变)
|
|
500
|
+
*/
|
|
501
|
+
function writeBaseCommitToTaskCard(taskFilePath, baseCommit) {
|
|
502
|
+
if (!taskFilePath || !baseCommit) return false
|
|
503
|
+
if (!existsSync(taskFilePath)) return false
|
|
504
|
+
let content
|
|
505
|
+
try {
|
|
506
|
+
content = readFileSync(taskFilePath, 'utf8')
|
|
507
|
+
} catch {
|
|
508
|
+
return false
|
|
509
|
+
}
|
|
510
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
511
|
+
if (!fmMatch) return false
|
|
512
|
+
const fm = fmMatch[1]
|
|
513
|
+
const baseLineRe = /^base_commit:\s*.*$/m
|
|
514
|
+
let newFm
|
|
515
|
+
if (baseLineRe.test(fm)) {
|
|
516
|
+
const existing = (fm.match(baseLineRe) || [''])[0]
|
|
517
|
+
if (existing === `base_commit: ${baseCommit}`) return false // 值未变,幂等跳过
|
|
518
|
+
newFm = fm.replace(baseLineRe, `base_commit: ${baseCommit}`)
|
|
519
|
+
} else {
|
|
520
|
+
// 紧跟 repo 行后插入(与 D-010 协议同源顺序 repo→base_commit→head_commit)
|
|
521
|
+
const repoLineRe = /^repo:\s*.*$/m
|
|
522
|
+
const anchor = `base_commit: ${baseCommit}\n`
|
|
523
|
+
if (repoLineRe.test(fm)) {
|
|
524
|
+
newFm = fm.replace(repoLineRe, m => m + '\n' + anchor.trimEnd())
|
|
525
|
+
} else {
|
|
526
|
+
// 无 repo 行:插到 frontmatter 首部(frontmatter 首字段)
|
|
527
|
+
newFm = anchor.trimEnd() + '\n' + fm
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const newContent = content.replace(/^---\n[\s\S]*?\n---/, `---\n${newFm}\n---`)
|
|
531
|
+
if (newContent === content) return false
|
|
532
|
+
try {
|
|
533
|
+
writeFileSync(taskFilePath, newContent)
|
|
534
|
+
return true
|
|
535
|
+
} catch {
|
|
536
|
+
return false
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
463
540
|
/**
|
|
464
541
|
* 为 Wave 生成 prompt(强制子代理执行)
|
|
542
|
+
*
|
|
543
|
+
* W3 task-08(D-012 per-task workdir):签名加可选 `ctx`(MultiRepoContext)参数。
|
|
544
|
+
* - 无 ctx(单仓 change / 旧调用方)→ 退化为单 worktreePath 单值 worktreeSection(零回归)
|
|
545
|
+
* - 有 ctx → 按 task 逐个解析 repo 归属,构造 per-task workdir 多值表,跨仓 task 派发前
|
|
546
|
+
* 落 base_commit 锡点(D-010),注入跨仓 task commit 指引 + head_commit 落盘指引。
|
|
547
|
+
* 同 Wave 允许主仓+跨仓 task 混合(各独立 Task 调用各传 workdir,不强制同 Wave 同 repo)。
|
|
465
548
|
*/
|
|
466
549
|
export function buildWavePrompt(wave, waveIndex, changeDir, worktreePath, options = {}) {
|
|
550
|
+
const ctx = options.ctx || null
|
|
467
551
|
// ── Contract Matrix:检查是否有 provider/consumer 契约需要注入 ──
|
|
468
552
|
let contractInjection = ''
|
|
469
553
|
let prototypeInjection = ''
|
|
@@ -558,8 +642,97 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
|
|
|
558
642
|
return s
|
|
559
643
|
}).join('\n')
|
|
560
644
|
|
|
561
|
-
|
|
562
|
-
|
|
645
|
+
// ── per-task workdir 解析(W3 task-08,D-012)──
|
|
646
|
+
// 无 ctx(单仓 change)→ 全 task workdir=主仓 worktreePath,沿用旧单值逻辑(零回归)。
|
|
647
|
+
// 有 ctx → 按 task 卡 repo: 字段逐个解析:主仓 task workdir=主仓 worktreePath,跨仓 task
|
|
648
|
+
// workdir=ctx.resolve(repo).worktreePath(跨仓仓根)。同 Wave 允许主仓+跨仓混合。
|
|
649
|
+
const taskRepos = wave.tasks.map(t => ctx ? resolveTaskRepo(t, changeDir) : 'main')
|
|
650
|
+
const hasCrossRepoWave = ctx ? taskRepos.some(r => r !== 'main') : false
|
|
651
|
+
|
|
652
|
+
// per-task workdir 映射(用于多值 worktreeSection + 跨仓 commit 指引)
|
|
653
|
+
// 无 ctx 或无跨仓 task → 不构造(沿用旧单值 worktreePath 注入)
|
|
654
|
+
let perTaskWorkdirs = null
|
|
655
|
+
let crossRepoCommitSection = ''
|
|
656
|
+
let baseCommitWrites = [] // [{ taskNum, repo, taskFile, written }] 跨仓 task base 锡点落盘记录
|
|
657
|
+
if (ctx && hasCrossRepoWave) {
|
|
658
|
+
perTaskWorkdirs = wave.tasks.map((t, ti) => {
|
|
659
|
+
const taskNum = String(t.index || (ti + 1)).padStart(2, '0')
|
|
660
|
+
const repo = taskRepos[ti]
|
|
661
|
+
const entry = ctx.resolve(repo)
|
|
662
|
+
const workdir = entry ? entry.worktreePath : (worktreePath || '')
|
|
663
|
+
return { taskNum, repo, workdir }
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
// D-010 跨仓 task base 锡点:派发前实时 git rev-parse HEAD 落 task 卡 base_commit
|
|
667
|
+
// 仅对跨仓 task(repo≠'main')落盘,主仓 task 锚 meta.baseHash(MultiRepoContext 已处理)
|
|
668
|
+
for (const item of perTaskWorkdirs) {
|
|
669
|
+
if (item.repo === 'main') continue
|
|
670
|
+
const entry = ctx.resolve(item.repo)
|
|
671
|
+
if (!entry) continue
|
|
672
|
+
const baseCommit = gitQuiet(entry.gitDir, ['rev-parse', 'HEAD'])
|
|
673
|
+
if (!baseCommit) continue // git 不可达已由 MultiRepoContext 构造时 fail-closed 拦截,此处置防御
|
|
674
|
+
const taskFile = changeDir
|
|
675
|
+
? path.join(changeDir, 'tasks', `task-${item.taskNum}.md`)
|
|
676
|
+
: null
|
|
677
|
+
const written = taskFile ? writeBaseCommitToTaskCard(taskFile, baseCommit) : false
|
|
678
|
+
baseCommitWrites.push({ taskNum: item.taskNum, repo: item.repo, taskFile, written, baseCommit })
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 跨仓 task commit 指引 + head_commit 落盘指引(D-010 回收时机)
|
|
682
|
+
const crossLines = perTaskWorkdirs
|
|
683
|
+
.filter(i => i.repo !== 'main')
|
|
684
|
+
.map(i => `- task-${i.taskNum} → repo \`${i.repo}\`,workdir=\`${i.workdir}\``)
|
|
685
|
+
.join('\n')
|
|
686
|
+
crossRepoCommitSection = `
|
|
687
|
+
### 跨仓 task 派发与双锡点(必须严格遵守)
|
|
688
|
+
|
|
689
|
+
本 Wave 含跨仓 task(直接改跨仓仓主干,**不经主仓 worktree**):
|
|
690
|
+
|
|
691
|
+
${crossLines}
|
|
692
|
+
|
|
693
|
+
**派发跨仓 task 前(base 锡点,CLI 已在 prompt 构造时落盘):**
|
|
694
|
+
- 跨仓 task 卡 frontmatter 的 \`base_commit\` 已由 CLI 实时 \`git -C <跨仓仓根> rev-parse HEAD\` 锁定(base 锡点,约束①)。子代理在此 HEAD 上改+commit,不受同 Wave 其他 task 推进 HEAD 影响。
|
|
695
|
+
|
|
696
|
+
**回收跨仓 task review 前(head 锡点,你必须在子代理完成后落盘):**
|
|
697
|
+
- 跨仓 task 子代理完成 commit 后、写 review.json / 勾选 checkbox **之前**,你必须运行 \`git -C <跨仓仓根> rev-parse HEAD\` 把结果写入该 task 卡 frontmatter \`head_commit:\` 字段(与 \`base_commit:\` 同源 frontmatter 锡点写入)。
|
|
698
|
+
- review.json 的 \`base\` 取 task 卡 \`base_commit\`、\`head\` 取 task 卡 \`head_commit\`(**非瞬时 HEAD**),锁 diff 范围不漂移。
|
|
699
|
+
|
|
700
|
+
**跨仓 task 子代理 prompt 必须注入:**
|
|
701
|
+
> 该 task 改的是 \`<repo>\` 仓,workdir=\`<跨仓仓根>\`。**直接在该仓主干工作区改+commit(git add + git commit 到该仓主干),不经主仓 worktree、不建分支。** commit 到该仓主干即落盘,apply 阶段对跨仓 task 为 no-op(design §5.4 G1)。
|
|
702
|
+
`
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// worktreeSection:无 ctx / 无跨仓 task → 旧单值(零回归);有 ctx 含跨仓 task → per-task 多值表
|
|
706
|
+
let worktreeSection
|
|
707
|
+
if (ctx && hasCrossRepoWave && perTaskWorkdirs) {
|
|
708
|
+
const workdirLines = perTaskWorkdirs.map(i =>
|
|
709
|
+
` - task-${i.taskNum} (repo: ${i.repo}) → workdir: "${i.workdir}"`
|
|
710
|
+
).join('\n')
|
|
711
|
+
worktreeSection = `
|
|
712
|
+
### 工作目录(必须严格遵守,per-task)
|
|
713
|
+
|
|
714
|
+
调用 Task 工具启动子代理时,**workdir 参数是强制必传的**,且本 Wave 内不同 task 的 workdir 不同(主仓 task vs 跨仓 task)。
|
|
715
|
+
|
|
716
|
+
**per-task workdir 表(每个子代理按其 task 对应的 workdir 启动):**
|
|
717
|
+
|
|
718
|
+
${workdirLines}
|
|
719
|
+
|
|
720
|
+
不传 workdir 或传错 workdir 会导致:主仓 task 写到跨仓仓 / 跨仓 task 写到主仓 worktree,破坏隔离与 apply 归属。
|
|
721
|
+
|
|
722
|
+
\`\`\`json
|
|
723
|
+
{
|
|
724
|
+
"subagent_type": "general",
|
|
725
|
+
"workdir": "<按上方 per-task 表选>",
|
|
726
|
+
"prompt": "在此编写任务描述..."
|
|
727
|
+
}
|
|
728
|
+
\`\`\`
|
|
729
|
+
|
|
730
|
+
### 注意
|
|
731
|
+
蓝图文件(tasks.md / design.md / proposal.md / requirements.md / tasks/task-XX.md)在主工作区 .sillyspec/changes/<change>/ 下,它们不在跨仓仓也不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree / 跨仓仓路径下。
|
|
732
|
+
`
|
|
733
|
+
} else {
|
|
734
|
+
worktreeSection = (worktreePath)
|
|
735
|
+
? `
|
|
563
736
|
### 工作目录(必须严格遵守)
|
|
564
737
|
|
|
565
738
|
调用 Task 工具启动子代理时,**workdir 参数是强制必传的**。
|
|
@@ -576,7 +749,8 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
|
|
|
576
749
|
### 注意
|
|
577
750
|
蓝图文件(tasks.md / design.md / proposal.md / requirements.md)在主工作区 .sillyspec/changes/<change>/ 下,它们可能不在 worktree 中。读取蓝图时使用主工作区路径,不要拼接到 worktree 路径下。
|
|
578
751
|
`
|
|
579
|
-
|
|
752
|
+
: ''
|
|
753
|
+
}
|
|
580
754
|
|
|
581
755
|
// ── 派发后端段注入(task-07,D-006 / D-007 / D-008 接入)──
|
|
582
756
|
// 同步判定(不发网络,零回归关键):local(无 MCP 配置)→ dispatchSection='' → 本 prompt 与改前
|
|
@@ -616,7 +790,7 @@ ${prototypes.map(p => `- \`${path.join(protoRelDir, p)}\``).join('\n')}
|
|
|
616
790
|
3. 勾选 plan.md 中的 checkbox
|
|
617
791
|
4. 记录改动文件和测试结果
|
|
618
792
|
|
|
619
|
-
${worktreeSection}${dispatchSection}
|
|
793
|
+
${worktreeSection}${crossRepoCommitSection}${dispatchSection}
|
|
620
794
|
### 任务摘要(按需读取完整蓝图)
|
|
621
795
|
为每个任务启动子代理时,**只需告知任务目标和蓝图文件路径,让子代理按需读取**:
|
|
622
796
|
|
|
@@ -627,6 +801,9 @@ ${taskSummary}
|
|
|
627
801
|
2. 蓝图文件路径(让子代理自行读取详情)
|
|
628
802
|
3. 编码铁律:先读后写、TDD、不编造方法、只做蓝图里写的事、遵守边界处理规则、不超出 allowed_paths
|
|
629
803
|
4. 如存在模块文档(.sillyspec/docs/*/modules/),按需读取涉及模块的 <module>.md 参考接口约定和数据流
|
|
804
|
+
5. 任务含测试代码时,把下方「测试用例设计」整段复制进子代理 prompt,要求子代理按此设计测试用例
|
|
805
|
+
|
|
806
|
+
{{include: testcase-design}}
|
|
630
807
|
|
|
631
808
|
### Wave 开始前
|
|
632
809
|
1. 读取 design.md 的「非目标」与「兼容策略」章节(如存在),确保子代理不超范围、不破坏旧逻辑
|
|
@@ -680,7 +857,7 @@ task-XX 对应:{SPEC_ROOT}/.runtime/execute-runs/{EXECUTE_RUN_ID}/tasks/task-X
|
|
|
680
857
|
|
|
681
858
|
**review.json 必填字段:**
|
|
682
859
|
|
|
683
|
-
{ "name_zh": "任务评审", "schemaVersion":
|
|
860
|
+
{ "name_zh": "任务评审", "schemaVersion": {REVIEW_SCHEMA_VERSION}, "task": "task-XX", "base": "<git-base-commit>", "head": "<git-head-commit>",
|
|
684
861
|
"changedFiles": ["src/foo.js"], "specVerdict": "pass|fail|cannot_verify",
|
|
685
862
|
"qualityVerdict": "pass|fail|cannot_verify", "reviewerNotes": "评审说明",
|
|
686
863
|
"requiredEvidence": [] }
|
|
@@ -729,10 +906,14 @@ export function buildExecuteSteps(planFilePath = null, options = {}) {
|
|
|
729
906
|
// 尝试获取 worktree 路径(可能由前缀步骤创建)
|
|
730
907
|
const worktreePath = options.worktreePath || null
|
|
731
908
|
|
|
909
|
+
// W3 task-08:ctx 由 execute 启动入口(task-09)透传,进程级单例贯穿 execute/apply/verify
|
|
910
|
+
// (D-013)。缺省=null → buildWavePrompt 退化为单仓单 worktreePath(零回归)。
|
|
911
|
+
const ctx = options.ctx || null
|
|
912
|
+
|
|
732
913
|
const waveSteps = waves.map((wave, i) => ({
|
|
733
914
|
name: `Wave ${i + 1} 执行`,
|
|
734
915
|
mode: 'implementation',
|
|
735
|
-
prompt: buildWavePrompt(wave, i + 1, changeDir, worktreePath, { dispatchMode: options.dispatchMode, branch: options.branch }),
|
|
916
|
+
prompt: buildWavePrompt(wave, i + 1, changeDir, worktreePath, { dispatchMode: options.dispatchMode, branch: options.branch, ctx }),
|
|
736
917
|
outputHint: `Wave ${i + 1} 执行结果`,
|
|
737
918
|
optional: false
|
|
738
919
|
}))
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 这些都是 noAI 步骤,不需要 LLM 参与。
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, readFileSync as _readFileSync, readdirSync } from 'fs'
|
|
12
|
+
import { existsSync, readFileSync as _readFileSync, readdirSync, mkdtempSync, writeFileSync, rmSync } from 'fs'
|
|
13
13
|
// 归一化行尾为 LF:Windows 下 python/编辑器文本模式写 .md 会产生 CRLF,致本模块多处
|
|
14
14
|
// frontmatter/字段正则(`^---\n`、`allowed_paths:\s*\n…`、`^goal:` 等)失配,报「缺 frontmatter
|
|
15
15
|
// /缺字段」假错误(见缺陷 windows-python-crlf-taskcard)。读取时统一转 LF,一处覆盖全部正则。
|
|
16
16
|
const readFileSync = (filePath, encoding) => _readFileSync(filePath, encoding).replace(/\r\n/g, '\n')
|
|
17
17
|
import { join as pJoin } from 'path'
|
|
18
|
+
import { tmpdir } from 'os'
|
|
18
19
|
import jsYaml from 'js-yaml'
|
|
19
20
|
import { parseFileChangeList, pathMatches } from '../change-list.js'
|
|
20
21
|
import { getRule } from '../stage-contract-spec.js'
|
|
@@ -80,6 +81,111 @@ export function parseAllowedPaths(content) {
|
|
|
80
81
|
return []
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/**
|
|
85
|
+
* 从 task-NN.md frontmatter 解析标量字段(repo / base_commit / head_commit)。
|
|
86
|
+
* 复用 parseTaskContracts 的 frontmatter 提取 + js-yaml load 模式(标量可选字段,正则对引号/空值脆弱)。
|
|
87
|
+
*
|
|
88
|
+
* design §7.2 跨仓 task 卡 frontmatter 协议:
|
|
89
|
+
* repo: sillyspec # 可选,缺省='main'(调用方视 null 为 main)
|
|
90
|
+
* base_commit: <sha> # 可选,CLI 锡点写入(派发前落盘 base)
|
|
91
|
+
* head_commit: <sha> # 可选,CLI 锡点写入(回收 review 前落盘 head)
|
|
92
|
+
*
|
|
93
|
+
* 缺省 / 空值 / 无 frontmatter → null(向后兼容旧 task 卡)。
|
|
94
|
+
* @param {string} content - task 文件内容
|
|
95
|
+
* @param {string} field - frontmatter 字段名
|
|
96
|
+
* @returns {string|null}
|
|
97
|
+
*/
|
|
98
|
+
function parseFrontmatterScalar(content, field) {
|
|
99
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/)
|
|
100
|
+
if (!fmMatch) return null
|
|
101
|
+
let fm
|
|
102
|
+
try {
|
|
103
|
+
fm = jsYaml.load(fmMatch[1]) || {}
|
|
104
|
+
} catch {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
const v = fm[field]
|
|
108
|
+
if (v == null) return null
|
|
109
|
+
const s = String(v).trim()
|
|
110
|
+
return s === '' ? null : s
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 解析 task-NN.md frontmatter 的 repo 字段(跨仓 task 声明)。
|
|
115
|
+
* 缺省=null(=main,调用方按主仓处理)。design §7.2 / D-001。
|
|
116
|
+
* @param {string} content
|
|
117
|
+
* @returns {string|null}
|
|
118
|
+
*/
|
|
119
|
+
export function parseRepo(content) {
|
|
120
|
+
return parseFrontmatterScalar(content, 'repo')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 解析 task-NN.md frontmatter 的 base_commit 字段(CLI 派发跨仓 task 前落盘的 base 锡点)。
|
|
125
|
+
* 缺省=null(未锡点 / 单仓 task)。design §7.2 / D-010。
|
|
126
|
+
* @param {string} content
|
|
127
|
+
* @returns {string|null}
|
|
128
|
+
*/
|
|
129
|
+
export function parseBaseCommit(content) {
|
|
130
|
+
return parseFrontmatterScalar(content, 'base_commit')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 解析 task-NN.md frontmatter 的 head_commit 字段(CLI 回收 review 前落盘的 head 锡点)。
|
|
135
|
+
* 缺省=null(未锡点 / 单仓 task)。design §7.2 / D-010。
|
|
136
|
+
* @param {string} content
|
|
137
|
+
* @returns {string|null}
|
|
138
|
+
*/
|
|
139
|
+
export function parseHeadCommit(content) {
|
|
140
|
+
return parseFrontmatterScalar(content, 'head_commit')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 从 local.yaml 文本解析 repos: 段(跨仓 workspace 注册表)。
|
|
145
|
+
* 与 parseLocalYamlModules 同风格(轻量行扫描,不引 yaml 依赖),结构 Map<repoKey, absolutePath>。
|
|
146
|
+
*
|
|
147
|
+
* design §7.3 local.yaml repos schema:
|
|
148
|
+
* repos:
|
|
149
|
+
* sillyspec: C:/Users/qinyi/IdeaProjects/sillyspec
|
|
150
|
+
* # main 不用注册(隐式 = cwd / specRoot 父目录)
|
|
151
|
+
*
|
|
152
|
+
* 无 repos 段 / 空段 / 入参空 → 空 Map(单仓 change 不读,向后兼容)。
|
|
153
|
+
*
|
|
154
|
+
* **职责边界**:本函数只 parse local.yaml 文本,不读 local.yaml 文件(文件读取入口归
|
|
155
|
+
* execute 启动 / task-09)。本函数不校验 declaredRepos ⊆ registry(D-007 fail-closed
|
|
156
|
+
* 校验在 MultiRepoContext 构造时做)。
|
|
157
|
+
* @param {string} yamlText
|
|
158
|
+
* @returns {Map<string, string>} repoKey → absolutePath
|
|
159
|
+
*/
|
|
160
|
+
export function parseRepoRegistry(yamlText) {
|
|
161
|
+
const reg = new Map()
|
|
162
|
+
if (!yamlText) return reg
|
|
163
|
+
const lines = yamlText.split('\n')
|
|
164
|
+
let startIdx = -1
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
if (/^repos:\s*(?:#.*)?$/.test(lines[i])) { startIdx = i; break }
|
|
167
|
+
}
|
|
168
|
+
if (startIdx === -1) return reg
|
|
169
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
170
|
+
const line = lines[i]
|
|
171
|
+
// 遇到新的顶层 key(行首非空格且非注释)→ repos 段结束
|
|
172
|
+
if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('\t') && !line.startsWith('#') && line.trim() !== '') break
|
|
173
|
+
// 条目格式: <key>: <value>(key 限 [A-Za-z0-9_.\-],与 parseLocalYamlModules 对齐)
|
|
174
|
+
const entry = line.match(/^\s+([A-Za-z0-9_.\-]+):\s*(.*)$/)
|
|
175
|
+
if (!entry) continue
|
|
176
|
+
const value = (entry[2] || '').trim()
|
|
177
|
+
if (value === '' || value.startsWith('#')) continue
|
|
178
|
+
// 去可选行内注释(`value # comment`)—— 注意路径不含 ` #`,注释前必带空格
|
|
179
|
+
const cleaned = value.replace(/\s+#.*$/, '').trim()
|
|
180
|
+
if (cleaned === '') continue
|
|
181
|
+
// 去首尾可选引号(YAML 字符串引用),保留路径内反斜杠原样
|
|
182
|
+
const path = cleaned.replace(/^['"]|['"]$/g, '')
|
|
183
|
+
if (path === '') continue
|
|
184
|
+
reg.set(entry[1], path)
|
|
185
|
+
}
|
|
186
|
+
return reg
|
|
187
|
+
}
|
|
188
|
+
|
|
83
189
|
/**
|
|
84
190
|
* 检查 task-NN.md 是否包含验收标准章节
|
|
85
191
|
* @param {string} content
|
|
@@ -253,6 +359,11 @@ export function validateBlueprintConsistency(changeDir) {
|
|
|
253
359
|
// task 卡片基础字段文案从 manifest 同源(plan.task-card-structure);${id} 由下方 `${taskId} (${file})` 替换。
|
|
254
360
|
const bsRule = getRule('plan.task-card-structure')
|
|
255
361
|
const taskInfo = new Map()
|
|
362
|
+
// pathOwners 按 (repo, path) 二元组聚合(design §5.3 约束③ / D-008):键 = `${repo}|${path}`,
|
|
363
|
+
// repo 缺省='main'(parseRepo 返 null → main)。跨仓 task 与主仓 task 同名物理路径分属不同 repo
|
|
364
|
+
// → 二元组键不同 → 不误判同 Wave 冲突(如主仓 src/task-review.js vs sillyspec src/task-review.js)。
|
|
365
|
+
// ⚠️ 键分隔符 `|` 假设 repo 名与 path 均不含 `|`(repo 名约束 [A-Za-z0-9_.\-]、path 含 / \,极低概率含 |)。
|
|
366
|
+
// value 结构 { repo, path, owners }:冲突文案对外仍显示纯 path(用户体验),内部按二元组判冲突。
|
|
256
367
|
const pathOwners = new Map()
|
|
257
368
|
|
|
258
369
|
for (const file of taskFiles) {
|
|
@@ -268,6 +379,8 @@ export function validateBlueprintConsistency(changeDir) {
|
|
|
268
379
|
const allowedPaths = parseAllowedPaths(content)
|
|
269
380
|
const hasAcceptance = hasAcceptanceCriteria(content)
|
|
270
381
|
const hasTdd = hasTddOrVerify(content)
|
|
382
|
+
// repo 缺省='main'(parseRepo 返 null → 单仓 task,与既有行为零回归)
|
|
383
|
+
const repo = parseRepo(content) || 'main'
|
|
271
384
|
|
|
272
385
|
taskInfo.set(taskId, { dependsOn, allowedPaths, hasAcceptance, hasTdd, file })
|
|
273
386
|
|
|
@@ -282,17 +395,19 @@ export function validateBlueprintConsistency(changeDir) {
|
|
|
282
395
|
}
|
|
283
396
|
|
|
284
397
|
for (const p of allowedPaths) {
|
|
285
|
-
|
|
286
|
-
pathOwners.
|
|
398
|
+
const key = `${repo}|${p}`
|
|
399
|
+
if (!pathOwners.has(key)) pathOwners.set(key, { repo, path: p, owners: [] })
|
|
400
|
+
pathOwners.get(key).owners.push(taskId)
|
|
287
401
|
}
|
|
288
402
|
}
|
|
289
403
|
|
|
290
|
-
// 路径冲突(Wave 感知):同 Wave 内 >1 task 共享 allowed_path → execute 强制并行(execute.js:603)
|
|
404
|
+
// 路径冲突(Wave 感知):同 repo 同 Wave 内 >1 task 共享 allowed_path → execute 强制并行(execute.js:603)
|
|
291
405
|
// 子代理会互相覆盖该文件 → error。跨 Wave 同文件 → 串行执行安全 → warning。
|
|
406
|
+
// 按 (repo, path) 二元组判冲突(约束③):跨仓 task 与主仓 task 同名路径不误判(不同 repo → 不同键)。
|
|
292
407
|
// Wave 口径 = plan.md 显式 `## Wave N`(parseTaskWavesFromPlan,与 execute 同源,非 topoSort 建议值);
|
|
293
|
-
// plan.md 无显式 Wave → execute 全并行(隐式单 Wave,execute.js:402-408)→ 同文件即冲突。
|
|
408
|
+
// plan.md 无显式 Wave → execute 全并行(隐式单 Wave,execute.js:402-408)→ 同 repo 同文件即冲突。
|
|
294
409
|
const waveOfTask = parseTaskWavesFromPlan(pJoin(changeDir, 'plan.md'))
|
|
295
|
-
for (const [p, owners] of pathOwners) {
|
|
410
|
+
for (const [, { repo, path: p, owners }] of pathOwners) {
|
|
296
411
|
if (owners.length < 2) continue
|
|
297
412
|
if (waveOfTask === null) {
|
|
298
413
|
errors.push(
|
|
@@ -515,6 +630,120 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
|
515
630
|
// 路径容差匹配(normalizePath / globMatch / pathMatches)复用自 change-list.js,
|
|
516
631
|
// design 清单解析与 allowed_paths 对账共用同一套匹配语义,避免两处逻辑漂移。
|
|
517
632
|
|
|
633
|
+
/**
|
|
634
|
+
* design §6 按仓分段段头识别(D-014):`## <repo> 仓变更`(如 `## sillyspec 仓变更`)。
|
|
635
|
+
* 段头为 h2,repo 名取段头首 token(去空白/编号前缀)。容忍可选编号前缀与尾随空白/冒号。
|
|
636
|
+
* 不命中(非仓变更段头)→ null。
|
|
637
|
+
*/
|
|
638
|
+
const REPO_SECTION_HEADER_RE = /^#{2,3}\s+(?:\d+[.)]\s*)?([A-Za-z0-9_.\-]+)\s*仓变更\s*[::]?\s*$/
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* design §6 文件清单章节标题(与 change-list.js FILE_LIST_SECTION_RE 同源,避免 import 私有常量)。
|
|
642
|
+
* 容忍可选编号前缀(`## 6. 文件变更清单`),与 change-list 解析口径一致。
|
|
643
|
+
*/
|
|
644
|
+
const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变更文件清单|文件清单|File Changes|Files to Change)/im
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 按 repo 解析 design §6 文件清单(约束③ / D-014)。
|
|
648
|
+
*
|
|
649
|
+
* 支持 design §6 按仓分段:`## <repo> 仓变更` 段头下的路径归该 repo;无段头时整章节归 'main'
|
|
650
|
+
* (向后兼容单仓 design,与原 parseFileChangeList 行为等价)。段内路径解析复用 parseFileChangeList
|
|
651
|
+
* (表格列定位 / exclude 子段 / 占位符过滤 / CRLF 容错全部继承,零漂移)——方法是:把每段内容包成
|
|
652
|
+
* 临时 design 文件(前置标准 `## 文件变更清单` 标题),写 OS tmp 调 parseFileChangeList,立即清理。
|
|
653
|
+
*
|
|
654
|
+
* 为什么不复用单次 parseFileChangeList:change-list.js 的通用 parser 把段头 `## <repo> 仓变更`
|
|
655
|
+
* 当成「下一个 ## 章节」截断主章节(line 173 `^##\s`),跨仓段路径会丢——这正是 task-03 要修的。
|
|
656
|
+
*
|
|
657
|
+
* 返回值结构(与 pathOwners 二元组对齐):
|
|
658
|
+
* Map<repo, Set<path>> —— 每个 repo 的 design 清单路径集
|
|
659
|
+
* 外加 _hasSegmentHeader 标记(调用方据此决定是否走分段对账路径)
|
|
660
|
+
*
|
|
661
|
+
* @param {string} designPath - design.md 绝对路径
|
|
662
|
+
* @returns {{ byRepo: Map<string, Set<string>>, hasSegmentHeader: boolean, allFiles: string[] }}
|
|
663
|
+
*/
|
|
664
|
+
function parseDesignCoverageByRepo(designPath) {
|
|
665
|
+
const byRepo = new Map()
|
|
666
|
+
const allFiles = []
|
|
667
|
+
if (!designPath || !existsSync(designPath)) {
|
|
668
|
+
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
669
|
+
}
|
|
670
|
+
const content = readFileSync(designPath, 'utf8')
|
|
671
|
+
|
|
672
|
+
const sectionMatch = content.match(FILE_LIST_SECTION_RE)
|
|
673
|
+
if (!sectionMatch) {
|
|
674
|
+
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 主章节起点(match index)→ 扫描到下一个非段头的 `## ` 标题或文件末尾。
|
|
678
|
+
// 段头 `## <repo> 仓变更` 是 h2 但属本章节的子分段,不应触发章节结束。
|
|
679
|
+
const lines = content.slice(sectionMatch.index).split('\n')
|
|
680
|
+
// 先扫一遍:本章节内是否含至少一个段头(决定 hasSegmentHeader / 走分段路径 vs 退化路径)
|
|
681
|
+
let hasSegmentHeader = false
|
|
682
|
+
const sectionLines = []
|
|
683
|
+
for (let i = 0; i < lines.length; i++) {
|
|
684
|
+
const line = lines[i]
|
|
685
|
+
if (i > 0 && /^##\s/.test(line) && !REPO_SECTION_HEADER_RE.test(line)) {
|
|
686
|
+
// 遇到非段头的 h2 标题(如 `## 7. 接口定义`)→ 主章节结束
|
|
687
|
+
break
|
|
688
|
+
}
|
|
689
|
+
if (REPO_SECTION_HEADER_RE.test(line)) hasSegmentHeader = true
|
|
690
|
+
sectionLines.push(line)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (!hasSegmentHeader) {
|
|
694
|
+
// 无段头 → 整章节归 main(等价原 parseFileChangeList 行为)
|
|
695
|
+
const main = new Set(parseFileChangeList(designPath))
|
|
696
|
+
if (main.size > 0) byRepo.set('main', main)
|
|
697
|
+
for (const p of main) allFiles.push(p)
|
|
698
|
+
return { byRepo, hasSegmentHeader: false, allFiles }
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 有段头 → 按 repo 切片,每段构造临时 design 调 parseFileChangeList
|
|
702
|
+
// 当前 repo:段头之前的内容(主章节标题到第一个段头之间)归 'main'
|
|
703
|
+
let currentRepo = 'main'
|
|
704
|
+
const segments = new Map() // repo -> 行数组(含表格/列表,不含段头本身)
|
|
705
|
+
let beforeFirstHeader = [] // 段头之前的 main 段内容
|
|
706
|
+
let seenHeader = false
|
|
707
|
+
for (const line of sectionLines.slice(1)) { // 跳过主章节标题行(sectionLines[0])
|
|
708
|
+
const hdr = line.match(REPO_SECTION_HEADER_RE)
|
|
709
|
+
if (hdr) {
|
|
710
|
+
currentRepo = hdr[1]
|
|
711
|
+
seenHeader = true
|
|
712
|
+
if (!segments.has(currentRepo)) segments.set(currentRepo, [])
|
|
713
|
+
continue
|
|
714
|
+
}
|
|
715
|
+
if (!seenHeader) {
|
|
716
|
+
beforeFirstHeader.push(line)
|
|
717
|
+
} else {
|
|
718
|
+
segments.get(currentRepo).push(line)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// 段头之前的 main 段(若有内容)也要对账
|
|
723
|
+
if (beforeFirstHeader.filter(l => l.trim()).length > 0) {
|
|
724
|
+
segments.set('main', beforeFirstHeader)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const tmpDir = mkdtempSync(pJoin(tmpdir(), 'design-cov-seg-'))
|
|
728
|
+
try {
|
|
729
|
+
for (const [repo, segLines] of segments) {
|
|
730
|
+
// 把段内容包成完整 design md(前置主章节标题,让 parseFileChangeList 识别章节起点)
|
|
731
|
+
const virtualDesign = ['## 文件变更清单', '', ...segLines, ''].join('\n')
|
|
732
|
+
const tmpPath = pJoin(tmpDir, `${repo}.md`)
|
|
733
|
+
writeFileSync(tmpPath, virtualDesign)
|
|
734
|
+
const segFiles = [...parseFileChangeList(tmpPath)]
|
|
735
|
+
if (segFiles.length === 0) continue
|
|
736
|
+
const set = byRepo.get(repo) || new Set()
|
|
737
|
+
for (const p of segFiles) { set.add(p); allFiles.push(p) }
|
|
738
|
+
byRepo.set(repo, set)
|
|
739
|
+
}
|
|
740
|
+
} finally {
|
|
741
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return { byRepo, hasSegmentHeader: true, allFiles }
|
|
745
|
+
}
|
|
746
|
+
|
|
518
747
|
/**
|
|
519
748
|
* design.md 文件变更清单 → tasks allowed_paths 覆盖对账
|
|
520
749
|
*
|
|
@@ -525,6 +754,10 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
|
525
754
|
* 在 plan-postcheck(execute 前)确定性拦截:design 清单中每个源码文件必须被
|
|
526
755
|
* 至少一个 task 的 allowed_paths 覆盖(前缀 / glob 容差匹配)。
|
|
527
756
|
*
|
|
757
|
+
* 跨仓分段对账(约束③ / D-014):design §6 支持 `## <repo> 仓变更` 段头按仓分段,
|
|
758
|
+
* task allowed_paths 按 (repo, path) 二元组归属——跨仓 task 的 allowed_paths 只覆盖
|
|
759
|
+
* 对应 repo 段的 design 清单,不与主仓段交叉误判。无段头时整章节归 main(零回归)。
|
|
760
|
+
*
|
|
528
761
|
* fail-open 边界(不阻断):design.md 不存在、无 task 卡片(none 级别)、
|
|
529
762
|
* task 卡片均无 allowed_paths(由 validatePlanFeasibility 把关)。
|
|
530
763
|
* 阻断边界:有 task 卡片但 design 缺清单章节(light/full 已生成 task → design 必有清单)、
|
|
@@ -552,7 +785,8 @@ export function validateDesignFileCoverage(changeDir) {
|
|
|
552
785
|
return { ok: true, errors, warnings, designFiles: [], uncovered: [] }
|
|
553
786
|
}
|
|
554
787
|
|
|
555
|
-
|
|
788
|
+
// 按仓分段解析 design §6(约束③):段头 `## <repo> 仓变更` → 段内路径归该 repo;无段头 → 全 main
|
|
789
|
+
const { byRepo: designByRepo, hasSegmentHeader, allFiles: designFiles } = parseDesignCoverageByRepo(designPath)
|
|
556
790
|
// 两种断裂文案(缺清单章节 / 文件未覆盖)从 manifest 同源(plan.design-file-coverage.data)。
|
|
557
791
|
const dcRule = getRule('plan.design-file-coverage')
|
|
558
792
|
if (designFiles.length === 0) {
|
|
@@ -561,16 +795,37 @@ export function validateDesignFileCoverage(changeDir) {
|
|
|
561
795
|
errors.push(dcRule.data.messageMissingList)
|
|
562
796
|
return { ok: false, errors, warnings, designFiles: [], uncovered: [] }
|
|
563
797
|
}
|
|
564
|
-
|
|
798
|
+
|
|
799
|
+
// task allowed_paths 也按 (repo, path) 二元组收集(与 pathOwners 同口径,约束③):
|
|
800
|
+
// 跨仓 task 的 allowed_paths 只覆盖对应 repo 的 design 段,不与主仓段交叉误判。
|
|
801
|
+
const taskAllowedByRepo = new Map() // repo -> string[]
|
|
802
|
+
let totalAllowed = 0
|
|
565
803
|
for (const file of taskFiles) {
|
|
566
804
|
const content = readFileSync(pJoin(tasksDir, file), 'utf8')
|
|
567
|
-
|
|
805
|
+
const repo = parseRepo(content) || 'main'
|
|
806
|
+
const allowed = parseAllowedPaths(content)
|
|
807
|
+
if (allowed.length === 0) continue
|
|
808
|
+
totalAllowed += allowed.length
|
|
809
|
+
const arr = taskAllowedByRepo.get(repo) || []
|
|
810
|
+
arr.push(...allowed)
|
|
811
|
+
taskAllowedByRepo.set(repo, arr)
|
|
568
812
|
}
|
|
569
|
-
if (
|
|
813
|
+
if (totalAllowed === 0) {
|
|
570
814
|
return { ok: true, errors, warnings, designFiles, uncovered: [] }
|
|
571
815
|
}
|
|
572
816
|
|
|
573
|
-
|
|
817
|
+
// 对账:design 的每条 (repo, path) 必须被同 repo 的 task allowed_paths 覆盖。
|
|
818
|
+
// 无分段(单仓)时 byRepo 只含 main,taskAllowedByRepo 也只含 main → 退化原扁平对账(零回归)。
|
|
819
|
+
const uncovered = []
|
|
820
|
+
for (const [repo, paths] of designByRepo) {
|
|
821
|
+
const allowed = taskAllowedByRepo.get(repo) || []
|
|
822
|
+
for (const df of paths) {
|
|
823
|
+
// 同 repo 无 task 声明 → 整段无覆盖;或有 task 但无 pathMatches → 未覆盖
|
|
824
|
+
if (!allowed.some(ap => pathMatches(df, ap))) {
|
|
825
|
+
uncovered.push(hasSegmentHeader ? `[${repo}] ${df}` : df)
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
574
829
|
if (uncovered.length > 0) {
|
|
575
830
|
errors.push(
|
|
576
831
|
dcRule.data.messageUncovered
|
package/src/stages/quick.js
CHANGED
|
@@ -50,9 +50,11 @@ export const definition = {
|
|
|
50
50
|
### 操作
|
|
51
51
|
1. 先读后写:调用已有方法前 \`cat\` 源文件确认签名,\`grep\` 确认方法存在
|
|
52
52
|
2. 写代码完成任务
|
|
53
|
-
3.
|
|
53
|
+
3. 如涉及逻辑变更,建议写单元测试验证(不强制,纯配置/文档/小改动可跳过);写测试时按下方「测试用例设计」检查
|
|
54
54
|
4. **不要做无谓的全量编译/构建。** 但——跑针对性的单元测试始终允许且鼓励(见 step3);若本任务就是修测试/构建/基础设施本身,跑测试/构建即验收,应当跑,不算"编译"
|
|
55
55
|
|
|
56
|
+
{{include: testcase-design}}
|
|
57
|
+
|
|
56
58
|
### 输出
|
|
57
59
|
实现摘要 + 修改文件列表
|
|
58
60
|
|
package/src/stages/verify.js
CHANGED
|
@@ -214,7 +214,7 @@ PASS / PASS WITH NOTES / FAIL
|
|
|
214
214
|
## 探针结果
|
|
215
215
|
- 未实现标记扫描:...
|
|
216
216
|
- 关键词覆盖:...
|
|
217
|
-
-
|
|
217
|
+
- 测试覆盖(含断言有效性抽查):...
|
|
218
218
|
- 决策追踪覆盖:...
|
|
219
219
|
- API 契约对账:...
|
|
220
220
|
- 代码删除对账:声明修改/新增却被整文件删除 = 高风险;清单未列出的删除 = 未声明(CLI 会用 git diff --name-status HEAD advisory 复核,仅 warn 不阻断——是否 FAIL blocker 由你诚实判定)
|