sillyspec 3.33.4 → 3.33.6
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-config/SKILL.md +70 -0
- package/.claude/skills/sillyspec-push/SKILL.md +79 -0
- package/AGENTS.md +1 -0
- package/README.md +2 -0
- package/SKILL.md +2 -0
- package/assets/command-cards/push.md +38 -0
- package/package.json +2 -2
- package/src/command-cards.js +1 -1
- package/src/config-schema.js +50 -0
- package/src/config-validate.js +180 -0
- package/src/deleted-ledger.js +3 -1
- package/src/flow-parity.js +62 -2
- package/src/flow-preflight.js +67 -0
- package/src/flow-review.js +19 -3
- package/src/flow.js +318 -70
- package/src/git-helper.js +22 -6
- package/src/index.js +48 -5
- package/src/knowledge-scope.js +82 -0
- package/src/progress/change-registry.js +15 -1
- package/src/push.js +934 -0
- package/src/review-dispatch.js +99 -0
- package/src/run/complete-handlers.js +49 -14
- package/src/run/quick-audit.js +4 -2
- package/src/run/shared.js +1 -1
- package/src/stages/knowledge.js +44 -1
- package/src/verify-postcheck.js +177 -10
- package/templates/agents-instruction.md +2 -1
package/src/flow-parity.js
CHANGED
|
@@ -516,12 +516,66 @@ export function detectPatchDrift({ cwd, change, freezeHead }) {
|
|
|
516
516
|
} catch { return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false } }
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
/**
|
|
520
|
+
* 交付面锚定判定(2026-10-24-flow-done-review-artifact-drift-loop,坑实证三连循环):
|
|
521
|
+
* 复审锚(review.json.reviewedAgainst)之后的本变更 own 提交只碰治理文件(.sillyspec/**,
|
|
522
|
+
* 典型形态=review.json 自身的归档前提交)且未触承诺面(requirements/design)→ 评审对象
|
|
523
|
+
* (交付面)自锚点起未变,结论有效。
|
|
524
|
+
*
|
|
525
|
+
* 堵 detectPatchDrift 两保留路径的复合盲区:reviewAnchoredToHead 要求锚==当前 HEAD——锚与
|
|
526
|
+
* HEAD 之间隔着 review.json 提交即恒 false;governanceOnly 看整个冻结窗——窗口内更早的
|
|
527
|
+
* 复审前修复提交(复审 reviewedAgainst 已覆盖)把 ownFiles 拖进交付文件 → 恒 false →
|
|
528
|
+
* 隔离重评 → 重评再提交 review.json 又前移……循环。本判定把「已覆盖」的度量单位从窗口
|
|
529
|
+
* 换成锚点:锚后的 own 提交才是复审未见的面。
|
|
530
|
+
*
|
|
531
|
+
* fail-safe:锚空/非祖先(rebase 重写)/git 失败 → false(宁可多评)。
|
|
532
|
+
* @returns {{ok: boolean, ownCount: number, files: string[], touchedPromiseFace: boolean}}
|
|
533
|
+
*/
|
|
534
|
+
export function deliveryFaceAnchoredAfter({ cwd, change, anchorSha }) {
|
|
535
|
+
try {
|
|
536
|
+
const a = String(anchorSha || '').trim().toLowerCase()
|
|
537
|
+
if (!/^[0-9a-f]{7,40}$/.test(a)) return { ok: false, ownCount: 0, files: [], touchedPromiseFace: false }
|
|
538
|
+
// 非零退出码(非祖先/git 失败)由 execFileSync 抛出走 catch fail-safe
|
|
539
|
+
execFileSync('git', ['merge-base', '--is-ancestor', a, 'HEAD'], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
|
|
540
|
+
const log = execFileSync('git', ['log', '--name-only', '--format=%x00%s', `${a}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
|
|
541
|
+
const files = []
|
|
542
|
+
let ownCount = 0
|
|
543
|
+
for (const block of String(log).split('\x00')) {
|
|
544
|
+
const lines = block.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
545
|
+
const subject = lines[0]
|
|
546
|
+
if (!subject) continue
|
|
547
|
+
if (parseChangeNamesFromSubject(subject).includes(change)) {
|
|
548
|
+
ownCount++
|
|
549
|
+
for (const f of lines.slice(1)) files.push(f.replace(/\\/g, '/'))
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const promiseFace = [`.sillyspec/changes/${change}/requirements.md`, `.sillyspec/changes/${change}/design.md`]
|
|
553
|
+
const touchedPromiseFace = files.some((f) => promiseFace.includes(f))
|
|
554
|
+
const ok = files.every((f) => f.startsWith('.sillyspec/')) && !touchedPromiseFace
|
|
555
|
+
return { ok, ownCount, files, touchedPromiseFace }
|
|
556
|
+
} catch { return { ok: false, ownCount: 0, files: [], touchedPromiseFace: false } }
|
|
557
|
+
}
|
|
558
|
+
|
|
519
559
|
/**
|
|
520
560
|
* patch 冻结面收集(2026-09-25-thin-r16-patches 修复①,R16 P2:agent 提交晚于 done →
|
|
521
561
|
* 冻结件只有治理件)。committed 提交面为主;exclusive(会话专属 worktree,调用方按
|
|
522
562
|
* gate-snapshot 同款判定注入)时未提交 dirty 交付面一并入冻结——独占树内 dirty 全归属本变更;
|
|
523
563
|
* 共享主仓无法归属 → 不入面只警告(他侧声明的 dirty 连警告都免)。
|
|
524
564
|
*/
|
|
565
|
+
/**
|
|
566
|
+
* dirty_gate_ignore 读取(2026-10-23-dirty-gate-ignore,module_gate_ignore/readFlowConfig
|
|
567
|
+
* 同款逐键正则防线):local.yaml 单行逗号串(目录/文件前缀,posix),列表块形态不支持。
|
|
568
|
+
* 三态 fail-soft——未配置/键值非法/local.yaml 不可读 → 空清单(dirty 门行为与现状逐字一致)。
|
|
569
|
+
*/
|
|
570
|
+
export function readDirtyGateIgnore(specBase) {
|
|
571
|
+
try {
|
|
572
|
+
const raw = readFileSync(join(specBase, 'local.yaml'), 'utf8')
|
|
573
|
+
const m = raw.match(/^\s*dirty_gate_ignore\s*:\s*(.+)$/m)
|
|
574
|
+
if (!m) return []
|
|
575
|
+
return m[1].split(',').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
576
|
+
} catch { return [] }
|
|
577
|
+
}
|
|
578
|
+
|
|
525
579
|
export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive }) {
|
|
526
580
|
const deliverable = (f) => !String(f).replace(/\\/g, '/').startsWith('.sillyspec/')
|
|
527
581
|
&& !isToolGovernancePath(f)
|
|
@@ -540,6 +594,12 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
|
|
|
540
594
|
if (deliverable(path) && !dirty.includes(path)) dirty.push(path)
|
|
541
595
|
}
|
|
542
596
|
} catch { /* git 失败零 dirty 面 */ }
|
|
597
|
+
// 已知残渣前缀剔除(2026-10-23-dirty-gate-ignore,单一源):仓主显式声明的残渣面
|
|
598
|
+
// (quicklog/、测试夹具运行库等)不进 dirty 冻结缺口——剔除进 dirtyIgnored 随返回值
|
|
599
|
+
// 留证(权威门打 ℹ️ 留痕行);预检(flow-preflight.js)经本函数自动同源继承。
|
|
600
|
+
const ignorePrefixes = readDirtyGateIgnore(specBase)
|
|
601
|
+
const dirtyIgnored = ignorePrefixes.length > 0 ? dirty.filter((f) => ignorePrefixes.some((pfx) => f.startsWith(pfx))) : []
|
|
602
|
+
if (dirtyIgnored.length > 0) dirty = dirty.filter((f) => !dirtyIgnored.includes(f))
|
|
543
603
|
// 归属切分单点:exclusive(worktree/--freeze-dirty)与共享主仓共用——他侧已声明 dirty 永不入冻
|
|
544
604
|
// (评审 P2 修复:--freeze-dirty 承诺「非他侧声明」,全量冻入会造成跨变更审计双计;worktree
|
|
545
605
|
// 下 own 恒为全量,切分无伤)
|
|
@@ -547,8 +607,8 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
|
|
|
547
607
|
try {
|
|
548
608
|
ownDirty = splitOwnVsForeignDiffFiles(cwd, change, dirty, { specBase }).own
|
|
549
609
|
} catch { /* 归属切分失败:exclusive 保留全量(声明即边界),共享主仓退全量警告 */ }
|
|
550
|
-
if (exclusive) return { files: [...new Set([...committed, ...ownDirty])], dirtyAdded: ownDirty, dirtyWarned: [] }
|
|
551
|
-
return { files: [...committed], dirtyAdded: [], dirtyWarned: ownDirty }
|
|
610
|
+
if (exclusive) return { files: [...new Set([...committed, ...ownDirty])], dirtyAdded: ownDirty, dirtyWarned: [], dirtyIgnored }
|
|
611
|
+
return { files: [...committed], dirtyAdded: [], dirtyWarned: ownDirty, dirtyIgnored }
|
|
552
612
|
}
|
|
553
613
|
|
|
554
614
|
/**
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* flow-preflight.js — 收口预检(fail-together 一次报全,2026-10-23-flow-done-preflight)
|
|
3
|
+
*
|
|
4
|
+
* 背景(收口画像实证):621 次 flow done 调用/255 变更=2.44 次/变更,净浪费 152 次 RTT(24.5%)
|
|
5
|
+
* 源于逐门 fail-fast 串行拒绝;dirty-gap 门在实测门之后——56% 触发率的门每次拦截都先付一轮
|
|
6
|
+
* 完整实测(58s 起、大仓分钟级)。
|
|
7
|
+
*
|
|
8
|
+
* 本模块是纯前移报告层:实测门前只读收集「独立可算的便宜阻断条件」,由 flow.js 侧渲染一次
|
|
9
|
+
* 报全后拦在付费实测之前。判定与权威门同源同判定(复用同一批函数而非重写),旗标与守卫
|
|
10
|
+
* 逐条镜像 patch 子步:
|
|
11
|
+
* - dirty-gap 预判镜像 collectFreezeFiles dirtyWarned 非空 && !freezeDirty && !acceptDirtyGap
|
|
12
|
+
* && !turbo(turbo 下权威门降 advisory,镜像不拦)
|
|
13
|
+
* - orphan-face 预判镜像仅会话独占 worktree(shouldSkipGateSnapshotForWorktree)且
|
|
14
|
+
* !acceptOrphanFace
|
|
15
|
+
* - 守卫:baseline 缺席跳过(patch 子步整体跳过面);patch 子步已 done 跳过——预检绝不
|
|
16
|
+
* 重复拦已过权威门的面(断点续不误拦)
|
|
17
|
+
*
|
|
18
|
+
* 放弃项(评审已裁决,见 design 风险与死路):review.json schema 预验不做——schema 违规的
|
|
19
|
+
* 成本是「修正文件+重跑」一轮 RTT,与检测时点无关(同轮提前检测不省轮次);且豁免轮
|
|
20
|
+
* (--no-review)权威门不读文件,预检先验会多拦权威门不拦的面(判定漂移),三路皆负。
|
|
21
|
+
*
|
|
22
|
+
* fail-safe 单向:预检异常静默放行(返回空 findings)——漏报由权威门兜底(仍在原子步原序),
|
|
23
|
+
* 预检不构成新失败面;全程零写盘(不落冻结件、不 mark 子步、不写 flow-state)。
|
|
24
|
+
*/
|
|
25
|
+
import { gitQuiet } from './git-helper.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 收集收口预检阻断条件(纯计算零打印)。
|
|
29
|
+
* @returns {{ findings: Array<{id:'dirty-gap',count:number,files:string[]} | {id:'orphan-face',commits:string[],count:number,files:string[]}> }}
|
|
30
|
+
*/
|
|
31
|
+
export async function collectCloseoutPreflight({ cwd, specBase, change, st, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, turbo = false }) {
|
|
32
|
+
const findings = []
|
|
33
|
+
const patchDone = st?.substeps?.patch === 'done'
|
|
34
|
+
// ── dirty-gap / orphan-face 预判(镜像 patch 子步判定链)──
|
|
35
|
+
if (!patchDone && st?.baseline_commit) {
|
|
36
|
+
try {
|
|
37
|
+
let exclusiveFrom = freezeDirty === true ? 'flag' : null
|
|
38
|
+
if (exclusiveFrom == null) {
|
|
39
|
+
try {
|
|
40
|
+
const { shouldSkipGateSnapshotForWorktree } = await import('./run/gate-snapshot.js')
|
|
41
|
+
if (shouldSkipGateSnapshotForWorktree(cwd)) exclusiveFrom = 'worktree'
|
|
42
|
+
} catch { /* 判定异常按共享主仓(权威门同款) */ }
|
|
43
|
+
}
|
|
44
|
+
const exclusive = exclusiveFrom != null
|
|
45
|
+
const committedRaw = String(gitQuiet(cwd, ['diff', '--name-only', `${st.baseline_commit}..HEAD`]) || '')
|
|
46
|
+
.split('\n').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
|
|
47
|
+
const { filterCommittedFace, collectFreezeFiles, scanOrphanFace } = await import('./flow-parity.js')
|
|
48
|
+
const ownPrefix = `.sillyspec/changes/${change}/`
|
|
49
|
+
const committed = filterCommittedFace(committedRaw, ownPrefix)
|
|
50
|
+
const freeze = collectFreezeFiles({ cwd, specBase, change, committed, exclusive })
|
|
51
|
+
if (freeze.dirtyWarned.length > 0 && !freezeDirty && !acceptDirtyGap && !turbo) {
|
|
52
|
+
findings.push({ id: 'dirty-gap', count: freeze.dirtyWarned.length, files: freeze.dirtyWarned })
|
|
53
|
+
}
|
|
54
|
+
if (exclusiveFrom === 'worktree' && !acceptOrphanFace) {
|
|
55
|
+
try {
|
|
56
|
+
const orphan = scanOrphanFace(cwd, { baseline: st.baseline_commit })
|
|
57
|
+
if (orphan.files.length > 0) {
|
|
58
|
+
findings.push({ id: 'orphan-face', commits: orphan.commits, count: orphan.files.length, files: orphan.files })
|
|
59
|
+
}
|
|
60
|
+
} catch { /* orphan 预判 fail-safe(权威门同款不阻断) */ }
|
|
61
|
+
}
|
|
62
|
+
} catch { /* 预检 fail-safe:异常静默放行(权威门仍在 patch 子步原序原判定) */ }
|
|
63
|
+
}
|
|
64
|
+
return { findings }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default { collectCloseoutPreflight }
|
package/src/flow-review.js
CHANGED
|
@@ -337,7 +337,9 @@ export function renderReviewerTaskbook({ change, changeDir, head = null, probeLi
|
|
|
337
337
|
] : [
|
|
338
338
|
` "reviewedAgainst": "<评审对象 HEAD 完整 sha——照抄任务书标注的 sha>",`,
|
|
339
339
|
]),
|
|
340
|
-
` "findings": [ { "severity": "P1|P2|P3", "title": "…", "evidence": "代码锚点+机理一句话", "location": "file:line" } ],`,
|
|
340
|
+
` "findings": [ { "severity": "P1|P2|P3", "status": "open|resolved|resolved-as-accepted|accepted|false-positive", "title": "…", "evidence": "代码锚点+机理一句话", "location": "file:line" } ],`,
|
|
341
|
+
` (增量复审:已解决/接受的历史条目保留在 findings 并标 status——收口阻断只计 open 的 P1;首评可省略 status,缺省=open)`,
|
|
342
|
+
` (写入纪律(2026-10-24-flow-done-review-artifact-drift-loop):review.json 写 ${'<changeDir>/review.json'}——目录不存在=变更已归档/被移,停止并报告,勿重建目录;评审产物保持工作区未提交态,随归档提交一次性入库(先行提交会触发审计时点漂移判定))`,
|
|
341
343
|
` "dimensionNotes": { "乱序": "ok|finding|n/a", "并发": "ok|finding|n/a", "切换": "ok|finding|n/a", "作用域": "ok|finding|n/a" },`,
|
|
342
344
|
` "reviewedAt": "<ISO 时间>"`,
|
|
343
345
|
`}`,
|
|
@@ -354,7 +356,7 @@ export function renderReviewerTaskbook({ change, changeDir, head = null, probeLi
|
|
|
354
356
|
* 不回写文件(校验器只读铁律)。误命中面(如连写分支名含 ≥7 位 hex 段)后果有限:
|
|
355
357
|
* reviewedAgainst 是溯源锚非门禁比对;风险评估见变更 design 风险1。
|
|
356
358
|
*/
|
|
357
|
-
const extractAnchorSha = (v) => {
|
|
359
|
+
export const extractAnchorSha = (v) => {
|
|
358
360
|
if (typeof v !== 'string') return null
|
|
359
361
|
const m = v.match(/(?<![0-9a-fA-F])[0-9a-fA-F]{7,40}(?![0-9a-fA-F])/)
|
|
360
362
|
return m ? m[0].toLowerCase() : null
|
|
@@ -376,11 +378,25 @@ function readCrossRepoKeys(changeDir) {
|
|
|
376
378
|
}
|
|
377
379
|
|
|
378
380
|
/**
|
|
379
|
-
* review
|
|
381
|
+
* finding 开放态判定(2026-10-24-flow-done-review-findings-status-blind,坑实证):增量复审
|
|
382
|
+
* (FAIL 后修复 → resume 同一评审员)天然在 findings 中保留历史条目并标 status——阻断判定
|
|
383
|
+
* 只数开放项。已解决集(评审员侧语义面,大小写不敏感):resolved / resolved-as-accepted /
|
|
384
|
+
* accepted / false-positive;status 缺省/未知值=开放(fail-closed:不因新词表静默放行)。
|
|
385
|
+
*/
|
|
386
|
+
export function isOpenFinding(f) {
|
|
387
|
+
const s = String((f && f.status) || '').trim().toLowerCase()
|
|
388
|
+
if (!s) return true
|
|
389
|
+
return !['resolved', 'resolved-as-accepted', 'accepted', 'false-positive'].includes(s)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* review.json 校验(三态:ok / schema 错误清单)。FAIL 或含开放 P1 由调用方拦截。
|
|
380
394
|
* reviewedAgainst(评审对象锚):可选字段——缺省容忍(旧产物向后兼容)。双形态
|
|
381
395
|
* (2026-10-12-tick-evidence-multi-anchor):字符串(既有,7-40 hex)或对象
|
|
382
396
|
* {workspace: sha, repos: {<key>: sha}}——跨仓交付的「对着哪个面评审」可机核;
|
|
383
397
|
* 仓键开放(local.yaml repos 注册表键空间,CLI 不枚举校验键集合)。
|
|
398
|
+
* findings[].status:可选字段(isOpenFinding 词表)——校验不拒绝未知值(展示面保留),
|
|
399
|
+
* 阻断语义由调用方按 isOpenFinding 收敛(fail-closed)。
|
|
384
400
|
*/
|
|
385
401
|
export function validateReviewJson(path) {
|
|
386
402
|
let raw
|