@xulthekl/team-flow 0.56.1 → 0.57.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/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +17 -11
- package/CHANGELOG.md +85 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +3 -3
- package/docs/usage-guide.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/lib/cmd-doctor.mjs +140 -1
- package/scripts/lib/cmd-solutions.mjs +14 -4
- package/scripts/lib/md-normalize.mjs +39 -0
- package/scripts/lib/solutions-backfill.mjs +116 -0
- package/scripts/lib/solutions-capture.mjs +67 -20
- package/scripts/lib/solutions-entry.mjs +185 -0
- package/scripts/lib/solutions-index-gen.mjs +117 -57
- package/scripts/lib/solutions-inject.mjs +102 -24
- package/scripts/lib/solutions-promote.mjs +147 -95
- package/scripts/lib/test-merge.mjs +73 -12
- package/scripts/team-flow.mjs +7 -3
- package/skills/architecture-design/SKILL.md +2 -2
- package/skills/architecture-design/references/s3.5-product-architecture.md +1 -1
- package/skills/build-executor/SKILL.md +5 -1
- package/skills/ce-brainstorm/references/grounding.md +2 -2
- package/skills/ce-compound/references/promotion-rules.md +26 -9
- package/skills/ce-compound/references/schema.yaml +4 -2
- package/skills/ce-compound/references/three-tier-index.md +10 -7
- package/skills/ce-compound/references/write-flow.md +22 -10
- package/skills/ce-ideate/references/agents/learnings-researcher.md +9 -2
- package/skills/ce-ideate/references/grounding.md +1 -1
- package/skills/ce-plan/references/agents/learnings-researcher.md +9 -2
- package/skills/ce-plan/references/research-workflow.md +2 -2
- package/skills/code-reviewer/SKILL.md +7 -0
- package/skills/code-reviewer/code-reviewer-prompt.md +6 -0
- package/skills/contract-builder/SKILL.md +9 -0
- package/skills/release-archivist/SKILL.md +2 -2
- package/skills/release-archivist/references/closing-procedures.md +3 -1
- package/skills/spec-writer/SKILL.md +1 -1
- package/skills/workflow-orchestrator/SKILL.md +2 -2
- package/skills/workflow-orchestrator/references/s1-path-router.md +4 -2
- package/skills/workflow-orchestrator/references/s3-plan-pipeline.md +1 -1
- package/templates/learnings.md +17 -5
|
@@ -7,53 +7,55 @@
|
|
|
7
7
|
*
|
|
8
8
|
* 功能:
|
|
9
9
|
* 1. 读取 change 根 learnings.md(v0.49.0 §83.3.5:统一路径——此前注释误写 specs/<cap>/,
|
|
10
|
-
*
|
|
11
|
-
* 2. 逐条检查晋升条件(severity ≥ medium 且 type = pitfall/pattern
|
|
12
|
-
* 3.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* 与本文件的实际实现及 compound-captured guard 均不一致)
|
|
11
|
+
* 2. 逐条检查晋升条件(severity ≥ medium 且 type = pitfall/pattern,phase 须在合法枚举内)
|
|
12
|
+
* 3. **幂等判据 = 文件名 + 正文签名**(`<date>-<slug>.md` + `entryBodySignature`):
|
|
13
|
+
* 两者都相同才是"同一条经验"——同标题不同正文是**两条**独立经验(见下方 P4 修复说明)
|
|
14
|
+
* 4. 文件不存在 → 新建条目;签名匹配 → 登记来源 change 并按其升级 severity(**不改内容**)
|
|
15
|
+
* 5. INDEX 由 `refreshIndex()` 派生(本脚本不直接写 INDEX)
|
|
15
16
|
*
|
|
16
17
|
* 变更说明(v0.5 / 2026-07-21):
|
|
17
18
|
* - 修正原稿在同步 run() 内使用 `await import('node:fs')` 取 mkdirSync 导致的语法错误,
|
|
18
19
|
* 改为在顶部静态 import mkdirSync,保持与其它 solutions-* 脚本一致的同步签名。
|
|
20
|
+
*
|
|
21
|
+
* v0.57.0 §4.2/§4.4 重构(设计文档 compound-lifecycle-governance-design v2.2):
|
|
22
|
+
* - **废止 `domain+type` 判重合并**(D1-B):旧实现命中同 domain+type 即"合并",只把
|
|
23
|
+
* `confirmed` 计数 +1 而**正文丢弃**(emp-auth 实测 13 条内容丢失,仅存于 change 的 learnings.md)。
|
|
24
|
+
* 新实现每条经验独立成条 ⇒ 内容零丢失;跨 change 的"复发"信号改由 `confirmations`
|
|
25
|
+
* 来源集合承载,并据此升级 severity(每新增一个来源 change 升一档,重跑不升)。
|
|
26
|
+
* - **severity 与 confirmations 落条目文件 frontmatter**(唯一不被 INDEX 重建抹掉的持久层)。
|
|
27
|
+
* 旧实现把升级只写在 INDEX 行里 ⇒ index-gen 重建会把 `critical` 打回 `high`(实测 3/20 条)。
|
|
28
|
+
* - **新增 phase 白名单校验**与 capture 对齐,防止写入不可被索引的目录。
|
|
29
|
+
*
|
|
30
|
+
* v0.57.0 P4 修复(评审轮 C1/M1,隔离复现证实):
|
|
31
|
+
* - **身份键塌缩丢条**:`cleanTitle` 剥离序号后,`## 3. 契约漂移` 与 `## 7. 契约漂移`
|
|
32
|
+
* 产出同一 slug ⇒ 同一文件名 ⇒ 第二条被判为"已登记"而**直接 continue**,
|
|
33
|
+
* 其**正文与 severity 整条消失**(无 WARN、不计 skipped、INDEX 无行)。这是本版
|
|
34
|
+
* 新引入的回归——cleanTitle 之前 slug 含序号(`3-契约漂移`),不会碰撞。
|
|
35
|
+
* 跨 change 同样中招(同日同标题 ⇒ 只回写 frontmatter、第二个 change 的正文被丢弃)。
|
|
36
|
+
* 修法:文件名降为身份的**标题分量**,另加 `entryBodySignature` 作**内容分量**,
|
|
37
|
+
* 碰撞时探测 `-2`/`-3` 后缀,签名匹配才算同一条 —— 与 D1-B「每条独立成条、内容零丢失」对齐。
|
|
38
|
+
* 两个分量的清洗口径必须**相反**:标题分量**要**清洗(序号是 change 局部编号,
|
|
39
|
+
* 不清洗则跨 change 复发永远匹配不上),内容分量**不能**清洗(清洗会抹掉同标题经验的区分度)。
|
|
19
40
|
*/
|
|
20
41
|
|
|
21
|
-
import { readFileSync, writeFileSync, existsSync,
|
|
42
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
22
43
|
import { join, basename } from 'node:path';
|
|
23
44
|
import { pathToFileURL } from 'node:url';
|
|
24
45
|
import { meetsMinSeverity, nextSeverity } from './severity.mjs';
|
|
25
|
-
import { normalizeInline } from './md-normalize.mjs';
|
|
26
46
|
import { slugify } from './slug.mjs';
|
|
47
|
+
import { SOLUTION_PHASES } from './solutions-phases.mjs';
|
|
48
|
+
import { refreshIndex } from './solutions-index-gen.mjs';
|
|
49
|
+
// 条目身份(文件名 + 正文签名)、碰撞定位、frontmatter 读取——与 capture/index-gen 共用的**共享层**
|
|
50
|
+
// (v0.57.0 P4;二轮评审把 frontmatter 读取也收敛进来:原先三处各有一份,只有本文件剥行尾注释)
|
|
51
|
+
import {
|
|
52
|
+
entryBodySignature, resolveEntryPath, MAX_COLLISION_SUFFIX,
|
|
53
|
+
stripInlineComment, parseFrontmatter,
|
|
54
|
+
} from './solutions-entry.mjs';
|
|
27
55
|
|
|
28
56
|
/** 文件名 slug 截断长度(与 solutions-capture 一致) */
|
|
29
57
|
const SLUG_MAX_LENGTH = 40;
|
|
30
58
|
|
|
31
|
-
/**
|
|
32
|
-
* 剥离 YAML 行尾注释(空格 + `#` 起始)——v0.23 §91 硬化:
|
|
33
|
-
* templates/learnings.md 的取值说明写在行尾注释里,若被逐字复制进 learnings.md,
|
|
34
|
-
* 注释会随取值一起进入 frontmatter,导致 severity 被判为未知值而静默不晋升。
|
|
35
|
-
*/
|
|
36
|
-
function stripInlineComment(raw) {
|
|
37
|
-
return raw.replace(/\s+#.*$/, '').trim();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseFrontmatter(content) {
|
|
41
|
-
// learnings.md 中每个条目以 `## 标题` 开头,frontmatter 紧跟标题行之后;
|
|
42
|
-
// 故此处兼容 frontmatter 位于段首或标题行之后两种位置(^--- 只能匹配段首会漏判)。
|
|
43
|
-
const match = content.match(/(?:^|\n)---\n([\s\S]*?)\n---/);
|
|
44
|
-
if (!match) return null;
|
|
45
|
-
const fm = {};
|
|
46
|
-
for (const line of match[1].split('\n')) {
|
|
47
|
-
// v0.55.0 §8.4.3 横展(FB-4):容忍全角冒号、`**key**` 加粗与反引号
|
|
48
|
-
// (走共享层 `normalizeInline`)。原实现只认半角 `:` + 裸键名,
|
|
49
|
-
// `severity:high` / `**severity**: high` 会整键丢失 → 该条静默不晋升。
|
|
50
|
-
const idx = line.search(/[::]/);
|
|
51
|
-
if (idx > 0) {
|
|
52
|
-
fm[normalizeInline(line.slice(0, idx))] = normalizeInline(stripInlineComment(line.slice(idx + 1)));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return fm;
|
|
56
|
-
}
|
|
57
59
|
|
|
58
60
|
function parseLearnings(content) {
|
|
59
61
|
// 解析 learnings.md 中的经验条目(## 标题分隔)
|
|
@@ -61,7 +63,10 @@ function parseLearnings(content) {
|
|
|
61
63
|
return sections.map(s => {
|
|
62
64
|
const lines = s.trim().split('\n');
|
|
63
65
|
const title = lines[0]?.trim() || '';
|
|
64
|
-
|
|
66
|
+
// 剔掉 `## 标题` 行(及随后的空行),使条目 frontmatter 落回**段首** ——
|
|
67
|
+
// 共享层的 `parseFrontmatter` 严格锚定段首,故不能用"容忍中置"换取方便:
|
|
68
|
+
// 那会让正文里的水平线 / 围栏样例也被当成元数据(v0.57.0 四轮实测的幽灵条目缺陷)。
|
|
69
|
+
const fm = parseFrontmatter(s.replace(/^[^\n]*\n/, '').trimStart());
|
|
65
70
|
return { title, fm, body: s.trim() };
|
|
66
71
|
// v0.49.0 §83.3.5:排除文件级标题(`# Session Learnings` 会被 split 成首个 section),
|
|
67
72
|
// 否则它会被当成一条无 frontmatter 的条目计入 skipped
|
|
@@ -82,41 +87,65 @@ function renderEntryBody(learning) {
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
90
|
+
* 清洗 learnings 标题中的序号前缀(`8. 改 tasks.md 文本…` → `改 tasks.md 文本…`)。
|
|
91
|
+
*
|
|
92
|
+
* v0.57.0:`title` 是**检索字段 + 文件名来源 + INDEX 摘要**三处复用(§4.1),
|
|
93
|
+
* 而 learnings.md 的 `## N. <标题>` 序号只在该文件内有意义,带出去会污染三处。
|
|
87
94
|
*/
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
91
|
-
// INDEX 列:| date | phase | domain | type | severity | summary | file |
|
|
92
|
-
const cols = lines[i].split('|').map(column => column.trim());
|
|
93
|
-
if (cols.length < 9 || cols[3] !== domain || cols[4] !== type) continue;
|
|
94
|
-
|
|
95
|
-
const currentSeverity = cols[5];
|
|
96
|
-
const upgraded = nextSeverity(currentSeverity);
|
|
97
|
-
if (upgraded !== currentSeverity) {
|
|
98
|
-
lines[i] = lines[i].replace(`| ${currentSeverity} |`, `| ${upgraded} |`);
|
|
99
|
-
}
|
|
100
|
-
markEntryConfirmed(join(solutionsDir, cols[7]));
|
|
101
|
-
return { indexContent: lines.join('\n'), from: currentSeverity, to: upgraded, file: cols[7] };
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
95
|
+
function cleanTitle(raw) {
|
|
96
|
+
return String(raw ?? '').replace(/^\d+[.、]\s*/, '').trim();
|
|
104
97
|
}
|
|
105
98
|
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
/** 解析 frontmatter 中的 `confirmations: [a, b]`(内联数组;容错缺失与加粗) */
|
|
100
|
+
function parseConfirmations(fmBlock) {
|
|
101
|
+
const m = fmBlock.match(/^\**confirmations\**\s*[::]\s*\[([^\]]*)\]\s*$/m);
|
|
102
|
+
if (!m) return [];
|
|
103
|
+
return m[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 读写 frontmatter 标量键(容错全角冒号与 `**key**` 加粗,沿用 FB-4 的容忍面) */
|
|
107
|
+
function readScalar(fmBlock, key) {
|
|
108
|
+
const m = fmBlock.match(new RegExp(`^\\**${key}\\**\\s*[::]\\s*(.+?)\\s*$`, 'm'));
|
|
109
|
+
// 剥行尾注释(v0.57.0 P4):手工条目常写 `severity: high # 说明`。不剥则 nextSeverity
|
|
110
|
+
// 收到未知值原样返回 ⇒ 该条 severity **永不升级且无任何提示**(实测 "high # 说明" → "high # 说明")。
|
|
111
|
+
return m ? stripInlineComment(m[1]) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeScalar(fmBlock, key, value) {
|
|
115
|
+
const re = new RegExp(`^\\**${key}\\**\\s*[::].*$`, 'm');
|
|
116
|
+
if (re.test(fmBlock)) return fmBlock.replace(re, `${key}: ${value}`);
|
|
117
|
+
return `${fmBlock}\n${key}: ${value}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* v0.57.0 §4.2/§4.4:确认既有条目 —— **只改条目文件,不写 INDEX**。
|
|
122
|
+
*
|
|
123
|
+
* 语义(与旧实现的三处差异):
|
|
124
|
+
* - 旧:命中 `domain+type` 即"合并",只把 `confirmed` 计数 +1(内容丢失,见设计文档 P-1)
|
|
125
|
+
* - 新:**不合并内容**(D1-B 每条独立成条),确认动作仅登记"来源 change"并按其升级 severity
|
|
126
|
+
* - 幂等:同一 change 重复 promote 时**不升 severity**(那是重跑,不是新证据);
|
|
127
|
+
* 只有**新出现的来源 change** 才升一档 —— 对应 §4.4 的"每新增一个来源 change 升一档"
|
|
128
|
+
* - 落点:severity 与 confirmations **都落条目文件 frontmatter**(唯一不被 INDEX 重建抹掉的持久层)
|
|
129
|
+
*/
|
|
130
|
+
function confirmByFile(filePath, changeName) {
|
|
108
131
|
const content = readFileSync(filePath, 'utf-8');
|
|
109
132
|
const block = content.match(/^---\n([\s\S]*?)\n---/);
|
|
110
|
-
if (!block) return;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
if (!block) return null;
|
|
134
|
+
|
|
135
|
+
const fm = block[1];
|
|
136
|
+
const seen = parseConfirmations(fm);
|
|
137
|
+
const currentSeverity = readScalar(fm, 'severity') || 'medium';
|
|
138
|
+
|
|
139
|
+
if (seen.includes(changeName)) {
|
|
140
|
+
return { alreadyConfirmed: true, from: currentSeverity, to: currentSeverity, confirmations: seen };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const upgraded = nextSeverity(currentSeverity);
|
|
144
|
+
let next = writeScalar(fm, 'severity', upgraded);
|
|
145
|
+
next = writeScalar(next, 'confirmations', `[${[...seen, changeName].join(', ')}]`);
|
|
146
|
+
writeFileSync(filePath, content.replace(block[0], `---\n${next}\n---`), 'utf-8');
|
|
147
|
+
|
|
148
|
+
return { alreadyConfirmed: false, from: currentSeverity, to: upgraded, confirmations: [...seen, changeName] };
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
export function run(args = {}) {
|
|
@@ -142,15 +171,14 @@ export function run(args = {}) {
|
|
|
142
171
|
return { promoted: 0, updated: 0, skipped: 0 };
|
|
143
172
|
}
|
|
144
173
|
|
|
145
|
-
// 读取全局 INDEX
|
|
146
|
-
const indexPath = join(solutionsDir, 'INDEX.md');
|
|
147
|
-
let indexContent = existsSync(indexPath) ? readFileSync(indexPath, 'utf-8') : '';
|
|
148
|
-
|
|
149
174
|
let promoted = 0;
|
|
150
175
|
let updated = 0;
|
|
176
|
+
let unchanged = 0;
|
|
151
177
|
// v0.49.0 §83.3.5:跳过原因可见化(原实现静默 continue,用户看不出为什么一条都没晋升)
|
|
152
178
|
const skipped = [];
|
|
153
179
|
|
|
180
|
+
const changeName = basename(changeDir);
|
|
181
|
+
|
|
154
182
|
for (const learning of learnings) {
|
|
155
183
|
const severity = learning.fm?.severity || 'low';
|
|
156
184
|
const type = learning.fm?.type || 'insight';
|
|
@@ -170,31 +198,54 @@ export function run(args = {}) {
|
|
|
170
198
|
continue;
|
|
171
199
|
}
|
|
172
200
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
indexContent = outcome.indexContent;
|
|
182
|
-
writeFileSync(indexPath, indexContent, 'utf-8');
|
|
183
|
-
console.log(`Confirmed: ${domain}/${type} severity ${outcome.from} → ${outcome.to}(${outcome.file})`);
|
|
184
|
-
updated++;
|
|
201
|
+
// v0.57.0 §4.4:phase 白名单校验 —— capture 一直有校验,promote 此前**没有**。
|
|
202
|
+
// 实测 promote 接受 `phase: tools` 会建出 docs/solutions/tools/ 并追加 INDEX 行,
|
|
203
|
+
// 随后 index-gen 重建时该目录不被扫描 ⇒ 条目**静默从 INDEX 消失且不计入 dropped**。
|
|
204
|
+
if (!SOLUTION_PHASES.includes(phase)) {
|
|
205
|
+
skipped.push({
|
|
206
|
+
title: learning.title,
|
|
207
|
+
reason: `phase="${phase}" 不在合法枚举(${SOLUTION_PHASES.join(' | ')})`,
|
|
208
|
+
});
|
|
185
209
|
continue;
|
|
186
210
|
}
|
|
187
211
|
|
|
188
|
-
// 写入全局
|
|
189
212
|
const date = learning.fm?.date || new Date().toISOString().slice(0, 10);
|
|
213
|
+
// title 仅供检索/摘要/frontmatter 三处复用(§4.1),**不进身份键**——清洗会抹掉区分度
|
|
214
|
+
const title = cleanTitle(learning.title);
|
|
215
|
+
const slug = slugify(title, { maxLength: SLUG_MAX_LENGTH });
|
|
190
216
|
const phaseDir = join(solutionsDir, phase);
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
|
|
218
|
+
// 正文标题也走清洗:签名基准须跨 change 可比(序号是 change 局部编号,见 entryBodySignature)
|
|
219
|
+
const entryBody = renderEntryBody({ ...learning, title });
|
|
220
|
+
const resolved = resolveEntryPath(phaseDir, date, slug, entryBodySignature(entryBody));
|
|
221
|
+
if (!resolved) {
|
|
222
|
+
skipped.push({
|
|
223
|
+
title: learning.title,
|
|
224
|
+
reason: `同标题条目已达上限 ${MAX_COLLISION_SUFFIX} 条,未写入:${phase}/${slug}-*.md`,
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const { filePath, fileName, isNew } = resolved;
|
|
229
|
+
|
|
230
|
+
// 既有条目:登记来源 change(同 change 重跑 = 幂等不升档;新来源 = 升一档)
|
|
231
|
+
if (!isNew) {
|
|
232
|
+
const outcome = confirmByFile(filePath, changeName);
|
|
233
|
+
if (!outcome) {
|
|
234
|
+
skipped.push({ title: learning.title, reason: `既有条目缺 frontmatter,无法登记确认:${phase}/${fileName}` });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (outcome.alreadyConfirmed) {
|
|
238
|
+
console.log(`Unchanged: ${phase}/${fileName}(${changeName} 已登记)`);
|
|
239
|
+
unchanged++;
|
|
240
|
+
} else {
|
|
241
|
+
console.log(`Confirmed: ${phase}/${fileName} severity ${outcome.from} → ${outcome.to}`);
|
|
242
|
+
updated++;
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
193
245
|
}
|
|
194
246
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const filePath = join(phaseDir, fileName);
|
|
247
|
+
// 新建条目
|
|
248
|
+
if (!existsSync(phaseDir)) mkdirSync(phaseDir, { recursive: true });
|
|
198
249
|
|
|
199
250
|
const fileContent = `---
|
|
200
251
|
phase: ${phase}
|
|
@@ -202,29 +253,30 @@ domain: ${domain}
|
|
|
202
253
|
type: ${type}
|
|
203
254
|
severity: ${severity}
|
|
204
255
|
date: ${date}
|
|
205
|
-
source: ${
|
|
256
|
+
source: ${changeName}
|
|
257
|
+
title: ${title}
|
|
258
|
+
confirmations: [${changeName}]
|
|
206
259
|
---
|
|
207
260
|
|
|
208
|
-
${
|
|
261
|
+
${entryBody}
|
|
209
262
|
`;
|
|
210
263
|
writeFileSync(filePath, fileContent, 'utf-8');
|
|
211
264
|
|
|
212
|
-
// 追加 INDEX
|
|
213
|
-
const indexRow = `| ${date} | ${phase} | ${domain} | ${type} | ${severity} | ${learning.title.slice(0, 80)} | ${phase}/${fileName} |\n`;
|
|
214
|
-
appendFileSync(indexPath, indexRow, 'utf-8');
|
|
215
|
-
indexContent += indexRow;
|
|
216
|
-
|
|
217
265
|
console.log(`Promoted: ${phase}/${fileName}`);
|
|
218
266
|
promoted++;
|
|
219
267
|
}
|
|
220
268
|
|
|
269
|
+
// v0.57.0 §4.2:INDEX 由**共享入口**派生(promote 不再直接写 INDEX)。
|
|
270
|
+
// refreshIndex 在目录缺失时创建,故首次晋升的项目也能正常产出索引。
|
|
271
|
+
refreshIndex(solutionsDir, { quiet: true });
|
|
272
|
+
|
|
221
273
|
// v0.49.0 §83.3.5:解析/晋升/确认/跳过汇总(原实现只报 promoted/updated)
|
|
222
|
-
console.log(`\nPromotion complete: ${promoted} promoted, ${updated} confirmed, ${skipped.length} skipped`);
|
|
274
|
+
console.log(`\nPromotion complete: ${promoted} promoted, ${updated} confirmed, ${unchanged} unchanged, ${skipped.length} skipped`);
|
|
223
275
|
if (skipped.length > 0) {
|
|
224
276
|
console.log('Skipped entries:');
|
|
225
277
|
for (const item of skipped) console.log(` - ${item.title}: ${item.reason}`);
|
|
226
278
|
}
|
|
227
|
-
return { promoted, updated, skipped: skipped.length };
|
|
279
|
+
return { promoted, updated, unchanged, skipped: skipped.length };
|
|
228
280
|
}
|
|
229
281
|
|
|
230
282
|
if (process.env.NODE_ENV !== 'test' && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -28,6 +28,7 @@ import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync, readdirSync
|
|
|
28
28
|
import { join, basename, relative, resolve } from 'node:path';
|
|
29
29
|
import { execSync } from 'node:child_process';
|
|
30
30
|
import { slugify } from './slug.mjs';
|
|
31
|
+
import { parseTableRow } from './md-normalize.mjs';
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* 解析 CLI 参数数组为结构化对象
|
|
@@ -94,12 +95,26 @@ export function extractSummary(content) {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
98
|
+
* test-matrix 中承载「测试模块」的 `## ` 段名白名单。
|
|
99
|
+
*
|
|
100
|
+
* v0.57.0 §4.3(设计文档 compound-lifecycle-governance-design v2.2):
|
|
101
|
+
* 原实现的 docstring 写「## Cases 下的 ### 子段」,**实现却是全文扫描所有 `### `**,
|
|
102
|
+
* 于是附录/台账段(`### 执行结果(三仓实测…)`、`### case_id ↔ 测试方法逐条对账`、
|
|
103
|
+
* `### Task 5.x`、`### W6-1 / W6-2 收口`)也被当作模块生成 baseline——
|
|
104
|
+
* emp-auth 实测 C1 的 21 个 `###` 中 **12 个属此类**,文件仅 440–520B、无测试复用价值。
|
|
105
|
+
*
|
|
106
|
+
* 两条硬约束:
|
|
107
|
+
* - MUST 用**精确段名**比较——`## Adversarial Cases` 含子串 `Cases`,用 `includes` 会误判边界
|
|
108
|
+
* - `## E2E / AC Verification` 不可省——它是 v0.38.0 引入 E2E 层级时的遗漏;实测
|
|
109
|
+
* `vrm4teamflow/changes/C2-policy-management` 的 `### AC1..AC8`(**32 个 AC/E2E 用例**)
|
|
110
|
+
* 位于该段下,只列 `## Cases` 会让这批台账**静默冻结**:落选文件不会从 INDEX 消失
|
|
111
|
+
* (`rewriteIndex` 扫 baselines 目录),故既无报错也无计数变化
|
|
99
112
|
*/
|
|
100
|
-
|
|
113
|
+
export const MODULE_SECTIONS = Object.freeze(['## Cases', '## E2E / AC Verification']);
|
|
114
|
+
|
|
115
|
+
/** 扫描给定行范围内的 `### ` 子段(不判定边界,边界由调用方给定) */
|
|
116
|
+
function scanModuleHeadings(lines) {
|
|
101
117
|
const sections = {};
|
|
102
|
-
const lines = content.split('\n');
|
|
103
118
|
let currentModule = null;
|
|
104
119
|
let currentLines = [];
|
|
105
120
|
|
|
@@ -120,6 +135,45 @@ function extractModuleSections(content) {
|
|
|
120
135
|
return sections;
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
/**
|
|
139
|
+
* 从 test-matrix.md 提取模块分段(**白名单段**下的 `### ` 子段)
|
|
140
|
+
* 返回 { moduleName: sectionContent } 映射
|
|
141
|
+
*
|
|
142
|
+
* @param {string} content
|
|
143
|
+
* @param {{sectionNames?: readonly string[], onWarn?: (msg: string) => void}} [options]
|
|
144
|
+
*/
|
|
145
|
+
export function extractModuleSections(content, { sectionNames = MODULE_SECTIONS, onWarn = null } = {}) {
|
|
146
|
+
const lines = content.split('\n');
|
|
147
|
+
|
|
148
|
+
// 定位各白名单段的区间:[起, 止),止于下一个 `## ` 标题
|
|
149
|
+
const bounds = [];
|
|
150
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
151
|
+
if (!sectionNames.includes(lines[i].trim())) continue;
|
|
152
|
+
let end = lines.length;
|
|
153
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
154
|
+
if (/^## /.test(lines[j])) { end = j; break; }
|
|
155
|
+
}
|
|
156
|
+
bounds.push([i + 1, end]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (bounds.length === 0) {
|
|
160
|
+
// 无白名单段 ⇒ 回退全文扫描并**显式告警**。
|
|
161
|
+
// 原实现的"静默空操作"(sections = {} → 打印 0 modules processed)不可接受:
|
|
162
|
+
// 那会让整类台账在无任何提示的情况下停止更新。
|
|
163
|
+
if (onWarn) {
|
|
164
|
+
onWarn(`test-matrix 未找到模块段(${sectionNames.join(' / ')}),已回退为扫描全文 ### 子段。`
|
|
165
|
+
+ '若该文件的模块段命名不同,请补入 MODULE_SECTIONS。');
|
|
166
|
+
}
|
|
167
|
+
return scanModuleHeadings(lines);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const sections = {};
|
|
171
|
+
for (const [start, end] of bounds) {
|
|
172
|
+
Object.assign(sections, scanModuleHeadings(lines.slice(start, end)));
|
|
173
|
+
}
|
|
174
|
+
return sections;
|
|
175
|
+
}
|
|
176
|
+
|
|
123
177
|
/**
|
|
124
178
|
* 从 test-matrix.md 提取 Candidate Coverage Ledger
|
|
125
179
|
*/
|
|
@@ -137,7 +191,9 @@ function extractCandidateLedger(content) {
|
|
|
137
191
|
break;
|
|
138
192
|
}
|
|
139
193
|
if (inLedger && line.startsWith('|') && !line.match(/^\|\s*[-:]+/)) {
|
|
140
|
-
|
|
194
|
+
// v0.57.0 §4.4:改用共享层 `parseTableRow`(原 `filter(Boolean)` 丢空列 →
|
|
195
|
+
// 台账中任何一个空单元格都会让后续列左移,`decision`/`caseIds`/`reason` 全部错位)
|
|
196
|
+
const cells = parseTableRow(line);
|
|
141
197
|
if (cells.length >= 3 && cells[0] !== 'candidate') {
|
|
142
198
|
candidates.push({
|
|
143
199
|
candidate: cells[0],
|
|
@@ -169,7 +225,8 @@ function extractDeferredItems(content) {
|
|
|
169
225
|
break;
|
|
170
226
|
}
|
|
171
227
|
if (inDeferred && line.startsWith('|') && !line.match(/^\|\s*[-:]+/)) {
|
|
172
|
-
|
|
228
|
+
// v0.57.0 §4.4:同 `extractCandidateLedger`,改用共享层 `parseTableRow`
|
|
229
|
+
const cells = parseTableRow(line);
|
|
173
230
|
if (cells.length >= 3 && cells[0] !== 'case_id') {
|
|
174
231
|
items.push({
|
|
175
232
|
caseId: cells[0],
|
|
@@ -200,7 +257,9 @@ function legacySlug(moduleName) {
|
|
|
200
257
|
*
|
|
201
258
|
* 故读取时双查找:新 slug 优先 → 旧 slug 回退 → 都不存在才按新规则新建。
|
|
202
259
|
* 命中旧命名文件后**原地更新**(保持旧名,不强制改名)——批量改名属 P1 的
|
|
203
|
-
* 稳定 `module_id` + `--rekey`
|
|
260
|
+
* 稳定 `module_id` + `--rekey` 迁移范畴(见设计文档 compound-lifecycle-governance-design §4.5;
|
|
261
|
+
* 该文档位于 team-flow-workspace 工作区,**不随插件发布**,故此处不给路径避免死引用);
|
|
262
|
+
* 该方案 v2.0 起已把「稳定 module_id」从 §4.3 移至 §4.5,§4.3 改为 test-matrix 模块边界)。
|
|
204
263
|
*/
|
|
205
264
|
function resolveBaselinePath(baselinesDir, moduleName) {
|
|
206
265
|
const next = join(baselinesDir, `${slugify(moduleName)}.md`);
|
|
@@ -274,7 +333,7 @@ export function createNewBaseline(moduleName, sectionContent, changeName, date,
|
|
|
274
333
|
l.startsWith('|') && !l.match(/^\|\s*[-:]+/) && !l.match(/\|\s*case_id/)
|
|
275
334
|
);
|
|
276
335
|
for (const line of caseLines) {
|
|
277
|
-
const cells = line
|
|
336
|
+
const cells = parseTableRow(line);
|
|
278
337
|
if (cells.length >= 11) {
|
|
279
338
|
lines.push(`| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`);
|
|
280
339
|
}
|
|
@@ -317,7 +376,7 @@ export function mergeExistingBaseline(existing, sectionContent, changeName, date
|
|
|
317
376
|
);
|
|
318
377
|
|
|
319
378
|
const renderCaseRow = (line) => {
|
|
320
|
-
const cells = line
|
|
379
|
+
const cells = parseTableRow(line);
|
|
321
380
|
if (cells.length < 11) return null;
|
|
322
381
|
return `| ${cells[0]} | ${cells[1]} | ${cells[2]} | ${cells[5]} | ${cells[6]} | ${cells[8]} | ${cells[9]} | ${cells[10]} | ${changeName} |`;
|
|
323
382
|
};
|
|
@@ -390,7 +449,7 @@ export function resolveDeferred(ledgerDir, changeName, newCaseIds, dryRun) {
|
|
|
390
449
|
continue;
|
|
391
450
|
}
|
|
392
451
|
if (inDeferred && line.startsWith('|') && !line.match(/^\|\s*[-:]/)) {
|
|
393
|
-
const cells = line
|
|
452
|
+
const cells = parseTableRow(line);
|
|
394
453
|
if (cells.length && newCaseIds.includes(cells[0])) {
|
|
395
454
|
changed = true;
|
|
396
455
|
resolved++;
|
|
@@ -547,7 +606,9 @@ async function main(argv, projectRoot) {
|
|
|
547
606
|
|
|
548
607
|
const matrixContent = preCheckResult.content;
|
|
549
608
|
const summary = extractSummary(matrixContent);
|
|
550
|
-
const moduleSections = extractModuleSections(matrixContent
|
|
609
|
+
const moduleSections = extractModuleSections(matrixContent, {
|
|
610
|
+
onWarn: msg => console.warn(`⚠ ${msg}`),
|
|
611
|
+
});
|
|
551
612
|
const candidateLedger = extractCandidateLedger(matrixContent);
|
|
552
613
|
const deferredItems = extractDeferredItems(matrixContent);
|
|
553
614
|
|
|
@@ -573,7 +634,7 @@ async function main(argv, projectRoot) {
|
|
|
573
634
|
const lines = section.split('\n');
|
|
574
635
|
for (const line of lines) {
|
|
575
636
|
if (line.startsWith('|') && !line.match(/^\|\s*[-:]+/) && !line.match(/\|\s*case_id/)) {
|
|
576
|
-
const cells = line
|
|
637
|
+
const cells = parseTableRow(line);
|
|
577
638
|
if (cells[0]) newCaseIds.push(cells[0]);
|
|
578
639
|
}
|
|
579
640
|
}
|
package/scripts/team-flow.mjs
CHANGED
|
@@ -128,12 +128,16 @@ Commands:
|
|
|
128
128
|
Read one allowlisted package asset for a skill
|
|
129
129
|
solutions index-gen [--dir <solutions-dir>]
|
|
130
130
|
Rebuild docs/solutions/INDEX.md (v0.5 compound-learning)
|
|
131
|
-
solutions inject --phase <p> [--domain <d>] [--dir <solutions-dir>]
|
|
132
|
-
Emit
|
|
131
|
+
solutions inject --phase <p> [--domain <d>] [--limit <n>|--all] [--dir <solutions-dir>]
|
|
132
|
+
Emit filtered solution summaries for skill context injection
|
|
133
|
+
(default: top 5; sorted by severity, then phase-match, then date desc)
|
|
133
134
|
solutions capture --phase <p> --domain <d> --type <t> --severity <s> --summary "<text>" [--source <src>] [--dir <solutions-dir>]
|
|
134
|
-
Capture one learning into docs/solutions/
|
|
135
|
+
Capture one learning into docs/solutions/ (INDEX rebuilt via index-gen)
|
|
135
136
|
solutions promote <change-dir> [--dir <solutions-dir>]
|
|
136
137
|
Promote qualifying change learnings into global docs/solutions/
|
|
138
|
+
(idempotent by file name; writes severity + confirmations to the entry file)
|
|
139
|
+
solutions backfill [--dry-run] [--dir <solutions-dir>]
|
|
140
|
+
Add missing title: field to existing entries (v0.57.0; needed by learnings-researcher)
|
|
137
141
|
install-cursor Deploy skills/scripts/docs to .cursor/ (local Cursor setup)
|
|
138
142
|
install-workbuddy Deploy skills to WorkBuddy marketplace and enable them
|
|
139
143
|
install-cline Deploy to .cline/ + .clinerules/ (Cline)
|
|
@@ -207,8 +207,8 @@ architecture-design 执行时的上下文组装:
|
|
|
207
207
|
4. 注入为架构决策约束(advisory 级)
|
|
208
208
|
|
|
209
209
|
**架构复利注入(v0.36.3,复利链路修复)**:
|
|
210
|
-
1. 执行 `tf solutions inject --phase architecture
|
|
211
|
-
2. 读取失败 / 无条目 →
|
|
210
|
+
1. 执行 `tf solutions inject --phase architecture --limit 15`(失败不阻断:无索引时 CLI 输出一行 WARN 并返回空结果)——注入 `docs/solutions/` 中 phase=architecture 的历史架构决策经验(BC 边界取舍/聚合划分理由/事件投影设计踩坑)
|
|
211
|
+
2. 读取失败 / 无条目 → 跳过、不阻断(advisory 级,与 S1 复利注入同语义;**无索引时 CLI 会输出一行 WARN 并返回空结果,那不是错误**)
|
|
212
212
|
|
|
213
213
|
**始终加载**(**Fast Path 除外**——见上文 `### Fast Path`,该路径不读 `docs/architecture/` 下任何文件):
|
|
214
214
|
1. `Read docs/architecture/INDEX.md`(~50行摘要)
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
- **触发场景**:BC 边界取舍、聚合划分理由、事件/读模型投影设计模式、技术选型决策、逆向重建低置信度修正
|
|
59
59
|
- **命令**:`tf solutions capture --phase architecture --domain <触及BC> --type pitfall|pattern --severity medium --summary "<经验>"`(source: `iterations/vN/architecture.md`)
|
|
60
|
-
- **注入端**:后续架构设计(变更级/产品级)执行时 `tf solutions inject --phase architecture` 自动注入这些经验(architecture-design 加载协议,v0.36.3
|
|
60
|
+
- **注入端**:后续架构设计(变更级/产品级)执行时 `tf solutions inject --phase architecture --limit 15` 自动注入这些经验(architecture-design 加载协议,v0.36.3;`--limit` v0.57.0 补)
|
|
61
61
|
- **晋升说明**:产品级架构经验直接 capture 写 `docs/solutions/`(不走 change 级 promote——promote 只认 `learnings.md`,是变更级机制)
|
|
62
62
|
|
|
63
63
|
## 正向设计 vs 逆向重建
|
|
@@ -11,7 +11,11 @@ Controls the implementation phase. Uses `execution-contract.md` as the workflow
|
|
|
11
11
|
|
|
12
12
|
Read: `execution-contract.md`, `tasks.md`, relevant `specs/`, relevant `design.md`. (Skip contract/spec requirements when workflow is `tweak`.)
|
|
13
13
|
|
|
14
|
-
**复利经验注入(v0.49.0 §83.3.5
|
|
14
|
+
**复利经验注入(v0.49.0 §83.3.5;v0.57.0 §4.1 调整 phase 取值与窗口)**:运行 `tf solutions inject --phase build --limit 15`(失败不阻断:无索引时 CLI 输出一行 WARN 并返回空结果,继续即可)——注入 `docs/solutions/` 中构建阶段与跨阶段的历史经验(踩坑/模式),供实施时规避已知问题。注入内容是**经验提示而非约束**:与当前 change 的契约/规格冲突时以契约/规格为准,并把冲突回报给 workflow-start。
|
|
15
|
+
|
|
16
|
+
> **为什么必须显式传 `--limit`**:默认 top-5 在 cross-phase 条目已达 5 条时**满载**,本阶段新增条目(低 severity)永不可达——`--limit` 才是那个杠杆。换 `--phase` 取值**不是**杠杆:cross-phase 是通配符,当库内无本阶段条目时两者注入的**就是同一批**,跑两次只会让同一批经验在上下文中出现两遍。
|
|
17
|
+
> **为什么取 15**:3× 默认窗口,够容纳"cross-phase 满载 + 若干本阶段新增"的典型情形;不取 `--all` 是因它无上界,与上下文预算口径(AGENTS.md 的 ~6.5k token)冲突。**调小前须确认本阶段新增条目不会被重新挤掉**——这正是本参数的设立原因。
|
|
18
|
+
> 排序为 severity 降序 → 阶段匹配度(本阶段优先于通配)→ date 降序;同源条目(同 `source`,即同一 change 来源)折叠为 1 条并标注条数。折叠是**展示策略**,条目本身仍在盘上。
|
|
15
19
|
|
|
16
20
|
When `architecture/sql/` exists, also read:
|
|
17
21
|
- `architecture/sql/ddl/*.sql` — executable DDL scripts; reference directly for DB schema change tasks instead of hand-writing DDL
|
|
@@ -8,8 +8,8 @@ Detailed context scanning logic for Phase 1.1. The main SKILL.md describes the h
|
|
|
8
8
|
|
|
9
9
|
**Standard and Deep** — Two passes:
|
|
10
10
|
|
|
11
|
-
*Constraint Check (inline)* — Use the project's active instructions and conventions already in your context. Read `STRATEGY.md` if it exists for product direction and `CONCEPTS.md` if it exists for canonical vocabulary. Use canonical names in dialogue, approaches, and the Product Contract.
|
|
12
|
-
- **Solutions index (v0.5)**: Read `docs/solutions/INDEX.md` if it exists. Filter entries where `phase = prd OR phase = cross-phase` and `domain` matches the current topic. Inject top
|
|
11
|
+
*Constraint Check (inline)* — Use the project's active instructions and conventions already in your context. Read `STRATEGY.md` if it exists for product direction and `CONCEPTS.md` if it exists for canonical vocabulary — it lives at **`docs/architecture/CONCEPTS.md`** (the repo-root location was retired in v0.23.0, so a root-only probe misses bootstrapped projects). Use canonical names in dialogue, approaches, and the Product Contract.
|
|
12
|
+
- **Solutions index (v0.5)**: Read `docs/solutions/INDEX.md` if it exists. Filter entries where `phase = prd OR phase = cross-phase` and `domain` matches the current topic. Inject the **top 5** summaries as context constraints, ordered **severity → phase-match → date** (the CLI's ordering). Widen the window with `tf solutions inject --phase prd --limit <n>` when cross-phase entries saturate it — otherwise this stage's own entries are unreachable. If INDEX.md does not exist or is empty, skip silently — a missing index makes the CLI print one WARN, which is not an error.
|
|
13
13
|
|
|
14
14
|
## Topic Scan (Grounding Scout)
|
|
15
15
|
|
|
@@ -18,28 +18,45 @@ tf solutions promote <change-dir>
|
|
|
18
18
|
|
|
19
19
|
满足以下条件的经验从 change 级别晋升到全局 `docs/solutions/`:
|
|
20
20
|
|
|
21
|
-
- **severity ≥ medium** 且 **type = pitfall 或 pattern** → 晋升到全局 `docs/solutions
|
|
22
|
-
-
|
|
21
|
+
- **severity ≥ medium** 且 **type = pitfall 或 pattern** → 晋升到全局 `docs/solutions/<phase>/`
|
|
22
|
+
- 与既有条目**文件名与正文签名都相同** → 同一条经验:登记来源 change + 升级 severity(见下「同名条目确认」),**不新建文件**
|
|
23
|
+
- 与既有条目**同标题但正文不同** → **另一条**经验:新建条目(文件名加 `-2`/`-3` 后缀)——**正文不合并**
|
|
24
|
+
|
|
25
|
+
> 判重的键是「**标题分量 + 内容分量**」,不是分类标签:旧实现按 `domain + type` 判重
|
|
26
|
+
> (`domain` 由 LLM 自由命名)⇒ 既不稳定(该合的没合)又太粗(不该合的硬合),
|
|
27
|
+
> 且命中即丢弃正文(实测丢 13 条)。v0.57.0 起每条经验独立成条,正文零丢失。
|
|
23
28
|
|
|
24
29
|
### Severity 标准
|
|
25
30
|
|
|
26
31
|
| Severity | 含义 | 晋升行为 |
|
|
27
32
|
|----------|------|----------|
|
|
28
|
-
| critical | 安全 / 数据 / 合规级高危经验(凭证熵源、越权、数据损坏等) | 满足 type 条件时必然晋升;INDEX
|
|
33
|
+
| critical | 安全 / 数据 / 合规级高危经验(凭证熵源、越权、数据损坏等) | 满足 type 条件时必然晋升;severity 是其 INDEX 排序的首键,故自动靠前(INDEX 是派生物,无需手动重建) |
|
|
29
34
|
| high | 阻塞性问题或关键模式 | 满足 type 条件时必然晋升 |
|
|
30
35
|
| medium | 有显著影响的问题或可复用模式 | 满足 type 条件时晋升 |
|
|
31
36
|
| low | 轻微问题或局部洞察 | 不晋升,保留在 change 级别 |
|
|
32
37
|
|
|
33
38
|
> **晋升是 severity × type 的合取判定**(`severity ≥ medium` **且** `type ∈ {pitfall, pattern}`)——
|
|
34
39
|
> 任一维度不满足即 skipped,critical 也不例外(如 `critical` + `insight` 不晋升)。
|
|
35
|
-
>
|
|
40
|
+
> **INDEX 是派生物**(v0.57.0 §4.2):promote 写完条目文件后调用 `refreshIndex()` 重建 INDEX
|
|
41
|
+
> (**入口内建**,不靠调用点枚举——promote 与 capture 各有多个落盘分支,逐处追加必然漏改),
|
|
42
|
+
> 故条目顺序(severity 降序 → date 降序 → file 兜底)与 severity 取值在下一次读写时即已一致,无需额外命令。
|
|
43
|
+
> (v0.57.0 之前 promote 只在 INDEX 行上做 append/原地改 severity,与 index-gen 的全量重建互为回滚——
|
|
44
|
+
> 旧文档"「置顶」需 `tf solutions index-gen` 重建后生效"描述的是那个已废止的行为。)
|
|
36
45
|
> 取值域与「序」由 `scripts/lib/severity.mjs` 唯一定义(v0.23 §91);新增等级只改该文件,不在消费点各自实现。
|
|
37
46
|
> 取值**区分大小写**,须全小写(`Critical` 会被判为未知值:排最后且不晋升)。
|
|
38
47
|
|
|
39
|
-
###
|
|
48
|
+
### 同名条目确认
|
|
49
|
+
|
|
50
|
+
当晋升的经验与既有条目的**文件名与正文签名都相同**时(同一天、同标题、同内容):
|
|
40
51
|
|
|
41
|
-
当晋升的经验与全局 INDEX 中已有条目的 domain + type 匹配时:
|
|
42
52
|
1. 不创建新文件
|
|
43
|
-
2.
|
|
44
|
-
3. severity
|
|
45
|
-
4.
|
|
53
|
+
2. 在**条目文件** frontmatter 的 `confirmations` 追加来源 change
|
|
54
|
+
3. **按其**升级 severity(low → medium → high → critical,`critical` 封顶)
|
|
55
|
+
4. **不写 INDEX** —— INDEX 由 `refreshIndex()` 从条目文件派生
|
|
56
|
+
|
|
57
|
+
- **幂等**:同一 change 重复 promote 记为 `unchanged`,**不**升档(重跑不是新证据)
|
|
58
|
+
- **升级依据**:每新增一个**来源 change**升一档,且 `critical` 封顶后不再升——故
|
|
59
|
+
`confirmations` 的长度是**升档次数的上界**,不等于最终档位(如 `[a, b, c]` 长度 3,
|
|
60
|
+
severity 也可能只是 `critical` 而非"3 档")
|
|
61
|
+
- 旧文档描述的"在已有条目中标记『已确认模式』"**从未落地**(代码里只有 `confirmed: N` 计数);
|
|
62
|
+
v0.57.0 起该字段改名为 `confirmations: [...]`,语义也从"计数"改为**来源 change 集合**
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
# v0.49.0 §83.3.5(来源:workflow-feedback 20260909):docs/solutions/ 有两条产出通道,
|
|
6
6
|
# frontmatter 契约不同 ——
|
|
7
7
|
# ① ce-compound 手工条目:使用本文件的 schema(module / problem_type / component / ...)
|
|
8
|
-
# ② tf solutions promote 自动晋升条目:phase / domain / type / severity / date /
|
|
9
|
-
# (见 scripts/lib/solutions-promote.mjs,字段源自 change 根
|
|
8
|
+
# ② tf solutions promote 自动晋升条目:phase / domain / type / severity / date /
|
|
9
|
+
# source / title / confirmations(见 scripts/lib/solutions-promote.mjs,字段源自 change 根
|
|
10
|
+
# learnings.md。`title` 是检索字段与 INDEX 摘要来源;`confirmations` 是确认过该条经验的
|
|
11
|
+
# 来源 change 集合——每新增一个来源升一档 severity,同 change 重跑不升)
|
|
10
12
|
# 共同消费面 = docs/solutions/INDEX.md(tf solutions inject 只读该文件,不读条目 frontmatter)。
|
|
11
13
|
# 下方 schema 仅适用于通道 ①。
|
|
12
14
|
#
|