sillyspec 3.23.4 → 3.23.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.
@@ -0,0 +1 @@
1
+ //registry.npmjs.org/:_authToken=${NPM_TOKEN}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.23.4",
3
+ "version": "3.23.6",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -70,8 +70,10 @@ export function pathMatches(a, b) {
70
70
  /**
71
71
  * 「文件变更清单」章节标题同义词(与 src/stage-contract.js 的识别集对齐,
72
72
  * 避免两个校验器对「有没有清单」给出矛盾结论)。
73
+ * 容忍可选编号前缀(`## 6. 文件变更清单` / `## 6) 文件变更清单`)——
74
+ * brainstorm Step11 模板鼓励 design 章节带编号,编号前缀不应让 plan-postcheck 解析失败。
73
75
  */
74
- const FILE_LIST_SECTION_RE = /^#{2,3}\s*(文件变更清单|变更文件清单|文件清单|File Changes|Files to Change)/im
76
+ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变更文件清单|文件清单|File Changes|Files to Change)/im
75
77
 
76
78
  /**
77
79
  * exclude 子标题词集:「不修改/暂缓/保留」类 —— 其下路径从清单移除,不强制 task 覆盖。
@@ -134,8 +134,10 @@ function parseTaskDependencies(planContent) {
134
134
  * @param {string} taskName - task-04
135
135
  * @returns {{ ok: boolean, endpoints: Array, artifactPath: string|null }}
136
136
  */
137
- export function extractProviderArtifact(changeDir, worktreePath, specBase, taskName) {
138
- const artifactDir = join(specBase, '.runtime', 'contract-artifacts', taskName)
137
+ export function extractProviderArtifact(changeDir, worktreePath, specBase, taskName, runtimeRoot) {
138
+ // 平台模式 contract-artifacts 落 runtimeRoot;否则落 specBase/.runtime
139
+ const artifactRoot = runtimeRoot || join(specBase, '.runtime')
140
+ const artifactDir = join(artifactRoot, 'contract-artifacts', taskName)
139
141
  const artifactPath = join(artifactDir, 'endpoints.json')
140
142
 
141
143
  if (!worktreePath || !existsSync(worktreePath)) {
@@ -179,13 +181,15 @@ export function extractProviderArtifact(changeDir, worktreePath, specBase, taskN
179
181
  * @param {Array<{ provider: string, consumer: string, type: string }>} contracts
180
182
  * @returns {string|null} 注入到 prompt 的契约文本,无契约时返回 null
181
183
  */
182
- export function buildConsumerInjection(changeDir, specBase, taskName, contracts) {
184
+ export function buildConsumerInjection(changeDir, specBase, taskName, contracts, runtimeRoot) {
185
+ // 平台模式 contract-artifacts 落 runtimeRoot;否则落 specBase/.runtime
186
+ const artifactRoot = runtimeRoot || join(specBase, '.runtime')
183
187
  const myContracts = contracts.filter(c => c.consumer === taskName)
184
188
  if (myContracts.length === 0) return null
185
189
 
186
190
  const parts = []
187
191
  for (const contract of myContracts) {
188
- const artifactDir = join(specBase, '.runtime', 'contract-artifacts', contract.provider)
192
+ const artifactDir = join(artifactRoot, 'contract-artifacts', contract.provider)
189
193
  const artifactFile = join(artifactDir, 'endpoints.json')
190
194
 
191
195
  let endpoints = []
@@ -293,11 +297,13 @@ export function buildContractFieldInjection(changeDir, taskName) {
293
297
  * @param {string} worktreePath - worktree 路径
294
298
  * @returns {{ ok: boolean, missingBackend: Array, unusedBackend: Array, summary: string }}
295
299
  */
296
- export function verifyApiParity(specBase, worktreePath) {
300
+ export function verifyApiParity(specBase, worktreePath, runtimeRoot) {
297
301
  const { diffApiParity } = require('./endpoint-extractor.js')
298
302
 
303
+ // 平台模式 contract-artifacts 落 runtimeRoot;否则落 specBase/.runtime
304
+ const artifactRoot = runtimeRoot || join(specBase, '.runtime')
299
305
  // 读取所有 provider artifacts
300
- const artifactBase = join(specBase, '.runtime', 'contract-artifacts')
306
+ const artifactBase = join(artifactRoot, 'contract-artifacts')
301
307
  const allProviderEndpoints = []
302
308
 
303
309
  if (existsSync(artifactBase)) {
package/src/progress.js CHANGED
@@ -731,7 +731,11 @@ export class ProgressManager {
731
731
  console.warn('⚠️ initChange: changeName 为空,跳过');
732
732
  return null;
733
733
  }
734
- this._ensureChangeDir(cwd, changeName);
734
+ // quick 会话 id(quick-<uuid8>,见 run.js QUICK_SID_RE)只作 progress 的跨进程 session key,
735
+ // 进度存 SQL 不需要实体 change 目录——跳过避免 changes/quick-<uuid>/ 空目录残留。
736
+ if (!/^quick-[0-9a-f]{8}$/.test(changeName)) {
737
+ this._ensureChangeDir(cwd, changeName);
738
+ }
735
739
 
736
740
  const db = await this._ensureDB(cwd);
737
741
  db.transaction((sqlDb) => {
package/src/run.js CHANGED
@@ -454,8 +454,10 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
454
454
  console.log(` - ${r}`)
455
455
  }
456
456
  }
457
- console.log(`\n 如确认接受这些变更,重新运行:sillyspec run quick --done --confirm --output "..."`)
458
- console.log(` 或使用 --force-baseline 允许覆盖 baseline 文件,--allow-new 允许新增文件`)
457
+ console.log(`\n 如确认接受这些变更,重新运行 --done 时带上对应 flag 即可解锁:`)
458
+ console.log(` sillyspec run quick --done --force-baseline --allow-new --change <id> --output "..."`)
459
+ console.log(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件)`)
460
+ console.log(` 或在首个 sillyspec run quick 启动(step 1)时就声明这些 flag,持久化进 guard。`)
459
461
  }
460
462
  } catch (e) {
461
463
  result.reasons.push(`审计失败: ${e.message}`)
@@ -1509,6 +1511,15 @@ export async function runCommand(args, cwd, specDir = null) {
1509
1511
  }
1510
1512
  }
1511
1513
 
1514
+ // execute 阶段必须带 --change:不允许自动检测或默认值,变更名必须由 agent 显式传入
1515
+ // --status 豁免(纯查看不需要指定变更)
1516
+ if (stageName === 'execute' && !changeName && !isStatus) {
1517
+ console.error('❌ execute 阶段必须用 --change <变更名> 指定要操作的变更。')
1518
+ console.error(' agent 必须传参,不设默认值、不做自动检测。')
1519
+ console.error(' 请加 --change <变更名> 重新执行。')
1520
+ process.exit(1)
1521
+ }
1522
+
1512
1523
  let progress = await pm.read(cwd, changeName)
1513
1524
 
1514
1525
  if (!progress) {
@@ -1692,7 +1703,7 @@ export async function runCommand(args, cwd, specDir = null) {
1692
1703
  // --done
1693
1704
  if (isDone) {
1694
1705
  const doneAnswer = getFlagValue('--answer')
1695
- return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, confirmMode, doneAnswer })
1706
+ return await completeStep(pm, progress, stageName, cwd, outputText, inputText, { confirm: isConfirm, changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, confirmMode, doneAnswer, isForceBaseline, isAllowNew })
1696
1707
  }
1697
1708
 
1698
1709
  // 默认:输出当前步骤
@@ -1842,7 +1853,7 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1842
1853
  if (stageName === 'execute') {
1843
1854
  const { generateExecuteRunId } = await import('./task-review.js')
1844
1855
  const execSpecBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
1845
- const runtimeRoot = join(execSpecBase, '.runtime')
1856
+ const runtimeRoot = platformOpts?.runtimeRoot || join(execSpecBase, '.runtime')
1846
1857
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
1847
1858
  mkdirSync(runtimeRoot, { recursive: true })
1848
1859
  // 优先读取已有的变更专属标记文件
@@ -1961,10 +1972,13 @@ async function runStage(pm, progress, stageName, cwd, changeName, skipApproval =
1961
1972
  try {
1962
1973
  const { execSync } = await import('child_process')
1963
1974
  const gitStatus = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 10000 })
1975
+ // 记录全部预存脏文件(含 untracked + .sillyspec/ 路径)。quick 会话期间自身写入的元数据
1976
+ // (quicklog/.runtime/modules/_module-map 等)由 auditQuickCompletion 的 isQuickMetadata 精确豁免,
1977
+ // 不需要这里粗放过滤 .sillyspec/——旧过滤致预存 untracked .sillyspec/changes/ 不进 baseline,
1978
+ // 却在 audit 被当「危险(.sillyspec/)+新增」误判永久 blocked(ql-20260713-002-7628 修复)。
1964
1979
  const baselineFiles = gitStatus
1965
1980
  .trim().split('\n').filter(Boolean)
1966
1981
  .map(line => line.slice(3).trim())
1967
- .filter(f => !f.startsWith('.sillyspec/'))
1968
1982
  const allowedFiles = quickOpts?.quickFiles || []
1969
1983
  const allowNew = quickOpts?.isAllowNew || false
1970
1984
  const forceBaseline = quickOpts?.isForceBaseline || false
@@ -2614,7 +2628,7 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
2614
2628
  }
2615
2629
 
2616
2630
  async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
2617
- const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, confirmMode = null } = options
2631
+ const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, confirmMode = null, isForceBaseline = false, isAllowNew = false } = options
2618
2632
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
2619
2633
  const stageData = progress.stages[stageName]
2620
2634
  const scanProfile = stageData?.scanProfile || null
@@ -2990,7 +3004,15 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
2990
3004
  : (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
2991
3005
  } catch {}
2992
3006
  if (guard) {
2993
- const review = await auditQuickCompletion(cwd, guard, { isConfirm: confirm })
3007
+ // --done --force-baseline/--allow-new 并入 guard(与 step1 持久化值取或)。
3008
+ // 修复 ql-20260713-002-7628:旧代码解析了这两个 flag 但只传 {isConfirm} 给审计,
3009
+ // 致 --done --force-baseline 静默无效、用户被误导「重跑 --confirm」也无法解锁。
3010
+ const mergedGuard = {
3011
+ ...guard,
3012
+ forceBaseline: guard.forceBaseline || isForceBaseline,
3013
+ allowNew: guard.allowNew || isAllowNew,
3014
+ }
3015
+ const review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
2994
3016
  printQuickAuditReview(review)
2995
3017
  if (review.status === 'blocked') {
2996
3018
  steps[currentIdx].status = 'pending'
@@ -3302,7 +3324,7 @@ async function completeStep(pm, progress, stageName, cwd, outputText, inputText
3302
3324
 
3303
3325
  if (planPath && existsSync(planPath)) {
3304
3326
  const planContent = readFileSync(planPath, 'utf8')
3305
- const runtimeRoot = join(effectiveSpecBase, '.runtime')
3327
+ const runtimeRoot = platformOpts?.runtimeRoot || join(effectiveSpecBase, '.runtime')
3306
3328
 
3307
3329
  // execute run id:从变更专属标记文件读取
3308
3330
  const runIdFile = join(runtimeRoot, `current-execute-run-id-${changeName}`)
@@ -258,8 +258,21 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
258
258
  const hasLifecycleKeyword = /\b(session|lease|agent[._-]?run|daemon|lifecycle|state[._-]?transition|claim|heartbeat)\b/i.test(content)
259
259
  if (hasLifecycleKeyword) {
260
260
  // 显式声明本变更不涉及生命周期契约(覆盖字段名/错误码/否定声明场景):
261
- // 历史教训:design 提到 daemon_id 字段名或 daemon_not_owned 错误码就触发,被迫加空表(B3a
262
- const declaresNotApplicable = /(生命周期|lifecycle)[^\n]{0,40}(n\/?a|不涉及|无|none|not[ _-]?applicable)|(不涉及|无需|没有)[^\n]{0,40}(生命周期|lifecycle)/i.test(content)
261
+ // 历史教训:design 提到 daemon_id 字段名或 daemon_not_owned 错误码就触发,被迫加空表(B3a)。
262
+ //
263
+ // 收紧原则(修:正常 design 不应被误判「已豁免」):
264
+ // - 否定词必须是明确多字短语(不涉及/不适用/未涉及/不包含/没有/n\/a/not applicable/none),
265
+ // 杜绝裸单字「无」与裸「na」在 40 字符宽窗口内任意命中——
266
+ // 「lifecycle 状态无变化」「本变更无需 lifecycle 事件」「lifecycle canal 不涉及」等不再误判;
267
+ // - 否定词必须与「生命周期(契约)/lifecycle(contract)」紧邻(仅允许少量空白/分隔符/「任何」),
268
+ // 不再用 40 字符宽松窗口;工具错误信息本身就指引写「不涉及生命周期契约」这个规范短语。
269
+ const declaresNotApplicable =
270
+ // 否定在前:「不涉及生命周期(契约)」「不适用 lifecycle contract」
271
+ /(?:不涉及|不适用|未涉及|不包含|没有(?:任何)?)\s?(?:任何\s?)?(?:生命周期(?:契约)?|lifecycle(?:[ _=-]?contract)?)/i.test(content) ||
272
+ // 主题在前(表格/列表单元「生命周期契约:不涉及 / N/A / 无」——分隔符强制,杜绝宽窗口)
273
+ /(?:生命周期(?:契约)?|lifecycle(?:[ _=-]?contract)?)\s?[::=]\s?(?:不涉及|不适用|未涉及|无|n\/?a\b|not[ _=-]?applicable|none\b)/i.test(content) ||
274
+ // 英文谓语句:「does not involve / not applicable ... lifecycle」
275
+ /(?:does[ _-]?not[ _-]?involve|not[ _-]?applicable)[^\n]{0,15}lifecycle/i.test(content)
263
276
  if (declaresNotApplicable) {
264
277
  warnings.push('design.md 显式声明不涉及生命周期契约 — 已豁免「生命周期契约表」要求')
265
278
  } else {