sillyspec 3.28.10 → 3.28.12
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 +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +3 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/change-risk-profile.js +76 -6
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +23 -0
- package/src/design-facts.js +46 -9
- package/src/diagnostic-codes.js +95 -0
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/doctor-diagnostics.js +141 -1
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +129 -1
- package/src/machine-interface.js +63 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +738 -8
- package/src/stage-review-checklist.js +16 -2
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +8 -3
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +95 -10
- package/src/verify-postcheck.js +346 -28
- package/src/verify-probes.js +1221 -46
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
package/src/stages/verify.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
|
|
2
|
+
|
|
1
3
|
export const definition = {
|
|
2
4
|
name: 'verify',
|
|
3
5
|
title: '验证确认',
|
|
@@ -160,7 +162,7 @@ export const definition = {
|
|
|
160
162
|
2. 如果存在:
|
|
161
163
|
- 逐个读取 tasks/task-NN.md,对照 frontmatter 的 \`acceptance:\` 列表逐条核验(TaskCard 协议的验收标准在 frontmatter YAML,正文无 checkbox)
|
|
162
164
|
- 每条 acceptance 对照实际实现/测试结果判定满足与否,未满足的项列为不通过
|
|
163
|
-
- 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/partial/uncovered/non-testable 四选一)与证据——covered/partial
|
|
165
|
+
- 覆盖对账走探针 7 矩阵:verify-result.md「探针 7:验收×测试覆盖矩阵」段已由 CLI 预填归属测试文件与关键词提示(骨架未生成时先跑 \`sillyspec verify-probes --change <change-name> --init\`,幂等不覆盖已有正文);逐行填判定(covered/partial/uncovered/non-testable 四选一)与证据——covered/partial 附测试锚点(file:line / \`.test.\` 测试文件名 / 反引号包裹的路径或测试名,行号可省),non-testable 写一句理由;判定/证据未填会被 verify \`--done\` 门禁阻断(fail-closed)
|
|
164
166
|
3. 如果不存在:跳过此步骤
|
|
165
167
|
|
|
166
168
|
### 输出
|
|
@@ -194,19 +196,22 @@ export const definition = {
|
|
|
194
196
|
- **误判时的诚实出路(豁免级)**:在 design.md 顶部 frontmatter 加一行 \`risk_level: <真实等级>\`(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical),CLI 会以声明为准覆盖关键词判级。声明后若是 unit-sufficient 等豁免级,PASS WITH NOTES 不再被强制拦;但结论为 PASS 仍需对应证据。
|
|
195
197
|
- **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计;若有命中被否定语境抑制,同样写明被抑制关键词与理由(抑制可审计,不许用来静默降级)。
|
|
196
198
|
你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
|
|
197
|
-
-
|
|
199
|
+
- **集成回执须真实执行——commands.smoke 配置后由 CLI 亲跑并机器落盘(source: cli-noai-smoke 标记,亲跑段谎报无效)**,其余形态仍须 agent 实跑后据实填写:CLI 对其余形态只校验**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
|
|
198
200
|
|
|
199
201
|
3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成十章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`{SPEC_ROOT}/changes/<change-name>/verify-result.md\`(CLI 替换出的**主仓绝对路径**;若当前 cwd 在 worktree 内也绝不落 worktree 副本——CLI 校验读主仓,副本随 worktree 清理蒸发)。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
|
|
200
202
|
4. **预填探针段不可篡改或删除**:verify \`--done\` gate 会重跑探针对比 verify-result.md 正文,不符即拦(ERROR);对探针结果有异议只能在预填段旁追加说明,不得改写原文
|
|
201
203
|
5. **verify-facts.json 为机器底稿,勿手改**:防篡改对比的基准是正文,改底稿无意义
|
|
202
204
|
6. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论只认骨架「结论枚举:」槽行(行首锚定,把 <待填:三选一> 整体替换为枚举值);槽留待填会被 gate 判不过,正文其他位置的 PASS/FAIL 字样不参与判定**
|
|
205
|
+
- **结论想写 PASS 前自查四事实条件(PASS 封顶语义,--done 门禁按 verify-facts.json 实拦)**:①集成实测未跑 ②「## 移交项(结构化)」含 blocking 级行 ③db/**/*.sql 未声明执行(verify-result.md 无「已对目标库执行」声明、回执 command 亦不含该文件)④验收×测试覆盖矩阵含 partial/uncovered 行且移交项零有效行——任一成立即不得写 PASS:改写 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)。severity 口径:db-script/env-blocked 恒 blocking;manual-acceptance/other 默认 advisory、须显式标 blocking 才计入封顶;确需把 blocking 降为 advisory 必须附「(降级:<理由>,依据 <file:line 或 D-xxx>)」
|
|
203
206
|
7. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
|
|
207
|
+
8. **smoke 纪律(local.yaml 配置 commands.smoke 或本变更判级 integration/deployment-critical 时执行以下纪律;本步 prompt 为静态导出,命中条件以文本内嵌说明注入——未命中时本段不适用)**:
|
|
208
|
+
${REVIEW_CHECKLISTS.verify.map((item) => ' - ' + item).join('\n')}
|
|
204
209
|
|
|
205
210
|
### verify-result.md 章节结构(骨架已含,占位替换即可)
|
|
206
211
|
|
|
207
212
|
结论(PASS / PASS WITH NOTES / FAIL)→ 证据账(cannot_verify 任务,槽段)→ 集成验证回执(槽段)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
|
|
208
213
|
|
|
209
|
-
**「## 集成验证回执」槽行结构**(integration/deployment-critical
|
|
214
|
+
**「## 集成验证回执」槽行结构**(integration/deployment-critical 变更必填;其余写「无」)双形态二选一(多行 YAML 形态推荐,字段序无关):\n- claim: <一句话>\n command: <命令>\n exit: <0 或非 0>\n log: <日志路径>\n亦认单行管道形态:\`- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径>\`——CLI 一致性校验四条件:log 存在 × mtime 在 verify 窗口内 × 日志尾无失败签名(error/exception/traceback/fatal 行首,剔除「0 errors」类良性行)× exit 0;全绿才算在场证据,字面措辞不再参与判定(v2 起 literals 仅存量回退)。commands.smoke 配置后由 CLI 亲跑并机器落盘(source: cli-noai-smoke 标记的机器段即其产物);其余形态仍须 agent 实跑后据实填写(CLI 不代跑其余集成进程——回执必须来自你真实执行过的命令)。
|
|
210
215
|
|
|
211
216
|
**Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
|
|
212
217
|
|
package/src/sync.js
CHANGED
|
@@ -1120,6 +1120,28 @@ export class SyncManager {
|
|
|
1120
1120
|
return { synced: 0, errors: [...errors, '无可用文档'] };
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
|
+
// 无变化推送去重(2026-09-17 用户反馈⑥-②,ql-20260917-005):每条 CLI 命令的自动同步都
|
|
1124
|
+
// 全量 POST 四件套 + 打「已同步 N 个文档」——内容未变时的重复推送是纯噪音/流量,也放大
|
|
1125
|
+
// 「同步在反复回写」的干扰体感。四件套内容集合的 sha256 与上次成功推送比对,一致即跳过
|
|
1126
|
+
// (debug 一行);marker 读取失败/缺失按「有变化」处理(多推一次无害,fail-open)。
|
|
1127
|
+
// manual 路径不去重(显式意图,允许空跑确认链路)。
|
|
1128
|
+
let docsFingerprint = null;
|
|
1129
|
+
try {
|
|
1130
|
+
docsFingerprint = createHash('sha256')
|
|
1131
|
+
.update(DOCUMENT_FILES.map(f => `${f}\u0000${documents[f] ?? ''}`).join('\u0001'))
|
|
1132
|
+
.digest('hex');
|
|
1133
|
+
} catch { /* 指纹失败按有变化处理 */ }
|
|
1134
|
+
if (!manual && docsFingerprint) {
|
|
1135
|
+
const markerPath = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime', `sync-docs-lastpush-${changeName}.json`);
|
|
1136
|
+
try {
|
|
1137
|
+
const prev = JSON.parse(readFileSync(markerPath, 'utf8'));
|
|
1138
|
+
if (prev && prev.hash === docsFingerprint) {
|
|
1139
|
+
debugLog(`[sync] 四件套内容与上次成功推送一致,跳过文档直推: ${changeName}`);
|
|
1140
|
+
return { synced: 0, errors, deduped: true };
|
|
1141
|
+
}
|
|
1142
|
+
} catch { /* 无 marker / 损坏 = 首推或按有变化处理 */ }
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1123
1145
|
const docUrl = `${platform.url}/api/changes/${encodeURIComponent(changeName)}/documents`;
|
|
1124
1146
|
const result = await fetchJson(docUrl, {
|
|
1125
1147
|
method: 'POST',
|
|
@@ -1135,6 +1157,14 @@ export class SyncManager {
|
|
|
1135
1157
|
return { synced: 0, errors: [...errors, '文档同步请求失败'] };
|
|
1136
1158
|
}
|
|
1137
1159
|
|
|
1160
|
+
// 推送成功才落指纹 marker(失败不落——下轮重推)
|
|
1161
|
+
if (docsFingerprint) {
|
|
1162
|
+
try {
|
|
1163
|
+
const runtimeDir = join(safePlatformSpecDir(this.cwd) || join(this.cwd, '.sillyspec'), '.runtime');
|
|
1164
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
1165
|
+
writeFileSync(join(runtimeDir, `sync-docs-lastpush-${changeName}.json`), JSON.stringify({ hash: docsFingerprint, at: Date.now() }) + '\n', 'utf8');
|
|
1166
|
+
} catch { /* marker 失败只损失去重(下轮多推一次),不影响同步 */ }
|
|
1167
|
+
}
|
|
1138
1168
|
console.log(`[sync] 已同步 ${syncedCount} 个文档: ${changeName}`);
|
|
1139
1169
|
return { synced: syncedCount, errors };
|
|
1140
1170
|
}
|
|
@@ -1726,6 +1756,8 @@ export class SyncManager {
|
|
|
1726
1756
|
return { ok: false, pulled: false, specDir, reason: `删除本地 specDir 失败${holdHint}: ${err.message}` };
|
|
1727
1757
|
}
|
|
1728
1758
|
try {
|
|
1759
|
+
let pulledFiles = 0;
|
|
1760
|
+
const crlfEntries = [];
|
|
1729
1761
|
for (const e of entries) {
|
|
1730
1762
|
_assertSafeTarName(e.name.replace(/\/+$/, ''), specDir);
|
|
1731
1763
|
const fullPath = join(specDir, e.name);
|
|
@@ -1734,8 +1766,23 @@ export class SyncManager {
|
|
|
1734
1766
|
} else {
|
|
1735
1767
|
mkdirSync(dirname(fullPath), { recursive: true });
|
|
1736
1768
|
writeFileSync(fullPath, e.data);
|
|
1769
|
+
pulledFiles++;
|
|
1770
|
+
if (e.data && e.data.length < 4 * 1024 * 1024) crlfEntries.push([e.name, e.data]); // CRLF 画像采样(超 4MB 大文件跳过采样,写入不受影响)
|
|
1737
1771
|
}
|
|
1738
1772
|
}
|
|
1773
|
+
// 批量写入留痕(2026-09-17 反馈⑥②法证收口):整树解包=逐字节回放,他机 CRLF 内容
|
|
1774
|
+
// 原样落盘——审计行记 时间/触发面/文件数/CRLF 命中数(fail-open)。
|
|
1775
|
+
try {
|
|
1776
|
+
const { appendWriteAudit, detectCrlfFiles, WRITE_AUDIT_FILE_CAP } = await import('./write-audit.js');
|
|
1777
|
+
const crlfPaths = detectCrlfFiles(crlfEntries);
|
|
1778
|
+
appendWriteAudit(specDir, {
|
|
1779
|
+
via: 'pull-spec-bundle',
|
|
1780
|
+
files: pulledFiles,
|
|
1781
|
+
crlfPaths: crlfPaths.slice(0, WRITE_AUDIT_FILE_CAP),
|
|
1782
|
+
crlfCount: crlfPaths.length,
|
|
1783
|
+
force: opts.force === true,
|
|
1784
|
+
});
|
|
1785
|
+
} catch { /* 审计 fail-open */ }
|
|
1739
1786
|
if (localYamlSaved !== null) {
|
|
1740
1787
|
mkdirSync(specDir, { recursive: true }); // 空 bundle(零条目)时树根未被条目重建
|
|
1741
1788
|
writeAtomicSync(localYamlPath, localYamlSaved); // fs-atomic 契约:hook/probe 并发读 local.yaml
|
|
@@ -1816,6 +1863,7 @@ export class SyncManager {
|
|
|
1816
1863
|
let overwritten = 0;
|
|
1817
1864
|
let removed = 0;
|
|
1818
1865
|
const skipped = [];
|
|
1866
|
+
const writtenEntries = []; // [path, data] 对——写后审计 CRLF 画像用(2026-09-17 反馈⑥②法证收口)
|
|
1819
1867
|
for (const p of normalized) {
|
|
1820
1868
|
try {
|
|
1821
1869
|
_assertSafeTarName(p, specDir);
|
|
@@ -1825,6 +1873,7 @@ export class SyncManager {
|
|
|
1825
1873
|
mkdirSync(dirname(full), { recursive: true });
|
|
1826
1874
|
writeFileSync(full, data);
|
|
1827
1875
|
overwritten++;
|
|
1876
|
+
writtenEntries.push([p, data]);
|
|
1828
1877
|
} else if (existsSync(join(specDir, p))) {
|
|
1829
1878
|
// 服务器已无该文件(远端删除)→ 接受服务器 = 删本地
|
|
1830
1879
|
rmSync(join(specDir, p), { force: true });
|
|
@@ -1835,6 +1884,26 @@ export class SyncManager {
|
|
|
1835
1884
|
}
|
|
1836
1885
|
}
|
|
1837
1886
|
|
|
1887
|
+
// 批量写入留痕(2026-09-17 用户反馈⑥②法证收口):take-platform 是「单命令批量整文件
|
|
1888
|
+
// 重写 + 逐字节保留来源行尾」的回放面——他机 CRLF 内容会原样落盘且事后无任何日志可查
|
|
1889
|
+
// (console 打印即蒸发,实证只能靠 mtime 考古)。落一条审计行:时间/触发面/文件集/CRLF
|
|
1890
|
+
// 命中。fail-open:审计失败不影响覆盖结果。
|
|
1891
|
+
try {
|
|
1892
|
+
const { appendWriteAudit, detectCrlfFiles, WRITE_AUDIT_FILE_CAP } = await import('./write-audit.js');
|
|
1893
|
+
const crlfPaths = detectCrlfFiles(writtenEntries);
|
|
1894
|
+
if (crlfPaths.length > 0) {
|
|
1895
|
+
console.warn(`⚠️ [sync] take-platform 覆盖的 ${crlfPaths.length} 个文件内容为 CRLF 行尾(他机编辑器产物按字节回放,本仓自产文件一律 LF)——git add 时 autocrlf 会提示转换,属预期:${crlfPaths.slice(0, 5).join('、')}${crlfPaths.length > 5 ? ' 等' : ''}(明细见 .runtime/write-audit.jsonl)`);
|
|
1896
|
+
}
|
|
1897
|
+
appendWriteAudit(specDir, {
|
|
1898
|
+
via: 'platform-resolve-take-platform',
|
|
1899
|
+
overwritten, removed,
|
|
1900
|
+
files: writtenEntries.map(([p]) => p).slice(0, WRITE_AUDIT_FILE_CAP)
|
|
1901
|
+
.concat(writtenEntries.length > WRITE_AUDIT_FILE_CAP ? [`…+${writtenEntries.length - WRITE_AUDIT_FILE_CAP}`] : []),
|
|
1902
|
+
crlfPaths: crlfPaths.slice(0, WRITE_AUDIT_FILE_CAP),
|
|
1903
|
+
crlfCount: crlfPaths.length,
|
|
1904
|
+
});
|
|
1905
|
+
} catch { /* 审计 fail-open */ }
|
|
1906
|
+
|
|
1838
1907
|
// 基线快照刷新(best-effort):被覆盖路径回写当前盘面 hash(= 服务器内容 hash)、被删
|
|
1839
1908
|
// 路径移除条目——下次同步这些路径按「本地未改动」豁免回推(take-platform 语义闭环)。
|
|
1840
1909
|
try {
|
package/src/task-review.js
CHANGED
|
@@ -341,8 +341,11 @@ export function readReview(reviewPath) {
|
|
|
341
341
|
* (ql-20260915-001 修复②,坑 review-write-draft-marker-stuck:execute 期间反复
|
|
342
342
|
* 「草稿 review changedFiles 为空,跳过自动勾选」,主仓文档面任务自动勾选断链。)
|
|
343
343
|
*
|
|
344
|
-
* 判据:reviewProvenanceStamp 落的 writtenBy
|
|
345
|
-
*
|
|
344
|
+
* 判据:reviewProvenanceStamp 落的 writtenBy 以显式写入通道前缀开头——writeTaskReview
|
|
345
|
+
* (`sillyspec review write`,含 ':force' 变体)或 adoptTaskReviewMechanics(adopt 通道
|
|
346
|
+
* 一键重算代填,落 writtenBy=adoptTaskReviewMechanics#pid;task-05 / FR-08 补入:该通道
|
|
347
|
+
* 合并写入的 review 带 agent 供给 verdict,此前被判草稿、跨仓 task 自动勾选断链)。
|
|
348
|
+
* 两通道均为显式写入(非草稿兜底通道),即使 reviewerNotes 字面残留
|
|
346
349
|
* 'auto-generated draft'(agent 升级草稿时 --notes 引用/复述草稿文案、或 adopt 通道合并
|
|
347
350
|
* 保留旧 notes 的变体)也一律按非草稿处理——verdict 判定不再受草稿零 diff 守卫限制。
|
|
348
351
|
*
|
|
@@ -352,8 +355,11 @@ export function readReview(reviewPath) {
|
|
|
352
355
|
* @param {object|null} review readReview 返回的 review 对象
|
|
353
356
|
* @returns {boolean}
|
|
354
357
|
*/
|
|
358
|
+
const EXPLICIT_REVIEW_WRITE_PREFIXES = ['writeTaskReview', 'adoptTaskReviewMechanics']
|
|
359
|
+
|
|
355
360
|
export function isExplicitReviewWrite(review) {
|
|
356
|
-
|
|
361
|
+
if (typeof review?.writtenBy !== 'string') return false
|
|
362
|
+
return EXPLICIT_REVIEW_WRITE_PREFIXES.some(p => review.writtenBy.startsWith(p))
|
|
357
363
|
}
|
|
358
364
|
|
|
359
365
|
/**
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* 双层写入模型(D-001@v2):机器数据(probes/tests/factsConsistency)CLI 直写;判断层
|
|
9
9
|
* (结论枚举/证据状态/回执声明)经 verify-result.md 受控槽段录入、--done 时 CLI 解析固化
|
|
10
10
|
* (slot-backfill),单向渲染约束对象是机器数据。
|
|
11
|
+
*
|
|
12
|
+
* PASS 封顶事实面扩展(2026-09-17-pass-cap-semantics task-01 / D-011):db 脚本执行声明的
|
|
13
|
+
* 录入文法(X-03)= 回执槽条目 command 含 db/<file>.sql,或声明行「已对目标库执行:db/<file>.sql」,
|
|
14
|
+
* 解析器 parseDbScriptDeclarations 落 verify-probes.js(producer:backfillFactsFromMdAndTests
|
|
15
|
+
* 产出 facts.dbScriptDeclarations;跨仓消费方:worktree-apply 兜底声明门按「文件集 ∩ db/*.sql
|
|
16
|
+
* ⊆ parseDbScriptDeclarations(verifyMd)」对账)。
|
|
11
17
|
*/
|
|
12
18
|
export const FACTS_SCHEMA_VERSION = 2
|
|
13
19
|
|
|
@@ -52,11 +58,22 @@ const EVIDENCE_LINE_RE = /^- (task-\d+):\s*(satisfied|missing|partial)\b([^\n]*)
|
|
|
52
58
|
*/
|
|
53
59
|
const RECEIPT_LINE_RE = /^- claim:\s*([^||\n]+)[||]\s*command:\s*([^||\n]+)[||]\s*exit:\s*(\d+)\s*[||]\s*log:\s*(.+)$/
|
|
54
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 多行 YAML 形态续行:` key: value`(缩进键值行,2026-09-16-friction5-hardening FR-01 / D-001@v1)。
|
|
63
|
+
* 单行管道形态 agent 手写时列错位/字段序调换/多行书写整行不命中 → 回执槽收 0 条 →
|
|
64
|
+
* integration-critical 误报无绿回执——多行聚合根除形态自由度问题;字段序无关(command 可在
|
|
65
|
+
* claim 前),聚合遇首个不匹配行(下一列表项/非缩进行/空行/标题)即止。
|
|
66
|
+
*/
|
|
67
|
+
const RECEIPT_CONT_LINE_RE = /^[ \t]+([A-Za-z_][\w-]*):\s*(.*)$/
|
|
68
|
+
|
|
55
69
|
/**
|
|
56
70
|
* 解析 verify-result.md 的两个受控槽段(D-001@v2 slot-backfill 录入界面)。
|
|
57
71
|
* 占位形态(<待填:三选一> / <待填:0 或非 0>)不含枚举词,行首锚定 + 枚举词双保险 fail-closed。
|
|
58
72
|
* @param {string} mdText verify-result.md 全文(LF 归一由调用方 readFileSync 层负责或此处容错)
|
|
59
|
-
* @returns {{ requiredEvidence: Array<{task,status,exempt:boolean,exemptionReason:string|null,verifiedFiles:string[]}>, runtimeEvidence: Array<{claim,command,exitCode,logPath}>, hasEvidenceSlot: boolean, hasReceiptSlot: boolean }}
|
|
73
|
+
* @returns {{ requiredEvidence: Array<{task,status,exempt:boolean,exemptionReason:string|null,verifiedFiles:string[]}>, runtimeEvidence: Array<{claim,command,exitCode,logPath,source?:string}>, hasEvidenceSlot: boolean, hasReceiptSlot: boolean }}
|
|
74
|
+
* runtimeEvidence[].source(2026-09-17-api-coverage-smoke task-02,additive):单行形态 |/|
|
|
75
|
+
* 尾注 `source: <token>` 或多行 YAML `source:` 字段回填(CLI 机器段标记 cli-noai-smoke 的
|
|
76
|
+
* 解析入口);无标记条目不落键,四字段 fail-closed 语义不动。
|
|
60
77
|
*/
|
|
61
78
|
export function parseEvidenceSlots(mdText) {
|
|
62
79
|
const text = String(mdText || '').replace(/\r\n/g, '\n')
|
|
@@ -101,15 +118,56 @@ export function parseEvidenceSlots(mdText) {
|
|
|
101
118
|
|
|
102
119
|
const runtimeEvidence = []
|
|
103
120
|
if (receiptBody) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
// 回执双形态解析(FR-01 / D-001@v1):①单行管道形态 RECEIPT_LINE_RE 命中即收(正则与
|
|
122
|
+
// 字段语义逐字节不变,存量回执零回归);②不命中且行首 `- claim:` 起头时进入多行 YAML
|
|
123
|
+
// 聚合——收集缩进续行(RECEIPT_CONT_LINE_RE)为 key→value(值 trim、剥首尾成对反引号),
|
|
124
|
+
// 首行行内剩余只作 claim 值(claim 含管道符不截断);四字段 = claim/command 非空(trim 后)
|
|
125
|
+
// + exit 纯数字 + log 非空(值内 |/| 尾注取首段,同单行口径),齐才 push——任一缺失/空
|
|
126
|
+
// 整条不收(fail-closed 宁漏不误收,坑 receipt-fullwidth-parse 形态自由度的根除收口)。
|
|
127
|
+
const stripPairedBackticks = (v) => {
|
|
128
|
+
const t = v.trim()
|
|
129
|
+
return t.length >= 2 && t.startsWith('`') && t.endsWith('`') ? t.slice(1, -1) : t
|
|
130
|
+
}
|
|
131
|
+
for (let i = 0; i < receiptBody.length; i++) {
|
|
132
|
+
const m = receiptBody[i].match(RECEIPT_LINE_RE)
|
|
133
|
+
if (m) {
|
|
134
|
+
// source 尾注提取(2026-09-17-api-coverage-smoke task-02,additive 第五字段):log 是
|
|
135
|
+
// rest-of-line,行内 |/| 尾注段中认 `source: <token>`(CLI 机器段行尾注形态
|
|
136
|
+
// 「| source: cli-noai-smoke」);首段 log 剥尾注口径零变化,四字段收取不动,无尾注
|
|
137
|
+
// 条目不落 source 键(存量回执零回归)。
|
|
138
|
+
const srcM = m[4].match(/[||]\s*source:\s*([^\s||]+)/)
|
|
139
|
+
runtimeEvidence.push({
|
|
140
|
+
claim: m[1].trim(),
|
|
141
|
+
command: m[2].trim(),
|
|
142
|
+
exitCode: Number(m[3]),
|
|
143
|
+
logPath: m[4].split(/[||]/)[0].trim().replace(/^`|`$/g, ''),
|
|
144
|
+
...(srcM ? { source: srcM[1] } : {}),
|
|
145
|
+
})
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
const head = receiptBody[i].match(/^-\s*claim:\s*(.*)$/)
|
|
149
|
+
if (!head) continue
|
|
150
|
+
const fields = {}
|
|
151
|
+
let j = i + 1
|
|
152
|
+
while (j < receiptBody.length) {
|
|
153
|
+
const cm = receiptBody[j].match(RECEIPT_CONT_LINE_RE)
|
|
154
|
+
if (!cm) break
|
|
155
|
+
if (!(cm[1] in fields)) fields[cm[1]] = stripPairedBackticks(cm[2])
|
|
156
|
+
j++
|
|
157
|
+
}
|
|
158
|
+
i = j - 1 // 聚合消费至 j-1,回退一格交还外层 for 推进
|
|
159
|
+
const claim = [head[1].trim(), typeof fields.claim === 'string' ? fields.claim.trim() : '']
|
|
160
|
+
.filter(s => s.length > 0).join(' ').trim()
|
|
161
|
+
const command = typeof fields.command === 'string' ? fields.command : ''
|
|
162
|
+
const exit = typeof fields.exit === 'string' ? fields.exit.trim() : ''
|
|
163
|
+
const log = typeof fields.log === 'string'
|
|
164
|
+
? fields.log.split(/[||]/)[0].trim().replace(/^`|`$/g, '')
|
|
165
|
+
: ''
|
|
166
|
+
if (!claim || !command || !/^\d+$/.test(exit) || !log) continue
|
|
167
|
+
// source 字段回填(多行 YAML 形态,task-02 additive):fields.source 聚合时已过
|
|
168
|
+
// stripPairedBackticks(同款口径),空值不落键——四字段 fail-closed 收取条件不动。
|
|
169
|
+
const src = typeof fields.source === 'string' ? fields.source.trim() : ''
|
|
170
|
+
runtimeEvidence.push({ claim, command, exitCode: Number(exit), logPath: log, ...(src ? { source: src } : {}) })
|
|
113
171
|
}
|
|
114
172
|
}
|
|
115
173
|
|
|
@@ -144,5 +202,32 @@ export function validateFactsV2(facts) {
|
|
|
144
202
|
errors.push(`requiredEvidence[${item.task}] satisfied 但 verifiedFiles 为空`)
|
|
145
203
|
}
|
|
146
204
|
}
|
|
205
|
+
// PASS 封顶事实面 additive 登记(2026-09-17-pass-cap-semantics task-01 / D-011 故障面;
|
|
206
|
+
// smokeRan 增 2026-09-17-api-coverage-smoke task-03 / FR-02):字段不在场一律不报错
|
|
207
|
+
// (存量 facts 零迁移通过,producer=backfillFactsFromMdAndTests 首次 backfill 产出);
|
|
208
|
+
// 在场才校验类型/枚举。schemaVersion 保持 2。
|
|
209
|
+
if (facts.integrationRan != null && !['ran', 'not-ran'].includes(facts.integrationRan)) {
|
|
210
|
+
errors.push(`integrationRan 非法枚举值:${facts.integrationRan}`)
|
|
211
|
+
}
|
|
212
|
+
if (facts.smokeRan != null && !['ran', 'not-ran', 'not-configured'].includes(facts.smokeRan)) {
|
|
213
|
+
errors.push(`smokeRan 非法枚举值:${facts.smokeRan}`)
|
|
214
|
+
}
|
|
215
|
+
if (facts.dbScriptDeclarations != null
|
|
216
|
+
&& (!Array.isArray(facts.dbScriptDeclarations) || !facts.dbScriptDeclarations.every(x => typeof x === 'string'))) {
|
|
217
|
+
errors.push('dbScriptDeclarations 应为 string[]')
|
|
218
|
+
}
|
|
219
|
+
if (facts.matrixPartialRows != null
|
|
220
|
+
&& (!Number.isInteger(facts.matrixPartialRows) || facts.matrixPartialRows < 0)) {
|
|
221
|
+
errors.push(`matrixPartialRows 应为非负整数,实际 ${facts.matrixPartialRows}`)
|
|
222
|
+
}
|
|
223
|
+
if (facts.runtimeEndpointExcluded != null && typeof facts.runtimeEndpointExcluded !== 'boolean') {
|
|
224
|
+
errors.push(`runtimeEndpointExcluded 应为 boolean,实际 ${typeof facts.runtimeEndpointExcluded}`)
|
|
225
|
+
}
|
|
226
|
+
const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
|
|
227
|
+
for (const [i, item] of handoverItems.entries()) {
|
|
228
|
+
if (item && item.severity != null && !['blocking', 'advisory'].includes(item.severity)) {
|
|
229
|
+
errors.push(`handover.items[${i}] severity 非法枚举值:${item.severity}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
147
232
|
return { ok: errors.length === 0, errors }
|
|
148
233
|
}
|