sillyspec 3.28.10 → 3.28.11

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.
Files changed (45) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
  4. package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
  5. package/package.json +1 -1
  6. package/src/change-risk-profile.js +51 -3
  7. package/src/config-schema.js +21 -0
  8. package/src/design-facts.js +46 -9
  9. package/src/docs-check.js +14 -10
  10. package/src/docs-debt.js +7 -2
  11. package/src/docs-gate.js +42 -15
  12. package/src/endpoint-extractor.js +50 -2
  13. package/src/foreign-declared.js +25 -16
  14. package/src/hooks/worktree-guard.js +75 -2
  15. package/src/index.js +129 -1
  16. package/src/machine-interface.js +26 -0
  17. package/src/probe7-anchor-check.js +20 -2
  18. package/src/progress/stage-machine.js +34 -5
  19. package/src/progress.js +1 -1
  20. package/src/quicklog.js +231 -1
  21. package/src/run/complete-handlers.js +172 -1
  22. package/src/run/complete.js +73 -3
  23. package/src/run/gate-snapshot.js +262 -0
  24. package/src/run/gates.js +41 -7
  25. package/src/run/prompt.js +79 -1
  26. package/src/run/shared.js +10 -0
  27. package/src/stage-contract-spec.js +14 -0
  28. package/src/stage-contract.js +314 -8
  29. package/src/stage-review-checklist.js +3 -1
  30. package/src/stage-review.js +137 -2
  31. package/src/stages/archive.js +9 -5
  32. package/src/stages/brainstorm.js +7 -1
  33. package/src/stages/execute.js +62 -8
  34. package/src/stages/plan-postcheck.js +42 -5
  35. package/src/stages/plan.js +8 -1
  36. package/src/stages/quick.js +5 -5
  37. package/src/stages/verify.js +3 -2
  38. package/src/sync.js +69 -0
  39. package/src/task-review.js +9 -3
  40. package/src/verify-facts-schema.js +78 -9
  41. package/src/verify-postcheck.js +260 -26
  42. package/src/verify-probes.js +780 -44
  43. package/src/worktree-apply.js +175 -3
  44. package/src/write-audit.js +55 -0
  45. package/.claude/full.json +0 -1
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 {
@@ -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 以 'writeTaskReview' 开头(含 ':force' 变体)。
345
- * 显式写入带 agent 供给的 verdict(非草稿兜底通道),即使 reviewerNotes 字面残留
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
- return typeof review?.writtenBy === 'string' && review.writtenBy.startsWith('writeTaskReview')
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,6 +58,14 @@ 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。
@@ -101,15 +115,47 @@ export function parseEvidenceSlots(mdText) {
101
115
 
102
116
  const runtimeEvidence = []
103
117
  if (receiptBody) {
104
- for (const line of receiptBody) {
105
- const m = line.match(RECEIPT_LINE_RE)
106
- if (!m) continue
107
- runtimeEvidence.push({
108
- claim: m[1].trim(),
109
- command: m[2].trim(),
110
- exitCode: Number(m[3]),
111
- logPath: m[4].split(/[||]/)[0].trim().replace(/^`|`$/g, ''),
112
- })
118
+ // 回执双形态解析(FR-01 / D-001@v1):①单行管道形态 RECEIPT_LINE_RE 命中即收(正则与
119
+ // 字段语义逐字节不变,存量回执零回归);②不命中且行首 `- claim:` 起头时进入多行 YAML
120
+ // 聚合——收集缩进续行(RECEIPT_CONT_LINE_RE)为 key→value(值 trim、剥首尾成对反引号),
121
+ // 首行行内剩余只作 claim 值(claim 含管道符不截断);四字段 = claim/command 非空(trim 后)
122
+ // + exit 纯数字 + log 非空(值内 |/| 尾注取首段,同单行口径),齐才 push——任一缺失/空
123
+ // 整条不收(fail-closed 宁漏不误收,坑 receipt-fullwidth-parse 形态自由度的根除收口)。
124
+ const stripPairedBackticks = (v) => {
125
+ const t = v.trim()
126
+ return t.length >= 2 && t.startsWith('`') && t.endsWith('`') ? t.slice(1, -1) : t
127
+ }
128
+ for (let i = 0; i < receiptBody.length; i++) {
129
+ const m = receiptBody[i].match(RECEIPT_LINE_RE)
130
+ if (m) {
131
+ runtimeEvidence.push({
132
+ claim: m[1].trim(),
133
+ command: m[2].trim(),
134
+ exitCode: Number(m[3]),
135
+ logPath: m[4].split(/[||]/)[0].trim().replace(/^`|`$/g, ''),
136
+ })
137
+ continue
138
+ }
139
+ const head = receiptBody[i].match(/^-\s*claim:\s*(.*)$/)
140
+ if (!head) continue
141
+ const fields = {}
142
+ let j = i + 1
143
+ while (j < receiptBody.length) {
144
+ const cm = receiptBody[j].match(RECEIPT_CONT_LINE_RE)
145
+ if (!cm) break
146
+ if (!(cm[1] in fields)) fields[cm[1]] = stripPairedBackticks(cm[2])
147
+ j++
148
+ }
149
+ i = j - 1 // 聚合消费至 j-1,回退一格交还外层 for 推进
150
+ const claim = [head[1].trim(), typeof fields.claim === 'string' ? fields.claim.trim() : '']
151
+ .filter(s => s.length > 0).join(' ').trim()
152
+ const command = typeof fields.command === 'string' ? fields.command : ''
153
+ const exit = typeof fields.exit === 'string' ? fields.exit.trim() : ''
154
+ const log = typeof fields.log === 'string'
155
+ ? fields.log.split(/[||]/)[0].trim().replace(/^`|`$/g, '')
156
+ : ''
157
+ if (!claim || !command || !/^\d+$/.test(exit) || !log) continue
158
+ runtimeEvidence.push({ claim, command, exitCode: Number(exit), logPath: log })
113
159
  }
114
160
  }
115
161
 
@@ -144,5 +190,28 @@ export function validateFactsV2(facts) {
144
190
  errors.push(`requiredEvidence[${item.task}] satisfied 但 verifiedFiles 为空`)
145
191
  }
146
192
  }
193
+ // PASS 封顶事实面五字段 additive 登记(2026-09-17-pass-cap-semantics task-01 / D-011 故障面):
194
+ // 字段不在场一律不报错(存量 facts 零迁移通过,producer=backfillFactsFromMdAndTests 首次
195
+ // backfill 产出);在场才校验类型/枚举。schemaVersion 保持 2。
196
+ if (facts.integrationRan != null && !['ran', 'not-ran'].includes(facts.integrationRan)) {
197
+ errors.push(`integrationRan 非法枚举值:${facts.integrationRan}`)
198
+ }
199
+ if (facts.dbScriptDeclarations != null
200
+ && (!Array.isArray(facts.dbScriptDeclarations) || !facts.dbScriptDeclarations.every(x => typeof x === 'string'))) {
201
+ errors.push('dbScriptDeclarations 应为 string[]')
202
+ }
203
+ if (facts.matrixPartialRows != null
204
+ && (!Number.isInteger(facts.matrixPartialRows) || facts.matrixPartialRows < 0)) {
205
+ errors.push(`matrixPartialRows 应为非负整数,实际 ${facts.matrixPartialRows}`)
206
+ }
207
+ if (facts.runtimeEndpointExcluded != null && typeof facts.runtimeEndpointExcluded !== 'boolean') {
208
+ errors.push(`runtimeEndpointExcluded 应为 boolean,实际 ${typeof facts.runtimeEndpointExcluded}`)
209
+ }
210
+ const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
211
+ for (const [i, item] of handoverItems.entries()) {
212
+ if (item && item.severity != null && !['blocking', 'advisory'].includes(item.severity)) {
213
+ errors.push(`handover.items[${i}] severity 非法枚举值:${item.severity}`)
214
+ }
215
+ }
147
216
  return { ok: errors.length === 0, errors }
148
217
  }