sillyspec 3.33.5 → 3.33.7
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 +22 -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/agent-session-log.js +4 -1
- package/src/command-cards.js +1 -1
- package/src/config-schema.js +22 -0
- package/src/deleted-ledger.js +3 -1
- package/src/flow-parity.js +40 -0
- package/src/flow-review.js +19 -3
- package/src/flow.js +70 -19
- package/src/git-helper.js +22 -6
- package/src/index.js +34 -3
- package/src/knowledge-hits.js +80 -1
- package/src/knowledge-scope.js +82 -0
- package/src/progress/change-registry.js +15 -1
- package/src/push.js +934 -0
- package/src/run/complete-handlers.js +30 -29
- package/src/run/shared.js +1 -1
- package/src/stages/knowledge.js +44 -1
- package/src/verify-postcheck.js +142 -3
- package/templates/agents-instruction.md +2 -1
package/src/git-helper.js
CHANGED
|
@@ -96,23 +96,31 @@ function gitChildEnv(env) {
|
|
|
96
96
|
* 安全执行 git:失败不抛,返回 { value, error } 结构。
|
|
97
97
|
* @param {string} cwd
|
|
98
98
|
* @param {string[]} args
|
|
99
|
-
* @param {{ trim?: boolean, timeout?: number, retryOnTimeout?: boolean }} [opts]
|
|
99
|
+
* @param {{ trim?: boolean, timeout?: number, retryOnTimeout?: boolean, input?: string|Buffer, stdio?: string[] }} [opts]
|
|
100
100
|
* - trim:false 保留原样输出(git status --porcelain 首行前导空格
|
|
101
101
|
* 是状态码的一部分,trim 会削掉致 parsePorcelainPath 丢首字符,坑见 auditQuickCompletion 注释)
|
|
102
102
|
* - timeout 默认 5000ms,长操作(大 diff / worktree list)可传更大值
|
|
103
103
|
* - retryOnTimeout:true 时,ETIMEDOUT 用 2× timeout 重试一次(默认 false 向后兼容)。
|
|
104
104
|
* 机器忙时 git 子进程启动慢易瞬时超时;审计锚点 git 调用失败即 blocked 中断 quick,
|
|
105
105
|
* 重试一次能消化绝大多数瞬时抖动,免去用户手工重跑。权限 / 损坏等非超时错误不重试(救不回)。
|
|
106
|
+
* - input: 写入子进程 stdin;传入时自动把 stdio[0] 设为 pipe(否则 Node 静默丢弃 input)
|
|
107
|
+
* - stdio: 显式覆盖子进程 stdio
|
|
106
108
|
* @returns {{ value: string|null, error: string|null }}
|
|
107
109
|
*/
|
|
108
110
|
export function safeGit(cwd, args, opts = {}) {
|
|
109
|
-
const { trim = true, timeout = 5000, retryOnTimeout = false } = opts
|
|
111
|
+
const { trim = true, timeout = 5000, retryOnTimeout = false, input, stdio = null } = opts
|
|
110
112
|
const fullArgs = buildFullArgs(cwd, args)
|
|
113
|
+
// stdin 形态:input 传入时必须把 stdin 设为 pipe——Node 的 execFileSync 在
|
|
114
|
+
// stdio[0]='ignore' 时会静默丢弃 input(commit-tree -F - 会收到空消息)。
|
|
115
|
+
const childStdio = stdio || (input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'])
|
|
111
116
|
// 单次尝试:返回 { value, errorObj }(errorObj 为原始 Error 或 null),不在内部格式化 message。
|
|
112
117
|
// 格式化(取首行)推迟到最终返回,避免重试分支重复格式化,并保留原始 code 供重试判定。
|
|
113
118
|
const attempt = (t) => {
|
|
114
119
|
try {
|
|
115
|
-
let value = execFileSync('git', fullArgs, {
|
|
120
|
+
let value = execFileSync('git', fullArgs, {
|
|
121
|
+
encoding: 'utf8', timeout: t, maxBuffer: GIT_MAX_BUFFER, stdio: childStdio,
|
|
122
|
+
...(input === undefined ? {} : { input }), env: gitChildEnv(), windowsHide: true,
|
|
123
|
+
})
|
|
116
124
|
if (trim) value = value.trim()
|
|
117
125
|
return { value, errorObj: null }
|
|
118
126
|
} catch (e) {
|
|
@@ -131,18 +139,26 @@ export function safeGit(cwd, args, opts = {}) {
|
|
|
131
139
|
* 调用方自行 catch;不经 shell,参数以数组元素传递。
|
|
132
140
|
* @param {string} cwd
|
|
133
141
|
* @param {string[]} args
|
|
134
|
-
* @param {{ trim?: boolean, timeout?: number, encoding?: string }} [opts]
|
|
142
|
+
* @param {{ trim?: boolean, timeout?: number, encoding?: string, env?: NodeJS.ProcessEnv, input?: string|Buffer, stdio?: string[] }} [opts]
|
|
135
143
|
* - encoding:'buffer' 返回原始 Buffer(git diff --binary 等**二进制输出**专用——
|
|
136
144
|
* utf8 解码会破坏 NUL 字节使补丁不可应用;Buffer 输出跳过 trim)
|
|
145
|
+
* - input: 写入子进程 stdin(如 commit-tree -F - 的多行消息);传入时自动把 stdio[0] 设为 pipe
|
|
146
|
+
* - stdio: 显式覆盖子进程 stdio(push 需要实时透传 hook 输出时用 inherit)
|
|
137
147
|
* @returns {string|Buffer}
|
|
138
148
|
*/
|
|
139
149
|
export function git(cwd, args, opts = {}) {
|
|
140
|
-
const { trim = true, timeout = 5000, encoding = 'utf8', env } = opts
|
|
150
|
+
const { trim = true, timeout = 5000, encoding = 'utf8', env, input, stdio = null } = opts
|
|
141
151
|
const fullArgs = buildFullArgs(cwd, args)
|
|
142
152
|
// env(终批-③):可选注入(如 baseline checkpoint 的临时 GIT identity)——调用方必须
|
|
143
153
|
// 用 { ...process.env, GIT_xxx } 展开形式,裸替换会丢 SystemRoot/USERPROFILE/TEMP(Windows);
|
|
144
154
|
// 最终经 gitChildEnv 展开合并并权威注入 GIT_OPTIONAL_LOCKS=0(调用方 GIT 键照常透传)
|
|
145
|
-
|
|
155
|
+
// stdio:input 传入时 stdin 必须 pipe——Node 在 stdio[0]='ignore' 下会静默丢弃 input
|
|
156
|
+
// (commit-tree -F - 收到空消息);push 用 inherit 透传 pre-push hook 输出。
|
|
157
|
+
const childStdio = stdio || (input === undefined ? ['ignore', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'])
|
|
158
|
+
const value = execFileSync('git', fullArgs, {
|
|
159
|
+
encoding, timeout, maxBuffer: GIT_MAX_BUFFER, stdio: childStdio,
|
|
160
|
+
...(input === undefined ? {} : { input }), env: gitChildEnv(env), windowsHide: true,
|
|
161
|
+
})
|
|
146
162
|
if (Buffer.isBuffer(value)) return value
|
|
147
163
|
return trim ? value.trim() : value
|
|
148
164
|
}
|
package/src/index.js
CHANGED
|
@@ -119,7 +119,7 @@ SillySpec CLI — 规范驱动开发工具包
|
|
|
119
119
|
sillyspec fourpiece-init --change <name> 生成 proposal/requirements/decisions 骨架(frontmatter+章节+模板占位;已存在不覆盖)
|
|
120
120
|
sillyspec delta --change <name> [--json] 生成变更 delta.md(Before/Delta/After 三段式聚合;幂等覆盖重跑即刷新)
|
|
121
121
|
sillyspec next 项目状态探测:输出当前状态 + 下一步命令 + 依据(吸收 continue/resume 手工探测表)
|
|
122
|
-
sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message
|
|
122
|
+
sillyspec commit [--json] 智能提交建议:收集 QUICKLOG/已勾 task/阶段产出语义,生成建议 message(只建议不执行);sillyspec push [--dry-run] [--mode by-change|one|none] [--change <名>] [--no-push] [--undo] [--json] 推送前压扁本地未推送提交并快进推送(默认按变更分段;不改写已推送历史、不 force)
|
|
123
123
|
sillyspec verify-probes --change <name> [--init [--force]] verify 机械探针(TODO 标记/测试覆盖/API 对账/删除对账);--init 生成 verify-result.md 骨架(--force 覆盖重生成,手填内容会重置)
|
|
124
124
|
sillyspec module-impact --change <name> 生成 module-impact.md 骨架(文件×模块归属按 module-map 预填 + 未匹配清单)
|
|
125
125
|
sillyspec scope-audit --change <name> [--json] 变更范围对账:计划×实际三态全表 + 行数(✓ 计划内/⚠️ 计划外/⚠️ 计划未动;quick 会话传 quick-<id> 出归属表;跨仓条目按 local.yaml repos 分仓真实对账——--json 出 repos[] 仓库维度汇总;已归档变更可查——快照记录态;advisory 只读不设门禁)
|
|
@@ -458,7 +458,7 @@ async function main() {
|
|
|
458
458
|
// 1-2 个交互来回)。守卫建议的事(cd 回主仓根重跑)自动做掉:⚠️警告 + 重锚定主仓根执行——
|
|
459
459
|
// 写入恒落主仓(单源不变量延续,worktree 副本零 CLI 写入);刻意以本 worktree 作独立实例跑
|
|
460
460
|
// 的显式路径(--allow-worktree-cwd --spec-dir)在警告中原样保留;wt-commit 豁免不变。
|
|
461
|
-
if (!args.includes('--allow-worktree-cwd') && command !== 'wt-commit') {
|
|
461
|
+
if (!args.includes('--allow-worktree-cwd') && command !== 'wt-commit' && command !== 'push') {
|
|
462
462
|
const guardDir = args.includes('--dir') ? dir : process.cwd();
|
|
463
463
|
const wtInfo = detectCwdInsideWorktree(guardDir);
|
|
464
464
|
if (wtInfo) {
|
|
@@ -5774,8 +5774,39 @@ SillySpec pull — 拉取服务器 spec 快照到本地(X2 / FR-07)
|
|
|
5774
5774
|
printed = wAll ? wRes.events.length : wRes.warnings.length;
|
|
5775
5775
|
}
|
|
5776
5776
|
}
|
|
5777
|
+
case 'push': {
|
|
5778
|
+
// 推送前压扁(2026-10-23-push-squash):显式命令,不接 pre-push hook 自动改写。
|
|
5779
|
+
// 默认 by-change,只处理 base..HEAD 本地未推送段;commit-tree + update-ref 保 index/工作区
|
|
5780
|
+
// 零变化,pre/post 备份 ref 可 undo;绝不 force / 不 --no-verify。--json 已由全局层剥离。
|
|
5781
|
+
// 放在 switch 末尾:避免插行造成 docs 里 index.js:N 的行号引用整体漂移。
|
|
5782
|
+
const { parsePushArgs, runPush, PUSH_USAGE } = await import('./push.js');
|
|
5783
|
+
const parsed = parsePushArgs(filteredArgs.slice(1));
|
|
5784
|
+
if (!parsed.ok) {
|
|
5785
|
+
for (const e of parsed.errors) console.error(`❌ ${e}`);
|
|
5786
|
+
console.error(PUSH_USAGE);
|
|
5787
|
+
process.exit(2);
|
|
5788
|
+
}
|
|
5789
|
+
if (parsed.options.help) {
|
|
5790
|
+
console.log(PUSH_USAGE);
|
|
5791
|
+
break;
|
|
5792
|
+
}
|
|
5793
|
+
const r = await runPush({ cwd: dir, specDir, ...parsed.options, json });
|
|
5794
|
+
if (json) {
|
|
5795
|
+
console.log(JSON.stringify(r.envelope, null, 2));
|
|
5796
|
+
for (const w of r.warnings || []) console.error(`⚠️ ${w}`);
|
|
5797
|
+
process.exitCode = r.exitCode;
|
|
5798
|
+
break;
|
|
5799
|
+
}
|
|
5800
|
+
for (const line of r.humanLines || []) console.log(line);
|
|
5801
|
+
for (const w of r.warnings || []) console.warn(`⚠️ ${w}`);
|
|
5802
|
+
if (!r.ok) {
|
|
5803
|
+
for (const e of r.errors || []) console.error(`❌ ${e}`);
|
|
5804
|
+
process.exitCode = r.exitCode || 1;
|
|
5805
|
+
}
|
|
5806
|
+
break;
|
|
5807
|
+
}
|
|
5777
5808
|
default: {
|
|
5778
|
-
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime', 'watcher'];
|
|
5809
|
+
const topCommands = ['init', 'setup', 'run', 'progress', 'worktree', 'dispatch', 'agent-log', 'local', 'workflow', 'gate', 'derive', 'backfill-reviews', 'register-stage-review', 'modules', 'change-rename', 'change-delete', 'knowledge', 'platform', 'pull', 'scan', 'brainstorm', 'plan', 'execute', 'verify', 'archive', 'quick', 'explore', 'status', 'doctor', 'auto', 'runtime', 'watcher', 'commit', 'push'];
|
|
5779
5810
|
const suggestion = didYouMean(command, topCommands);
|
|
5780
5811
|
console.error(`❌ 未知命令: ${command}`);
|
|
5781
5812
|
if (command === '--status') {
|
package/src/knowledge-hits.js
CHANGED
|
@@ -14,16 +14,92 @@
|
|
|
14
14
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs'
|
|
15
15
|
import { runtimeStatePath } from './runtime-paths.js'
|
|
16
16
|
import { join } from 'path'
|
|
17
|
+
import { readPlatformPushConfig } from './agent-session-log.js'
|
|
17
18
|
|
|
18
19
|
const HITS_FILENAME = 'knowledge-hits.jsonl'
|
|
19
20
|
|
|
21
|
+
// 直报超时上限(2026-10-28-knowledge-hits-cli-push):对齐 agent-log PUSH_TIMEOUT_MS
|
|
22
|
+
// 量级——最坏情况下进程退出被活跃 socket 拖住的时长有界。
|
|
23
|
+
const HITS_PUSH_TIMEOUT_MS = 10_000
|
|
24
|
+
|
|
20
25
|
function hitsFilePath(runtimeDir) {
|
|
21
26
|
return runtimeStatePath(runtimeDir, HITS_FILENAME)
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
/**
|
|
30
|
+
* runtimeDir → specBase 反推(剥尾 `.runtime` 段;跨平台:正反斜杠与尾分隔符容忍)。
|
|
31
|
+
*
|
|
32
|
+
* 全部 appendKnowledgeHit 调用形态的 runtimeDir 最终都是 `<specBase>/.runtime`
|
|
33
|
+
* (join(specBase,'.runtime') 或 resolveRuntimeRoot 产物)——统一剥最后一段反推
|
|
34
|
+
* specBase 供平台配置读取(local.yaml 在 specBase 根)。结果保留输入的分隔符
|
|
35
|
+
* 形态(不做平台归一——Node fs 对 / 与 \ 双向兼容,归一反而扭曲 POSIX 形态输入)。
|
|
36
|
+
*
|
|
37
|
+
* @param {string} runtimeDir
|
|
38
|
+
* @returns {string} specBase(runtimeDir 末段不是 .runtime 时原样返回——容错面)
|
|
39
|
+
*/
|
|
40
|
+
export function specBaseFromRuntimeDir(runtimeDir) {
|
|
41
|
+
const raw = String(runtimeDir)
|
|
42
|
+
const norm = raw.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
43
|
+
const idx = norm.lastIndexOf('/')
|
|
44
|
+
const last = idx === -1 ? norm : norm.slice(idx + 1)
|
|
45
|
+
if (last !== '.runtime') return runtimeDir
|
|
46
|
+
// \→/ 等长替换、去尾 / 只动末尾——norm 前 idx 字符与原串位置一一对应,
|
|
47
|
+
// 直接按剥点切片即保留原分隔符形态的 specBase。
|
|
48
|
+
return raw.slice(0, idx)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 单行遥测直报平台(2026-10-28-knowledge-hits-cli-push FR-01)。
|
|
53
|
+
*
|
|
54
|
+
* POST {url}/api/changes/-/hits-sync(Bearer shpsync_ 令牌——workspace 由令牌派生),
|
|
55
|
+
* body {lines:[line], daemon_local_id:'cli'};接收端行级幂等(行 sha256 去重,
|
|
56
|
+
* 平台仓 2026-10-28-knowledge-hits-cli-direct)。best-effort 语义照
|
|
57
|
+
* pushAgentLogToPlatform 先例:无配置/被禁用静默跳过(返回 pushed:null);
|
|
58
|
+
* 网络/非 2xx/超时 warn 一行不抛(返回 pushed:false, reason)——本地行已落盘、
|
|
59
|
+
* daemon 周期上行与平台按钮 flush 为兜底通道。
|
|
60
|
+
*
|
|
61
|
+
* @param {object} p
|
|
62
|
+
* @param {string} p.specBase - local.yaml 所在 spec 基路径
|
|
63
|
+
* @param {object} [p.env] - env(默认 process.env;测试注入用)
|
|
64
|
+
* @param {string} p.line - 已落盘的遥测行(JSON 字符串)
|
|
65
|
+
* @returns {Promise<{pushed:boolean|null, reason?:string}>}
|
|
66
|
+
*/
|
|
67
|
+
export async function pushKnowledgeHitToPlatform({ specBase, env = process.env, line }) {
|
|
68
|
+
if (env.SILLYSPEC_HITS_PUSH === '0') {
|
|
69
|
+
return { pushed: null, reason: 'disabled' }
|
|
70
|
+
}
|
|
71
|
+
const cfg = readPlatformPushConfig(specBase, env)
|
|
72
|
+
if (!cfg) return { pushed: null, reason: 'no-config' }
|
|
73
|
+
const controller = new AbortController()
|
|
74
|
+
const timer = setTimeout(() => controller.abort(), HITS_PUSH_TIMEOUT_MS)
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${cfg.url.replace(/\/+$/, '')}/api/changes/-/hits-sync`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cfg.token}` },
|
|
79
|
+
body: JSON.stringify({ lines: [line], daemon_local_id: 'cli' }),
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
})
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
console.warn(`[knowledge-hits-push] → HTTP ${res.status}(本地行已落盘,daemon 兜底)`)
|
|
84
|
+
return { pushed: false, reason: `http-${res.status}` }
|
|
85
|
+
}
|
|
86
|
+
return { pushed: true }
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const msg = err && err.name === 'AbortError' ? '超时' : (err && err.message ? err.message : err)
|
|
89
|
+
console.warn(`[knowledge-hits-push] 直报失败: ${msg}(本地行已落盘,daemon 兜底)`)
|
|
90
|
+
return { pushed: false, reason: 'network' }
|
|
91
|
+
} finally {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
24
96
|
/**
|
|
25
97
|
* 追加一条遥测记录(目录/文件缺失自动创建)。
|
|
26
98
|
*
|
|
99
|
+
* 2026-10-28-knowledge-hits-cli-push:落盘成功后 void fire 平台直报(FR-01/02)——
|
|
100
|
+
* 不 await 不抛(失败 warn 一行由 pushKnowledgeHitToPlatform 内部收口),主流程
|
|
101
|
+
* 同步语义与返回值不变;无配置/被禁用时零行为差异。
|
|
102
|
+
*
|
|
27
103
|
* @param {string} runtimeDir - .runtime 目录
|
|
28
104
|
* @param {object} hit - 记录对象,常用字段 { type:'inject'|'classify', change, query, matchedFiles, at }
|
|
29
105
|
* (at 缺省时补当前 ISO 时间;其余字段原样序列化,classify 的 qlId/targetFile 等照写)
|
|
@@ -35,7 +111,10 @@ export function appendKnowledgeHit(runtimeDir, hit) {
|
|
|
35
111
|
record.at = new Date().toISOString()
|
|
36
112
|
}
|
|
37
113
|
mkdirSync(runtimeDir, { recursive: true })
|
|
38
|
-
|
|
114
|
+
const line = JSON.stringify(record)
|
|
115
|
+
appendFileSync(hitsFilePath(runtimeDir), line + '\n', 'utf8')
|
|
116
|
+
// 写后直报(void fire):本地已落盘,直报成败不影响本函数语义(FR-02)。
|
|
117
|
+
void pushKnowledgeHitToPlatform({ specBase: specBaseFromRuntimeDir(runtimeDir), line }).catch(() => {})
|
|
39
118
|
return record
|
|
40
119
|
}
|
|
41
120
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* knowledge-scope.js — 知识注入作用域解析(2026-10-23-knowledge-scope-inject)
|
|
3
|
+
*
|
|
4
|
+
* 背景:知识注入此前只有「触达域 FR + 关键词命中」两类——AGENTS 规约 00(不枚举定义
|
|
5
|
+
* 开放世界/不锁死开发语言框架)一直在指令文件里,但决策时点没有「每次都注入」通道,
|
|
6
|
+
* agent 在配置/机制设计上犯语言锁定即实证(py_runner_flags 一案)。本模块让知识条目
|
|
7
|
+
* 声明作用域,注入逻辑按类分流:
|
|
8
|
+
* - always:每次变更都注入(跨仓通用规约,如工具中立性)
|
|
9
|
+
* - repo:本仓库每次变更都注入(仓级约束)
|
|
10
|
+
* - module:<域>[,<域>]:仅当触达域(resolveTouchedDomains 同源)命中时注入
|
|
11
|
+
* - 无标记:关键词命中现状(不恒注入)
|
|
12
|
+
*
|
|
13
|
+
* 标记形态(HTML 注释,主观内容用 HTML 注释不污染渲染;置于 section heading 之后的
|
|
14
|
+
* 本节内任意位置有效):
|
|
15
|
+
* <!-- scope: always -->
|
|
16
|
+
* <!-- scope: repo -->
|
|
17
|
+
* <!-- scope: module:cli-entry,core-engine -->
|
|
18
|
+
*
|
|
19
|
+
* 条目粒度 = section(heading)——knowledge 的 INDEX 路由与命中面都以 file#heading 为
|
|
20
|
+
* 单位,标记跟随原文、旧知识零迁移。纯读 fail-soft(坏文件/不可读跳过,异常返回空态)。
|
|
21
|
+
*/
|
|
22
|
+
import { readdirSync, readFileSync } from 'fs'
|
|
23
|
+
import { join } from 'path'
|
|
24
|
+
|
|
25
|
+
const SCOPE_RE = /<!--\s*scope\s*:\s*(always|repo|module\s*:\s*([^>]+?))\s*-->/i
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 扫描 knowledgeRoot 顶层 *.md(INDEX.md 除外)收集作用域条目。
|
|
29
|
+
* @returns {{ always: Array, repo: Array, modules: Record<string, Array> }}
|
|
30
|
+
* 条目形如 { id: 'file.md#heading', file, heading, summary, scope, domains? }
|
|
31
|
+
*/
|
|
32
|
+
export function collectScopedKnowledge(knowledgeRoot) {
|
|
33
|
+
const out = { always: [], repo: [], modules: {} }
|
|
34
|
+
let entries = []
|
|
35
|
+
try { entries = readdirSync(knowledgeRoot) } catch { return out }
|
|
36
|
+
for (const f of entries) {
|
|
37
|
+
if (!String(f).endsWith('.md') || f === 'INDEX.md') continue
|
|
38
|
+
let text
|
|
39
|
+
try { text = readFileSync(join(knowledgeRoot, f), 'utf8') } catch { continue }
|
|
40
|
+
let cur = null // { heading, scope, domains, summary }
|
|
41
|
+
const flush = () => {
|
|
42
|
+
if (!cur || !cur.scope) return
|
|
43
|
+
const entry = {
|
|
44
|
+
id: `${f}#${cur.heading}`,
|
|
45
|
+
file: f,
|
|
46
|
+
heading: cur.heading,
|
|
47
|
+
summary: cur.summary || '',
|
|
48
|
+
scope: cur.scope,
|
|
49
|
+
...(cur.scope === 'module' ? { domains: cur.domains } : {}),
|
|
50
|
+
}
|
|
51
|
+
if (cur.scope === 'always') out.always.push(entry)
|
|
52
|
+
else if (cur.scope === 'repo') out.repo.push(entry)
|
|
53
|
+
else for (const d of cur.domains) (out.modules[d] ||= []).push(entry)
|
|
54
|
+
}
|
|
55
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
56
|
+
const h = /^(#{1,6})\s+(.+?)\s*$/.exec(line)
|
|
57
|
+
if (h) { flush(); cur = { heading: h[2].trim(), scope: null, domains: [], summary: '' }; continue }
|
|
58
|
+
if (!cur) continue
|
|
59
|
+
const m = SCOPE_RE.exec(line)
|
|
60
|
+
if (m) {
|
|
61
|
+
const kind = m[1].toLowerCase()
|
|
62
|
+
if (kind === 'always' || kind === 'repo') { cur.scope = kind; cur.domains = [] }
|
|
63
|
+
else {
|
|
64
|
+
cur.scope = 'module'
|
|
65
|
+
cur.domains = String(m[2] || '').split(',').map((s) => s.trim()).filter(Boolean)
|
|
66
|
+
}
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
// 首行有效正文作摘要(跳空行/注释/代码围栏),≤80 字
|
|
70
|
+
if (!cur.summary) {
|
|
71
|
+
const t = line.trim()
|
|
72
|
+
if (t && !t.startsWith('<!--') && !t.startsWith('```') && !t.startsWith('|')) {
|
|
73
|
+
cur.summary = t.slice(0, 80)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
flush()
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default { collectScopedKnowledge }
|
|
@@ -513,6 +513,9 @@ export class ChangeRegistry {
|
|
|
513
513
|
* @param {string} cwd - 项目根目录
|
|
514
514
|
* @param {string} oldName - 旧变更名
|
|
515
515
|
* @param {string} newName - 新变更名
|
|
516
|
+
*
|
|
517
|
+
* 成功后旧名落删除台账 status='deleted'(2026-10-23-rename-platform-tombstone):
|
|
518
|
+
* 平台按 change_key 建行,旧平台名行靠 sync 台账墓碑分支收敛,防幽灵永挂。
|
|
516
519
|
*/
|
|
517
520
|
renameChange(cwd, oldName, newName) {
|
|
518
521
|
if (!oldName || !newName) {
|
|
@@ -588,7 +591,18 @@ export class ChangeRegistry {
|
|
|
588
591
|
} else {
|
|
589
592
|
mkdirSync(newDir, { recursive: true });
|
|
590
593
|
}
|
|
591
|
-
if (renamed)
|
|
594
|
+
if (renamed) {
|
|
595
|
+
console.log(`✅ 变更已重命名:${oldName} → ${newName}`);
|
|
596
|
+
// 平台旧名墓碑凭证(2026-10-23-rename-platform-tombstone,坑9 幽灵家族第三变种):
|
|
597
|
+
// 平台按 change_key 建行而 rename 只改本地,旧平台名行无终态凭证会永挂 active
|
|
598
|
+
// (2026-10-23-new-change-43a74c51 实证:自动建名 16 秒后被改名,平台幽灵)。
|
|
599
|
+
// 旧名对平台即终态弃名,语义用既有 'deleted' 承载(平台 _apply_cli_tombstone 只认
|
|
600
|
+
// deleted/archived,不扩表面);daemon reconcile / 显式 platform sync --change 旧名
|
|
601
|
+
// 时由 sync 台账分支合成墓碑收敛。仅 renamed(DB+目录双成)落账:missing/conflict/
|
|
602
|
+
// stolen/FS 回滚分支到此为止(行还叫旧名时不得有其终态台账)。fail-open 同删除/归档。
|
|
603
|
+
appendDeletedLedger(this.pm._getSpecDir(cwd), oldName, 'deleted');
|
|
604
|
+
console.log('ℹ️ 旧名已记删除台账(status=deleted),平台旧名行将由墓碑同步收敛');
|
|
605
|
+
}
|
|
592
606
|
}
|
|
593
607
|
|
|
594
608
|
/**
|