sillyspec 3.29.4 → 3.30.0
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/run/quick-audit.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* 本模块被 command.js 静态 import,顶部静态拉 @inquirer/prompts 会进**每条** run 命令
|
|
12
12
|
* 启动路径(实测冷加载 100-150ms),而 checkbox 仅"≥2 活跃变更 + TTY"分支用到
|
|
13
13
|
*/
|
|
14
|
-
import { join } from 'node:path'
|
|
14
|
+
import { join, dirname } from 'node:path'
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
15
16
|
import { parsePorcelainPath, safeGit } from './shared.js'
|
|
16
17
|
import { collectRecentForeignDelivery, detectAssertionRewrites, readSemanticGuardEnabled } from '../semantic-guard.js'
|
|
17
18
|
|
|
@@ -352,6 +353,55 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
352
353
|
|
|
353
354
|
// ============ quick --done test+lint 硬门禁(2026-09-02 跨 agent 工单 P0-2)============
|
|
354
355
|
|
|
356
|
+
// ── 测试最低面 advisory(资产三小件②,2026-09-23)口径常量 ──
|
|
357
|
+
// 代码文件判定双通道(原为 runQuickTestLintGate 函数内常量,advisory 复用同口径提为模块级;
|
|
358
|
+
// 无 /g 标志无状态,提级零行为差):
|
|
359
|
+
// ① 路径段匹配:任意层出现 src / test / tests / __tests__ 段(src-guide 等长名不误蹭——按段全等)
|
|
360
|
+
// ② 代码扩展名兜底:src/test 目录约定外的代码(backend/app/**.py、scripts/*.go 等)
|
|
361
|
+
const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
|
|
362
|
+
const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
|
|
363
|
+
|
|
364
|
+
// 测试文件判定:目录段(test/tests/__tests__——比 quick-gate-profile 的 TEST_DIR_RE 多认裸
|
|
365
|
+
// test/ 段,本仓 test/ 下即有无 .test. 后缀的 .mjs)+ basename test_ 前缀 + .test./_test./.spec.
|
|
366
|
+
// 命名三信号任一命中(与 quick-gate-profile isTestPath 同判法补 test 段)。
|
|
367
|
+
const ADVISORY_TEST_DIR_RE = /(^|\/)(test|tests|__tests__)(\/|$)/i
|
|
368
|
+
|
|
369
|
+
function isAdvisoryTestPath(p) {
|
|
370
|
+
if (ADVISORY_TEST_DIR_RE.test(p)) return true
|
|
371
|
+
const base = p.slice(p.lastIndexOf('/') + 1)
|
|
372
|
+
if (/^test_/i.test(base)) return true
|
|
373
|
+
return /[._](test|spec)\.[^.]+$/i.test(base)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 最低测试面 advisory 阈值:src 类交付文件 ≥3 且测试改动 0 才出警示(R7-L 重放校准) */
|
|
377
|
+
const MIN_SRC_FILES_FOR_TEST_ADVISORY = 3
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 测试最低面 advisory(资产三小件②,advisory 不阻断——测试门 fail-closed 语义零改动)。
|
|
381
|
+
*
|
|
382
|
+
* 背景:R7-L 重放实测测试量仅为旧流程 40%(1162 vs 2909 行)——轻量变更/burst 收口走
|
|
383
|
+
* runQuickTestLintGate,无 quick 出口 L1 门禁的 testDelta 检查,测试厚度零约束。
|
|
384
|
+
* 判定:变更交付文件(非 .sillyspec)中 src 类文件(代码双通道判定 × 非测试文件)
|
|
385
|
+
* ≥3 个而测试文件 0 个改动(P2 账本测试面亦无增量可对账——账本测试面=test/ 目录
|
|
386
|
+
* 内容摘要,测试零改动=两信号同态)→ 返回一行警告文案;负例(有测试改动/纯 doc/
|
|
387
|
+
* 少文件)返回 null。
|
|
388
|
+
* @param {string[]} files 变更文件清单(changedFiles 审计口径或 declaredFiles 兜底口径)
|
|
389
|
+
* @returns {string|null}
|
|
390
|
+
*/
|
|
391
|
+
export function buildTestSurfaceAdvisory(files) {
|
|
392
|
+
const list = (Array.isArray(files) ? files : [])
|
|
393
|
+
.map((f) => String(f).replace(/\\/g, '/'))
|
|
394
|
+
.filter((f) => f && !f.startsWith('.sillyspec/'))
|
|
395
|
+
let srcCount = 0
|
|
396
|
+
let testCount = 0
|
|
397
|
+
for (const f of list) {
|
|
398
|
+
if (isAdvisoryTestPath(f)) { testCount++; continue }
|
|
399
|
+
if (CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f)) srcCount++
|
|
400
|
+
}
|
|
401
|
+
if (srcCount < MIN_SRC_FILES_FOR_TEST_ADVISORY || testCount > 0) return null
|
|
402
|
+
return `⚠️ 测试面厚度 advisory:交付面 ${srcCount} 个 src 类文件但测试面为空(0 个测试文件改动,P2 账本测试面无增量可对账),确认无需测试增量?(advisory 不阻断——轻量变更/burst 实证测试量仅为旧流程 40%)`
|
|
403
|
+
}
|
|
404
|
+
|
|
355
405
|
/**
|
|
356
406
|
* 语义护栏命中组装(change: 2026-09-11-cross-change-decision-guard,task-06,FR-04,
|
|
357
407
|
* D-001@v1):断言重写检测 ∩ 近因他者交付归因 → hits 清单,gate 内一行调用拼进返回对象。
|
|
@@ -407,22 +457,42 @@ export function buildSemanticGuardHits({ cwd, specBase, files, changeName } = {}
|
|
|
407
457
|
* semanticGuard 仅检测跑过(三条早退 skip 路径不跑检测、无字段)时挂载;WARNING 级
|
|
408
458
|
* 非阻断——action/failed 判定不受它影响(调用方 complete-handlers.js 只读 action/failed,零改动)。
|
|
409
459
|
*/
|
|
410
|
-
|
|
460
|
+
/**
|
|
461
|
+
* 门禁文件集合并(ql-018):审计口径 ∪ 声明边界,去重保序(审计在前)。声明文件即使被
|
|
462
|
+
* 审计判为「前序 baseline 未计入本轮」也随会话进快照——声明即边界(治 declared∩前序脏
|
|
463
|
+
* 被二选一丢弃→快照 HEAD 旧版→门禁恒假红,2026-09-23 ql-017 三轮实证)。纯函数。
|
|
464
|
+
*/
|
|
465
|
+
export function mergeGateFiles(audited, declared) {
|
|
466
|
+
const out = []
|
|
467
|
+
const seen = new Set()
|
|
468
|
+
for (const f of [...(Array.isArray(audited) ? audited : []), ...(Array.isArray(declared) ? declared : [])]) {
|
|
469
|
+
if (typeof f !== 'string' || !f || seen.has(f)) continue
|
|
470
|
+
seen.add(f)
|
|
471
|
+
out.push(f)
|
|
472
|
+
}
|
|
473
|
+
return out
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], declaredFiles = [], changeName = null, skipSentinel = false }) {
|
|
411
477
|
if (process.env.SILLYSPEC_QUICK_TEST_GATE === 'skip') {
|
|
412
478
|
return { action: 'skip', failed: [], reason: 'SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(审计留痕)', test: null, lint: null }
|
|
413
479
|
}
|
|
414
480
|
const audited = Array.isArray(changedFiles) ? changedFiles : []
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
481
|
+
// 门禁文件集 = 审计 ∪ 声明边界(并集,2026-09-23 ql-018 修复):旧口径二选一(审计非空时
|
|
482
|
+
// 丢弃 declaredFiles)致「declared ∩ 会话启动前已脏」的文件不进快照 overlay——快照装 HEAD
|
|
483
|
+
// 旧版,本会话新增导出缺失,快照内测试 import 即炸且重跑恒红(快照分叉家族第三 sibling
|
|
484
|
+
// 的根治件:声明过的文件无论审计口径是否计入,一律随会话进快照——声明即边界)。倒推 B
|
|
485
|
+
// 兜底语义保留(审计为空时 declaredFiles 独撑,与旧版一致);并集对快照零成本(overlay 与
|
|
486
|
+
// HEAD 同内容的文件不产生差异)。
|
|
487
|
+
const files = mergeGateFiles(audited, Array.isArray(declaredFiles) ? declaredFiles : [])
|
|
488
|
+
const fileSource = audited.length > 0
|
|
489
|
+
? (files.length > audited.length ? `审计∪声明(审计 ${audited.length} + 声明补入 ${files.length - audited.length})` : '审计')
|
|
490
|
+
: (files.length > 0 ? '声明边界兜底(倒推 B:文件早于会话启动被基线吸收)' : '无')
|
|
418
491
|
// 代码文件判定(2026-09-19 monorepo 子包实证修复:multi-agent-platform 回带收口 6 个
|
|
419
492
|
// sillyhub-daemon/src/**、frontend/src/** 文件被「纯 doc/配置」误判跳过实测):旧口径只认
|
|
420
493
|
// 仓根 src/、test/ 前缀——monorepo 子包(<pkg>/src/**、backend/app/**.py)全漏。
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
// ② 代码扩展名兜底:src/test 目录约定外的代码(backend/app/**.py、scripts/*.go 等)
|
|
424
|
-
const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
|
|
425
|
-
const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
|
|
494
|
+
// 新口径双通道,方向取「宁可多跑不可漏跑」(门禁漏跑=静默放行,多跑只是费一次实测),
|
|
495
|
+
// 常量已提为模块级 CODE_PATH_SEGMENT_RE / CODE_EXTENSION_RE(advisory 同口径复用)。
|
|
426
496
|
const codeFiles = files.filter(
|
|
427
497
|
(f) => typeof f === 'string' && (CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f)),
|
|
428
498
|
)
|
|
@@ -433,6 +503,11 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
433
503
|
return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null }
|
|
434
504
|
}
|
|
435
505
|
|
|
506
|
+
// ── 测试最低面 advisory(资产三小件②):放在两条早退之后——纯 doc/env skip/空清单
|
|
507
|
+
// 天然零输出(负例语义);此处测试门确定要跑,厚度提示与实测结论同场可见。──
|
|
508
|
+
const surfaceAdvisory = buildTestSurfaceAdvisory(files)
|
|
509
|
+
if (surfaceAdvisory) console.warn(`\n${surfaceAdvisory}`)
|
|
510
|
+
|
|
436
511
|
// ── 语义护栏检测(task-06,FR-04 断言重写 WARNING,D-001@v1 非阻断)──
|
|
437
512
|
// 三条早退(env skip / 无文件 / 纯 doc)已在上文 return,检测不跑——语义一致。
|
|
438
513
|
// 检测对主仓 cwd 执行而非 gateCwd 隔离快照(下方才建快照):检测对象是本会话工作树
|
|
@@ -451,18 +526,83 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
451
526
|
let gateSpecBase = specBase
|
|
452
527
|
let snapshot = null
|
|
453
528
|
if (files.length > 0 && !process.env.SILLYSPEC_QUICK_GATE_SNAPSHOT_OFF) {
|
|
529
|
+
// worktree 会话跳快照(2026-09-23 R9/R10 根治,与 verify-quality-scan 同款):worktree
|
|
530
|
+
// 已是会话独占隔离,快照只剩 junction 冻结/慢 I/O 纯成本——直接 worktree 实测。
|
|
531
|
+
let skipForWorktree = false
|
|
532
|
+
try {
|
|
533
|
+
const { shouldSkipGateSnapshotForWorktree } = await import('./gate-snapshot.js')
|
|
534
|
+
skipForWorktree = shouldSkipGateSnapshotForWorktree(cwd)
|
|
535
|
+
} catch { /* 判定异常按不跳走原路 */ }
|
|
536
|
+
if (skipForWorktree) {
|
|
537
|
+
console.log('🔀 cwd 是会话专属 worktree——隔离快照冗余(并行会话不在场),直接在 worktree 实测')
|
|
538
|
+
} else {
|
|
454
539
|
try {
|
|
455
540
|
const { createGateSnapshot } = await import('./gate-snapshot.js')
|
|
456
|
-
|
|
541
|
+
// 账本 runtimeRoot(2026-09-24-gate-snapshot-lifecycle task-03):同本文件 P2 三键
|
|
542
|
+
// 账本咨询口径(下方 ~570 行 resolveRuntimeRoot(null, specBase))单源;解析失败传 null
|
|
543
|
+
// (账本链路 no-op,禁从 cwd 猜)
|
|
544
|
+
let runtimeRoot = null
|
|
545
|
+
try {
|
|
546
|
+
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
547
|
+
runtimeRoot = resolveRuntimeRoot(null, specBase)
|
|
548
|
+
} catch { runtimeRoot = null }
|
|
549
|
+
snapshot = createGateSnapshot({ cwd, files, runtimeRoot })
|
|
457
550
|
if (snapshot) {
|
|
458
551
|
gateCwd = snapshot.snapshotRoot
|
|
459
552
|
gateSpecBase = join(snapshot.snapshotRoot, '.sillyspec')
|
|
460
553
|
console.log(`🧪 门禁隔离快照(根:${snapshot.snapshotRoot}):HEAD + 本会话 ${snapshot.overlaid} 个文件(并行会话脏文件不参与判定)`)
|
|
461
554
|
}
|
|
462
555
|
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
556
|
+
}
|
|
463
557
|
}
|
|
464
558
|
try {
|
|
465
|
-
|
|
559
|
+
// ── P2 三键账本(D-001@v1,batch3 task-01):同码同环境免重跑(fail-closed——无记录/
|
|
560
|
+
// 键不等/分量不可得=真跑,行为不变)。quick 会话名 per-change 天然隔离账本文件。──
|
|
561
|
+
let testLedgerReuse = null
|
|
562
|
+
if (changeName) {
|
|
563
|
+
try {
|
|
564
|
+
const { consultTestLedger } = await import('./test-ledger.js')
|
|
565
|
+
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
566
|
+
const consult = consultTestLedger({
|
|
567
|
+
runtimeRoot: resolveRuntimeRoot(null, specBase), changeName,
|
|
568
|
+
projectRoot: gateCwd, testRoot: join(gateCwd, 'test'), specBase: gateSpecBase, cwd: gateCwd,
|
|
569
|
+
})
|
|
570
|
+
if (consult.reuse) testLedgerReuse = consult
|
|
571
|
+
} catch { /* 咨询异常 → 真跑 */ }
|
|
572
|
+
}
|
|
573
|
+
if (!testLedgerReuse) {
|
|
574
|
+
// 沙箱实测预告(坑 quick-done-长静默与快照行尾假阳性 坑1,2026-09-21 实证):快照内
|
|
575
|
+
// test+lint 实测大仓可达 841-1054s 且期间零输出——外层 exec 按「疑似挂死」杀进程每轮
|
|
576
|
+
// 重建沙箱耗时成倍。预告时长预期与勿杀指引(心跳输出需异步改造留专项,预告先消除
|
|
577
|
+
// 「零信息误判」主因)。
|
|
578
|
+
console.log(`⏳ 开始 test+lint 实测${snapshot ? '(隔离快照内)' : ''}——大仓全量可达 15-20 分钟,期间无输出属正常;后台跑 + 长容忍(≥20 分钟),勿按超时杀进程(杀掉不丢进度,但每轮重建沙箱重跑实测耗时成倍)。`)
|
|
579
|
+
}
|
|
580
|
+
let test = testLedgerReuse
|
|
581
|
+
? { status: 'passed', reason: `♻️ P2 三键账本复用(代码×测试面×环境全等;实测于 ${testLedgerReuse.result.ranAt})`, command: `${testLedgerReuse.runPlan?.[0]?.runner || 'npm test'} (ledger-reuse)`, exitCode: 0, durationMs: 0, outputTail: null }
|
|
582
|
+
// restrictFiles = 本会话声明文件(坑 quick-gate-并行全流程变更脏文件误伤):模块选择
|
|
583
|
+
// 与 deps(auto) 只取「实际变更 ∩ 本会话声明」,并行全流程变更 WIP 不再挡死本会话
|
|
584
|
+
// faceOverride = 同一清单作权威文件面(R23-thin 实证盲区修复:快照锚 HEAD 看不见
|
|
585
|
+
// 「先提交再收口」的已提交交付——files 源自 flow done 主仓侧 baseline..HEAD+status
|
|
586
|
+
// 归因,含已提交交付;快照只隔离执行不重推文件面)
|
|
587
|
+
: runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, restrictFiles: files, faceOverride: files })
|
|
588
|
+
// 快照 test 超时/零输出冻结 → 主仓复跑(R9 实证 2026-09-23;对齐 2026-09-12 lint 同款
|
|
589
|
+
// 回退先例——快照内 junction I/O 病态慢/venv 冻结无解,主仓口径保硬门;并行噪声混入时
|
|
590
|
+
// 失败输出带归属鉴定提示兜底)。冻结特征=超时且 outputTail 空(真慢套件有持续输出)。
|
|
591
|
+
if (snapshot && test.status === 'failed' && /超时/.test(String(test.reason || ''))) {
|
|
592
|
+
console.warn(`⚠️ 快照 test ${test.outputTail ? '超时' : '超时且零输出(疑似冻结)'}(junction I/O 病态慢/冻结)→ 主仓复跑 test(并行噪声可能混入——失败先做归属鉴定)`)
|
|
593
|
+
test = runVerifyTestCheck({ cwd, specBase, changeName, restrictFiles: files, faceOverride: files })
|
|
594
|
+
}
|
|
595
|
+
if (!testLedgerReuse && changeName && test.status === 'passed') {
|
|
596
|
+
try {
|
|
597
|
+
const { recordTestLedger } = await import('./test-ledger.js')
|
|
598
|
+
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
599
|
+
recordTestLedger({
|
|
600
|
+
runtimeRoot: resolveRuntimeRoot(null, specBase), changeName,
|
|
601
|
+
projectRoot: gateCwd, testRoot: join(gateCwd, 'test'), specBase: gateSpecBase, cwd: gateCwd,
|
|
602
|
+
result: { pass: true, durationMs: test.durationMs ?? null, strategy: test.strategy ?? null },
|
|
603
|
+
})
|
|
604
|
+
} catch { /* 记账异常不影响门禁(下次仍真跑) */ }
|
|
605
|
+
}
|
|
466
606
|
let lint = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined })
|
|
467
607
|
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
468
608
|
// 超时杀——主仓 60~110s 正常。按用户建议「自动回退」而非假败/advisory:主仓复跑保硬门,
|
|
@@ -472,8 +612,67 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
472
612
|
lint = runVerifyLintCheck({ cwd, specBase })
|
|
473
613
|
}
|
|
474
614
|
const failed = []
|
|
475
|
-
|
|
476
|
-
|
|
615
|
+
// 哨兵断言(2026-09-25-sentinel-wiring,与 flow done 同判):tasks.md 全勾但零完成
|
|
616
|
+
// 证据(区间提交消息标题与正文均无 task-NN token 且无对应 review.json)→ 计入 failed 拒收。
|
|
617
|
+
// 证据面=整条提交消息(2026-09-25-thin-done-gate-calibration 坑2,同 flow 侧 %B%x1e 口径)。
|
|
618
|
+
// skipSentinel:flow done 调用方自带 baseline 哨兵(更可靠区间),传 true 跳过免双判。
|
|
619
|
+
if (!skipSentinel) try {
|
|
620
|
+
const { detectFakeCheckCompletion } = await import('../sentinel-assertions.js')
|
|
621
|
+
const tasksDir = changeName ? join(specBase, 'changes', changeName, 'tasks.md') : null
|
|
622
|
+
if (tasksDir && existsSync(tasksDir)) {
|
|
623
|
+
const _log = safeGit ? safeGit(cwd, ['log', '--format=%B%x1e', 'HEAD~10..HEAD']) : null
|
|
624
|
+
const commitMsgs = String((_log && !_log.error) ? _log.value : '') || ''
|
|
625
|
+
if (commitMsgs) {
|
|
626
|
+
const sent = detectFakeCheckCompletion({ changeDir: dirname(tasksDir), tasksMd: readFileSync(tasksDir, 'utf8'), commits: commitMsgs.split('\x1e').map((x) => x.trim()).filter(Boolean) })
|
|
627
|
+
if (sent.status === 'fake') {
|
|
628
|
+
console.error(`\n🚫 哨兵断言拒收:tasks.md 全勾(${sent.checked}/${sent.claimTotal})但 ${sent.missing.length} 个任务零完成证据(区间提交标题与正文均无 token):${sent.missing.join('、')}`)
|
|
629
|
+
console.error(' 补证据(提交标题或正文带 task-NN,或产 review.json)或取消勾选后重跑——假完成主张不许过门')
|
|
630
|
+
failed.push('sentinel')
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
} catch (e) { console.warn(`⚠️ 哨兵断言失败(fail-open 放行): ${(e && e.message) || e}`) }
|
|
635
|
+
// 纯超时降档(R4 门禁价值考古:36/106 失败是 600s 帽杀纯超时假拦——超时=未完成非测试挂)。
|
|
636
|
+
// 所有失败单元 reason 均含超时 → advisory 不计入 failed;任一单元真实挂测 → 维持硬拦。
|
|
637
|
+
if (test.status === 'failed') {
|
|
638
|
+
const { isTimeoutOnlyTestFailure } = await import('../verify-postcheck.js')
|
|
639
|
+
if (isTimeoutOnlyTestFailure(test)) {
|
|
640
|
+
console.warn(`\n⚠️ quick test 实测纯超时(无任何挂掉的测试)——不拦完成:${test.reason || ''}`)
|
|
641
|
+
console.warn(' 处置:定向复跑触碰模块自证;常发超时建议 local.yaml 配 modules: 块启用模块子集实测。')
|
|
642
|
+
} else {
|
|
643
|
+
failed.push('test')
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
// ── lint 归属鉴定降档(R4-S-F/R4-S-Q 同根因:隔离快照含 HEAD 存量债文件时 lint 恒败,
|
|
647
|
+
// agent 被逼范围外清偿才过门)。失败输出提及文件 × 本会话文件集零交集 → 存量债
|
|
648
|
+
// advisory 放行不拦完成;有交集 / 输出无可识别路径(unattributable)→ 维持硬拦。──
|
|
649
|
+
if (lint.status === 'failed') {
|
|
650
|
+
const { triageLintOwnership } = await import('../verify-postcheck.js')
|
|
651
|
+
const own = triageLintOwnership({
|
|
652
|
+
failureFiles: lint.failureFiles || [],
|
|
653
|
+
changeFiles: files.map(f => String(f).replace(/\\/g, '/')),
|
|
654
|
+
})
|
|
655
|
+
if (own.verdict === 'pre-existing') {
|
|
656
|
+
console.warn(`\n⚠️ quick lint 实测失败,但归属鉴定为 HEAD 存量债(失败提及文件与本会话文件集零交集)——不拦完成`)
|
|
657
|
+
console.warn(` 存量债文件:${(lint.failureFiles || []).join('、') || '(见上方输出)'}`)
|
|
658
|
+
console.warn(' 处置建议:单独 quick 机械清偿解锁全仓 lint 门(顺手清债);确与本会话无关可忽略本行。')
|
|
659
|
+
} else if (snapshot) {
|
|
660
|
+
// 主仓对照复核(坑 quick-done-长静默与快照行尾假阳性 坑2,2026-09-21 实证:快照
|
|
661
|
+
// Would reformat 本会话文件、主仓同命令 1294 文件全绿——快照 checkout 行尾转换
|
|
662
|
+
// 与 ruff 字节级比对不一致的环境假阳性)。失败文件与本会话有交集时主仓复跑同命令:
|
|
663
|
+
// 主仓绿 = 环境假阳性 advisory 放行(本会话文件在真实工作区是格式合规的);主仓
|
|
664
|
+
// 也红 = 真实格式债维持硬拦(主仓可能有并行噪声,失败输出带归属鉴定提示)。
|
|
665
|
+
const mainLint = runVerifyLintCheck({ cwd, specBase })
|
|
666
|
+
if (mainLint.status === 'passed') {
|
|
667
|
+
console.warn(`\n⚠️ 快照 lint 失败但主仓同命令全绿——判定为快照环境假阳性(行尾转换类),不拦完成`)
|
|
668
|
+
console.warn(` 快照失败面:${(lint.failureFiles || []).join('、') || '(见上方输出)'};主仓口径已实证格式合规。`)
|
|
669
|
+
} else {
|
|
670
|
+
failed.push('lint')
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
failed.push('lint')
|
|
674
|
+
}
|
|
675
|
+
}
|
|
477
676
|
if (failed.length > 0 && snapshot) {
|
|
478
677
|
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
479
678
|
printSnapshotFailureHint({ snapshotRoot: snapshot.snapshotRoot, changeFileCount: snapshot.overlaid, sourceRoot: snapshot.sourceRoot || null }, { offEnv: 'SILLYSPEC_QUICK_GATE_SNAPSHOT_OFF' })
|
package/src/run/shared.js
CHANGED
|
@@ -152,8 +152,14 @@ export function resolveSpecDir(cwd, opts = {}) {
|
|
|
152
152
|
// 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
|
|
153
153
|
// passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
|
|
154
154
|
if (!(passedHome || (dir === home && originBelowHome))) {
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// 嵌套回环防护(坑 2026-09-27-spec-sync-413 根因2,2026-08-15 遗留实证):
|
|
156
|
+
// .sillyspec 的合法布局恒为 <root>/.sillyspec(.runtime 在其内层),<…>/.runtime/
|
|
157
|
+
// .sillyspec 是历史残骸形态(内部常再递归 .runtime/,目录遍历/状态采集进圈即卡死
|
|
158
|
+
// 30s 超时)——该层候选不判命中,继续上溯真根。
|
|
159
|
+
if (basename(dir) !== '.runtime') {
|
|
160
|
+
const candidate = join(dir, '.sillyspec')
|
|
161
|
+
if (existsSync(candidate)) return candidate
|
|
162
|
+
}
|
|
157
163
|
}
|
|
158
164
|
if (dir === home) passedHome = true
|
|
159
165
|
const parent = dirname(dir)
|
|
@@ -853,6 +859,26 @@ async function raceWithAbort(op, timeoutMs = resolveSyncTotalTimeoutMs()) {
|
|
|
853
859
|
// 局部复制正则保持轻载——改形态时三处同改(prompt.js 经本导出消费,勿再复制一份)。
|
|
854
860
|
export const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
855
861
|
|
|
862
|
+
// 未连接平台显眼告警(坑 2026-09-27-spec-sync-413-and-nested-runtime:主仓 local.yaml 无
|
|
863
|
+
// platform 段 → --done 自动同步静默 no-op,用户误判「已同步」——平台断档 8 天才发现)。
|
|
864
|
+
// 节流每日至多一次:本地独立使用是合法默认状态(sync.js 头注「不每步」契约保持),告警
|
|
865
|
+
// 只破「静默」不变骚扰;marker 落 runtimeRoot(跨进程同源),读写失败不拦告警本身。
|
|
866
|
+
function warnPlatformNotConnectedDaily(cwd, platformOpts = {}) {
|
|
867
|
+
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
868
|
+
try {
|
|
869
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
870
|
+
const marker = join(runtimeRoot, 'platform-not-connected-notice.date')
|
|
871
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
872
|
+
if (existsSync(marker) && readFileSync(marker, 'utf8').trim() === today) return
|
|
873
|
+
mkdirSync(runtimeRoot, { recursive: true })
|
|
874
|
+
writeFileSync(marker, today, 'utf8')
|
|
875
|
+
} catch { /* marker 失败仍告警(宁可重复不静默) */ }
|
|
876
|
+
// 走 stdout 而非 stderr:advisory 性质;且顶层别名 vs run 前缀的字节级 stderr 平价
|
|
877
|
+
// 契约(cli-top-level-aliases)不被首报/复报差异打破。
|
|
878
|
+
console.log('⚠️ [spec-sync] 未连接平台——本命令的进度与 spec 树不会同步到平台(平台面板将停更)。')
|
|
879
|
+
console.log(' 如需同步:sillyspec platform connect <平台地址> --token <token> 连接一次,此后 --done 自动增量同步;本地独立使用可忽略(本提示每日至多一次)。')
|
|
880
|
+
}
|
|
881
|
+
|
|
856
882
|
export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
857
883
|
// ── 后台异步化(2026-09-20 用户反馈:--done 被网络尾巴拖住分钟级)──
|
|
858
884
|
// 默认把同步整体挪进 detached 后台子进程:17 个调用点全部 fire-and-forget,但进程
|
|
@@ -873,7 +899,14 @@ export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {})
|
|
|
873
899
|
return
|
|
874
900
|
}
|
|
875
901
|
if (r.status === 'coalesced') return // 活后台轮在跑,本轮状态已并入其下一轮
|
|
876
|
-
// not-connected →
|
|
902
|
+
// not-connected → 完成时刻显眼告警(每日一次,坑 2026-09-27-spec-sync-413;opts.completion
|
|
903
|
+
// 由 complete.js 步进/收口调用点传——渲染/诊断类调用点不告警,防顶层别名 vs run 前缀的
|
|
904
|
+
// 字节级输出平价契约被节流态首报/复报差异打破)+ 落回 inline(内部静默 no-op,旧行为)
|
|
905
|
+
if (r.status === 'not-connected') {
|
|
906
|
+
if (opts.completion) {
|
|
907
|
+
try { warnPlatformNotConnectedDaily(cwd, platformOpts) } catch { /* 告警失败不阻断 */ }
|
|
908
|
+
}
|
|
909
|
+
}
|
|
877
910
|
} catch { /* 后台化失败降级 inline(best-effort 契约不变) */ }
|
|
878
911
|
}
|
|
879
912
|
// 平台模式不再整体跳过(2026-08-26 用户决策):上行回传(进度 + 四件套 + spec 树)
|
|
@@ -1291,7 +1324,9 @@ async function matchQuickModules(srcChanged, specBase, projectName) {
|
|
|
1291
1324
|
* @param {string} projectName 项目名(progress.project)
|
|
1292
1325
|
* @returns {Promise<object|null>} 模块索引(扁平 { id: {...} });不可得 → null
|
|
1293
1326
|
*/
|
|
1294
|
-
|
|
1327
|
+
// quick 资产尾②(changelog 边车)消费:complete-handlers 动态 import 解构取用——
|
|
1328
|
+
// 缺 export 曾致「loadQuickModuleIndex is not a function」fail-open 死路(R4-S-Q 实证)。
|
|
1329
|
+
export async function loadQuickModuleIndex(specBase, projectName) {
|
|
1295
1330
|
if (!specBase || !projectName) return null
|
|
1296
1331
|
try {
|
|
1297
1332
|
const mapPath = join(specBase, 'docs', projectName, 'modules', '_module-map.yaml')
|
|
@@ -1942,6 +1977,90 @@ function readLocalYamlRaw(cwd) {
|
|
|
1942
1977
|
}
|
|
1943
1978
|
}
|
|
1944
1979
|
|
|
1980
|
+
/**
|
|
1981
|
+
* 读 stage.burst 开关(2026-09-22-stage-burst-fold D-001/D-007,FR-01/FR-04):local.yaml
|
|
1982
|
+
* `stage.burst === true` 开启旧流程阶段折叠(burst);env SILLYSPEC_STAGE_BURST=0 强制关 /
|
|
1983
|
+
* =1 强制开(优先于配置,逃生阀);**缺省 OFF**——读失败/坏 YAML/解析异常一律 false
|
|
1984
|
+
* (fail-safe,burst 缺省不翻是 D-001 边界:全量测试面零迁移)。读取走本模块 readLocalYamlRaw
|
|
1985
|
+
* + js-yaml 动态 import(resolveLivingDocs 同款范式,绕开 parseSimpleYaml 缩进坑——known-issues
|
|
1986
|
+
* 实证)。锚定 cwd 而非 specBase:burst 是仓库本地开发偏好,不随平台/worktree specRoot 漂移。
|
|
1987
|
+
* @param {string} cwd 仓库根(local.yaml 在 <cwd>/.sillyspec/local.yaml)
|
|
1988
|
+
* @returns {Promise<boolean>}
|
|
1989
|
+
*/
|
|
1990
|
+
export async function readStageBurst(cwd) {
|
|
1991
|
+
const env = process.env.SILLYSPEC_STAGE_BURST
|
|
1992
|
+
if (env === '0') return false
|
|
1993
|
+
if (env === '1') return true
|
|
1994
|
+
const raw = readLocalYamlRaw(cwd)
|
|
1995
|
+
if (!raw) return false
|
|
1996
|
+
try {
|
|
1997
|
+
const mod = await import('js-yaml')
|
|
1998
|
+
const yamlLoad = mod.load || mod.default?.load
|
|
1999
|
+
const doc = yamlLoad(raw)
|
|
2000
|
+
const stage = doc && typeof doc === 'object' ? doc.stage : null
|
|
2001
|
+
return stage && typeof stage === 'object' ? stage.burst === true : false
|
|
2002
|
+
} catch { /* 坏 YAML / js-yaml 不可用 → 缺省 OFF */ }
|
|
2003
|
+
return false
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* burst 作用域白名单(D-006):三主阶段。quick 排除(末步四字段硬契约与 P0-2 事实合成不兼容,
|
|
2008
|
+
* complete.js:224 已把 quick 列为合成豁免);verify/archive 任务书明确不动(--init --draft 与
|
|
2009
|
+
* 就绪度已压到 1-2 次调用);辅助阶段(scan/doctor/explore)不在本轮验收面。渲染侧(stage.js
|
|
2010
|
+
* renderStageBurst 门)与完成侧(command.js --done 分发门)共用本单一事实源。
|
|
2011
|
+
*/
|
|
2012
|
+
export const STAGE_BURST_STAGES = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* 阶段墙档位(R16 减负批次 A 相位,2026-09-24):advisory(缺省——阶段完成时提示可 handoff
|
|
2016
|
+
* 换会话,不阻断)| hard(同会话刚收口前驱阶段后硬续墙后阶段 → run <stage> 拒启动,
|
|
2017
|
+
* --same-session 逃生口)。墙后阶段 = STAGE_WALL_STAGES(execute/verify——上下文最贵两段,
|
|
2018
|
+
* R15 实测 execute 均轮 263K,断崖拆会话后回落 120-170K)。env SILLYSPEC_STAGE_WALL 强制。
|
|
2019
|
+
*/
|
|
2020
|
+
export async function readStageWall(cwd) {
|
|
2021
|
+
const env = process.env.SILLYSPEC_STAGE_WALL
|
|
2022
|
+
if (env === 'hard') return 'hard'
|
|
2023
|
+
if (env === 'advisory') return 'advisory'
|
|
2024
|
+
const raw = readLocalYamlRaw(cwd)
|
|
2025
|
+
if (!raw) return 'advisory'
|
|
2026
|
+
try {
|
|
2027
|
+
const mod = await import('js-yaml')
|
|
2028
|
+
const yamlLoad = mod.load || mod.default?.load
|
|
2029
|
+
const doc = yamlLoad(raw)
|
|
2030
|
+
const stage = doc && typeof doc === 'object' ? doc.stage : null
|
|
2031
|
+
return stage && typeof stage === 'object' && stage.wall === 'hard' ? 'hard' : 'advisory'
|
|
2032
|
+
} catch { /* 坏 YAML / js-yaml 不可用 → 缺省 advisory */ }
|
|
2033
|
+
return 'advisory'
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/** 墙后阶段(runStage 入口 hard 门的作用面;stage-session ledger 判定「同会话刚收口前驱」) */
|
|
2037
|
+
export const STAGE_WALL_STAGES = ['execute', 'verify']
|
|
2038
|
+
/** 墙后阶段的直接前驱(execute←plan / verify←execute) */
|
|
2039
|
+
export const STAGE_WALL_PREDECESSOR = { execute: 'plan', verify: 'execute' }
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* 阶段墙拦截判定(纯函数,R16-b 实证修正 2026-09-25):
|
|
2043
|
+
* SILLYSPEC_SESSION_ID 是**变更所有权标识**(handoff 协议要求新会话保持不变)——它相同
|
|
2044
|
+
* 不能当「同会话硬续」的证据。真切割的充分信号 = 前驱收口(ledger.at)之后执行过
|
|
2045
|
+
* `sillyspec handoff`(ledger.handoffAt 更新):交接块生成动作即切割凭证。墙只拦
|
|
2046
|
+
* 「前驱刚收口、无更新 handoff、目标阶段首次进入」一种形态。
|
|
2047
|
+
* @param {object} p
|
|
2048
|
+
* @param {object|null} p.ledger - stage-session ledger(缺 → 放行 fail-open)
|
|
2049
|
+
* @param {string} p.curSession - 当前 SILLYSPEC_SESSION_ID(空 → 放行)
|
|
2050
|
+
* @param {string} p.stageName - 目标阶段(须 ∈ STAGE_WALL_STAGES)
|
|
2051
|
+
* @param {boolean} p.stageHasData - 目标阶段已有进度(reopen/复跑重入 → 放行)
|
|
2052
|
+
* @returns {boolean} true=拦截(runStage 应 exit 1 并指引 handoff)
|
|
2053
|
+
*/
|
|
2054
|
+
export function isStageWallBlocked({ ledger, curSession, stageName, stageHasData }) {
|
|
2055
|
+
if (!STAGE_WALL_STAGES.includes(stageName)) return false
|
|
2056
|
+
if (!ledger || typeof ledger !== 'object') return false
|
|
2057
|
+
if (!curSession || ledger.sessionId !== curSession) return false
|
|
2058
|
+
if (ledger.lastStage !== STAGE_WALL_PREDECESSOR[stageName]) return false
|
|
2059
|
+
if (stageHasData) return false
|
|
2060
|
+
if (ledger.handoffAt && (!ledger.at || String(ledger.handoffAt) > String(ledger.at))) return false
|
|
2061
|
+
return true
|
|
2062
|
+
}
|
|
2063
|
+
|
|
1945
2064
|
/**
|
|
1946
2065
|
* 从 plan.md 内容聚合所有 task 卡片声明的 repo:(去重,含 'main' 隐式)。
|
|
1947
2066
|
*
|