@xulthekl/team-flow 0.56.1 → 0.58.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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- package/.cursor-plugin/marketplace.json +2 -2
- package/.cursor-plugin/plugin.json +2 -2
- package/.github/plugin/marketplace.json +2 -2
- package/AGENTS.md +37 -17
- package/CHANGELOG.md +123 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +8 -7
- package/agents/code-reviewer.md +3 -0
- package/agents/cross-change-consistency-checker.md +34 -6
- package/agents/release-archivist.md +6 -0
- package/docs/README_en.md +1 -1
- package/docs/release-checklist.md +1 -1
- package/docs/solutions/INDEX.md +3 -3
- package/docs/usage-guide.md +2 -2
- package/gemini-extension.json +2 -2
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/plugin.json +2 -2
- 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/architecture-design/templates/conventions/frontend-patterns.md +7 -0
- package/skills/build-executor/SKILL.md +7 -1
- package/skills/build-executor/implementer-prompt.md +19 -0
- package/skills/build-executor/task-reviewer-prompt.md +51 -5
- 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/clean-code/SKILL.md +116 -0
- package/skills/clean-code/references/judgement-cases.md +83 -0
- package/skills/clean-code/references/shared-layer-rules.md +50 -0
- package/skills/code-reviewer/SKILL.md +17 -0
- package/skills/code-reviewer/code-reviewer-prompt.md +74 -2
- 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/skills/workflow-orchestrator/references/s5-monitoring.md +8 -4
- package/skills/workflow-start/SKILL.md +4 -0
- package/templates/conventions/glaf4-compliant/java-testing.md +3 -3
- package/templates/conventions/js-testing.md +1 -1
- package/templates/conventions/python-testing.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 逆向重建
|
|
@@ -10,6 +10,13 @@ date: 2026-07-31
|
|
|
10
10
|
> 本文件由 team-flow 被动式自动沉淀机制维护(v0.11 §33)。
|
|
11
11
|
> 请勿手动编辑模板——此文件作为参考模板,实际项目规范在 `.team-flow/conventions/frontend-patterns.md` 中维护。
|
|
12
12
|
|
|
13
|
+
## 魔法值(v0.58.0)
|
|
14
|
+
|
|
15
|
+
- **数值 / 字符串字面量 MUST NOT 散落在模板与脚本中**——收敛到 `src/constants/` 下的常量模块,或组件内具名常量。
|
|
16
|
+
- 判据归属:`clean-code` skill 的「魔法值(前端)」判据,分级 **Critical**(与后端「不允许魔法常量」对齐;此前前端无任何等价约束)。
|
|
17
|
+
- 可豁免:结构性索引(如 `arr[0]`)、CSS 取值、仅出现一次且语义自明的字面量。
|
|
18
|
+
- 背景:v1 实测中前端因无约束,同一 change 内出现 21 处硬编码业务码与状态值,被审查降级为 Minor。
|
|
19
|
+
|
|
13
20
|
## 框架选型
|
|
14
21
|
- (项目级规范在此补充,如:Vue 2 Options API)
|
|
15
22
|
|
|
@@ -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
|
|
@@ -68,6 +72,8 @@ RED (write test, see it fail) → GREEN (write minimal code, see it pass) → RE
|
|
|
68
72
|
### Law 3: Review Before Drift
|
|
69
73
|
Block on: logic defects, spec violations, missing required tests, unintended scope expansion.
|
|
70
74
|
|
|
75
|
+
**Structural criteria(clean-code,v0.58.0)**:魔法值(未命名字面量)= Critical;函数长度 / 嵌套深度 / 参数个数 / 命名形式 = Minor;单一职责与 DRY 为审查必答项(无阈值)。**执行处是三个派发模板**(`implementer-prompt.md` / `task-reviewer-prompt.md` / `code-reviewer-prompt.md`)——它们各自内联了完整判据,因为由模板派发的子代理读不到 skill。本摘要仅供编排参考;判据真相源见 `clean-code` skill。
|
|
76
|
+
|
|
71
77
|
### Law 4: Rewind on Contract Break
|
|
72
78
|
Return to `specifying` or `bridging` if: new behavior appears, interfaces change materially, design assumptions fail, artifacts no longer define intended implementation.
|
|
73
79
|
|
|
@@ -130,6 +130,25 @@ Subagent (general-purpose):
|
|
|
130
130
|
- Are names clear and accurate (match what things do, not how they work)?
|
|
131
131
|
- Is the code clean and maintainable?
|
|
132
132
|
|
|
133
|
+
**Structural criteria** — apply to every function this change adds or modifies:
|
|
134
|
+
|
|
135
|
+
- **Magic values** (unnamed numeric/string literals): name them as constants or enums.
|
|
136
|
+
This is **Critical** — do not ship unnamed literals. Applies to front-end code too.
|
|
137
|
+
- **Function length** >20 lines, **nesting depth** >2 levels, parameters >3,
|
|
138
|
+
**naming form** (constants SCREAMING_SNAKE; booleans `is`/`has`/`can` prefix):
|
|
139
|
+
Minor. When counting nesting, exclude try/catch's own level and guard-clause
|
|
140
|
+
`return`/`continue` levels; framework-fixed signatures are exempt from the
|
|
141
|
+
parameter rule.
|
|
142
|
+
- **Single responsibility**: can the function name cover ALL steps in the body?
|
|
143
|
+
If not, split it — or state in your report which steps it cannot cover.
|
|
144
|
+
- **DRY**: is there a structurally equivalent logic block elsewhere in THIS repository
|
|
145
|
+
(ignore comments, whitespace, identifier names)? If the duplicate lives in the same
|
|
146
|
+
shared publish unit (same package / module / directory), extract a shared layer
|
|
147
|
+
rather than copying — if you judge copying is right, say why in your report.
|
|
148
|
+
- **Pre-existing code**: these criteria apply only to what this change touches. A hit
|
|
149
|
+
inside a function that already existed is Minor — register it in your report as
|
|
150
|
+
`存量待整改` rather than restructuring outside your task scope.
|
|
151
|
+
|
|
133
152
|
**Discipline:**
|
|
134
153
|
- Did I avoid overbuilding (YAGNI)?
|
|
135
154
|
- Did I only build what was requested?
|