sillyspec 3.32.6 → 3.32.8

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.
@@ -204,7 +204,7 @@ export function classifyReviewNeed({ changeDir, input = '', patchText = null, ed
204
204
  * 印出完整 sha 并要求 review.json 的 reviewedAgainst 照抄;漂移重冻结据此区分「对着旧冻结面的
205
205
  * 过期评审」与「对着当前 HEAD 的刚完成评审」(实测竞态:复审 PASS 落盘 26 秒后被误隔离)。
206
206
  */
207
- export function renderReviewerTaskbook({ change, changeDir, head = null }) {
207
+ export function renderReviewerTaskbook({ change, changeDir, head = null, probeLines = null }) {
208
208
  const headFull = String(head || '').trim()
209
209
  // 前轮 findings 注入(2026-10-07-wave-auto-adopt-review-dedup task-02):复审场景(前轮 FAIL/
210
210
  // 评审隔离重评)下任务书带上前轮清单——已报项只验修复与回归,重点找新增问题,勿整轮重报
@@ -261,6 +261,15 @@ export function renderReviewerTaskbook({ change, changeDir, head = null }) {
261
261
  ` 没有任何声明边界时写「无声明边界」。未裁决=未审。`,
262
262
  ` 4. 作答中的防护声明(如有):声称的防护机制在代码里真实存在且生效?`,
263
263
  ` 5. 生成物同步(如涉及):类型/契约文件与实现对齐?`,
264
+ // 项目评审探针(2026-10-12-review-probe-injection):local.yaml review_probes 数据驱动注入——
265
+ // 检查单生产权收归 CLI 的项目特有面(dogfood 实证:主代理手写清单没想到的面无人查)。
266
+ // 未配置零注入——本段缺省时任务书与历史形态逐字一致(零回归)。
267
+ ...(Array.isArray(probeLines) && probeLines.length > 0 ? [
268
+ ``,
269
+ `【项目评审探针(local.yaml review_probes 注入——CLI 单源生产,项目特有面必查)】`,
270
+ ...probeLines.map((l) => ` - ${l}`),
271
+ ` (逐条作答:pass 给证据锚点 / gap 给缺件 / fail 给违诺实证——与上方检查单同口径,未答=未审)`,
272
+ ] : []),
264
273
  ``,
265
274
  `【纪律】只读——禁止修改任何文件、禁止 git 操作;唯一产物是 review.json。`,
266
275
  // 代行惯例(坑 flow-done-sentinel-token-split ②,2026-10-08 实证):宿主无已注册子代理
@@ -272,7 +281,14 @@ export function renderReviewerTaskbook({ change, changeDir, head = null }) {
272
281
  `{`,
273
282
  ` "schemaVersion": 1, "change": "${change}", "reviewer": "subagent",`,
274
283
  ` "verdict": "PASS" | "FAIL",`,
275
- ` "reviewedAgainst": "<评审对象 HEAD 完整 sha——照抄任务书标注的 sha>",`,
284
+ // reviewedAgainst 双形态(2026-10-12-tick-evidence-multi-anchor):跨仓冻结面在场时对象
285
+ // 形态优先(各仓 rev-parse HEAD 可机核「对着哪个面评审」);单仓维持单字符串形态零变化。
286
+ ...(readCrossRepoKeys(changeDir) ? [
287
+ ` "reviewedAgainst": { "workspace": "<主仓 HEAD 完整 sha——照抄任务书标注>", "repos": { "<仓键>": "<该仓 git rev-parse HEAD 完整 sha>" } },`,
288
+ ` (跨仓冻结面在场——对象形态优先;仓键=change-patch.json repos 的 key,各仓 sha 在该仓根 rev-parse 取;字符串单 sha 形态仍兼容)`,
289
+ ] : [
290
+ ` "reviewedAgainst": "<评审对象 HEAD 完整 sha——照抄任务书标注的 sha>",`,
291
+ ]),
276
292
  ` "findings": [ { "severity": "P1|P2|P3", "title": "…", "evidence": "代码锚点+机理一句话", "location": "file:line" } ],`,
277
293
  ` "dimensionNotes": { "乱序": "ok|finding|n/a", "并发": "ok|finding|n/a", "切换": "ok|finding|n/a", "作用域": "ok|finding|n/a" },`,
278
294
  ` "reviewedAt": "<ISO 时间>"`,
@@ -283,10 +299,30 @@ export function renderReviewerTaskbook({ change, changeDir, head = null }) {
283
299
  return taskbook
284
300
  }
285
301
 
302
+ /** 锚 sha 形态(7-40 位 hex)。 */
303
+ const isAnchorSha = (v) => typeof v === 'string' && /^[0-9a-f]{7,40}$/i.test(v.trim())
304
+
305
+ /**
306
+ * 变更的跨仓冻结面仓键(无 change-patch.json / 无 repos / 不可读 → null=单仓形态)。
307
+ * 形状对齐 cross-repo patch 冻结件(repos[] 条目含 key)。
308
+ */
309
+ function readCrossRepoKeys(changeDir) {
310
+ try {
311
+ const cp = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
312
+ if (cp && Array.isArray(cp.repos)) {
313
+ const keys = cp.repos.map((x) => x && (x.key || x.repoKey)).filter(Boolean)
314
+ return keys.length > 0 ? keys : null
315
+ }
316
+ } catch { /* 单仓/不可读 → null */ }
317
+ return null
318
+ }
319
+
286
320
  /**
287
321
  * review.json 校验(三态:ok / schema 错误清单)。FAIL 或含 P1 由调用方拦截。
288
- * reviewedAgainst(评审对象锚,2026-10-06-review-anchor-and-negation):可选字段——缺省
289
- * 容忍(旧产物向后兼容),存在时须为 7-40 位 hex sha。
322
+ * reviewedAgainst(评审对象锚):可选字段——缺省容忍(旧产物向后兼容)。双形态
323
+ * (2026-10-12-tick-evidence-multi-anchor):字符串(既有,7-40 hex)或对象
324
+ * {workspace: sha, repos: {<key>: sha}}——跨仓交付的「对着哪个面评审」可机核;
325
+ * 仓键开放(local.yaml repos 注册表键空间,CLI 不枚举校验键集合)。
290
326
  */
291
327
  export function validateReviewJson(path) {
292
328
  let raw
@@ -296,9 +332,24 @@ export function validateReviewJson(path) {
296
332
  const errors = []
297
333
  if (r.schemaVersion !== 1) errors.push('schemaVersion 必须为 1')
298
334
  if (!['PASS', 'FAIL'].includes(r.verdict)) errors.push('verdict 必须为 PASS|FAIL')
299
- if (r.reviewedAgainst !== undefined
300
- && (typeof r.reviewedAgainst !== 'string' || !/^[0-9a-f]{7,40}$/i.test(r.reviewedAgainst.trim()))) {
301
- errors.push('reviewedAgainst 须为 7-40 位 hex sha(git rev-parse HEAD 输出,照抄任务书标注)')
335
+ if (r.reviewedAgainst !== undefined) {
336
+ if (typeof r.reviewedAgainst === 'string') {
337
+ if (!isAnchorSha(r.reviewedAgainst)) errors.push('reviewedAgainst 须为 7-40 位 hex sha(git rev-parse HEAD 输出,照抄任务书标注)')
338
+ } else if (r.reviewedAgainst && typeof r.reviewedAgainst === 'object' && !Array.isArray(r.reviewedAgainst)) {
339
+ if (!isAnchorSha(r.reviewedAgainst.workspace)) errors.push('reviewedAgainst 对象形态须含 workspace: 7-40 位 hex sha(主仓评审对象锚)')
340
+ if (r.reviewedAgainst.repos !== undefined) {
341
+ const repos = r.reviewedAgainst.repos
342
+ if (!repos || typeof repos !== 'object' || Array.isArray(repos)) {
343
+ errors.push('reviewedAgainst.repos 须为对象(仓键 → sha;仓键取 local.yaml repos 注册表键)')
344
+ } else {
345
+ for (const [k, v] of Object.entries(repos)) {
346
+ if (!isAnchorSha(v)) errors.push(`reviewedAgainst.repos.${k} 须为 7-40 位 hex sha(该仓 git rev-parse HEAD)`)
347
+ }
348
+ }
349
+ }
350
+ } else {
351
+ errors.push('reviewedAgainst 须为 7-40 位 hex sha 字符串,或 {workspace: sha, repos: {<仓键>: sha}} 对象')
352
+ }
302
353
  }
303
354
  if (!Array.isArray(r.findings)) errors.push('findings 必须为数组')
304
355
  else {
@@ -319,6 +370,9 @@ export function validateReviewJson(path) {
319
370
  * 落盘 26 秒后被「重冻结时刻在场=过期」口径误隔离)。reviewedAgainst 缺省/读失败/
320
371
  * 格式非法一律 false(fail-safe:宁可多评一次,不放行无锚证据)。
321
372
  * 匹配口径:双向前缀(reviewedAgainst 允许短 sha 形态,与完整 head 任一方向前缀命中即锚定)。
373
+ * 对象形态(2026-10-12-tick-evidence-multi-anchor)取 workspace 锚参与判定——主仓漂移
374
+ * 判定面不变;repos 各仓锚是记录面(跨仓 HEAD 逐机不可靠 rev-parse,不参与判定),
375
+ * workspace 缺失/非法判 false fail-safe。
322
376
  * @param {string} reviewPath - review.json 路径
323
377
  * @param {string} head - 当前完整 HEAD sha
324
378
  * @returns {boolean}
@@ -328,7 +382,9 @@ export function reviewAnchoredToHead(reviewPath, head) {
328
382
  if (!h) return false
329
383
  try {
330
384
  const r = JSON.parse(readFileSync(reviewPath, 'utf8'))
331
- const ra = typeof r?.reviewedAgainst === 'string' ? r.reviewedAgainst.trim().toLowerCase() : ''
385
+ const raRaw = r?.reviewedAgainst
386
+ const ra = typeof raRaw === 'string' ? raRaw.trim().toLowerCase()
387
+ : (raRaw && typeof raRaw === 'object' && typeof raRaw.workspace === 'string' ? raRaw.workspace.trim().toLowerCase() : '')
332
388
  if (!/^[0-9a-f]{7,40}$/.test(ra)) return false
333
389
  return h === ra || h.startsWith(ra) || ra.startsWith(h)
334
390
  } catch { return false }
@@ -0,0 +1,21 @@
1
+ // flow-state.yaml 单一源读取(2026-10-13-thin-sync-stage:自 flow.js 上移的叶子模块)。
2
+ // 依赖仅 node:fs / node:path / js-yaml——供 progress.js(同步投影)与 flow.js(协议主
3
+ // 实现,re-export 保持 `import('../flow.js')` 既有引用面)共享;叶子化让 progress.js
4
+ // 不必为读一个状态文件拉入 flow 协议整条依赖链(评审 P3:非循环依赖问题,flow.js 静态
5
+ // import 闭包实测不达 progress.js)。
6
+ import { readFileSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+ import yaml from 'js-yaml'
9
+
10
+ export const FLOW_STATE_FILE = 'flow-state.yaml'
11
+
12
+ /** 读 flow-state(缺文件/损坏 → null = 未参与 thin)。 */
13
+ export function readFlowState(changeDir) {
14
+ try {
15
+ const raw = readFileSync(join(changeDir, FLOW_STATE_FILE), 'utf8')
16
+ const obj = yaml.load(raw)
17
+ return obj && typeof obj === 'object' ? obj : null
18
+ } catch {
19
+ return null
20
+ }
21
+ }