@xulthekl/team-flow 0.22.4 → 0.23.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 +3 -2
- package/CHANGELOG.md +53 -0
- package/GEMINI.md +1 -1
- package/HANDOFF.md +123 -98
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/docs/README_en.md +1 -1
- package/gemini-extension.json +1 -1
- package/hooks/session-start +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/guard/checks/arch-design.mjs +79 -0
- package/scripts/guard/guard.mjs +3 -1
- package/scripts/lib/arch-merge.mjs +459 -0
- package/scripts/lib/cmd-state.mjs +3 -0
- package/scripts/lib/hash.mjs +18 -0
- package/scripts/lib/state-loader.mjs +11 -0
- package/scripts/team-flow.mjs +3 -0
- package/skills/architecture-design/SKILL.md +42 -3
- package/skills/architecture-design/templates/api.md +71 -0
- package/skills/architecture-design/templates/architecture.md +82 -0
- package/skills/architecture-design/templates/change-brief.md +29 -0
- package/skills/architecture-design/templates/database.md +69 -0
- package/skills/architecture-design/templates/index.md +33 -0
- package/skills/architecture-design/templates/physical-model.md +93 -0
- package/skills/bug-investigator/SKILL.md +1 -1
- package/skills/build-executor/SKILL.md +23 -19
- package/skills/build-executor/implementer-prompt.md +1 -1
- package/skills/build-executor/references/execution-modes.md +6 -6
- package/skills/build-executor/task-reviewer-prompt.md +1 -1
- package/skills/ce-brainstorm/SKILL.md +6 -0
- package/skills/code-reviewer/SKILL.md +6 -2
- package/skills/code-reviewer/code-reviewer-prompt.md +1 -1
- package/skills/contract-builder/SKILL.md +6 -6
- package/skills/need-explorer/SKILL.md +2 -2
- package/skills/release-archivist/SKILL.md +25 -12
- package/skills/release-archivist/references/closing-procedures.md +8 -8
- package/skills/spec-merger/SKILL.md +2 -2
- package/skills/spec-writer/SKILL.md +11 -9
- package/skills/workflow-bootstrap/SKILL.md +28 -5
- package/skills/workflow-bootstrap/references/b1-reconnaissance.md +16 -1
- package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +1 -1
- package/skills/workflow-orchestrator/references/s4-split-validate.md +1 -1
- package/skills/workflow-orchestrator/references/s5-monitoring.md +1 -1
- package/skills/workflow-start/SKILL.md +44 -16
- package/skills/workflow-start/references/routing-rules.md +17 -17
- package/templates/api.md +177 -0
- package/templates/architecture.md +122 -0
- package/templates/change-brief.md +24 -0
- package/templates/database.md +114 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// scripts/guard/checks/arch-design.mjs — architecture-design artifacts guard
|
|
2
|
+
// v0.23.0 §30: 架构设计门控——exploring:specifying 增加 arch 维度
|
|
3
|
+
//
|
|
4
|
+
// 规则:
|
|
5
|
+
// - arch_design_decision == "required" → architecture/*.md 必须存在且非空
|
|
6
|
+
// - arch_design_decision == "skipped" → arch_design_reason 非空(advisory,不阻断)
|
|
7
|
+
// - arch_design_decision == null → 未执行 architecture-design,不阻断(向后兼容)
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 从 .team-flow.yaml 提取顶层字段值(简单正则,与 state-loader 兼容)
|
|
14
|
+
*/
|
|
15
|
+
function extractYamlField(content, field) {
|
|
16
|
+
for (const line of content.split('\n')) {
|
|
17
|
+
const match = line.match(new RegExp(`^${field}:\\s*(.*)`));
|
|
18
|
+
if (match) {
|
|
19
|
+
const val = match[1].trim();
|
|
20
|
+
return val === 'null' || val === '' ? null : val;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check architecture-design artifacts for the exploring:specifying transition.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} changeDir - change directory path
|
|
30
|
+
* @returns {{ pass: boolean, failures: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
export function checkArchDesign(changeDir) {
|
|
33
|
+
const failures = [];
|
|
34
|
+
const stateFile = path.join(changeDir, '.team-flow.yaml');
|
|
35
|
+
|
|
36
|
+
// 无状态文件 → 不阻断(向后兼容存量 change)
|
|
37
|
+
if (!fs.existsSync(stateFile)) {
|
|
38
|
+
return { pass: true, failures: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const raw = fs.readFileSync(stateFile, 'utf-8');
|
|
42
|
+
const decision = extractYamlField(raw, 'arch_design_decision');
|
|
43
|
+
|
|
44
|
+
// 未执行 architecture-design(null)→ 不阻断,向后兼容
|
|
45
|
+
if (!decision) {
|
|
46
|
+
return { pass: true, failures: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// skipped → 检查 reason 非空(advisory,不阻断)
|
|
50
|
+
if (decision === 'skipped') {
|
|
51
|
+
const reason = extractYamlField(raw, 'arch_design_reason');
|
|
52
|
+
if (!reason) {
|
|
53
|
+
// advisory: 输出 warning 但不加入 failures
|
|
54
|
+
console.warn(' [WARN] arch-design: arch_design_decision=skipped but arch_design_reason is empty');
|
|
55
|
+
}
|
|
56
|
+
return { pass: true, failures: [] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// required → 检查 architecture/*.md 存在且非空
|
|
60
|
+
if (decision === 'required') {
|
|
61
|
+
const archDir = path.join(changeDir, 'architecture');
|
|
62
|
+
if (!fs.existsSync(archDir)) {
|
|
63
|
+
failures.push('architecture/: directory missing (arch_design_decision=required)');
|
|
64
|
+
return { pass: false, failures };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const requiredFiles = ['architecture.md', 'database.md', 'api.md'];
|
|
68
|
+
for (const f of requiredFiles) {
|
|
69
|
+
const fp = path.join(archDir, f);
|
|
70
|
+
if (!fs.existsSync(fp)) {
|
|
71
|
+
failures.push(`architecture/${f}: missing (arch_design_decision=required)`);
|
|
72
|
+
} else if (fs.readFileSync(fp, 'utf-8').trim().length === 0) {
|
|
73
|
+
failures.push(`architecture/${f}: empty (arch_design_decision=required)`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { pass: failures.length === 0, failures };
|
|
79
|
+
}
|
package/scripts/guard/guard.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { checkTestsPassing } from './checks/tests-passing.mjs';
|
|
|
8
8
|
import { checkContractFresh } from './checks/contract-fresh.mjs';
|
|
9
9
|
import { check as checkDpGate } from './checks/dp-gate-passed.mjs';
|
|
10
10
|
import { checkSpecsMerged } from './checks/specs-merged.mjs';
|
|
11
|
+
import { checkArchDesign } from './checks/arch-design.mjs';
|
|
11
12
|
import { checkContractCurrent } from './checks/contract-current.mjs';
|
|
12
13
|
import { checkDp3Approved } from './checks/dp3-approved.mjs';
|
|
13
14
|
import { checkExecutionPlanReady } from './checks/execution-plan-ready.mjs';
|
|
@@ -16,7 +17,7 @@ import { checkExecutionReviewsPassed } from './checks/execution-reviews-passed.m
|
|
|
16
17
|
// Transition matrix: <from>:<to> → required check dimensions
|
|
17
18
|
const TRANSITION_CHECKS = {
|
|
18
19
|
// Forward transitions
|
|
19
|
-
'exploring:specifying': ['artifacts-exist'],
|
|
20
|
+
'exploring:specifying': ['artifacts-exist', 'arch-design'],
|
|
20
21
|
'specifying:bridging': ['artifacts-exist', 'schema-valid'],
|
|
21
22
|
'bridging:approved-for-build': ['artifacts-exist', 'schema-valid', 'contract-fresh', 'dp-gate-passed'],
|
|
22
23
|
'approved-for-build:executing': ['artifacts-exist', 'contract-fresh', 'dp-gate-passed', 'execution-plan-ready'],
|
|
@@ -163,6 +164,7 @@ async function main() {
|
|
|
163
164
|
'dp3-approved': (dir) => checkDp3Approved(dir),
|
|
164
165
|
'execution-plan-ready': (dir) => checkExecutionPlanReady(dir),
|
|
165
166
|
'execution-reviews-passed': (dir) => checkExecutionReviewsPassed(dir),
|
|
167
|
+
'arch-design': (dir) => checkArchDesign(dir),
|
|
166
168
|
};
|
|
167
169
|
|
|
168
170
|
const checks = [];
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* arch-merge — change 完成后将架构增量合并回全局 docs/architecture/
|
|
4
|
+
*
|
|
5
|
+
* v0.10 §28.4 定义,v0.23.0 实现为 CLI 子命令
|
|
6
|
+
* 用法:tf arch-merge <change-dir> [--project-root <path>] [--dry-run]
|
|
7
|
+
*
|
|
8
|
+
* 功能(8 步流程):
|
|
9
|
+
* 1. 一致性预检(结构冲突 / 语义冲突 / 跨域一致性)
|
|
10
|
+
* 2. ARCHITECTURE.md 回写(当前态覆盖 + 演进日志 append)
|
|
11
|
+
* 3. PHYSICAL-MODEL.md 增量合并(变更涉及的表字段级合并)
|
|
12
|
+
* 4. schema-baseline.sql 同步(追加新 DDL,保持完整可执行)
|
|
13
|
+
* 5. 变更脚本归档(sql/ddl/ + sql/migration/ → changelog/)
|
|
14
|
+
* 6. API-INDEX.md 增量更新
|
|
15
|
+
* 7. INDEX.md 更新(摘要统计)
|
|
16
|
+
* 8. 单次 git commit(原子性提交)
|
|
17
|
+
*
|
|
18
|
+
* 回写顺序:arch-merge → prototype-sync(同一 change closing 内,顺序提交)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, writeFileSync, existsSync, cpSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
22
|
+
import { join, basename, relative, dirname, resolve, sep } from 'node:path';
|
|
23
|
+
import { execSync } from 'node:child_process';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 解析 CLI 参数数组为结构化对象
|
|
27
|
+
*/
|
|
28
|
+
function parseArgv(argv) {
|
|
29
|
+
const parsed = { _: [] };
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
if (argv[i] === '--project-root' && argv[i + 1] && !argv[i + 1].startsWith('--')) {
|
|
32
|
+
parsed.projectRoot = argv[++i];
|
|
33
|
+
} else if (argv[i] === '--dry-run') {
|
|
34
|
+
parsed.dryRun = true;
|
|
35
|
+
} else if (!argv[i].startsWith('--')) {
|
|
36
|
+
parsed._.push(argv[i]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 从 change 目录提取 change name
|
|
44
|
+
*/
|
|
45
|
+
function extractChangeName(changeDir) {
|
|
46
|
+
return basename(changeDir.replace(/\/$/, ''));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 读取 markdown 文件中的 frontmatter
|
|
51
|
+
*/
|
|
52
|
+
function readFrontmatter(content) {
|
|
53
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
54
|
+
if (!match) return {};
|
|
55
|
+
const fm = {};
|
|
56
|
+
for (const line of match[1].split('\n')) {
|
|
57
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
58
|
+
if (m) fm[m[1]] = m[2].trim();
|
|
59
|
+
}
|
|
60
|
+
return fm;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Step 1: 一致性预检
|
|
65
|
+
*/
|
|
66
|
+
function preCheck(changeDir, archDir, projectRoot) {
|
|
67
|
+
const conflicts = [];
|
|
68
|
+
|
|
69
|
+
// 检查 architecture/ 目录是否存在
|
|
70
|
+
if (!existsSync(archDir)) {
|
|
71
|
+
return { pass: false, conflicts: ['architecture/ directory not found in change'] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 检查必要文件
|
|
75
|
+
const requiredFiles = ['architecture.md', 'database.md', 'api.md'];
|
|
76
|
+
for (const f of requiredFiles) {
|
|
77
|
+
const fp = join(archDir, f);
|
|
78
|
+
if (!existsSync(fp)) {
|
|
79
|
+
conflicts.push(`architecture/${f} missing`);
|
|
80
|
+
} else if (readFileSync(fp, 'utf-8').trim().length === 0) {
|
|
81
|
+
conflicts.push(`architecture/${f} empty`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// TODO: 结构冲突检测(重复 BC/聚合 key)
|
|
86
|
+
// TODO: 语义冲突检测(同义异名)
|
|
87
|
+
// TODO: 跨域一致性检测(AA ≥ 1 IA 实体)
|
|
88
|
+
|
|
89
|
+
return { pass: conflicts.length === 0, conflicts };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Step 2: ARCHITECTURE.md 回写
|
|
94
|
+
* 当前态覆盖 + 演进日志 append
|
|
95
|
+
*/
|
|
96
|
+
function mergeArchitecture(changeDir, archDir, globalArchDir, changeName) {
|
|
97
|
+
const srcPath = join(archDir, 'architecture.md');
|
|
98
|
+
const dstPath = join(globalArchDir, 'ARCHITECTURE.md');
|
|
99
|
+
|
|
100
|
+
if (!existsSync(srcPath)) return { merged: false, reason: 'no-source' };
|
|
101
|
+
|
|
102
|
+
const srcContent = readFileSync(srcPath, 'utf-8');
|
|
103
|
+
|
|
104
|
+
// 提取 To-Be 增量设计段
|
|
105
|
+
const toBeMatch = srcContent.match(/## 2\. To-Be 增量设计[\s\S]*?(?=\n## 3\.|\n---|\n$)/);
|
|
106
|
+
const toBeContent = toBeMatch ? toBeMatch[0] : '';
|
|
107
|
+
|
|
108
|
+
// 提取演进日志
|
|
109
|
+
const logMatch = srcContent.match(/## 5\. 演进日志[\s\S]*?(?=\n## |\n---|\n$)/);
|
|
110
|
+
const logEntries = logMatch ? logMatch[0] : '';
|
|
111
|
+
|
|
112
|
+
if (existsSync(dstPath)) {
|
|
113
|
+
const existing = readFileSync(dstPath, 'utf-8');
|
|
114
|
+
|
|
115
|
+
// 追加演进日志(append-only)
|
|
116
|
+
if (logEntries) {
|
|
117
|
+
writeFileSync(dstPath, existing + '\n\n' + logEntries, 'utf-8');
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
// 首次创建:直接使用 To-Be 段作为当前态
|
|
121
|
+
const header = `# Architecture Baseline\n\n> Last updated: change=${changeName}, ${new Date().toISOString().slice(0, 10)}\n\n`;
|
|
122
|
+
writeFileSync(dstPath, header + toBeContent + '\n\n' + logEntries, 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { merged: true, file: 'ARCHITECTURE.md' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Step 3: PHYSICAL-MODEL.md 增量合并
|
|
130
|
+
* 仅合并变更涉及的表,未涉及的保持不变
|
|
131
|
+
*/
|
|
132
|
+
function mergePhysicalModel(changeDir, archDir, globalArchDir, changeName) {
|
|
133
|
+
const dbPath = join(archDir, 'database.md');
|
|
134
|
+
const dstPath = join(globalArchDir, 'PHYSICAL-MODEL.md');
|
|
135
|
+
|
|
136
|
+
if (!existsSync(dbPath)) return { merged: false, reason: 'no-source' };
|
|
137
|
+
|
|
138
|
+
const dbContent = readFileSync(dbPath, 'utf-8');
|
|
139
|
+
|
|
140
|
+
// 提取 Schema 变更表中的目标表名
|
|
141
|
+
const tableMatches = dbContent.matchAll(/\|\s*`(?:CREATE|ALTER)\s+TABLE`?\s*\|\s*(\w+)/gi);
|
|
142
|
+
const affectedTables = new Set();
|
|
143
|
+
for (const m of tableMatches) {
|
|
144
|
+
affectedTables.add(m[1]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// TODO: 完整的字段级增量合并逻辑
|
|
148
|
+
// 当前实现:追加变更记录到演进日志段
|
|
149
|
+
|
|
150
|
+
if (existsSync(dstPath) && affectedTables.size > 0) {
|
|
151
|
+
const existing = readFileSync(dstPath, 'utf-8');
|
|
152
|
+
const footer = `\n\n<!-- arch-merge: change=${changeName}, ${new Date().toISOString().slice(0, 10)}, tables=${[...affectedTables].join(',')} -->\n`;
|
|
153
|
+
writeFileSync(dstPath, existing + footer, 'utf-8');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { merged: true, file: 'PHYSICAL-MODEL.md', tables: [...affectedTables] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Step 4: schema-baseline.sql 同步
|
|
161
|
+
*/
|
|
162
|
+
function syncSchemaBaseline(changeDir, archDir, globalArchDir, changeName) {
|
|
163
|
+
const sqlDir = join(archDir, 'sql', 'ddl');
|
|
164
|
+
const dstPath = join(globalArchDir, 'schema-baseline.sql');
|
|
165
|
+
|
|
166
|
+
if (!existsSync(sqlDir)) return { synced: false, reason: 'no-ddl-dir' };
|
|
167
|
+
|
|
168
|
+
const ddlFiles = readdirSync(sqlDir).filter(f => f.endsWith('.sql'));
|
|
169
|
+
if (ddlFiles.length === 0) return { synced: false, reason: 'no-ddl-files' };
|
|
170
|
+
|
|
171
|
+
let baseline = '';
|
|
172
|
+
if (existsSync(dstPath)) {
|
|
173
|
+
baseline = readFileSync(dstPath, 'utf-8');
|
|
174
|
+
} else {
|
|
175
|
+
baseline = `-- Schema Baseline\n-- Auto-maintained by arch-merge\n-- Last updated: change=${changeName}, ${new Date().toISOString().slice(0, 10)}\n\n`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 追加新 DDL
|
|
179
|
+
for (const f of ddlFiles) {
|
|
180
|
+
const content = readFileSync(join(sqlDir, f), 'utf-8');
|
|
181
|
+
baseline += `\n-- === ${f} (change: ${changeName}) ===\n${content}\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
writeFileSync(dstPath, baseline, 'utf-8');
|
|
185
|
+
return { synced: true, file: 'schema-baseline.sql', ddlCount: ddlFiles.length };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Step 5: 变更脚本归档
|
|
190
|
+
*/
|
|
191
|
+
function archiveChangeScripts(changeDir, archDir, globalArchDir, changeName) {
|
|
192
|
+
const report = { ddl: 0, migration: 0 };
|
|
193
|
+
|
|
194
|
+
for (const subDir of ['ddl', 'migration']) {
|
|
195
|
+
const srcDir = join(archDir, 'sql', subDir);
|
|
196
|
+
const dstDir = join(globalArchDir, 'changelog', subDir);
|
|
197
|
+
|
|
198
|
+
if (!existsSync(srcDir)) continue;
|
|
199
|
+
|
|
200
|
+
if (!existsSync(dstDir)) {
|
|
201
|
+
mkdirSync(dstDir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const files = readdirSync(srcDir).filter(f => f.endsWith('.sql'));
|
|
205
|
+
for (const f of files) {
|
|
206
|
+
cpSync(join(srcDir, f), join(dstDir, f));
|
|
207
|
+
report[subDir]++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return report;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Step 6: API-INDEX.md 增量更新
|
|
216
|
+
*/
|
|
217
|
+
function mergeApiIndex(changeDir, archDir, globalArchDir, changeName) {
|
|
218
|
+
const apiPath = join(archDir, 'api.md');
|
|
219
|
+
const dstPath = join(globalArchDir, 'API-INDEX.md');
|
|
220
|
+
|
|
221
|
+
if (!existsSync(apiPath)) return { merged: false, reason: 'no-source' };
|
|
222
|
+
|
|
223
|
+
const apiContent = readFileSync(apiPath, 'utf-8');
|
|
224
|
+
|
|
225
|
+
// 提取 To-Be 增量中的端点表格
|
|
226
|
+
const endpointMatches = apiContent.matchAll(/\|\s*(\/api\/[^\s|]+)\s*\|\s*(\w+)\s*\|/g);
|
|
227
|
+
const endpoints = [];
|
|
228
|
+
for (const m of endpointMatches) {
|
|
229
|
+
endpoints.push({ path: m[1], method: m[2] });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// TODO: 完整的增量合并逻辑(新增/修改/删除端点)
|
|
233
|
+
// 当前实现:追加记录
|
|
234
|
+
|
|
235
|
+
if (existsSync(dstPath) && endpoints.length > 0) {
|
|
236
|
+
const existing = readFileSync(dstPath, 'utf-8');
|
|
237
|
+
const footer = `\n\n<!-- arch-merge: change=${changeName}, ${new Date().toISOString().slice(0, 10)}, endpoints=${endpoints.length} -->\n`;
|
|
238
|
+
writeFileSync(dstPath, existing + footer, 'utf-8');
|
|
239
|
+
} else if (!existsSync(dstPath)) {
|
|
240
|
+
const header = `# API Index\n\n> Last updated: change=${changeName}, ${new Date().toISOString().slice(0, 10)}\n\n`;
|
|
241
|
+
const sections = `## Command API\n\n| 端点 | 方法 | 聚合 | 事务边界 | 说明 |\n|------|------|------|---------|------|\n\n## Read API\n\n| 端点 | 方法 | 聚合 | 数据来源 | 说明 |\n|------|------|------|---------|------|\n\n## Query API\n\n| 端点 | 方法 | 查询模型 | 阻断测试 | 说明 |\n|------|------|---------|---------|------|\n`;
|
|
242
|
+
writeFileSync(dstPath, header + sections, 'utf-8');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { merged: true, file: 'API-INDEX.md', endpoints: endpoints.length };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Step 7: INDEX.md 更新
|
|
250
|
+
*/
|
|
251
|
+
function updateIndex(globalArchDir, changeName) {
|
|
252
|
+
const indexPath = join(globalArchDir, 'INDEX.md');
|
|
253
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
254
|
+
|
|
255
|
+
// 统计各产物
|
|
256
|
+
const stats = {};
|
|
257
|
+
|
|
258
|
+
// ARCHITECTURE.md
|
|
259
|
+
if (existsSync(join(globalArchDir, 'ARCHITECTURE.md'))) {
|
|
260
|
+
const content = readFileSync(join(globalArchDir, 'ARCHITECTURE.md'), 'utf-8');
|
|
261
|
+
const bcMatches = content.matchAll(/### \d+\.\s+(.+)/g);
|
|
262
|
+
stats.architecture = { bcs: [...bcMatches].length };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// PHYSICAL-MODEL.md
|
|
266
|
+
if (existsSync(join(globalArchDir, 'PHYSICAL-MODEL.md'))) {
|
|
267
|
+
const content = readFileSync(join(globalArchDir, 'PHYSICAL-MODEL.md'), 'utf-8');
|
|
268
|
+
const tableMatches = content.matchAll(/^### \d+\.\s+(.+) \((\w+)\)/gm);
|
|
269
|
+
stats.physicalModel = { tables: [...tableMatches].length };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// API-INDEX.md
|
|
273
|
+
if (existsSync(join(globalArchDir, 'API-INDEX.md'))) {
|
|
274
|
+
const content = readFileSync(join(globalArchDir, 'API-INDEX.md'), 'utf-8');
|
|
275
|
+
const epMatches = content.matchAll(/\|\s*\/api\//g);
|
|
276
|
+
stats.apiIndex = { endpoints: [...epMatches].length };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 生成 INDEX.md
|
|
280
|
+
const lines = [
|
|
281
|
+
`# Architecture Index`,
|
|
282
|
+
``,
|
|
283
|
+
`> 本文件始终加载,各产物按需 Read。`,
|
|
284
|
+
`> Last updated: change=${changeName}, ${date}`,
|
|
285
|
+
``,
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
if (stats.architecture) {
|
|
289
|
+
lines.push(
|
|
290
|
+
`## ARCHITECTURE.md`,
|
|
291
|
+
`- 限界上下文: ${stats.architecture.bcs}`,
|
|
292
|
+
`- 最后更新: change=${changeName}, ${date}`,
|
|
293
|
+
`- 读取建议: 按 BC 段落读取`,
|
|
294
|
+
``
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (stats.physicalModel) {
|
|
299
|
+
lines.push(
|
|
300
|
+
`## PHYSICAL-MODEL.md`,
|
|
301
|
+
`- 表数量: ${stats.physicalModel.tables}`,
|
|
302
|
+
`- 最后更新: change=${changeName}, ${date}`,
|
|
303
|
+
`- 读取建议: 按业务域段落读取`,
|
|
304
|
+
``
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (existsSync(join(globalArchDir, 'schema-baseline.sql'))) {
|
|
309
|
+
lines.push(
|
|
310
|
+
`## schema-baseline.sql`,
|
|
311
|
+
`- 最后更新: change=${changeName}, ${date}`,
|
|
312
|
+
`- 读取建议: 通常不需加载,DDL 由 sql/ 目录独立管理`,
|
|
313
|
+
``
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (stats.apiIndex) {
|
|
318
|
+
lines.push(
|
|
319
|
+
`## API-INDEX.md`,
|
|
320
|
+
`- 端点数量: ${stats.apiIndex.endpoints}`,
|
|
321
|
+
`- 最后更新: change=${changeName}, ${date}`,
|
|
322
|
+
`- 读取建议: 按分流区域读取`,
|
|
323
|
+
``
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (existsSync(join(globalArchDir, 'CONCEPTS.md'))) {
|
|
328
|
+
lines.push(
|
|
329
|
+
`## CONCEPTS.md`,
|
|
330
|
+
`- 最后更新: bootstrap`,
|
|
331
|
+
``
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
writeFileSync(indexPath, lines.join('\n') + '\n', 'utf-8');
|
|
336
|
+
return { updated: true, stats };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Step 8: Git commit
|
|
341
|
+
*/
|
|
342
|
+
function gitCommit(globalArchDir, changeName, dryRun) {
|
|
343
|
+
const commitMsg = `arch-merge: ${changeName} — sync architecture artifacts to global docs/architecture/`;
|
|
344
|
+
|
|
345
|
+
if (dryRun) {
|
|
346
|
+
console.log(` [DRY-RUN] Would commit: ${commitMsg}`);
|
|
347
|
+
return { committed: false, dryRun: true };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
execSync(`git add ${globalArchDir}/`, { stdio: 'pipe' });
|
|
352
|
+
execSync(`git commit -m "${commitMsg}"`, { stdio: 'pipe' });
|
|
353
|
+
return { committed: true, message: commitMsg };
|
|
354
|
+
} catch (e) {
|
|
355
|
+
return { committed: false, error: e.message };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 执行 arch-merge 合并
|
|
361
|
+
*/
|
|
362
|
+
export function run(args = {}) {
|
|
363
|
+
if (Array.isArray(args)) {
|
|
364
|
+
args = parseArgv(args);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const changeDir = args._?.[0] || args.changeDir;
|
|
368
|
+
const dryRun = args.dryRun || false;
|
|
369
|
+
|
|
370
|
+
if (!changeDir) {
|
|
371
|
+
console.error('Usage: tf arch-merge <change-dir> [--project-root <path>] [--dry-run]');
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// project-root: 显式指定 > 从 changeDir 向上推导(changes/xxx → 项目根)
|
|
376
|
+
let projectRoot = args.projectRoot;
|
|
377
|
+
if (!projectRoot) {
|
|
378
|
+
const abs = resolve(changeDir);
|
|
379
|
+
const changesIdx = abs.lastIndexOf(sep + 'changes' + sep);
|
|
380
|
+
projectRoot = changesIdx > 0 ? abs.slice(0, changesIdx) : process.cwd();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const changeName = extractChangeName(changeDir);
|
|
384
|
+
const archDir = join(changeDir, 'architecture');
|
|
385
|
+
const globalArchDir = join(projectRoot, 'docs', 'architecture');
|
|
386
|
+
|
|
387
|
+
console.log(`\narch-merge: ${changeName}`);
|
|
388
|
+
console.log(` Source: ${archDir}`);
|
|
389
|
+
console.log(` Target: ${globalArchDir}`);
|
|
390
|
+
if (dryRun) console.log(` Mode: DRY-RUN`);
|
|
391
|
+
|
|
392
|
+
// Step 1: 一致性预检
|
|
393
|
+
console.log('\n[Step 1] Pre-check...');
|
|
394
|
+
const preCheckResult = preCheck(changeDir, archDir, projectRoot);
|
|
395
|
+
if (!preCheckResult.pass) {
|
|
396
|
+
console.error(' Pre-check FAILED:');
|
|
397
|
+
for (const c of preCheckResult.conflicts) {
|
|
398
|
+
console.error(` - ${c}`);
|
|
399
|
+
}
|
|
400
|
+
return { merged: false, reason: 'pre-check-failed', conflicts: preCheckResult.conflicts };
|
|
401
|
+
}
|
|
402
|
+
console.log(' Pre-check passed');
|
|
403
|
+
|
|
404
|
+
// 确保全局目录存在
|
|
405
|
+
if (!existsSync(globalArchDir)) {
|
|
406
|
+
mkdirSync(globalArchDir, { recursive: true });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Step 2: ARCHITECTURE.md
|
|
410
|
+
console.log('[Step 2] Merging ARCHITECTURE.md...');
|
|
411
|
+
const archResult = mergeArchitecture(changeDir, archDir, globalArchDir, changeName);
|
|
412
|
+
console.log(` ${archResult.merged ? '✓' : '✗'} ${archResult.file || archResult.reason}`);
|
|
413
|
+
|
|
414
|
+
// Step 3: PHYSICAL-MODEL.md
|
|
415
|
+
console.log('[Step 3] Merging PHYSICAL-MODEL.md...');
|
|
416
|
+
const pmResult = mergePhysicalModel(changeDir, archDir, globalArchDir, changeName);
|
|
417
|
+
console.log(` ${pmResult.merged ? '✓' : '✗'} ${pmResult.file || pmResult.reason}`);
|
|
418
|
+
if (pmResult.tables?.length > 0) {
|
|
419
|
+
console.log(` Affected tables: ${pmResult.tables.join(', ')}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Step 4: schema-baseline.sql
|
|
423
|
+
console.log('[Step 4] Syncing schema-baseline.sql...');
|
|
424
|
+
const schemaResult = syncSchemaBaseline(changeDir, archDir, globalArchDir, changeName);
|
|
425
|
+
console.log(` ${schemaResult.synced ? '✓' : '✗'} ${schemaResult.file || schemaResult.reason}`);
|
|
426
|
+
|
|
427
|
+
// Step 5: 变更脚本归档
|
|
428
|
+
console.log('[Step 5] Archiving change scripts...');
|
|
429
|
+
const archiveResult = archiveChangeScripts(changeDir, archDir, globalArchDir, changeName);
|
|
430
|
+
console.log(` DDL: ${archiveResult.ddl}, Migration: ${archiveResult.migration}`);
|
|
431
|
+
|
|
432
|
+
// Step 6: API-INDEX.md
|
|
433
|
+
console.log('[Step 6] Merging API-INDEX.md...');
|
|
434
|
+
const apiResult = mergeApiIndex(changeDir, archDir, globalArchDir, changeName);
|
|
435
|
+
console.log(` ${apiResult.merged ? '✓' : '✗'} ${apiResult.file || apiResult.reason}`);
|
|
436
|
+
|
|
437
|
+
// Step 7: INDEX.md
|
|
438
|
+
console.log('[Step 7] Updating INDEX.md...');
|
|
439
|
+
const indexResult = updateIndex(globalArchDir, changeName);
|
|
440
|
+
console.log(` ${indexResult.updated ? '✓' : '✗'} Updated`);
|
|
441
|
+
|
|
442
|
+
// Step 8: Git commit
|
|
443
|
+
console.log('[Step 8] Git commit...');
|
|
444
|
+
const commitResult = gitCommit(globalArchDir, changeName, dryRun);
|
|
445
|
+
console.log(` ${commitResult.committed ? '✓' : (commitResult.dryRun ? '⊘ dry-run' : '✗')} ${commitResult.message || commitResult.error || ''}`);
|
|
446
|
+
|
|
447
|
+
console.log('\narch-merge complete.');
|
|
448
|
+
return {
|
|
449
|
+
merged: true,
|
|
450
|
+
changeName,
|
|
451
|
+
steps: { architecture: archResult, physicalModel: pmResult, schema: schemaResult, archive: archiveResult, apiIndex: apiResult, index: indexResult, commit: commitResult },
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 支持直接执行
|
|
456
|
+
if (process.argv[1]?.includes('arch-merge')) {
|
|
457
|
+
const args = parseArgv(process.argv.slice(2));
|
|
458
|
+
run(args);
|
|
459
|
+
}
|
|
@@ -23,6 +23,9 @@ const SETTABLE_FIELDS = [
|
|
|
23
23
|
'dp_5_result', 'dp_5_timestamp', 'dp_5_decisions', 'dp_5_confirmed',
|
|
24
24
|
'dp_6_result', 'dp_6_timestamp', 'dp_6_decisions', 'dp_6_confirmed',
|
|
25
25
|
'dp_7_result', 'dp_7_timestamp', 'dp_7_decisions', 'dp_7_confirmed',
|
|
26
|
+
// Architecture design gate (v0.9 §26, v0.22.5 评审修复 F02)
|
|
27
|
+
'arch_design_decision', 'arch_design_reason',
|
|
28
|
+
'arch_design_timestamp', 'arch_design_artifacts',
|
|
26
29
|
];
|
|
27
30
|
|
|
28
31
|
export async function run(args) {
|
package/scripts/lib/hash.mjs
CHANGED
|
@@ -37,6 +37,24 @@ export function computeArtifactsHash(changeDir) {
|
|
|
37
37
|
hasContent = true;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// architecture/*.md (v0.23.0 §30: arch-design artifacts纳入hash)
|
|
41
|
+
// 三个文件全空则不参与 hash(兼容 arch_design_decision=skipped)
|
|
42
|
+
// sql/ 目录下的 .sql 文件不纳入 hash——可执行制品由版本控制保证
|
|
43
|
+
const archDir = path.join(changeDir, 'architecture');
|
|
44
|
+
if (fs.existsSync(archDir)) {
|
|
45
|
+
const archFiles = ['architecture.md', 'database.md', 'api.md'];
|
|
46
|
+
for (const f of archFiles) {
|
|
47
|
+
const fp = path.join(archDir, f);
|
|
48
|
+
if (fs.existsSync(fp)) {
|
|
49
|
+
const content = fs.readFileSync(fp, 'utf-8');
|
|
50
|
+
if (content.trim().length > 0) {
|
|
51
|
+
hash.update(content);
|
|
52
|
+
hasContent = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
return hasContent ? `sha256:${hash.digest('hex')}` : null;
|
|
41
59
|
}
|
|
42
60
|
|
|
@@ -38,6 +38,11 @@ const BUILTIN_DEFAULTS = {
|
|
|
38
38
|
dp_6_timestamp: null,
|
|
39
39
|
dp_7_result: null,
|
|
40
40
|
dp_7_timestamp: null,
|
|
41
|
+
// Architecture design gate (v0.9 §26, v0.22.5 评审修复 F02)
|
|
42
|
+
arch_design_decision: null,
|
|
43
|
+
arch_design_reason: null,
|
|
44
|
+
arch_design_timestamp: null,
|
|
45
|
+
arch_design_artifacts: null,
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
/**
|
|
@@ -106,6 +111,12 @@ export function writeState(changeDir, state) {
|
|
|
106
111
|
lines.push(`dp_6_timestamp: ${state.dp_6_timestamp ?? 'null'}`);
|
|
107
112
|
lines.push(`dp_7_result: ${state.dp_7_result ?? 'null'}`);
|
|
108
113
|
lines.push(`dp_7_timestamp: ${state.dp_7_timestamp ?? 'null'}`);
|
|
114
|
+
lines.push('');
|
|
115
|
+
lines.push('# === Architecture design gate (v0.9 §26) ===');
|
|
116
|
+
lines.push(`arch_design_decision: ${state.arch_design_decision ?? 'null'}`);
|
|
117
|
+
lines.push(`arch_design_reason: ${state.arch_design_reason ?? 'null'}`);
|
|
118
|
+
lines.push(`arch_design_timestamp: ${state.arch_design_timestamp ?? 'null'}`);
|
|
119
|
+
lines.push(`arch_design_artifacts: ${state.arch_design_artifacts ?? 'null'}`);
|
|
109
120
|
|
|
110
121
|
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
|
|
111
122
|
}
|
package/scripts/team-flow.mjs
CHANGED
|
@@ -33,6 +33,7 @@ const COMMANDS = {
|
|
|
33
33
|
'install-qoder': () => import('./lib/cmd-install-qoder.mjs'),
|
|
34
34
|
'install-zcode': () => import('./lib/cmd-install-zcode.mjs'),
|
|
35
35
|
'prototype-sync': () => import('./lib/prototype-sync.mjs'),
|
|
36
|
+
'arch-merge': () => import('./lib/arch-merge.mjs'),
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
const HELP = `team-flow (tf) — Unified workflow plugin CLI
|
|
@@ -47,6 +48,8 @@ Commands:
|
|
|
47
48
|
sync <change-dir> Merge delta specs into main specs
|
|
48
49
|
prototype-sync <change-dir> [--source <path>] [--prototype-dir <path>]
|
|
49
50
|
Merge UX delta into global prototype/ + design-system.md
|
|
51
|
+
arch-merge <change-dir> [--project-root <path>] [--dry-run]
|
|
52
|
+
Merge architecture delta into global docs/architecture/
|
|
50
53
|
config [options] Display or modify configuration
|
|
51
54
|
config --resolve-model <profile> Resolve a configured model profile without switching models
|
|
52
55
|
state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild)
|